From c290c91e3d5ffac16ea48b8cbe8ea452412bf807 Mon Sep 17 00:00:00 2001 From: Nan Yu Date: Tue, 27 Jan 2026 21:14:24 +0000 Subject: [PATCH] feat: implement schema packaging and runtime loading - Add a base InferenceStrategy class - Add PackSpecsBuildHook to copy JSON schemas into the package during build time. - Update pyproject.toml to include assets and configure the build hook. - Implement A2uiSchemaManager for robust schema loading, pruning, and prompt generation. --- .../python_a2ui_agent_build_and_test.yml | 8 +- a2a_agents/python/a2ui_agent/.gitignore | 1 + .../python/a2ui_agent/pack_specs_hook.py | 62 ++++ a2a_agents/python/a2ui_agent/pyproject.toml | 7 + .../a2ui_agent/src/a2ui/inference/__init__.py | 13 + .../src/a2ui/inference/inference_strategy.py | 33 ++ .../src/a2ui/inference/schema/__init__.py | 13 + .../src/a2ui/inference/schema/loader.py | 69 +++++ .../src/a2ui/inference/schema/manager.py | 233 ++++++++++++++ .../src/a2ui/inference/template/__init__.py | 13 + .../src/a2ui/inference/template/manager.py | 30 ++ .../tests/inference/test_schema_manager.py | 289 ++++++++++++++++++ .../tests/integration/verify_load_real.py | 38 +++ a2a_agents/python/a2ui_agent/uv.lock | 119 ++++---- 14 files changed, 870 insertions(+), 58 deletions(-) create mode 100644 a2a_agents/python/a2ui_agent/.gitignore create mode 100644 a2a_agents/python/a2ui_agent/pack_specs_hook.py create mode 100644 a2a_agents/python/a2ui_agent/src/a2ui/inference/__init__.py create mode 100644 a2a_agents/python/a2ui_agent/src/a2ui/inference/inference_strategy.py create mode 100644 a2a_agents/python/a2ui_agent/src/a2ui/inference/schema/__init__.py create mode 100644 a2a_agents/python/a2ui_agent/src/a2ui/inference/schema/loader.py create mode 100644 a2a_agents/python/a2ui_agent/src/a2ui/inference/schema/manager.py create mode 100644 a2a_agents/python/a2ui_agent/src/a2ui/inference/template/__init__.py create mode 100644 a2a_agents/python/a2ui_agent/src/a2ui/inference/template/manager.py create mode 100644 a2a_agents/python/a2ui_agent/tests/inference/test_schema_manager.py create mode 100644 a2a_agents/python/a2ui_agent/tests/integration/verify_load_real.py diff --git a/.github/workflows/python_a2ui_agent_build_and_test.yml b/.github/workflows/python_a2ui_agent_build_and_test.yml index 4d3f44e8e..4ecd60d27 100644 --- a/.github/workflows/python_a2ui_agent_build_and_test.yml +++ b/.github/workflows/python_a2ui_agent_build_and_test.yml @@ -46,10 +46,14 @@ jobs: working-directory: a2a_agents/python/a2ui_agent run: uv run pyink --check . + - name: Run unit tests + working-directory: a2a_agents/python/a2ui_agent + run: uv run --with pytest pytest tests/ + - name: Build the python SDK working-directory: a2a_agents/python/a2ui_agent run: uv build . - - name: Run unit tests + - name: Run validation scripts on assets packing working-directory: a2a_agents/python/a2ui_agent - run: uv run --with pytest pytest tests/ + run: uv run python tests/integration/verify_load_real.py diff --git a/a2a_agents/python/a2ui_agent/.gitignore b/a2a_agents/python/a2ui_agent/.gitignore new file mode 100644 index 000000000..22b91a08b --- /dev/null +++ b/a2a_agents/python/a2ui_agent/.gitignore @@ -0,0 +1 @@ +src/a2ui/assets/**/*.json diff --git a/a2a_agents/python/a2ui_agent/pack_specs_hook.py b/a2a_agents/python/a2ui_agent/pack_specs_hook.py new file mode 100644 index 000000000..f846f2a7c --- /dev/null +++ b/a2a_agents/python/a2ui_agent/pack_specs_hook.py @@ -0,0 +1,62 @@ +import os +import shutil +import sys +from hatchling.builders.hooks.plugin.interface import BuildHookInterface + + +class PackSpecsBuildHook(BuildHookInterface): + + def initialize(self, version, build_data): + project_root = self.root + + # Add src to sys.path to import the constant + src_path = os.path.join(project_root, "src") + if src_path not in sys.path: + sys.path.insert(0, src_path) + + from a2ui.inference.schema.manager import ( + SPEC_VERSION_MAP, + A2UI_ASSET_PACKAGE, + SPECIFICATION_DIR, + find_repo_root, + ) + + # project root is in a2a_agents/python/a2ui_agent + # Dynamically find repo root by looking for SPECIFICATION_DIR + repo_root = find_repo_root(project_root) + if not repo_root: + # Check for PKG-INFO which implies a packaged state (sdist). + # If PKG-INFO is present, trust the bundled assets. + if os.path.exists(os.path.join(project_root, "PKG-INFO")): + print("Repository root not found, but PKG-INFO present (sdist). Skipping copy.") + return + + raise RuntimeError( + f"Could not find repository root (looked for '{SPECIFICATION_DIR}'" + " directory)." + ) + + # Target directory: src/a2ui/assets + target_base = os.path.join( + project_root, "src", A2UI_ASSET_PACKAGE.replace(".", os.sep) + ) + + for ver, schema_map in SPEC_VERSION_MAP.items(): + target_dir = os.path.join(target_base, ver) + os.makedirs(target_dir, exist_ok=True) + + for _schema_key, source_rel_path in schema_map.items(): + source_path = os.path.join(repo_root, source_rel_path) + + if not os.path.exists(source_path): + print( + f"WARNING: Source schema file not found at {source_path}. Build might" + " produce incomplete wheel if not running from monorepo root." + ) + continue + + filename = os.path.basename(source_path) + dst_file = os.path.join(target_dir, filename) + + print(f"Copying {source_path} -> {dst_file}") + shutil.copy2(source_path, dst_file) diff --git a/a2a_agents/python/a2ui_agent/pyproject.toml b/a2a_agents/python/a2ui_agent/pyproject.toml index 513db8577..f8e3c9fd5 100644 --- a/a2a_agents/python/a2ui_agent/pyproject.toml +++ b/a2a_agents/python/a2ui_agent/pyproject.toml @@ -17,6 +17,13 @@ build-backend = "hatchling.build" [tool.hatch.build.targets.wheel] packages = ["src/a2ui"] +artifacts = ["src/a2ui/assets/**"] + +[tool.hatch.build.targets.sdist] +artifacts = ["src/a2ui/assets/**"] + +[tool.hatch.build.hooks.custom] +path = "pack_specs_hook.py" [[tool.uv.index]] url = "https://pypi.org/simple" diff --git a/a2a_agents/python/a2ui_agent/src/a2ui/inference/__init__.py b/a2a_agents/python/a2ui_agent/src/a2ui/inference/__init__.py new file mode 100644 index 000000000..4df27a209 --- /dev/null +++ b/a2a_agents/python/a2ui_agent/src/a2ui/inference/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2026 Google LLC +# +# 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. diff --git a/a2a_agents/python/a2ui_agent/src/a2ui/inference/inference_strategy.py b/a2a_agents/python/a2ui_agent/src/a2ui/inference/inference_strategy.py new file mode 100644 index 000000000..db737324e --- /dev/null +++ b/a2a_agents/python/a2ui_agent/src/a2ui/inference/inference_strategy.py @@ -0,0 +1,33 @@ +# Copyright 2026 Google LLC +# +# 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. + +from abc import ABC, abstractmethod +from typing import List + + +class InferenceStrategy(ABC): + + @abstractmethod + def generate_system_prompt( + self, + role_description: str, + workflow_description: str = "", + ui_description: str = "", + selected_components: List[str] = [], + examples: str = "", + ) -> str: + """ + Abstract method to be implemented by subclasses. + """ + pass diff --git a/a2a_agents/python/a2ui_agent/src/a2ui/inference/schema/__init__.py b/a2a_agents/python/a2ui_agent/src/a2ui/inference/schema/__init__.py new file mode 100644 index 000000000..4df27a209 --- /dev/null +++ b/a2a_agents/python/a2ui_agent/src/a2ui/inference/schema/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2026 Google LLC +# +# 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. diff --git a/a2a_agents/python/a2ui_agent/src/a2ui/inference/schema/loader.py b/a2a_agents/python/a2ui_agent/src/a2ui/inference/schema/loader.py new file mode 100644 index 000000000..5ef09c175 --- /dev/null +++ b/a2a_agents/python/a2ui_agent/src/a2ui/inference/schema/loader.py @@ -0,0 +1,69 @@ +# Copyright 2026 Google LLC +# +# 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. + +import json +import os +import importlib.resources +from typing import List, Dict, Any + +from abc import ABC, abstractmethod + +ENCODING = "utf-8" + + +class A2uiSchemaLoader(ABC): + """Abstract base class for loading schema files.""" + + @abstractmethod + def load(self, filename: str) -> Any: + """Loads a JSON file.""" + pass + + +class FileSystemLoader(A2uiSchemaLoader): + """Loads schema files from the local filesystem. + + This loader assumes that all referenced schema files are located in the + same flat directory structure. + """ + + def __init__(self, base_dir: str): + self.base_dir = base_dir + + def load(self, filename: str) -> Any: + path = os.path.join(self.base_dir, filename) + with open(path, "r", encoding=ENCODING) as f: + return json.load(f) + + +class PackageLoader(A2uiSchemaLoader): + """Loads schema files from package resources. + + This loader assumes that all referenced schema files are located in the + same flat package structure. + """ + + def __init__(self, package_path: str): + self.package_path = package_path + + def load(self, filename: str) -> Any: + try: + traversable = importlib.resources.files(self.package_path) + resource_path = traversable.joinpath(filename) + with resource_path.open("r", encoding=ENCODING) as f: + return json.load(f) + except Exception as e: + raise IOError( + f"Could not load package resource {filename} in {self.package_path}: {e}" + ) from e diff --git a/a2a_agents/python/a2ui_agent/src/a2ui/inference/schema/manager.py b/a2a_agents/python/a2ui_agent/src/a2ui/inference/schema/manager.py new file mode 100644 index 000000000..1a6ee418b --- /dev/null +++ b/a2a_agents/python/a2ui_agent/src/a2ui/inference/schema/manager.py @@ -0,0 +1,233 @@ +# Copyright 2026 Google LLC +# +# 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. + +import copy +import json +import logging +import os +import importlib.resources +from typing import List, Dict, Any +from .loader import A2uiSchemaLoader, PackageLoader, FileSystemLoader +from ..inference_strategy import InferenceStrategy + +# Helper constants for schema management shared by build hook and runtime + +# Mapping of version to the relative path of the source schema in the monorepo +# Paths are relative to the repository root +A2UI_ASSET_PACKAGE = "a2ui.assets" +SERVER_TO_CLIENT_SCHEMA_KEY = "server_to_client" +COMMON_TYPES_SCHEMA_KEY = "common_types" +CATALOG_SCHEMA_KEY = "catalog" +CATALOG_COMPONENTS_KEY = "components" + +SPEC_VERSION_MAP = { + "0.8": { + SERVER_TO_CLIENT_SCHEMA_KEY: "specification/v0_8/json/server_to_client.json", + CATALOG_SCHEMA_KEY: "specification/v0_8/json/standard_catalog_definition.json", + }, + "0.9": { + SERVER_TO_CLIENT_SCHEMA_KEY: "specification/v0_9/json/server_to_client.json", + CATALOG_SCHEMA_KEY: "specification/v0_9/json/standard_catalog.json", + COMMON_TYPES_SCHEMA_KEY: "specification/v0_9/json/common_types.json", + }, +} + +SPECIFICATION_DIR = "specification" + + +class A2uiSchemaManager(InferenceStrategy): + """Manages A2UI schema pruning and prompt injection.""" + + def __init__(self, version: str, custom_catalog_path: str = None): + self.version = version + self.server_to_client_schema = None + self.catalog_schema = None + self.common_types_schema = None + self._load_schemas(version, custom_catalog_path) + + def _load_schemas(self, version: str, custom_catalog_path: str = None): + """ + Loads separate schema components. + """ + if version in SPEC_VERSION_MAP: + spec_map = SPEC_VERSION_MAP[version] + + self.server_to_client_schema = self._load_schema( + version, spec_map.get(SERVER_TO_CLIENT_SCHEMA_KEY) + ) + if COMMON_TYPES_SCHEMA_KEY in spec_map: + self.common_types_schema = self._load_schema( + version, spec_map.get(COMMON_TYPES_SCHEMA_KEY) + ) + + if custom_catalog_path: + self.catalog_schema = self._load_from_path(custom_catalog_path) + elif CATALOG_SCHEMA_KEY in spec_map: + self.catalog_schema = self._load_schema( + version, spec_map.get(CATALOG_SCHEMA_KEY) + ) + else: + raise ValueError(f"Could not load catalog schema for version {version}") + else: + raise ValueError( + f"Unknown A2UI specification version: {version}. Supported:" + f" {list(SPEC_VERSION_MAP.keys())}" + ) + + def _load_schema(self, version: str, path: str) -> Dict: + if not path: + return {} + + filename = os.path.basename(path) + + # 1. Try to load from package resources + try: + loader = PackageLoader(f"{A2UI_ASSET_PACKAGE}.{version}") + return loader.load(filename) + except Exception as e: + logging.debug("Could not load schema '%s' from package: %s", filename, e) + + # 2. Fallback: Local Assets + # This handles cases where assets might be present in src but not installed + try: + potential_path = os.path.abspath( + os.path.join( + os.path.dirname(__file__), + "..", + "assets", + version, + filename, + ) + ) + loader = FileSystemLoader(os.path.dirname(potential_path)) + return loader.load(filename) + except Exception as e: + logging.debug("Could not load schema '%s' from local assets: %s", filename, e) + + # 3. Fallback: Source Repository (specification/...) + # This handles cases where we are running directly from source tree + # And assets are not yet copied to src/a2ui/assets + # schema_manager.py is at a2a_agents/python/a2ui_agent/src/a2ui/inference/schema/manager.py + # Dynamically find repo root by looking for "specification" directory + try: + repo_root = find_repo_root(os.path.dirname(__file__)) + except Exception as e: + logging.debug("Could not find repo root: %s", e) + + if repo_root: + source_path = os.path.join(repo_root, path) + if os.path.exists(source_path): + loader = FileSystemLoader(os.path.dirname(source_path)) + return loader.load(filename) + + raise IOError( + f"Could not load schema component {filename} for version {version}" + ) from e + + def _load_from_path(self, path: str) -> Dict: + try: + loader = FileSystemLoader(os.path.dirname(path)) + return loader.load(os.path.basename(path)) + except Exception as e: + raise ValueError(f"Failed to load custom catalog at {path}: {e}") + + def get_pruned_catalog(self, selected_components: List[str] = None) -> Dict: + """Dynamically filters the catalog to reduce context window tokens.""" + if not self.catalog_schema: + return {} + + schema = copy.deepcopy(self.catalog_schema) + + # If no components selected, return full catalog + if not selected_components: + return schema + + if CATALOG_COMPONENTS_KEY in schema and isinstance( + schema[CATALOG_COMPONENTS_KEY], dict + ): + all_comps = schema[CATALOG_COMPONENTS_KEY] + schema[CATALOG_COMPONENTS_KEY] = { + k: v for k, v in all_comps.items() if k in selected_components + } + + # Filter anyComponent oneOf if it exists + # Path: $defs -> anyComponent -> oneOf + if "$defs" in schema and "anyComponent" in schema["$defs"]: + any_comp = schema["$defs"]["anyComponent"] + if "oneOf" in any_comp and isinstance(any_comp["oneOf"], list): + filtered_one_of = [] + for item in any_comp["oneOf"]: + if "$ref" in item: + ref = item["$ref"] + if ref.startswith(f"#/{CATALOG_COMPONENTS_KEY}/"): + comp_name = ref.split("/")[-1] + if comp_name in selected_components: + filtered_one_of.append(item) + else: + logging.warning(f"Skipping unknown ref format: {ref}") + else: + logging.warning(f"Skipping non-ref item in anyComponent oneOf: {item}") + + any_comp["oneOf"] = filtered_one_of + + return schema + + def generate_system_prompt( + self, + role_description: str, + workflow_description: str = "", + ui_description: str = "", + selected_components: List[str] = [], + examples: str = "", + ) -> str: + """Assembles the final system instruction for the LLM.""" + catalog_str = json.dumps(self.get_pruned_catalog(selected_components), indent=2) + server_client_str = ( + json.dumps(self.server_to_client_schema, indent=2) + if self.server_to_client_schema + else "{}" + ) + + parts = [role_description] + if workflow_description: + parts.append(f"## Workflow Description:\n{workflow_description}") + if ui_description: + parts.append(f"## UI Description:\n{ui_description}") + + if examples: + parts.append(f"## Examples:\n{examples}") + + parts.append("---BEGIN A2UI JSON SCHEMA---") + parts.append(f"### Server To Client Schema:\n{server_client_str}") + + if self.common_types_schema: + common_str = json.dumps(self.common_types_schema, indent=2) + parts.append(f"### Common Types Schema:\n{common_str}") + + parts.append(f"### Catalog Schema:\n{catalog_str}") + parts.append("---END A2UI JSON SCHEMA---") + + return "\n\n".join(parts) + + +def find_repo_root(start_path: str) -> str | None: + """Finds the repository root by looking for the 'specification' directory.""" + current = os.path.abspath(start_path) + while True: + if os.path.isdir(os.path.join(current, SPECIFICATION_DIR)): + return current + parent = os.path.dirname(current) + if parent == current: + return None + current = parent diff --git a/a2a_agents/python/a2ui_agent/src/a2ui/inference/template/__init__.py b/a2a_agents/python/a2ui_agent/src/a2ui/inference/template/__init__.py new file mode 100644 index 000000000..4df27a209 --- /dev/null +++ b/a2a_agents/python/a2ui_agent/src/a2ui/inference/template/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2026 Google LLC +# +# 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. diff --git a/a2a_agents/python/a2ui_agent/src/a2ui/inference/template/manager.py b/a2a_agents/python/a2ui_agent/src/a2ui/inference/template/manager.py new file mode 100644 index 000000000..e64a6c8d3 --- /dev/null +++ b/a2a_agents/python/a2ui_agent/src/a2ui/inference/template/manager.py @@ -0,0 +1,30 @@ +# Copyright 2026 Google LLC +# +# 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. + +from ..inference_strategy import InferenceStrategy +from typing import List + + +class A2uiTemplateManager(InferenceStrategy): + + def generate_system_prompt( + self, + role_description: str, + workflow_description: str = "", + ui_description: str = "", + selected_components: List[str] = [], + examples: str = "", + ) -> str: + # TODO: Implementation logic for Template Manager + return "" diff --git a/a2a_agents/python/a2ui_agent/tests/inference/test_schema_manager.py b/a2a_agents/python/a2ui_agent/tests/inference/test_schema_manager.py new file mode 100644 index 000000000..3cd35cd8b --- /dev/null +++ b/a2a_agents/python/a2ui_agent/tests/inference/test_schema_manager.py @@ -0,0 +1,289 @@ +# Copyright 2026 Google LLC +# +# 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. + +import io +import pytest +import json +from unittest.mock import patch, MagicMock +from a2ui.inference.schema.manager import A2uiSchemaManager + +test_version = "0.8" + + +@pytest.fixture +def mock_importlib_resources(): + with patch("importlib.resources.files") as mock_files: + yield mock_files + + +def test_schema_manager_init_valid_version(mock_importlib_resources): + mock_files = mock_importlib_resources + mock_traversable = MagicMock() + + def files_side_effect(package): + if package == f"a2ui.assets.{test_version}": + return mock_traversable + return MagicMock() + + mock_files.side_effect = files_side_effect + + # Mock file open calls for server_to_client and catalog + def joinpath_side_effect(path): + mock_file = MagicMock() + if path == "server_to_client.json": + content = '{"version": "0.8", "defs": "server_defs"}' + elif path == "standard_catalog_definition.json": + content = '{"version": "0.8", "components": {"Text": {}}}' + else: + content = "{}" + + mock_file.open.return_value.__enter__.return_value = io.StringIO(content) + return mock_file + + mock_traversable.joinpath.side_effect = joinpath_side_effect + + manager = A2uiSchemaManager(test_version) + + assert manager.server_to_client_schema["defs"] == "server_defs" + assert manager.catalog_schema["version"] == test_version + assert "Text" in manager.catalog_schema["components"] + + +def test_schema_manager_fallback_local_assets(mock_importlib_resources): + # Force importlib to fail + mock_importlib_resources.side_effect = FileNotFoundError("Package not found") + + with ( + patch("os.path.exists") as mock_exists, + patch("builtins.open", new_callable=MagicMock) as mock_open, + ): + + def open_side_effect(path, *args, **kwargs): + path_str = str(path) + if "server_to_client.json" in path_str: + return io.StringIO('{"defs": "local_server"}') + elif "standard_catalog_definition.json" in path_str: + return io.StringIO('{"components": {"LocalText": {}}}') + raise FileNotFoundError(path) + + mock_open.side_effect = open_side_effect + + manager = A2uiSchemaManager(test_version) + + assert manager.server_to_client_schema["defs"] == "local_server" + assert "LocalText" in manager.catalog_schema["components"] + + +def test_schema_manager_init_invalid_version(): + with pytest.raises(ValueError, match="Unknown A2UI specification version"): + A2uiSchemaManager("invalid_version") + + +def test_schema_manager_init_custom_catalog(tmp_path, mock_importlib_resources): + mock_files = mock_importlib_resources + mock_traversable = MagicMock() + mock_files.return_value = mock_traversable + + def joinpath_side_effect(path): + mock_file = MagicMock() + if path == "server_to_client.json": + mock_file.open.return_value.__enter__.return_value = io.StringIO("{}") + else: + mock_file.open.return_value.__enter__.return_value = io.StringIO("{}") + return mock_file + + mock_traversable.joinpath.side_effect = joinpath_side_effect + + d = tmp_path / "custom_catalog.json" + d.write_text('{"components": {"Custom": {}}}', encoding="utf-8") + + manager = A2uiSchemaManager(test_version, custom_catalog_path=str(d)) + + assert "Custom" in manager.catalog_schema["components"] + + +def test_get_pruned_catalog(mock_importlib_resources): + mock_files = mock_importlib_resources + mock_traversable = MagicMock() + mock_files.return_value = mock_traversable + + def joinpath_side_effect(path): + mock_file = MagicMock() + if path == "standard_catalog_definition.json": + content = json.dumps({"components": {"Text": {}, "Button": {}, "Image": {}}}) + else: + content = "{}" + mock_file.open.return_value.__enter__.return_value = io.StringIO(content) + return mock_file + + mock_traversable.joinpath.side_effect = joinpath_side_effect + + manager = A2uiSchemaManager(test_version) + + # Test with explicit selection + pruned = manager.get_pruned_catalog(["Text", "Button"]) + assert "Text" in pruned["components"] + assert "Button" in pruned["components"] + assert "Image" not in pruned["components"] + + # Test with None (default) + full_catalog_none = manager.get_pruned_catalog() + assert "Text" in full_catalog_none["components"] + assert "Button" in full_catalog_none["components"] + assert "Image" in full_catalog_none["components"] + + # Test with empty list + full_catalog_empty = manager.get_pruned_catalog([]) + assert "Text" in full_catalog_empty["components"] + assert "Button" in full_catalog_empty["components"] + assert "Image" in full_catalog_empty["components"] + + +def test_get_pruned_catalog_filters_any_component(mock_importlib_resources): + manager = A2uiSchemaManager("0.8") + manager.catalog_schema = { + "version": "0.8", + "$defs": { + "anyComponent": { + "oneOf": [ + {"$ref": "#/components/Text"}, + {"$ref": "#/components/Button"}, + {"$ref": "#/components/Image"}, + ] + } + }, + "components": {"Text": {}, "Button": {}, "Image": {}}, + } + + selected = ["Text"] + pruned = manager.get_pruned_catalog(selected) + + assert "Text" in pruned["components"] + assert "Button" not in pruned["components"] + assert "Image" not in pruned["components"] + + if "$defs" in pruned and "anyComponent" in pruned["$defs"]: + any_comp_one_of = pruned["$defs"]["anyComponent"]["oneOf"] + assert len(any_comp_one_of) == 1 + assert any_comp_one_of[0]["$ref"] == "#/components/Text" + + +def test_generate_system_prompt(mock_importlib_resources): + mock_files = mock_importlib_resources + mock_traversable = MagicMock() + mock_files.return_value = mock_traversable + + def joinpath_side_effect(path): + mock_file = MagicMock() + if path == "server_to_client.json": + content = '{"type": "server_schema"}' + elif path == "standard_catalog_definition.json": + content = '{"components": {"Text": {}}}' + else: + content = "{}" + mock_file.open.return_value.__enter__.return_value = io.StringIO(content) + return mock_file + + mock_traversable.joinpath.side_effect = joinpath_side_effect + + manager = A2uiSchemaManager("0.8") + prompt = manager.generate_system_prompt( + "You are a helpful assistant.", + "Manage workflow.", + "Render UI.", + ["Text"], + ) + + assert "You are a helpful assistant." in prompt + assert "## Workflow Description:" in prompt + assert "Manage workflow." in prompt + assert "## UI Description:" in prompt + assert "Render UI." in prompt + assert "---BEGIN A2UI JSON SCHEMA---" in prompt + assert "### Server To Client Schema:" in prompt + assert '"type": "server_schema"' in prompt + assert "### Catalog Schema:" in prompt + assert "---END A2UI JSON SCHEMA---" in prompt + assert '"Text":{}' in prompt.replace(" ", "") + + +def test_generate_system_prompt_with_examples(mock_importlib_resources): + mock_files = mock_importlib_resources + mock_traversable = MagicMock() + mock_files.return_value = mock_traversable + + def joinpath_side_effect(path): + mock_file = MagicMock() + mock_file.open.return_value.__enter__.return_value = io.StringIO("{}") + return mock_file + + mock_traversable.joinpath.side_effect = joinpath_side_effect + + manager = A2uiSchemaManager("0.8") + + # Test with examples + examples = json.dumps([{"description": "example1", "code": "..."}]) + prompt = manager.generate_system_prompt("Role description", examples=examples) + assert "## Examples:" in prompt + assert '"example1"' in prompt + + # Test without examples + prompt_no_examples = manager.generate_system_prompt("Role description") + assert "## Examples:" not in prompt_no_examples + + +def test_generate_system_prompt_v0_9_common_types(mock_importlib_resources): + mock_files = mock_importlib_resources + mock_traversable = MagicMock() + mock_files.return_value = mock_traversable + + def joinpath_side_effect(path): + mock_file = MagicMock() + content = "{}" + if path == "common_types.json": + content = '{"types": {"Common": {}}}' + elif path == "server_to_client.json": + content = '{"type": "server_schema"}' + + mock_file.open.return_value.__enter__.return_value = io.StringIO(content) + return mock_file + + mock_traversable.joinpath.side_effect = joinpath_side_effect + + # Initialize with version 0.9 which expects common types + manager = A2uiSchemaManager("0.9") + + prompt = manager.generate_system_prompt("Role") + + assert "### Common Types Schema:" in prompt + assert '"types":{"Common":{}}' in prompt.replace(" ", "").replace("\n", "") + + +def test_generate_system_prompt_minimal_args(mock_importlib_resources): + mock_files = mock_importlib_resources + mock_traversable = MagicMock() + mock_files.return_value = mock_traversable + mock_traversable.joinpath.return_value.open.return_value.__enter__.return_value = ( + io.StringIO("{}") + ) + + manager = A2uiSchemaManager("0.8") + prompt = manager.generate_system_prompt("Just Role") + + # Check that optional sections are missing + assert "## Workflow Description:" not in prompt + assert "## UI Description:" not in prompt + assert "## Examples:" not in prompt + assert "Just Role" in prompt + assert "---BEGIN A2UI JSON SCHEMA---" in prompt diff --git a/a2a_agents/python/a2ui_agent/tests/integration/verify_load_real.py b/a2a_agents/python/a2ui_agent/tests/integration/verify_load_real.py new file mode 100644 index 000000000..d658ea3db --- /dev/null +++ b/a2a_agents/python/a2ui_agent/tests/integration/verify_load_real.py @@ -0,0 +1,38 @@ +import sys + +from a2ui.inference.schema.manager import A2uiSchemaManager + + +def verify(): + print("Verifying A2uiSchemaManager...") + try: + manager = A2uiSchemaManager("0.8") + print( + f"Successfully loaded 0.8: {len(manager.catalog_schema.get('components', {}))}" + " components" + ) + print( + "Components found:" + f" {list(manager.catalog_schema.get('components', {}).keys())[:5]}..." + ) + except Exception as e: + print(f"Failed to load 0.8: {e}") + sys.exit(1) + + try: + manager = A2uiSchemaManager("0.9") + print( + f"Successfully loaded 0.9: {len(manager.catalog_schema.get('components', {}))}" + " components" + ) + print( + "Components found:" + f" {list(manager.catalog_schema.get('components', {}).keys())}..." + ) + except Exception as e: + print(f"Failed to load 0.9: {e}") + sys.exit(1) + + +if __name__ == "__main__": + verify() diff --git a/a2a_agents/python/a2ui_agent/uv.lock b/a2a_agents/python/a2ui_agent/uv.lock index 3c413688b..62f5effd3 100644 --- a/a2a_agents/python/a2ui_agent/uv.lock +++ b/a2a_agents/python/a2ui_agent/uv.lock @@ -485,7 +485,7 @@ name = "exceptiongroup" version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } wheels = [ @@ -509,7 +509,7 @@ wheels = [ [[package]] name = "google-adk" -version = "1.22.1" +version = "1.23.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiosqlite" }, @@ -556,9 +556,9 @@ dependencies = [ { name = "watchdog" }, { name = "websockets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a9/7e/1fe2704d8079d93bffb5888aa2bd1081251855e8bf14d97f648abd9fd7fa/google_adk-1.22.1.tar.gz", hash = "sha256:4590df0a9340cf05cf5a9899986dfcc3db1c624c6165d76c04be16de535e6404", size = 2046783, upload-time = "2026-01-12T20:50:08.064Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7c/25/a8c7058812ae3a6046c1c909da31b4c95a6534f555ec50730fe215b2592c/google_adk-1.23.0.tar.gz", hash = "sha256:07829b3198d412ecddb8b102c6bc9511607a234989b7659be102d806e4c92258", size = 2072294, upload-time = "2026-01-22T23:26:52.352Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/17/ea/5fee720ca26eff38e338ff0dc1eae4abd06fc712b841333689f5caf4c55f/google_adk-1.22.1-py3-none-any.whl", hash = "sha256:65c921a1343220eb7823ec8972479c046e7d9464f17c0829fb5508551678a9ef", size = 2368855, upload-time = "2026-01-12T20:50:06.087Z" }, + { url = "https://files.pythonhosted.org/packages/60/36/2abbcaad2bd4691863ac05189070c1e9f8d12ec16194f41a975c984883af/google_adk-1.23.0-py3-none-any.whl", hash = "sha256:94b77c9afa39042e77a35c2b3dad7e122d940e065cb5a9ba9e7b5de73786f993", size = 2418796, upload-time = "2026-01-22T23:26:50.289Z" }, ] [[package]] @@ -994,7 +994,7 @@ wheels = [ [[package]] name = "google-genai" -version = "1.59.0" +version = "1.60.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -1008,9 +1008,9 @@ dependencies = [ { name = "typing-extensions" }, { name = "websockets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/40/34/c03bcbc759d67ac3d96077838cdc1eac85417de6ea3b65b313fe53043eee/google_genai-1.59.0.tar.gz", hash = "sha256:0b7a2dc24582850ae57294209d8dfc2c4f5fcfde0a3f11d81dc5aca75fb619e2", size = 487374, upload-time = "2026-01-15T20:29:46.619Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0a/3f/a753be0dcee352b7d63bc6d1ba14a72591d63b6391dac0cdff7ac168c530/google_genai-1.60.0.tar.gz", hash = "sha256:9768061775fddfaecfefb0d6d7a6cabefb3952ebd246cd5f65247151c07d33d1", size = 487721, upload-time = "2026-01-21T22:17:30.398Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/aa/53/6d00692fe50d73409b3406ae90c71bc4499c8ae7fac377ba16e283da917c/google_genai-1.59.0-py3-none-any.whl", hash = "sha256:59fc01a225d074fe9d1e626c3433da292f33249dadce4deb34edea698305a6df", size = 719099, upload-time = "2026-01-15T20:29:44.604Z" }, + { url = "https://files.pythonhosted.org/packages/31/e5/384b1f383917b5f0ae92e28f47bc27b16e3d26cd9bacb25e9f8ecab3c8fe/google_genai-1.60.0-py3-none-any.whl", hash = "sha256:967338378ffecebec19a8ed90cf8797b26818bacbefd7846a9280beb1099f7f3", size = 719431, upload-time = "2026-01-21T22:17:28.086Z" }, ] [[package]] @@ -1703,11 +1703,11 @@ wheels = [ [[package]] name = "packaging" -version = "25.0" +version = "26.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, ] [[package]] @@ -1844,11 +1844,11 @@ wheels = [ [[package]] name = "pycparser" -version = "2.23" +version = "3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" } +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/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" }, + { 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]] @@ -2043,11 +2043,11 @@ crypto = [ [[package]] name = "pyparsing" -version = "3.3.1" +version = "3.3.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/33/c1/1d9de9aeaa1b89b0186e5fe23294ff6517fce1bc69149185577cd31016b2/pyparsing-3.3.1.tar.gz", hash = "sha256:47fad0f17ac1e2cad3de3b458570fbc9b03560aa029ed5e16ee5554da9a2251c", size = 1550512, upload-time = "2025-12-23T03:14:04.391Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8b/40/2614036cdd416452f5bf98ec037f38a1afb17f327cb8e6b652d4729e0af8/pyparsing-3.3.1-py3-none-any.whl", hash = "sha256:023b5e7e5520ad96642e2c6db4cb683d3970bd640cdf7115049a6e9c3682df82", size = 121793, upload-time = "2025-12-23T03:14:02.103Z" }, + { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, ] [[package]] @@ -2420,51 +2420,58 @@ wheels = [ [[package]] name = "sqlalchemy" -version = "2.0.45" +version = "2.0.46" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/be/f9/5e4491e5ccf42f5d9cfc663741d261b3e6e1683ae7812114e7636409fcc6/sqlalchemy-2.0.45.tar.gz", hash = "sha256:1632a4bda8d2d25703fdad6363058d882541bdaaee0e5e3ddfa0cd3229efce88", size = 9869912, upload-time = "2025-12-09T21:05:16.737Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fe/70/75b1387d72e2847220441166c5eb4e9846dd753895208c13e6d66523b2d9/sqlalchemy-2.0.45-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c64772786d9eee72d4d3784c28f0a636af5b0a29f3fe26ff11f55efe90c0bd85", size = 2154148, upload-time = "2025-12-10T20:03:21.023Z" }, - { url = "https://files.pythonhosted.org/packages/d8/a4/7805e02323c49cb9d1ae5cd4913b28c97103079765f520043f914fca4cb3/sqlalchemy-2.0.45-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7ae64ebf7657395824a19bca98ab10eb9a3ecb026bf09524014f1bb81cb598d4", size = 3233051, upload-time = "2025-12-09T22:06:04.768Z" }, - { url = "https://files.pythonhosted.org/packages/d7/ec/32ae09139f61bef3de3142e85c47abdee8db9a55af2bb438da54a4549263/sqlalchemy-2.0.45-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f02325709d1b1a1489f23a39b318e175a171497374149eae74d612634b234c0", size = 3232781, upload-time = "2025-12-09T22:09:54.435Z" }, - { url = "https://files.pythonhosted.org/packages/ad/bd/bf7b869b6f5585eac34222e1cf4405f4ba8c3b85dd6b1af5d4ce8bca695f/sqlalchemy-2.0.45-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d2c3684fca8a05f0ac1d9a21c1f4a266983a7ea9180efb80ffeb03861ecd01a0", size = 3182096, upload-time = "2025-12-09T22:06:06.169Z" }, - { url = "https://files.pythonhosted.org/packages/21/6a/c219720a241bb8f35c88815ccc27761f5af7fdef04b987b0e8a2c1a6dcaa/sqlalchemy-2.0.45-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:040f6f0545b3b7da6b9317fc3e922c9a98fc7243b2a1b39f78390fc0942f7826", size = 3205109, upload-time = "2025-12-09T22:09:55.969Z" }, - { url = "https://files.pythonhosted.org/packages/bd/c4/6ccf31b2bc925d5d95fab403ffd50d20d7c82b858cf1a4855664ca054dce/sqlalchemy-2.0.45-cp310-cp310-win32.whl", hash = "sha256:830d434d609fe7bfa47c425c445a8b37929f140a7a44cdaf77f6d34df3a7296a", size = 2114240, upload-time = "2025-12-09T21:29:54.007Z" }, - { url = "https://files.pythonhosted.org/packages/de/29/a27a31fca07316def418db6f7c70ab14010506616a2decef1906050a0587/sqlalchemy-2.0.45-cp310-cp310-win_amd64.whl", hash = "sha256:0209d9753671b0da74da2cfbb9ecf9c02f72a759e4b018b3ab35f244c91842c7", size = 2137615, upload-time = "2025-12-09T21:29:55.85Z" }, - { url = "https://files.pythonhosted.org/packages/a2/1c/769552a9d840065137272ebe86ffbb0bc92b0f1e0a68ee5266a225f8cd7b/sqlalchemy-2.0.45-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2e90a344c644a4fa871eb01809c32096487928bd2038bf10f3e4515cb688cc56", size = 2153860, upload-time = "2025-12-10T20:03:23.843Z" }, - { url = "https://files.pythonhosted.org/packages/f3/f8/9be54ff620e5b796ca7b44670ef58bc678095d51b0e89d6e3102ea468216/sqlalchemy-2.0.45-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8c8b41b97fba5f62349aa285654230296829672fc9939cd7f35aab246d1c08b", size = 3309379, upload-time = "2025-12-09T22:06:07.461Z" }, - { url = "https://files.pythonhosted.org/packages/f6/2b/60ce3ee7a5ae172bfcd419ce23259bb874d2cddd44f67c5df3760a1e22f9/sqlalchemy-2.0.45-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:12c694ed6468333a090d2f60950e4250b928f457e4962389553d6ba5fe9951ac", size = 3309948, upload-time = "2025-12-09T22:09:57.643Z" }, - { url = "https://files.pythonhosted.org/packages/a3/42/bac8d393f5db550e4e466d03d16daaafd2bad1f74e48c12673fb499a7fc1/sqlalchemy-2.0.45-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f7d27a1d977a1cfef38a0e2e1ca86f09c4212666ce34e6ae542f3ed0a33bc606", size = 3261239, upload-time = "2025-12-09T22:06:08.879Z" }, - { url = "https://files.pythonhosted.org/packages/6f/12/43dc70a0528c59842b04ea1c1ed176f072a9b383190eb015384dd102fb19/sqlalchemy-2.0.45-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d62e47f5d8a50099b17e2bfc1b0c7d7ecd8ba6b46b1507b58cc4f05eefc3bb1c", size = 3284065, upload-time = "2025-12-09T22:09:59.454Z" }, - { url = "https://files.pythonhosted.org/packages/cf/9c/563049cf761d9a2ec7bc489f7879e9d94e7b590496bea5bbee9ed7b4cc32/sqlalchemy-2.0.45-cp311-cp311-win32.whl", hash = "sha256:3c5f76216e7b85770d5bb5130ddd11ee89f4d52b11783674a662c7dd57018177", size = 2113480, upload-time = "2025-12-09T21:29:57.03Z" }, - { url = "https://files.pythonhosted.org/packages/bc/fa/09d0a11fe9f15c7fa5c7f0dd26be3d235b0c0cbf2f9544f43bc42efc8a24/sqlalchemy-2.0.45-cp311-cp311-win_amd64.whl", hash = "sha256:a15b98adb7f277316f2c276c090259129ee4afca783495e212048daf846654b2", size = 2138407, upload-time = "2025-12-09T21:29:58.556Z" }, - { url = "https://files.pythonhosted.org/packages/2d/c7/1900b56ce19bff1c26f39a4ce427faec7716c81ac792bfac8b6a9f3dca93/sqlalchemy-2.0.45-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b3ee2aac15169fb0d45822983631466d60b762085bc4535cd39e66bea362df5f", size = 3333760, upload-time = "2025-12-09T22:11:02.66Z" }, - { url = "https://files.pythonhosted.org/packages/0a/93/3be94d96bb442d0d9a60e55a6bb6e0958dd3457751c6f8502e56ef95fed0/sqlalchemy-2.0.45-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba547ac0b361ab4f1608afbc8432db669bd0819b3e12e29fb5fa9529a8bba81d", size = 3348268, upload-time = "2025-12-09T22:13:49.054Z" }, - { url = "https://files.pythonhosted.org/packages/48/4b/f88ded696e61513595e4a9778f9d3f2bf7332cce4eb0c7cedaabddd6687b/sqlalchemy-2.0.45-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:215f0528b914e5c75ef2559f69dca86878a3beeb0c1be7279d77f18e8d180ed4", size = 3278144, upload-time = "2025-12-09T22:11:04.14Z" }, - { url = "https://files.pythonhosted.org/packages/ed/6a/310ecb5657221f3e1bd5288ed83aa554923fb5da48d760a9f7622afeb065/sqlalchemy-2.0.45-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:107029bf4f43d076d4011f1afb74f7c3e2ea029ec82eb23d8527d5e909e97aa6", size = 3313907, upload-time = "2025-12-09T22:13:50.598Z" }, - { url = "https://files.pythonhosted.org/packages/5c/39/69c0b4051079addd57c84a5bfb34920d87456dd4c90cf7ee0df6efafc8ff/sqlalchemy-2.0.45-cp312-cp312-win32.whl", hash = "sha256:0c9f6ada57b58420a2c0277ff853abe40b9e9449f8d7d231763c6bc30f5c4953", size = 2112182, upload-time = "2025-12-09T21:39:30.824Z" }, - { url = "https://files.pythonhosted.org/packages/f7/4e/510db49dd89fc3a6e994bee51848c94c48c4a00dc905e8d0133c251f41a7/sqlalchemy-2.0.45-cp312-cp312-win_amd64.whl", hash = "sha256:8defe5737c6d2179c7997242d6473587c3beb52e557f5ef0187277009f73e5e1", size = 2139200, upload-time = "2025-12-09T21:39:32.321Z" }, - { url = "https://files.pythonhosted.org/packages/6a/c8/7cc5221b47a54edc72a0140a1efa56e0a2730eefa4058d7ed0b4c4357ff8/sqlalchemy-2.0.45-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fe187fc31a54d7fd90352f34e8c008cf3ad5d064d08fedd3de2e8df83eb4a1cf", size = 3277082, upload-time = "2025-12-09T22:11:06.167Z" }, - { url = "https://files.pythonhosted.org/packages/0e/50/80a8d080ac7d3d321e5e5d420c9a522b0aa770ec7013ea91f9a8b7d36e4a/sqlalchemy-2.0.45-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:672c45cae53ba88e0dad74b9027dddd09ef6f441e927786b05bec75d949fbb2e", size = 3293131, upload-time = "2025-12-09T22:13:52.626Z" }, - { url = "https://files.pythonhosted.org/packages/da/4c/13dab31266fc9904f7609a5dc308a2432a066141d65b857760c3bef97e69/sqlalchemy-2.0.45-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:470daea2c1ce73910f08caf10575676a37159a6d16c4da33d0033546bddebc9b", size = 3225389, upload-time = "2025-12-09T22:11:08.093Z" }, - { url = "https://files.pythonhosted.org/packages/74/04/891b5c2e9f83589de202e7abaf24cd4e4fa59e1837d64d528829ad6cc107/sqlalchemy-2.0.45-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9c6378449e0940476577047150fd09e242529b761dc887c9808a9a937fe990c8", size = 3266054, upload-time = "2025-12-09T22:13:54.262Z" }, - { url = "https://files.pythonhosted.org/packages/f1/24/fc59e7f71b0948cdd4cff7a286210e86b0443ef1d18a23b0d83b87e4b1f7/sqlalchemy-2.0.45-cp313-cp313-win32.whl", hash = "sha256:4b6bec67ca45bc166c8729910bd2a87f1c0407ee955df110d78948f5b5827e8a", size = 2110299, upload-time = "2025-12-09T21:39:33.486Z" }, - { url = "https://files.pythonhosted.org/packages/c0/c5/d17113020b2d43073412aeca09b60d2009442420372123b8d49cc253f8b8/sqlalchemy-2.0.45-cp313-cp313-win_amd64.whl", hash = "sha256:afbf47dc4de31fa38fd491f3705cac5307d21d4bb828a4f020ee59af412744ee", size = 2136264, upload-time = "2025-12-09T21:39:36.801Z" }, - { url = "https://files.pythonhosted.org/packages/3d/8d/bb40a5d10e7a5f2195f235c0b2f2c79b0bf6e8f00c0c223130a4fbd2db09/sqlalchemy-2.0.45-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:83d7009f40ce619d483d26ac1b757dfe3167b39921379a8bd1b596cf02dab4a6", size = 3521998, upload-time = "2025-12-09T22:13:28.622Z" }, - { url = "https://files.pythonhosted.org/packages/75/a5/346128b0464886f036c039ea287b7332a410aa2d3fb0bb5d404cb8861635/sqlalchemy-2.0.45-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d8a2ca754e5415cde2b656c27900b19d50ba076aa05ce66e2207623d3fe41f5a", size = 3473434, upload-time = "2025-12-09T22:13:30.188Z" }, - { url = "https://files.pythonhosted.org/packages/cc/64/4e1913772646b060b025d3fc52ce91a58967fe58957df32b455de5a12b4f/sqlalchemy-2.0.45-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f46ec744e7f51275582e6a24326e10c49fbdd3fc99103e01376841213028774", size = 3272404, upload-time = "2025-12-09T22:11:09.662Z" }, - { url = "https://files.pythonhosted.org/packages/b3/27/caf606ee924282fe4747ee4fd454b335a72a6e018f97eab5ff7f28199e16/sqlalchemy-2.0.45-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:883c600c345123c033c2f6caca18def08f1f7f4c3ebeb591a63b6fceffc95cce", size = 3277057, upload-time = "2025-12-09T22:13:56.213Z" }, - { url = "https://files.pythonhosted.org/packages/85/d0/3d64218c9724e91f3d1574d12eb7ff8f19f937643815d8daf792046d88ab/sqlalchemy-2.0.45-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2c0b74aa79e2deade948fe8593654c8ef4228c44ba862bb7c9585c8e0db90f33", size = 3222279, upload-time = "2025-12-09T22:11:11.1Z" }, - { url = "https://files.pythonhosted.org/packages/24/10/dd7688a81c5bc7690c2a3764d55a238c524cd1a5a19487928844cb247695/sqlalchemy-2.0.45-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8a420169cef179d4c9064365f42d779f1e5895ad26ca0c8b4c0233920973db74", size = 3244508, upload-time = "2025-12-09T22:13:57.932Z" }, - { url = "https://files.pythonhosted.org/packages/aa/41/db75756ca49f777e029968d9c9fee338c7907c563267740c6d310a8e3f60/sqlalchemy-2.0.45-cp314-cp314-win32.whl", hash = "sha256:e50dcb81a5dfe4b7b4a4aa8f338116d127cb209559124f3694c70d6cd072b68f", size = 2113204, upload-time = "2025-12-09T21:39:38.365Z" }, - { url = "https://files.pythonhosted.org/packages/89/a2/0e1590e9adb292b1d576dbcf67ff7df8cf55e56e78d2c927686d01080f4b/sqlalchemy-2.0.45-cp314-cp314-win_amd64.whl", hash = "sha256:4748601c8ea959e37e03d13dcda4a44837afcd1b21338e637f7c935b8da06177", size = 2138785, upload-time = "2025-12-09T21:39:39.503Z" }, - { url = "https://files.pythonhosted.org/packages/42/39/f05f0ed54d451156bbed0e23eb0516bcad7cbb9f18b3bf219c786371b3f0/sqlalchemy-2.0.45-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd337d3526ec5298f67d6a30bbbe4ed7e5e68862f0bf6dd21d289f8d37b7d60b", size = 3522029, upload-time = "2025-12-09T22:13:32.09Z" }, - { url = "https://files.pythonhosted.org/packages/54/0f/d15398b98b65c2bce288d5ee3f7d0a81f77ab89d9456994d5c7cc8b2a9db/sqlalchemy-2.0.45-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9a62b446b7d86a3909abbcd1cd3cc550a832f99c2bc37c5b22e1925438b9367b", size = 3475142, upload-time = "2025-12-09T22:13:33.739Z" }, - { url = "https://files.pythonhosted.org/packages/bf/e1/3ccb13c643399d22289c6a9786c1a91e3dcbb68bce4beb44926ac2c557bf/sqlalchemy-2.0.45-py3-none-any.whl", hash = "sha256:5225a288e4c8cc2308dbdd874edad6e7d0fd38eac1e9e5f23503425c8eee20d0", size = 1936672, upload-time = "2025-12-09T21:54:52.608Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/06/aa/9ce0f3e7a9829ead5c8ce549392f33a12c4555a6c0609bb27d882e9c7ddf/sqlalchemy-2.0.46.tar.gz", hash = "sha256:cf36851ee7219c170bb0793dbc3da3e80c582e04a5437bc601bfe8c85c9216d7", size = 9865393, upload-time = "2026-01-21T18:03:45.119Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/26/66ba59328dc25e523bfcb0f8db48bdebe2035e0159d600e1f01c0fc93967/sqlalchemy-2.0.46-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:895296687ad06dc9b11a024cf68e8d9d3943aa0b4964278d2553b86f1b267735", size = 2155051, upload-time = "2026-01-21T18:27:28.965Z" }, + { url = "https://files.pythonhosted.org/packages/21/cd/9336732941df972fbbfa394db9caa8bb0cf9fe03656ec728d12e9cbd6edc/sqlalchemy-2.0.46-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab65cb2885a9f80f979b85aa4e9c9165a31381ca322cbde7c638fe6eefd1ec39", size = 3234666, upload-time = "2026-01-21T18:32:28.72Z" }, + { url = "https://files.pythonhosted.org/packages/38/62/865ae8b739930ec433cd4123760bee7f8dafdc10abefd725a025604fb0de/sqlalchemy-2.0.46-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:52fe29b3817bd191cc20bad564237c808967972c97fa683c04b28ec8979ae36f", size = 3232917, upload-time = "2026-01-21T18:44:54.064Z" }, + { url = "https://files.pythonhosted.org/packages/24/38/805904b911857f2b5e00fdea44e9570df62110f834378706939825579296/sqlalchemy-2.0.46-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:09168817d6c19954d3b7655da6ba87fcb3a62bb575fb396a81a8b6a9fadfe8b5", size = 3185790, upload-time = "2026-01-21T18:32:30.581Z" }, + { url = "https://files.pythonhosted.org/packages/69/4f/3260bb53aabd2d274856337456ea52f6a7eccf6cce208e558f870cec766b/sqlalchemy-2.0.46-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:be6c0466b4c25b44c5d82b0426b5501de3c424d7a3220e86cd32f319ba56798e", size = 3207206, upload-time = "2026-01-21T18:44:55.93Z" }, + { url = "https://files.pythonhosted.org/packages/ce/b3/67c432d7f9d88bb1a61909b67e29f6354d59186c168fb5d381cf438d3b73/sqlalchemy-2.0.46-cp310-cp310-win32.whl", hash = "sha256:1bc3f601f0a818d27bfe139f6766487d9c88502062a2cd3a7ee6c342e81d5047", size = 2115296, upload-time = "2026-01-21T18:33:12.498Z" }, + { url = "https://files.pythonhosted.org/packages/4a/8c/25fb284f570f9d48e6c240f0269a50cec9cf009a7e08be4c0aaaf0654972/sqlalchemy-2.0.46-cp310-cp310-win_amd64.whl", hash = "sha256:e0c05aff5c6b1bb5fb46a87e0f9d2f733f83ef6cbbbcd5c642b6c01678268061", size = 2138540, upload-time = "2026-01-21T18:33:14.22Z" }, + { url = "https://files.pythonhosted.org/packages/69/ac/b42ad16800d0885105b59380ad69aad0cce5a65276e269ce2729a2343b6a/sqlalchemy-2.0.46-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:261c4b1f101b4a411154f1da2b76497d73abbfc42740029205d4d01fa1052684", size = 2154851, upload-time = "2026-01-21T18:27:30.54Z" }, + { url = "https://files.pythonhosted.org/packages/a0/60/d8710068cb79f64d002ebed62a7263c00c8fd95f4ebd4b5be8f7ca93f2bc/sqlalchemy-2.0.46-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:181903fe8c1b9082995325f1b2e84ac078b1189e2819380c2303a5f90e114a62", size = 3311241, upload-time = "2026-01-21T18:32:33.45Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/20c71487c7219ab3aa7421c7c62d93824c97c1460f2e8bb72404b0192d13/sqlalchemy-2.0.46-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:590be24e20e2424a4c3c1b0835e9405fa3d0af5823a1a9fc02e5dff56471515f", size = 3310741, upload-time = "2026-01-21T18:44:57.887Z" }, + { url = "https://files.pythonhosted.org/packages/65/80/d26d00b3b249ae000eee4db206fcfc564bf6ca5030e4747adf451f4b5108/sqlalchemy-2.0.46-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7568fe771f974abadce52669ef3a03150ff03186d8eb82613bc8adc435a03f01", size = 3263116, upload-time = "2026-01-21T18:32:35.044Z" }, + { url = "https://files.pythonhosted.org/packages/da/ee/74dda7506640923821340541e8e45bd3edd8df78664f1f2e0aae8077192b/sqlalchemy-2.0.46-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf7e1e78af38047e08836d33502c7a278915698b7c2145d045f780201679999", size = 3285327, upload-time = "2026-01-21T18:44:59.254Z" }, + { url = "https://files.pythonhosted.org/packages/9f/25/6dcf8abafff1389a21c7185364de145107b7394ecdcb05233815b236330d/sqlalchemy-2.0.46-cp311-cp311-win32.whl", hash = "sha256:9d80ea2ac519c364a7286e8d765d6cd08648f5b21ca855a8017d9871f075542d", size = 2114564, upload-time = "2026-01-21T18:33:15.85Z" }, + { url = "https://files.pythonhosted.org/packages/93/5f/e081490f8523adc0088f777e4ebad3cac21e498ec8a3d4067074e21447a1/sqlalchemy-2.0.46-cp311-cp311-win_amd64.whl", hash = "sha256:585af6afe518732d9ccd3aea33af2edaae4a7aa881af5d8f6f4fe3a368699597", size = 2139233, upload-time = "2026-01-21T18:33:17.528Z" }, + { url = "https://files.pythonhosted.org/packages/b6/35/d16bfa235c8b7caba3730bba43e20b1e376d2224f407c178fbf59559f23e/sqlalchemy-2.0.46-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3a9a72b0da8387f15d5810f1facca8f879de9b85af8c645138cba61ea147968c", size = 2153405, upload-time = "2026-01-21T19:05:54.143Z" }, + { url = "https://files.pythonhosted.org/packages/06/6c/3192e24486749862f495ddc6584ed730c0c994a67550ec395d872a2ad650/sqlalchemy-2.0.46-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2347c3f0efc4de367ba00218e0ae5c4ba2306e47216ef80d6e31761ac97cb0b9", size = 3334702, upload-time = "2026-01-21T18:46:45.384Z" }, + { url = "https://files.pythonhosted.org/packages/ea/a2/b9f33c8d68a3747d972a0bb758c6b63691f8fb8a49014bc3379ba15d4274/sqlalchemy-2.0.46-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9094c8b3197db12aa6f05c51c05daaad0a92b8c9af5388569847b03b1007fb1b", size = 3347664, upload-time = "2026-01-21T18:40:09.979Z" }, + { url = "https://files.pythonhosted.org/packages/aa/d2/3e59e2a91eaec9db7e8dc6b37b91489b5caeb054f670f32c95bcba98940f/sqlalchemy-2.0.46-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37fee2164cf21417478b6a906adc1a91d69ae9aba8f9533e67ce882f4bb1de53", size = 3277372, upload-time = "2026-01-21T18:46:47.168Z" }, + { url = "https://files.pythonhosted.org/packages/dd/dd/67bc2e368b524e2192c3927b423798deda72c003e73a1e94c21e74b20a85/sqlalchemy-2.0.46-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b1e14b2f6965a685c7128bd315e27387205429c2e339eeec55cb75ca4ab0ea2e", size = 3312425, upload-time = "2026-01-21T18:40:11.548Z" }, + { url = "https://files.pythonhosted.org/packages/43/82/0ecd68e172bfe62247e96cb47867c2d68752566811a4e8c9d8f6e7c38a65/sqlalchemy-2.0.46-cp312-cp312-win32.whl", hash = "sha256:412f26bb4ba942d52016edc8d12fb15d91d3cd46b0047ba46e424213ad407bcb", size = 2113155, upload-time = "2026-01-21T18:42:49.748Z" }, + { url = "https://files.pythonhosted.org/packages/bc/2a/2821a45742073fc0331dc132552b30de68ba9563230853437cac54b2b53e/sqlalchemy-2.0.46-cp312-cp312-win_amd64.whl", hash = "sha256:ea3cd46b6713a10216323cda3333514944e510aa691c945334713fca6b5279ff", size = 2140078, upload-time = "2026-01-21T18:42:51.197Z" }, + { url = "https://files.pythonhosted.org/packages/b3/4b/fa7838fe20bb752810feed60e45625a9a8b0102c0c09971e2d1d95362992/sqlalchemy-2.0.46-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:93a12da97cca70cea10d4b4fc602589c4511f96c1f8f6c11817620c021d21d00", size = 2150268, upload-time = "2026-01-21T19:05:56.621Z" }, + { url = "https://files.pythonhosted.org/packages/46/c1/b34dccd712e8ea846edf396e00973dda82d598cb93762e55e43e6835eba9/sqlalchemy-2.0.46-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:af865c18752d416798dae13f83f38927c52f085c52e2f32b8ab0fef46fdd02c2", size = 3276511, upload-time = "2026-01-21T18:46:49.022Z" }, + { url = "https://files.pythonhosted.org/packages/96/48/a04d9c94753e5d5d096c628c82a98c4793b9c08ca0e7155c3eb7d7db9f24/sqlalchemy-2.0.46-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8d679b5f318423eacb61f933a9a0f75535bfca7056daeadbf6bd5bcee6183aee", size = 3292881, upload-time = "2026-01-21T18:40:13.089Z" }, + { url = "https://files.pythonhosted.org/packages/be/f4/06eda6e91476f90a7d8058f74311cb65a2fb68d988171aced81707189131/sqlalchemy-2.0.46-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:64901e08c33462acc9ec3bad27fc7a5c2b6491665f2aa57564e57a4f5d7c52ad", size = 3224559, upload-time = "2026-01-21T18:46:50.974Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a2/d2af04095412ca6345ac22b33b89fe8d6f32a481e613ffcb2377d931d8d0/sqlalchemy-2.0.46-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e8ac45e8f4eaac0f9f8043ea0e224158855c6a4329fd4ee37c45c61e3beb518e", size = 3262728, upload-time = "2026-01-21T18:40:14.883Z" }, + { url = "https://files.pythonhosted.org/packages/31/48/1980c7caa5978a3b8225b4d230e69a2a6538a3562b8b31cea679b6933c83/sqlalchemy-2.0.46-cp313-cp313-win32.whl", hash = "sha256:8d3b44b3d0ab2f1319d71d9863d76eeb46766f8cf9e921ac293511804d39813f", size = 2111295, upload-time = "2026-01-21T18:42:52.366Z" }, + { url = "https://files.pythonhosted.org/packages/2d/54/f8d65bbde3d877617c4720f3c9f60e99bb7266df0d5d78b6e25e7c149f35/sqlalchemy-2.0.46-cp313-cp313-win_amd64.whl", hash = "sha256:77f8071d8fbcbb2dd11b7fd40dedd04e8ebe2eb80497916efedba844298065ef", size = 2137076, upload-time = "2026-01-21T18:42:53.924Z" }, + { url = "https://files.pythonhosted.org/packages/56/ba/9be4f97c7eb2b9d5544f2624adfc2853e796ed51d2bb8aec90bc94b7137e/sqlalchemy-2.0.46-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1e8cc6cc01da346dc92d9509a63033b9b1bda4fed7a7a7807ed385c7dccdc10", size = 3556533, upload-time = "2026-01-21T18:33:06.636Z" }, + { url = "https://files.pythonhosted.org/packages/20/a6/b1fc6634564dbb4415b7ed6419cdfeaadefd2c39cdab1e3aa07a5f2474c2/sqlalchemy-2.0.46-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:96c7cca1a4babaaf3bfff3e4e606e38578856917e52f0384635a95b226c87764", size = 3523208, upload-time = "2026-01-21T18:45:08.436Z" }, + { url = "https://files.pythonhosted.org/packages/a1/d8/41e0bdfc0f930ff236f86fccd12962d8fa03713f17ed57332d38af6a3782/sqlalchemy-2.0.46-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b2a9f9aee38039cf4755891a1e50e1effcc42ea6ba053743f452c372c3152b1b", size = 3464292, upload-time = "2026-01-21T18:33:08.208Z" }, + { url = "https://files.pythonhosted.org/packages/f0/8b/9dcbec62d95bea85f5ecad9b8d65b78cc30fb0ffceeb3597961f3712549b/sqlalchemy-2.0.46-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:db23b1bf8cfe1f7fda19018e7207b20cdb5168f83c437ff7e95d19e39289c447", size = 3473497, upload-time = "2026-01-21T18:45:10.552Z" }, + { url = "https://files.pythonhosted.org/packages/e9/f8/5ecdfc73383ec496de038ed1614de9e740a82db9ad67e6e4514ebc0708a3/sqlalchemy-2.0.46-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:56bdd261bfd0895452006d5316cbf35739c53b9bb71a170a331fa0ea560b2ada", size = 2152079, upload-time = "2026-01-21T19:05:58.477Z" }, + { url = "https://files.pythonhosted.org/packages/e5/bf/eba3036be7663ce4d9c050bc3d63794dc29fbe01691f2bf5ccb64e048d20/sqlalchemy-2.0.46-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:33e462154edb9493f6c3ad2125931e273bbd0be8ae53f3ecd1c161ea9a1dd366", size = 3272216, upload-time = "2026-01-21T18:46:52.634Z" }, + { url = "https://files.pythonhosted.org/packages/05/45/1256fb597bb83b58a01ddb600c59fe6fdf0e5afe333f0456ed75c0f8d7bd/sqlalchemy-2.0.46-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9bcdce05f056622a632f1d44bb47dbdb677f58cad393612280406ce37530eb6d", size = 3277208, upload-time = "2026-01-21T18:40:16.38Z" }, + { url = "https://files.pythonhosted.org/packages/d9/a0/2053b39e4e63b5d7ceb3372cface0859a067c1ddbd575ea7e9985716f771/sqlalchemy-2.0.46-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8e84b09a9b0f19accedcbeff5c2caf36e0dd537341a33aad8d680336152dc34e", size = 3221994, upload-time = "2026-01-21T18:46:54.622Z" }, + { url = "https://files.pythonhosted.org/packages/1e/87/97713497d9502553c68f105a1cb62786ba1ee91dea3852ae4067ed956a50/sqlalchemy-2.0.46-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4f52f7291a92381e9b4de9050b0a65ce5d6a763333406861e33906b8aa4906bf", size = 3243990, upload-time = "2026-01-21T18:40:18.253Z" }, + { url = "https://files.pythonhosted.org/packages/a8/87/5d1b23548f420ff823c236f8bea36b1a997250fd2f892e44a3838ca424f4/sqlalchemy-2.0.46-cp314-cp314-win32.whl", hash = "sha256:70ed2830b169a9960193f4d4322d22be5c0925357d82cbf485b3369893350908", size = 2114215, upload-time = "2026-01-21T18:42:55.232Z" }, + { url = "https://files.pythonhosted.org/packages/3a/20/555f39cbcf0c10cf452988b6a93c2a12495035f68b3dbd1a408531049d31/sqlalchemy-2.0.46-cp314-cp314-win_amd64.whl", hash = "sha256:3c32e993bc57be6d177f7d5d31edb93f30726d798ad86ff9066d75d9bf2e0b6b", size = 2139867, upload-time = "2026-01-21T18:42:56.474Z" }, + { url = "https://files.pythonhosted.org/packages/3e/f0/f96c8057c982d9d8a7a68f45d69c674bc6f78cad401099692fe16521640a/sqlalchemy-2.0.46-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4dafb537740eef640c4d6a7c254611dca2df87eaf6d14d6a5fca9d1f4c3fc0fa", size = 3561202, upload-time = "2026-01-21T18:33:10.337Z" }, + { url = "https://files.pythonhosted.org/packages/d7/53/3b37dda0a5b137f21ef608d8dfc77b08477bab0fe2ac9d3e0a66eaeab6fc/sqlalchemy-2.0.46-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:42a1643dc5427b69aca967dae540a90b0fbf57eaf248f13a90ea5930e0966863", size = 3526296, upload-time = "2026-01-21T18:45:12.657Z" }, + { url = "https://files.pythonhosted.org/packages/33/75/f28622ba6dde79cd545055ea7bd4062dc934e0621f7b3be2891f8563f8de/sqlalchemy-2.0.46-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ff33c6e6ad006bbc0f34f5faf941cfc62c45841c64c0a058ac38c799f15b5ede", size = 3470008, upload-time = "2026-01-21T18:33:11.725Z" }, + { url = "https://files.pythonhosted.org/packages/a9/42/4afecbbc38d5e99b18acef446453c76eec6fbd03db0a457a12a056836e22/sqlalchemy-2.0.46-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:82ec52100ec1e6ec671563bbd02d7c7c8d0b9e71a0723c72f22ecf52d1755330", size = 3476137, upload-time = "2026-01-21T18:45:15.001Z" }, + { url = "https://files.pythonhosted.org/packages/fc/a1/9c4efa03300926601c19c18582531b45aededfb961ab3c3585f1e24f120b/sqlalchemy-2.0.46-py3-none-any.whl", hash = "sha256:f9c11766e7e7c0a2767dda5acb006a118640c9fc0a4104214b96269bfb78399e", size = 1937882, upload-time = "2026-01-21T18:22:10.456Z" }, ] [[package]]