From 6a416cbb670f132fe2fb5f9fbf8f2847add57aae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nahuel=20Defoss=C3=A9?= Date: Thu, 29 Jan 2026 17:02:36 +0300 Subject: [PATCH] src updates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fix: pre-commit trailing whitespace issues Signed-off-by: Nahuel Defossé --- .github/actionlint-config.yaml | 4 +- CONTRIBUTING.md | 12 +-- README.md | 26 ++--- .../project_submission_guidelines.md | 20 ++-- pyproject.toml | 3 + src/agentics/core/agentics.py | 94 +++++++++++-------- src/agentics/core/async_executor.py | 2 +- src/agentics/core/atype.py | 55 +++++++++-- src/agentics/core/default_types.py | 9 ++ src/agentics/core/transducible_functions.py | 45 +++++++-- uv.lock | 37 ++++++++ 11 files changed, 222 insertions(+), 85 deletions(-) diff --git a/.github/actionlint-config.yaml b/.github/actionlint-config.yaml index da78515c..981481e4 100644 --- a/.github/actionlint-config.yaml +++ b/.github/actionlint-config.yaml @@ -4,7 +4,7 @@ rules: # Enable all rules by default all: true - + # Disable specific rules if needed # actions-have-safe-quotes: disable # expression-syntax: disable @@ -14,6 +14,6 @@ shellcheck: # Enable shellcheck for run steps enabled: true -# Pyflakes configuration +# Pyflakes configuration pyflakes: enabled: false diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 856031ed..bda44d9e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -30,15 +30,15 @@ Thank you for your interest in contributing to Agentics! This document provides ```bash uv run pytest ``` - + Also, to ensure the [version is correctly computed from Git tags](#versioning-scheme) try running: - + ```bash uvx --with uv-dynamic-versioning hatchling version ``` - - + + ## Pre-commit Hooks We use pre-commit hooks to ensure code quality and consistency. These hooks automatically run checks before each commit. @@ -145,8 +145,8 @@ The test report will be saved as `report.html` in the project root for later ana ### Running Tests with Coverage -**Code coverage** measures the percentage of your codebase that is exercised by tests. -It's an important metric that helps you understand how thoroughly your how much code +**Code coverage** measures the percentage of your codebase that is exercised by tests. +It's an important metric that helps you understand how thoroughly your how much code is not exercised by your tests. diff --git a/README.md b/README.md index 87b04bfe..73a7b98a 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ Install Agentics in your current env, set up your environment variable, and run ```bash uv pip install agentics-py ``` -set up your .env using the required parameters for your LLM provider of choice. Use [.env_sample](.env_sample) as a reference. +set up your .env using the required parameters for your LLM provider of choice. Use [.env_sample](.env_sample) as a reference. Find out more 👉 **Getting Started**: [docs/getting_started.md](docs/getting_started.md) @@ -90,7 +90,7 @@ genre, explanation = await classify_genre( ## 📘 Documentation and Notebooks -Complete documentation available [here](./docs/index.md) +Complete documentation available [here](./docs/index.md) | Notebook | Description | |---|---| @@ -120,25 +120,25 @@ Apache 2.0 ## 👥 Authors -**Project Lead** +**Project Lead** - Alfio Massimiliano Gliozzo (IBM Research) — gliozzo@us.ibm.com -**Core Contributors** -- Nahuel Defosse (IBM Research) — nahuel.defosse@ibm.com -- Junkyu Lee (IBM Research) — Junkyu.Lee@ibm.com -- Naweed Aghmad Khan (IBM Research) — naweed.khan@ibm.com -- Christodoulos Constantinides (IBM Watson) — Christodoulos.Constantinides@ibm.com -- Mustafa Eyceoz (Red Hat) — Mustafa.Eyceoz@partner.ibm.com +**Core Contributors** +- Nahuel Defosse (IBM Research) — nahuel.defosse@ibm.com +- Junkyu Lee (IBM Research) — Junkyu.Lee@ibm.com +- Naweed Aghmad Khan (IBM Research) — naweed.khan@ibm.com +- Christodoulos Constantinides (IBM Watson) — Christodoulos.Constantinides@ibm.com +- Mustafa Eyceoz (Red Hat) — Mustafa.Eyceoz@partner.ibm.com --- ## 🧠 Conceptual Overview -Most “agent frameworks” let untyped text flow through a pipeline. Agentics flips that: **types are the interface**. +Most “agent frameworks” let untyped text flow through a pipeline. Agentics flips that: **types are the interface**. Workflows are expressed as transformations between structured states, with predictable schemas and composable operators. -Because every step is a typed transformation, you can **compose** workflows safely (merge and compose types/instances, chain transductions, and reuse `@transducible` functions) without losing semantic structure. +Because every step is a typed transformation, you can **compose** workflows safely (merge and compose types/instances, chain transductions, and reuse `@transducible` functions) without losing semantic structure. Agentics makes it natural to **scale out**: apply transformations over collections with async `amap`, and aggregate results with `areduce`. @@ -158,8 +158,8 @@ Core operations: Agentics implements **Logical Transduction Algebra**, described in: -- Alfio Gliozzo, Naweed Khan, Christodoulos Constantinides, Nandana Mihindukulasooriya, Nahuel Defosse, Junkyu Lee. - *Transduction is All You Need for Structured Data Workflows* (August 2025). +- Alfio Gliozzo, Naweed Khan, Christodoulos Constantinides, Nandana Mihindukulasooriya, Nahuel Defosse, Junkyu Lee. + *Transduction is All You Need for Structured Data Workflows* (August 2025). arXiv:2508.15610 — https://arxiv.org/abs/2508.15610 diff --git a/agentics_full_course/project_submission_guidelines.md b/agentics_full_course/project_submission_guidelines.md index 1c73ca6a..9b890a7f 100644 --- a/agentics_full_course/project_submission_guidelines.md +++ b/agentics_full_course/project_submission_guidelines.md @@ -49,8 +49,8 @@ Your project folder should be named after your project and organized as follows: Create a **5-minute recorded video** presenting your project. -- Introduce the project goals, methods, and key results. -- Optionally include a demo or short code walkthrough with slides. +- Introduce the project goals, methods, and key results. +- Optionally include a demo or short code walkthrough with slides. - You will present the same video **in person** during the student workshop. --- @@ -59,20 +59,20 @@ Create a **5-minute recorded video** presenting your project. Schedule a meeting with professors **at least two weeks before the final submission** to receive feedback on: -- The draft of your short paper -- Your runnable code and documentation +- The draft of your short paper +- Your runnable code and documentation - Your recorded video presentation --- ## ✅ Submission Checklist -- [ ] Conference-style project paper (PDF) -- [ ] Runnable code in `applications/` -- [ ] `README.md` with install/test instructions -- [ ] Documentation in `docs/` folder -- [ ] 5-minute recorded presentation video -- [ ] Faculty feedback meeting completed +- [ ] Conference-style project paper (PDF) +- [ ] Runnable code in `applications/` +- [ ] `README.md` with install/test instructions +- [ ] Documentation in `docs/` folder +- [ ] 5-minute recorded presentation video +- [ ] Faculty feedback meeting completed --- diff --git a/pyproject.toml b/pyproject.toml index 0eaaf57f..de43fa3e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,6 +40,9 @@ dependencies = [ "mellea", "plotly>=6.5.0", "rich>=13.0.0,<14.0.0", + "duckdb>=1.4.3", + "pandas>=2.3.3", + "async-lru>=2.0.5", ] diff --git a/src/agentics/core/agentics.py b/src/agentics/core/agentics.py index c68ad412..c1957054 100644 --- a/src/agentics/core/agentics.py +++ b/src/agentics/core/agentics.py @@ -623,11 +623,14 @@ async def llm_call(input: AGString) -> AGString: reasoning=self.reasoning, **self.crew_prompt_params, ) - transduced_results = await pt.execute( - *input_prompts, - description=f"Transducing {self.__name__} << {'AG[str]' if not isinstance(other, AG) else other.__name__}", - transient_pbar=self.transient_pbar, - ) + chunks = chunk_list(input_prompts, chunk_size=self.amap_batch_size) + transduced_results = [] + for chunk in chunks: + transduced_results += await pt.execute( + *chunk, + description=f"Transducing {self.__name__[:30]} << {'AG[str]' if not isinstance(other, AG) else other.__name__[:30]}", + transient_pbar=self.transient_pbar, + ) except Exception as e: transduced_results = self.states @@ -660,14 +663,15 @@ async def llm_call(input: AGString) -> AGString: output_state_dict = dict([output_state]) else: output_state_dict = output_state.model_dump() - - merged = self.atype( - **( - (self[i].model_dump() if len(self) > i else {}) - | other[i].model_dump() - | output_state_dict - ) + data = ( + (self[i].model_dump() if len(self) > i else {}) + | other[i].model_dump() + | output_state_dict ) + allowed = self.atype.model_fields.keys() # pydantic v2 + filtered = {k: v for k, v in data.items() if k in allowed} + merged = self.atype(**filtered) + output.states.append(merged) # elif is_str_or_list_of_str(other): elif isinstance(other, list): @@ -682,15 +686,18 @@ async def llm_call(input: AGString) -> AGString: if self.provide_explanations and isinstance(other, AG): target_explanation = AG(atype=Explanation) + output.prompt_template = None + output.transduce_fields = None target_explanation.instructions = f""" - You have been presented with two Pydantic Objects: - a left object that was logically derived from a right object. - Your task is to provide a detailed explanation of how the left object was derived from the right object.""" - target_explanation = await ( - target_explanation << output.compose_states(other) - ) - - self.explanations = target_explanation.states + You have previously transduced an object of type {other.atype.__name__} (source) from an object of type {self.atype.__name__} (target). + Now look back at both objects and provide a detailed explanation on how each field of the target object was logically derived from the source object. + Provide short and concise, data grounded explanations, field by field, avoiding redundancy. + If you think that transduction was wrong or not logically supported by the source object, say it clearly in the explanation and provide low confidence score (0.0). + Provide high confidence score (1.0) only if you are certain that the transduction is logically correct and fully supported by the source object. + """ + explanation = await (target_explanation << output.compose_states(other)) + + self.explanations = explanation.states self.states = output.states return self else: @@ -1058,35 +1065,40 @@ def merge_states(self, other: AG) -> AG: Merge states of two AGs pairwise """ - merged = self.clone() - merged.states = [] - merged.explanations = [] - merged.atype = merge_pydantic_models( - self.atype, - other.atype, - name=f"Merged{self.atype.__name__}#{other.atype.__name__}", - ) - for self_state in self: - for other_state in other: + if len(self) == len(other): + merged = self.clone() + merged.states = [] + merged.explanations = [] + merged.atype = merge_pydantic_models( + self.atype, + other.atype, + name=f"Merged{self.atype.__name__}#{other.atype.__name__}", + ) + for self_state, other_state in zip(self, other): merged.states.append( merged.atype(**other_state.model_dump(), **self_state.model_dump()) ) - return merged + return merged + else: + raise ValueError( + f"Cannot merge states of AGs with different lengths: {len(self)} != {len(other)}" + ) def compose_states(self, other: AG) -> AG: """ - compose states of two AGs pairwise, + compose states of two AGs, """ - merged = self.clone() - merged.states = [] - merged.explanations = [] - merged.atype = self.atype @ other.atype - - for self_state in self: - for other_state in other: - merged.states.append(merged.atype(right=other_state, left=self_state)) - return merged + composed = self.clone() + composed.states = [] + composed.explanations = [] + composed.atype = self.atype @ other.atype + + for self_state, other_state in zip(self.states, other.states): + composed.states.append( + composed.atype(source=other_state, target=self_state) + ) + return composed async def map_atypes(self, other: AG) -> ATypeMapping: if self.verbose_agent: diff --git a/src/agentics/core/async_executor.py b/src/agentics/core/async_executor.py index d9f7455e..a8d7f73d 100644 --- a/src/agentics/core/async_executor.py +++ b/src/agentics/core/async_executor.py @@ -373,7 +373,7 @@ def __init__( async def _execute(self, input: str) -> BaseModel: instructions = f""" -You are a Logical Transducer. Your goal is to generate a JSON object that strictly +You are a Logical Transducer. Your goal is to generate a JSON object that strictly conforms to the Output Pydantic schema below: {self.atype.model_json_schema()} diff --git a/src/agentics/core/atype.py b/src/agentics/core/atype.py index eb7ea727..f30fa088 100644 --- a/src/agentics/core/atype.py +++ b/src/agentics/core/atype.py @@ -202,6 +202,50 @@ def infer_pydantic_type(dtype: Any, sample_values: pd.Series = None) -> Any: def pydantic_model_from_dict(dict) -> type[BaseModel]: + """ + Create a dynamic Pydantic model class from a sample dictionary. + + This utility inspects the provided mapping and generates a new `pydantic.BaseModel` + subclass whose fields correspond to the dictionary keys. For each key, the field + type is inferred from the sample value using `infer_pydantic_type(...)`, and the + resulting field is created with a default of `None` (i.e., optional-by-default in + practice, depending on the inferred type). + + Field names are normalized via `sanitize_field_name(...)` to ensure they are valid + Python identifiers and compatible with Pydantic model field naming rules. + + The model class name is synthesized as: + "AType#::...:" + + Parameters + ---------- + dict : Mapping[str, Any] + A representative dictionary whose keys define field names and whose values + are used to infer field types. + + Returns + ------- + type[BaseModel] + A newly created Pydantic model class (subclass of `BaseModel`) with fields + derived from the input dictionary. + + Notes + ----- + - This function uses only the *sample values* present in the input mapping to + infer types; it does not scan multiple rows/records unless you pass richer + `sample_values` to `infer_pydantic_type` yourself. + - All fields are created with `Field(default=None)`, which makes them effectively + nullable unless additional validation is enforced by the inferred type. + - If two different keys sanitize to the same field name, the latter will overwrite + the former in `new_fields`. + + Examples + -------- + >>> Sample = pydantic_model_from_dict({"reviewId": 123, "reviewText": "Great!"}) + >>> obj = Sample(reviewId=1, reviewText="Nice movie") + >>> obj.model_dump() + {'reviewId': 1, 'reviewText': 'Nice movie'} + """ model_name = "AType#" + ":".join(dict.keys()) fields = {} @@ -256,7 +300,7 @@ def create_pydantic_model( Dynamically create a Pydantic model from a list of field definitions. Args: - fields: A list of (field_name, type_name, description) tuples. + fields: A list of (field_name, type_name, description, required) tuples. name: Optional name of the model. Returns: @@ -281,7 +325,6 @@ def create_pydantic_model( model_name = name field_definitions = {} - print(fields) for field_name, type_name, description, required in fields: ptype = type_mapping[type_name] if type_name in type_mapping else Any if required: @@ -592,8 +635,8 @@ def compose_types(A, B, *, name=None): Composite = create_model( name, - left=(Optional[A], None), - right=(Optional[B], None), + target=(Optional[A], None), + source=(Optional[B], None), __base__=BaseModel, ) @@ -626,7 +669,7 @@ def _istype_matmul(A, B): def _instance_matmul(a: BaseModel, b: BaseModel): """ INSTANCE composition: - a @ b → Composite(left=a, right=b) + a @ b → Composite(target=a, surce=b) """ if not isinstance(b, BaseModel): raise TypeError(f"Cannot compose instance {a} with {b}") @@ -637,7 +680,7 @@ def _instance_matmul(a: BaseModel, b: BaseModel): CompositeModel = A @ B # Build structural composite - return CompositeModel(left=a, right=b) + return CompositeModel(target=a, source=b) BaseModel.__matmul__ = _instance_matmul diff --git a/src/agentics/core/default_types.py b/src/agentics/core/default_types.py index e5a136b0..7068d820 100644 --- a/src/agentics/core/default_types.py +++ b/src/agentics/core/default_types.py @@ -13,6 +13,15 @@ def __init__(self, value): super().__init__(value=value) +class Abool(BaseModel): + value: bool + + def __init__(self, value): + if not isinstance(value, bool): + raise TypeError("Abool must be constructed with a bool") + super().__init__(value=value) + + class Explanation(BaseModel): explanation: Optional[str] = Field( None, diff --git a/src/agentics/core/transducible_functions.py b/src/agentics/core/transducible_functions.py index 2a6430a2..461e4aab 100644 --- a/src/agentics/core/transducible_functions.py +++ b/src/agentics/core/transducible_functions.py @@ -28,6 +28,33 @@ class Transduce: def __init__(self, object: BaseModel | list[BaseModel]): self.object = object + def __str__(self) -> str: + obj = self.object + + # List case + if isinstance(obj, list): + return "\n".join(self._one_to_str(x) for x in obj) + + # Single object + return self._one_to_str(obj) + + def __repr__(self) -> str: + return f"Transduce(object={self._one_to_str(self.object)})" + + @staticmethod + def _one_to_str(x: Any) -> str: + if isinstance(x, BaseModel): + return x.model_dump_json(indent=2) + if isinstance(x, (dict, list, tuple)): + # readable generic fallback + import json + + try: + return json.dumps(x, indent=2, ensure_ascii=False, default=str) + except TypeError: + return str(x) + return str(x) + class TransductionResult: def __init__(self, value, explanation): @@ -107,6 +134,8 @@ def transducible( timeout: int = 300, post_processing_function: Optional[Callable[[BaseModel], BaseModel]] = None, persist_output: str = None, + transduce_fields: list[str] = None, + prompt_template: str = None, ): if tools is None: tools = [] @@ -141,19 +170,21 @@ def _transducible(fn: Callable): transduction_timeout=timeout, save_amap_batches_to_path=persist_output, provide_explanations=provide_explanation, + prompt_template=prompt_template, ) source_ag_template = AG( atype=SourceModel, amap_batch_size=batch_size, transduction_timeout=timeout, save_amap_batches_to_path=persist_output, + transduce_fields=transduce_fields, ) target_ag_template.instructions = f""" =============================================== -TASK : +TASK : You are transducing the function {fn.__name__}. -Input Type: {SourceModel.__name__} +Input Type: {SourceModel.__name__} Output Type: {TargetModel.__name__}. INSTRUCTIONS: @@ -199,7 +230,7 @@ async def wrap_single(input_obj): if provide_explanation and len(target_ag.explanations) == 1: return TransductionResult(out, target_ag.explanations[0]) - return out + return TransductionResult(out, None) raise RuntimeError("Transduction returned no output.") @@ -516,15 +547,17 @@ async def semantic_merge(instance1: BaseModel, instance2: BaseModel) -> BaseMode async def generate_prototypical_instances( type: Type[BaseModel], n_instances: int = 10, llm: Any = AG.get_llm_provider() ) -> list[BaseModel]: + DynamicModel = create_model( - "ListOfObjectsOfGivenType", instances=(list[type], ...) # REQUIRED field + "ListOfObjectsOfGivenType", + instances=(list[type] | None, None), # REQUIRED field ) target = AG( atype=DynamicModel, instructions=f""" - Generate list of {n_instances} random instances of the following type - {type.model_json_schema()}. + Generate list of {n_instances} random instances of the following type + {type.model_json_schema()}. Try to fill most of the attributed for each generated instance as possible """, llm=llm, diff --git a/uv.lock b/uv.lock index 2c8efb8e..2feba4f6 100644 --- a/uv.lock +++ b/uv.lock @@ -49,10 +49,12 @@ name = "agentics-py" source = { editable = "." } dependencies = [ { name = "aiosqlite" }, + { name = "async-lru" }, { name = "crewai", extra = ["google-genai", "tools"] }, { name = "crewai-tools", extra = ["mcp"] }, { name = "datasets" }, { name = "ddgs" }, + { name = "duckdb" }, { name = "google" }, { name = "hnswlib" }, { name = "ipywidgets" }, @@ -66,6 +68,7 @@ dependencies = [ { name = "numerize" }, { name = "openai" }, { name = "openapi-python-client" }, + { name = "pandas" }, { name = "plotly" }, { name = "pydantic" }, { name = "pyyaml" }, @@ -105,11 +108,13 @@ rhel = [ [package.metadata] requires-dist = [ { name = "aiosqlite", specifier = ">=0.21.0" }, + { name = "async-lru", specifier = ">=2.0.5" }, { name = "crewai", extras = ["google-genai"] }, { name = "crewai", extras = ["tools"] }, { name = "crewai-tools", extras = ["mcp"] }, { name = "datasets", specifier = ">=4.0.0,<5.0.0" }, { name = "ddgs" }, + { name = "duckdb", specifier = ">=1.4.3" }, { name = "google", specifier = ">=3.0.0,<4.0.0" }, { name = "hnswlib", specifier = ">=0.8.0" }, { name = "ipywidgets", specifier = ">=8.1.7" }, @@ -123,6 +128,7 @@ requires-dist = [ { name = "numerize", specifier = ">=0.12" }, { name = "openai", specifier = ">=1.88.0,<2.0.0" }, { name = "openapi-python-client", specifier = ">=0.24.3,<0.25.0" }, + { name = "pandas", specifier = ">=2.3.3" }, { name = "plotly", specifier = ">=6.5.0" }, { name = "pydantic" }, { name = "pyyaml", specifier = ">=6.0.2,<7.0.0" }, @@ -322,6 +328,15 @@ 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 = "async-lru" +version = "2.0.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/4d/71ec4d3939dc755264f680f6c2b4906423a304c3d18e96853f0a595dfe97/async_lru-2.0.5.tar.gz", hash = "sha256:481d52ccdd27275f42c43a928b4a50c3bfb2d67af4e78b170e3e0bb39c66e5bb", size = 10380, upload-time = "2025-03-16T17:25:36.919Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/49/d10027df9fce941cb8184e78a02857af36360d33e1721df81c5ed2179a1a/async_lru-2.0.5-py3-none-any.whl", hash = "sha256:ab95404d8d2605310d345932697371a5f40def0487c03d6d0ad9138de52c9943", size = 6069, upload-time = "2025-03-16T17:25:35.422Z" }, +] + [[package]] name = "attrs" version = "25.4.0" @@ -1091,6 +1106,28 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/55/e2/2537ebcff11c1ee1ff17d8d0b6f4db75873e3b0fb32c2d4a2ee31ecb310a/docstring_parser-0.17.0-py3-none-any.whl", hash = "sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708", size = 36896, upload-time = "2025-07-21T07:35:00.684Z" }, ] +[[package]] +name = "duckdb" +version = "1.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7f/da/17c3eb5458af69d54dedc8d18e4a32ceaa8ce4d4c699d45d6d8287e790c3/duckdb-1.4.3.tar.gz", hash = "sha256:fea43e03604c713e25a25211ada87d30cd2a044d8f27afab5deba26ac49e5268", size = 18478418, upload-time = "2025-12-09T10:59:22.945Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/bc/7c5e50e440c8629495678bc57bdfc1bb8e62f61090f2d5441e2bd0a0ed96/duckdb-1.4.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:366bf607088053dce845c9d24c202c04d78022436cc5d8e4c9f0492de04afbe7", size = 29019361, upload-time = "2025-12-09T10:57:59.845Z" }, + { url = "https://files.pythonhosted.org/packages/26/15/c04a4faf0dfddad2259cab72bf0bd4b3d010f2347642541bd254d516bf93/duckdb-1.4.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8d080e8d1bf2d226423ec781f539c8f6b6ef3fd42a9a58a7160de0a00877a21f", size = 15407465, upload-time = "2025-12-09T10:58:02.465Z" }, + { url = "https://files.pythonhosted.org/packages/cb/54/a049490187c9529932fc153f7e1b92a9e145586281fe4e03ce0535a0497c/duckdb-1.4.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9dc049ba7e906cb49ca2b6d4fbf7b6615ec3883193e8abb93f0bef2652e42dda", size = 13735781, upload-time = "2025-12-09T10:58:04.847Z" }, + { url = "https://files.pythonhosted.org/packages/14/b7/ee594dcecbc9469ec3cd1fb1f81cb5fa289ab444b80cfb5640c8f467f75f/duckdb-1.4.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b30245375ea94ab528c87c61fc3ab3e36331180b16af92ee3a37b810a745d24", size = 18470729, upload-time = "2025-12-09T10:58:07.116Z" }, + { url = "https://files.pythonhosted.org/packages/df/5f/a6c1862ed8a96d8d930feb6af5e55aadd983310aab75142468c2cb32a2a3/duckdb-1.4.3-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7c864df027da1ee95f0c32def67e15d02cd4a906c9c1cbae82c09c5112f526b", size = 20471399, upload-time = "2025-12-09T10:58:09.714Z" }, + { url = "https://files.pythonhosted.org/packages/5b/80/c05c0b6a6107b618927b7dcabe3bba6a7eecd951f25c9dbcd9c1f9577cc8/duckdb-1.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:813f189039b46877b5517f1909c7b94a8fe01b4bde2640ab217537ea0fe9b59b", size = 12329359, upload-time = "2025-12-09T10:58:12.147Z" }, + { url = "https://files.pythonhosted.org/packages/b0/83/9d8fc3413f854effa680dcad1781f68f3ada8679863c0c94ba3b36bae6ff/duckdb-1.4.3-cp311-cp311-win_arm64.whl", hash = "sha256:fbc63ffdd03835f660155b37a1b6db2005bcd46e5ad398b8cac141eb305d2a3d", size = 13070898, upload-time = "2025-12-09T10:58:14.301Z" }, + { url = "https://files.pythonhosted.org/packages/5a/d7/fdc2139b94297fc5659110a38adde293d025e320673ae5e472b95d323c50/duckdb-1.4.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:6302452e57aef29aae3977063810ed7b2927967b97912947b9cca45c1c21955f", size = 29033112, upload-time = "2025-12-09T10:58:16.52Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d9/ca93df1ce19aef8f799e3aaacf754a4dde7e9169c0b333557752d21d076a/duckdb-1.4.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:deab351ac43b6282a3270e3d40e3d57b3b50f472d9fd8c30975d88a31be41231", size = 15414646, upload-time = "2025-12-09T10:58:19.36Z" }, + { url = "https://files.pythonhosted.org/packages/16/90/9f2748e740f5fc05b739e7c5c25aab6ab4363e5da4c3c70419c7121dc806/duckdb-1.4.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5634e40e1e2d972e4f75bced1fbdd9e9e90faa26445c1052b27de97ee546944a", size = 13740477, upload-time = "2025-12-09T10:58:21.778Z" }, + { url = "https://files.pythonhosted.org/packages/5f/ec/279723615b4fb454efd823b7efe97cf2504569e2e74d15defbbd6b027901/duckdb-1.4.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:274d4a31aba63115f23e7e7b401e3e3a937f3626dc9dea820a9c7d3073f450d2", size = 18483715, upload-time = "2025-12-09T10:58:24.346Z" }, + { url = "https://files.pythonhosted.org/packages/10/63/af20cd20fd7fd6565ea5a1578c16157b6a6e07923e459a6f9b0dc9ada308/duckdb-1.4.3-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f868a7e6d9b37274a1aa34849ea92aa964e9bd59a5237d6c17e8540533a1e4f", size = 20495188, upload-time = "2025-12-09T10:58:26.806Z" }, + { url = "https://files.pythonhosted.org/packages/8c/ab/0acb4b64afb2cc6c1d458a391c64e36be40137460f176c04686c965ce0e0/duckdb-1.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:ef7ef15347ce97201b1b5182a5697682679b04c3374d5a01ac10ba31cf791b95", size = 12335622, upload-time = "2025-12-09T10:58:29.707Z" }, + { url = "https://files.pythonhosted.org/packages/50/d5/2a795745f6597a5e65770141da6efdc4fd754e5ee6d652f74bcb7f9c7759/duckdb-1.4.3-cp312-cp312-win_arm64.whl", hash = "sha256:1b9b445970fd18274d5ac07a0b24c032e228f967332fb5ebab3d7db27738c0e4", size = 13075834, upload-time = "2025-12-09T10:58:32.036Z" }, +] + [[package]] name = "durationpy" version = "0.10"