From d8d3da21ee36e0b83068077e4f9d430624f1a8fd Mon Sep 17 00:00:00 2001 From: jniestroy Date: Thu, 12 Mar 2026 09:56:04 -0400 Subject: [PATCH 01/27] condense endpoint --- mds/src/fairscape_mds/crud/condensation.py | 875 +++++++++++++++++++++ mds/src/fairscape_mds/routers/rocrate.py | 237 +++++- mds/src/fairscape_mds/worker.py | 62 ++ 3 files changed, 1173 insertions(+), 1 deletion(-) create mode 100644 mds/src/fairscape_mds/crud/condensation.py diff --git a/mds/src/fairscape_mds/crud/condensation.py b/mds/src/fairscape_mds/crud/condensation.py new file mode 100644 index 0000000..03ce695 --- /dev/null +++ b/mds/src/fairscape_mds/crud/condensation.py @@ -0,0 +1,875 @@ +""" +condensation.py -- RO-Crate Graph Condensation for the Fairscape Server + +Collects a full provenance graph from MongoDB (resolving cross-crate references +via BFS expansion) and then condenses it by collapsing repetitive provenance +chains into DatasetGroup summary nodes. + +Ported from fairscape-interpreter/condense_crate.py with a new MongoDB-backed +graph collection layer replacing the local-file reader. +""" + +import datetime +from collections import defaultdict, deque +from typing import Any + +from fairscape_mds.crud.fairscape_request import FairscapeRequest +from fairscape_mds.crud.fairscape_response import FairscapeResponse +from fairscape_mds.models.identifier import ( + MetadataTypeEnum, + StoredIdentifier, + PublicationStatusEnum, +) +from fairscape_mds.models.user import Permissions + + +# --------------------------------------------------------------------------- +# Type detection helpers (operate on raw JSON dicts) +# --------------------------------------------------------------------------- + +EVI_TYPES = { + "Dataset", "Software", "MLModel", "Computation", "Annotation", + "Experiment", "ROCrate", "CreativeWork", "Schema", +} + + +def _extract_short_types(node: dict) -> list[str]: + """Extract all short EVI type names from a node's @type field.""" + raw = node.get("@type", []) + if isinstance(raw, str): + raw = [raw] + shorts = [] + for t in raw: + short = t.split("#")[-1] if "#" in t else t.split(":")[-1] if ":" in t else t + if short in EVI_TYPES: + shorts.append(short) + return shorts + + +def get_evi_type(node: dict) -> str | None: + """Extract the primary EVI type from a node's @type field.""" + shorts = _extract_short_types(node) + if not shorts: + return None + for preferred in ("ROCrate", "Computation", "Software", "MLModel", + "Experiment", "Annotation", "Schema"): + if preferred in shorts: + return preferred + return shorts[0] + + +def is_dataset(node: dict) -> bool: + types = _extract_short_types(node) + return "Dataset" in types and "ROCrate" not in types + + +def is_computation(node: dict) -> bool: + return get_evi_type(node) == "Computation" + + +def is_software(node: dict) -> bool: + return get_evi_type(node) == "Software" + + +def is_rocrate_root(node: dict) -> bool: + return "ROCrate" in _extract_short_types(node) + + +# --------------------------------------------------------------------------- +# Reference helpers +# --------------------------------------------------------------------------- + +def get_id_list(node: dict, *fields) -> list[str]: + """Extract a list of @id strings from one or more reference fields.""" + ids = [] + for field in fields: + val = node.get(field, []) + if val is None: + continue + if isinstance(val, dict): + val = [val] + if isinstance(val, list): + for item in val: + if isinstance(item, dict) and "@id" in item: + ids.append(item["@id"]) + elif isinstance(item, str): + ids.append(item) + return ids + + +def get_generatedby_ids(dataset: dict) -> list[str]: + """Get the @ids of computations that generated this dataset.""" + return get_id_list(dataset, "generatedBy", "prov:wasGeneratedBy") + + +def make_id_ref(entity_id: str) -> dict: + """Create a {"@id": ...} reference.""" + return {"@id": entity_id} + + +# --------------------------------------------------------------------------- +# Provenance Signature +# --------------------------------------------------------------------------- + +def compute_provenance_signature( + dataset_id: str, + index: dict[str, dict], + cache: dict[str, tuple], +) -> tuple: + """ + Compute a hashable signature for a dataset's provenance structure. + + Two datasets with the same signature went through identical software + pipelines, regardless of which specific data/computation instances + were involved. + """ + if dataset_id in cache: + return cache[dataset_id] + + dataset = index.get(dataset_id) + if dataset is None: + sig = ("unknown", (), None) + cache[dataset_id] = sig + return sig + + fmt = dataset.get("format", "unknown") + schema_ids = tuple(sorted(get_id_list(dataset, "evi:Schema"))) + + gen_comp_ids = get_generatedby_ids(dataset) + + if not gen_comp_ids: + sig = (fmt, schema_ids, None) + else: + comp_sigs = [] + for comp_id in sorted(gen_comp_ids): + comp = index.get(comp_id) + if comp is None: + comp_sigs.append(((), ())) + continue + + sw_ids = tuple(sorted(get_id_list(comp, "usedSoftware"))) + + input_dataset_ids = get_id_list(comp, "usedDataset") + input_sigs = tuple(sorted( + compute_provenance_signature(ds_id, index, cache) + for ds_id in input_dataset_ids + )) + + comp_sigs.append((sw_ids, input_sigs)) + + sig = (fmt, schema_ids, tuple(sorted(comp_sigs))) + + cache[dataset_id] = sig + return sig + + +# --------------------------------------------------------------------------- +# Output-first backward traversal with inline condensation +# --------------------------------------------------------------------------- + +def traverse_and_condense( + index: dict[str, dict], + threshold: int, + max_member_ids: int = 0, +) -> tuple[set[str], set[str], list[dict], dict[str, list[tuple[list[str], str]]]]: + """ + Traverse backward from the primary crate's outputs, keeping everything + reachable and collapsing large groups of sibling datasets that share the + same provenance signature. + + Returns: + keep_ids, remove_ids, group_nodes, comp_updates + """ + # Find root crate node + root_id = None + for nid, node in index.items(): + if is_rocrate_root(node): + root_id = nid + break + + if root_id is None: + return set(index.keys()), set(), [], {} + + root = index[root_id] + + output_ids = get_id_list(root, "EVI:outputs") + part_ids = get_id_list(root, "hasPart", "EVI:outputs") + + keep_ids: set[str] = {root_id, "ro-crate-metadata.json"} + collapsed_ids: set[str] = set() + group_nodes: list[dict] = [] + comp_updates: dict[str, list[tuple[list[str], str]]] = defaultdict(list) + sig_cache: dict[str, tuple] = {} + + # Pre-pass: condense repetitive top-level outputs + output_dataset_ids = [ + oid for oid in output_ids + if oid in index and is_dataset(index[oid]) + ] + if len(output_dataset_ids) > threshold: + sig_to_ids: dict[tuple, list[str]] = defaultdict(list) + for ds_id in output_dataset_ids: + sig = compute_provenance_signature(ds_id, index, sig_cache) + sig_to_ids[sig].append(ds_id) + + for sig, member_ids in sig_to_ids.items(): + if len(member_ids) > threshold: + representative_id = sorted(member_ids)[0] + non_rep_ids = [mid for mid in member_ids + if mid != representative_id] + collapsed_ids.update(non_rep_ids) + + for mid in non_rep_ids: + _collect_exclusive_backward( + mid, representative_id, index, collapsed_ids, + ) + + group = { + "consuming_comp_id": root_id, + "signature": sig, + "member_ids": member_ids, + "representative_id": representative_id, + } + group_node = create_dataset_group_node(group, index, max_member_ids) + group_nodes.append(group_node) + comp_updates[root_id].append( + (member_ids, group_node["@id"]) + ) + + # Backward traversal + visited: set[str] = set() + stack = list(part_ids) + + while stack: + current_id = stack.pop() + if current_id in visited or current_id in collapsed_ids: + continue + visited.add(current_id) + keep_ids.add(current_id) + + node = index.get(current_id) + if node is None: + continue + + evi_type = get_evi_type(node) + + if is_dataset(node): + for comp_id in get_generatedby_ids(node): + stack.append(comp_id) + for schema_id in get_id_list(node, "evi:Schema"): + stack.append(schema_id) + + elif evi_type == "Computation": + for sw_id in get_id_list(node, "usedSoftware"): + keep_ids.add(sw_id) + stack.append(sw_id) + + for ml_id in get_id_list(node, "usedMLModel"): + keep_ids.add(ml_id) + stack.append(ml_id) + + input_ids = get_id_list(node, "usedDataset") + + if len(input_ids) > threshold: + sig_to_ids = defaultdict(list) + for ds_id in input_ids: + sig = compute_provenance_signature(ds_id, index, sig_cache) + sig_to_ids[sig].append(ds_id) + + for sig, member_ids in sig_to_ids.items(): + if len(member_ids) > threshold: + representative_id = sorted(member_ids)[0] + non_rep_ids = [mid for mid in member_ids + if mid != representative_id] + collapsed_ids.update(non_rep_ids) + + for mid in non_rep_ids: + _collect_exclusive_backward( + mid, representative_id, index, collapsed_ids, + ) + + group = { + "consuming_comp_id": current_id, + "signature": sig, + "member_ids": member_ids, + "representative_id": representative_id, + } + group_node = create_dataset_group_node(group, index, max_member_ids) + group_nodes.append(group_node) + comp_updates[current_id].append( + (member_ids, group_node["@id"]) + ) + + stack.append(representative_id) + else: + for ds_id in member_ids: + stack.append(ds_id) + else: + for ds_id in input_ids: + stack.append(ds_id) + + for out_id in get_id_list(node, "generated"): + stack.append(out_id) + + elif evi_type == "Experiment": + for ref_id in get_id_list(node, "usedSample", "usedInstrument", + "usedTreatment", "usedStain"): + stack.append(ref_id) + + elif is_software(node): + pass + + # Keep all software nodes + for nid, n in index.items(): + if is_software(n): + keep_ids.add(nid) + + keep_ids -= collapsed_ids + + return keep_ids, collapsed_ids, group_nodes, comp_updates + + +def _collect_exclusive_backward( + dataset_id: str, + representative_id: str, + index: dict[str, dict], + collapsed: set[str], +) -> None: + """ + Collect backward chain entities of a non-representative dataset that are + NOT shared with the representative's chain. Add them to collapsed set. + """ + rep_chain = _collect_backward_chain(representative_id, index) + stack = [dataset_id] + visited = set() + + while stack: + cid = stack.pop() + if cid in visited: + continue + visited.add(cid) + + if cid in rep_chain: + continue + + node = index.get(cid) + if node is None: + continue + + if is_software(node): + continue + + collapsed.add(cid) + + if is_dataset(node): + for comp_id in get_generatedby_ids(node): + stack.append(comp_id) + + if is_computation(node): + for ref_id in get_id_list(node, "usedDataset"): + stack.append(ref_id) + + +def _collect_backward_chain(dataset_id: str, index: dict[str, dict]) -> set[str]: + """Collect all entity @ids in the backward provenance chain.""" + visited = set() + stack = [dataset_id] + while stack: + cid = stack.pop() + if cid in visited: + continue + visited.add(cid) + node = index.get(cid) + if node is None: + continue + if is_dataset(node): + for comp_id in get_generatedby_ids(node): + stack.append(comp_id) + if is_computation(node): + for ref_id in get_id_list(node, "usedDataset", "usedSoftware", + "usedMLModel"): + stack.append(ref_id) + return visited + + +# --------------------------------------------------------------------------- +# Build condensed graph +# --------------------------------------------------------------------------- + +def create_dataset_group_node( + group: dict, + index: dict[str, dict], + max_member_ids: int = 0, +) -> dict: + """Create a DatasetGroup summary node for a group of similar datasets.""" + representative = index[group["representative_id"]] + member_ids = group["member_ids"] + count = len(member_ids) + sig = group["signature"] + + common_sw_ids = [] + if sig[2]: + for comp_sig in sig[2]: + sw_ids, _ = comp_sig + common_sw_ids.extend(sw_ids) + common_sw_ids = sorted(set(common_sw_ids)) + + fmt = sig[0] + + consuming_node = index.get(group["consuming_comp_id"], {}) + if is_rocrate_root(consuming_node): + crate_name = consuming_node.get("name", "unknown").lower().replace(" ", "-") + group_id = f"ark:group/{crate_name}-{fmt.replace('/', '_').lstrip('.')}-outputs" + else: + comp_name = consuming_node.get("name", "unknown").lower().replace(" ", "-") + group_id = f"ark:group/{comp_name}-{fmt.replace('/', '_').lstrip('.')}-inputs" + + sw_names = [] + for sw_id in common_sw_ids: + sw = index.get(sw_id, {}) + sw_names.append(sw.get("name", sw_id)) + + description = f"{count} {fmt} files with identical provenance structure." + if sw_names: + description += f" All processed by {', '.join(sw_names)}." + + schema_ids = list(sig[1]) if sig[1] else [] + + node = { + "@id": group_id, + "@type": ["prov:Entity", "https://w3id.org/EVI#DatasetGroup"], + "name": f"{representative.get('name', fmt + ' files')} (and {count - 1} similar)", + "description": description, + "format": fmt, + "evi:memberCount": count, + "evi:representativeDataset": make_id_ref(group["representative_id"]), + "evi:commonFormat": fmt, + "evi:commonSoftware": [make_id_ref(sw_id) for sw_id in common_sw_ids], + "evi:provenanceSignature": str(sig), + "evi:memberIds": _truncate_member_ids(sorted(member_ids), max_member_ids), + } + + if schema_ids: + node["evi:commonSchema"] = [make_id_ref(sid) for sid in schema_ids] + + return node + + +def _truncate_member_ids(ids: list[str], max_ids: int) -> list[str]: + """Truncate member ID list if max_ids > 0, appending a summary entry.""" + if max_ids <= 0 or len(ids) <= max_ids: + return ids + excluded = len(ids) - max_ids + return ids[:max_ids] + [f"... and {excluded} more (total: {len(ids)})"] + + +def condense_graph( + graph: list[dict], + threshold: int, + max_member_ids: int = 0, +) -> tuple[list[dict], dict]: + """ + Condense an RO-Crate @graph by collapsing repetitive provenance. + + Returns (condensed_graph, stats). + """ + index: dict[str, dict] = {} + for node in graph: + node_id = node.get("@id") + if node_id: + index[node_id] = node + + original_count = len(graph) + + keep_ids, collapsed_ids, group_nodes, comp_updates = \ + traverse_and_condense(index, threshold, max_member_ids) + + if not group_nodes: + stats = { + "condensed": False, + "originalEntityCount": original_count, + "condensedEntityCount": original_count, + "datasetGroupCount": 0, + "note": "No repetitive provenance found above threshold.", + } + return graph, stats + + new_graph = [] + for node in graph: + node_id = node.get("@id") + + if node_id not in keep_ids: + continue + + if node_id in comp_updates: + node = dict(node) + for member_ids, group_id in comp_updates[node_id]: + member_set = set(member_ids) + + if "usedDataset" in node and node["usedDataset"]: + kept = [ref for ref in node["usedDataset"] + if ref.get("@id") not in member_set] + kept.append(make_id_ref(group_id)) + node["usedDataset"] = kept + + if "prov:used" in node and node["prov:used"]: + kept = [ref for ref in node["prov:used"] + if ref.get("@id") not in member_set] + kept.append(make_id_ref(group_id)) + node["prov:used"] = kept + + if is_rocrate_root(node): + node = dict(node) + condensed_count = len(keep_ids) + len(group_nodes) + node["evi:condensed"] = True + node["evi:condensationThreshold"] = threshold + node["evi:condensationDate"] = str(datetime.date.today()) + node["evi:originalEntityCount"] = original_count + node["evi:condensedEntityCount"] = condensed_count + node["evi:datasetGroupCount"] = len(group_nodes) + + total_collapsed = len(collapsed_ids) + node["evi:condensationNote"] = ( + f"Condensed from {original_count} entities to " + f"{condensed_count}. " + f"{len(group_nodes)} dataset group(s) created by collapsing " + f"{total_collapsed} datasets with identical provenance signatures " + f"(same software chain). Full member lists preserved in evi:memberIds." + ) + + if "hasPart" in node and node["hasPart"]: + kept = [ref for ref in node["hasPart"] + if ref.get("@id") not in collapsed_ids] + for gn in group_nodes: + kept.append(make_id_ref(gn["@id"])) + node["hasPart"] = kept + + if node_id in comp_updates: + for member_ids, group_id in comp_updates[node_id]: + member_set = set(member_ids) + if "EVI:outputs" in node and node["EVI:outputs"]: + kept = [ref for ref in node["EVI:outputs"] + if ref.get("@id") not in member_set] + kept.append(make_id_ref(group_id)) + node["EVI:outputs"] = kept + + new_graph.append(node) + + new_graph.extend(group_nodes) + + # Clean up dangling references + final_ids = {n.get("@id") for n in new_graph if "@id" in n} + ref_fields = ("usedDataset", "usedSoftware", "usedMLModel", "generated", + "hasPart", "prov:used", "generatedBy", "prov:wasGeneratedBy", + "derivedFrom", "prov:wasDerivedFrom", "usedByComputation", + "isPartOf", "EVI:outputs") + + for i, node in enumerate(new_graph): + modified = False + node_copy = None + for field in ref_fields: + val = node.get(field) + if val is None: + continue + if isinstance(val, dict) and "@id" in val: + if val["@id"] not in final_ids: + if not modified: + node_copy = dict(node) + modified = True + node_copy[field] = [] + elif isinstance(val, list): + cleaned = [ref for ref in val + if not (isinstance(ref, dict) and "@id" in ref + and ref["@id"] not in final_ids)] + if len(cleaned) != len(val): + if not modified: + node_copy = dict(node) + modified = True + node_copy[field] = cleaned + if modified: + new_graph[i] = node_copy + + condensed_count = len(new_graph) + groups_info = [] + for gn in group_nodes: + groups_info.append({ + "memberCount": gn["evi:memberCount"], + "format": gn["format"], + "groupId": gn["@id"], + }) + + stats = { + "condensed": True, + "originalEntityCount": original_count, + "condensedEntityCount": condensed_count, + "datasetGroupCount": len(group_nodes), + "entitiesRemoved": len(collapsed_ids), + "groups": groups_info, + } + + return new_graph, stats + + +# --------------------------------------------------------------------------- +# MongoDB graph collection (replaces local file merge_additional_crates) +# --------------------------------------------------------------------------- + +# Fields that contain ARK references to other entities +_ARK_REF_FIELDS = ( + "hasPart", "EVI:outputs", "outputs", + "generatedBy", "prov:wasGeneratedBy", + "usedDataset", "usedSoftware", "usedMLModel", + "derivedFrom", "prov:wasDerivedFrom", + "evi:Schema", + "usedSample", "usedInstrument", "usedTreatment", "usedStain", + "generated", "prov:used", + "usedByComputation", + "isPartOf", +) + +# Batch size for MongoDB $in queries +_BATCH_SIZE = 500 + + +def _flatten_metadata(doc: dict) -> dict: + """Flatten a StoredIdentifier document's metadata sub-dict to top level. + + MongoDB stores entities as {"@id": ..., "@type": ..., "metadata": {...}, ...}. + The condensation algorithm expects flat RO-Crate nodes like + {"@id": ..., "@type": ..., "name": ..., "generatedBy": [...]}. + """ + flattened = {k: v for k, v in doc.items() if k != "metadata"} + if "metadata" in doc and isinstance(doc["metadata"], dict): + flattened.update(doc["metadata"]) + return flattened + + +def _extract_all_ark_refs(entity: dict) -> list[str]: + """Extract all ARK identifier references from an entity's provenance fields.""" + refs = [] + for field in _ARK_REF_FIELDS: + val = entity.get(field) + if val is None: + continue + if isinstance(val, dict): + aid = val.get("@id") + if aid and isinstance(aid, str) and "ark:" in aid: + refs.append(aid) + elif isinstance(val, list): + for item in val: + if isinstance(item, dict): + aid = item.get("@id") + if aid and isinstance(aid, str) and "ark:" in aid: + refs.append(aid) + elif isinstance(item, str) and "ark:" in item: + refs.append(item) + return refs + + +# --------------------------------------------------------------------------- +# Main CRUD class +# --------------------------------------------------------------------------- + +class FairscapeCondensationRequest(FairscapeRequest): + """Handles condensed ROCrate creation, retrieval, and deletion.""" + + def build_full_graph_for_rocrate(self, rocrate_id: str) -> list[dict]: + """Collect the full provenance graph for an ROCrate from MongoDB. + + Does BFS expansion: starts from the ROCrate's hasPart/outputs and + recursively resolves all ARK references, even those pointing to + entities in other crates. + + Returns a flat list[dict] suitable for condense_graph(). + """ + collected: dict[str, dict | None] = {} + queue: deque[str] = deque() + + # Seed with the ROCrate root itself + queue.append(rocrate_id) + + while queue: + # Drain up to _BATCH_SIZE IDs that we haven't seen yet + batch: list[str] = [] + while queue and len(batch) < _BATCH_SIZE: + eid = queue.popleft() + if eid not in collected: + batch.append(eid) + + if not batch: + break + + # Batch fetch from MongoDB + cursor = self.config.identifierCollection.find( + {"@id": {"$in": batch}}, + {"_id": 0} + ) + + found_ids: set[str] = set() + for doc in cursor: + flat = _flatten_metadata(doc) + entity_id = flat.get("@id") + if not entity_id: + continue + found_ids.add(entity_id) + collected[entity_id] = flat + + # Enqueue all referenced ARKs for expansion + for ref_id in _extract_all_ark_refs(flat): + if ref_id not in collected: + queue.append(ref_id) + + # Mark not-found IDs so we don't re-query them + for eid in batch: + if eid not in found_ids and eid not in collected: + collected[eid] = None + + return [v for v in collected.values() if v is not None] + + def condense_rocrate( + self, + rocrate_id: str, + threshold: int = 5, + max_member_ids: int = 0, + owner_email: str = "system@fairscape.org", + ) -> FairscapeResponse: + """Build the full graph, condense it, and store the result. + + Returns a FairscapeResponse with the condensed ROCrate's StoredIdentifier. + """ + condensed_id = f"{rocrate_id}-condensed" + + # Check if already exists + existing = self.config.identifierCollection.find_one({"@id": condensed_id}) + if existing: + return FairscapeResponse( + success=False, + statusCode=409, + error={"message": f"Condensed ROCrate {condensed_id} already exists"} + ) + + # Collect the full graph from MongoDB + graph = self.build_full_graph_for_rocrate(rocrate_id) + if not graph: + return FairscapeResponse( + success=False, + statusCode=404, + error={"message": f"No metadata found for RO-Crate {rocrate_id}"} + ) + + # Run condensation + condensed_graph, stats = condense_graph(graph, threshold, max_member_ids) + + if not stats.get("condensed"): + return FairscapeResponse( + success=True, + statusCode=200, + model={"message": "Nothing to condense", "stats": stats} + ) + + # Find context from the original ROCrate root doc + rocrate_doc = self.config.identifierCollection.find_one( + {"@id": rocrate_id}, {"_id": 0} + ) + rocrate_name = "Unknown RO-Crate" + if rocrate_doc: + meta = rocrate_doc.get("metadata", {}) + if isinstance(meta, dict): + rocrate_name = meta.get("name", rocrate_doc.get("name", rocrate_id)) + else: + rocrate_name = rocrate_doc.get("name", rocrate_id) + + # Build the condensed ROCrate document as metadata + # Store the condensed @graph and stats as the metadata payload + condensed_metadata = { + "@type": ["https://w3id.org/EVI#Dataset", "https://w3id.org/EVI#ROCrate"], + "@id": condensed_id, + "name": f"Condensed: {rocrate_name}", + "description": ( + f"Condensed version of {rocrate_name}. " + f"{stats['datasetGroupCount']} dataset group(s), " + f"reduced from {stats['originalEntityCount']} to " + f"{stats['condensedEntityCount']} entities." + ), + "evi:condensed": True, + "evi:sourceROCrate": {"@id": rocrate_id}, + "evi:condensationThreshold": threshold, + "evi:condensationDate": str(datetime.date.today()), + "evi:condensationStats": stats, + "keywords": ["condensed", "provenance-summary"], + "version": "1.0", + "author": "fairscape-server", + "hasPart": [], + "@graph": condensed_graph, + } + + now = datetime.datetime.utcnow() + stored_doc = { + "@id": condensed_id, + "@type": MetadataTypeEnum.ROCRATE.value, + "metadata": condensed_metadata, + "publicationStatus": PublicationStatusEnum.PUBLISHED.value, + "permissions": { + "owner": owner_email, + "group": None, + }, + "distribution": None, + "descriptiveStatistics": {}, + "contentSummary": None, + "dateCreated": now, + "dateModified": now, + } + + try: + self.config.identifierCollection.insert_one(stored_doc) + + # Update original ROCrate with pointer + self.config.identifierCollection.update_one( + {"@id": rocrate_id}, + {"$set": {"metadata.hasCondensedROCrate": {"@id": condensed_id}}} + ) + + return FairscapeResponse( + success=True, + statusCode=201, + model={"condensed_id": condensed_id, "stats": stats} + ) + except Exception as e: + return FairscapeResponse( + success=False, + statusCode=500, + error={"message": f"Error storing condensed ROCrate: {str(e)}"} + ) + + def delete_condensed_rocrate(self, rocrate_id: str) -> FairscapeResponse: + """Delete an existing condensed ROCrate and clear the pointer.""" + condensed_id = f"{rocrate_id}-condensed" + + try: + delete_result = self.config.identifierCollection.delete_one( + {"@id": condensed_id} + ) + + if delete_result.deleted_count == 0: + return FairscapeResponse( + success=False, + statusCode=404, + error={"message": f"Condensed ROCrate {condensed_id} not found"} + ) + + self.config.identifierCollection.update_one( + {"@id": rocrate_id}, + {"$unset": {"metadata.hasCondensedROCrate": ""}} + ) + + return FairscapeResponse( + success=True, + statusCode=200, + model={"message": f"Condensed ROCrate {condensed_id} deleted successfully"} + ) + except Exception as e: + return FairscapeResponse( + success=False, + statusCode=500, + error={"message": f"Error deleting condensed ROCrate: {str(e)}"} + ) diff --git a/mds/src/fairscape_mds/routers/rocrate.py b/mds/src/fairscape_mds/routers/rocrate.py index 458cfa5..e73b925 100644 --- a/mds/src/fairscape_mds/routers/rocrate.py +++ b/mds/src/fairscape_mds/routers/rocrate.py @@ -24,7 +24,7 @@ from fairscape_mds.core.config import appConfig from fairscape_models.rocrate import ROCrateV1_2, ROCrateMetadataElem from fairscape_mds.deps import getCurrentUser -from fairscape_mds.worker import celeryUploadROCrate, score_ai_ready_task +from fairscape_mds.worker import celeryUploadROCrate, score_ai_ready_task, condense_rocrate_task from fairscape_models.conversion.converter import ROCToTargetConverter from fairscape_models.conversion.mapping.croissant import MAPPING_CONFIGURATION as CROISSANT_MAPPING @@ -509,4 +509,239 @@ def rescore_ai_ready_score( "task_id": task_guid, "status_endpoint": f"/rocrate/ai-ready-score/status/{task_guid}" } + ) + + +# --------------------------------------------------------------------------- +# Condensation endpoints +# --------------------------------------------------------------------------- + +@rocrateRouter.post( + "/rocrate/condense/ark:/{NAAN}/{postfix}", + summary="Trigger condensation of an RO-Crate", + status_code=202, +) +@rocrateRouter.post( + "/rocrate/condense/ark:{NAAN}/{postfix}", + summary="Trigger condensation of an RO-Crate", + status_code=202, +) +def trigger_condense_rocrate( + currentUser: Annotated[UserWriteModel, Depends(getCurrentUser)], + NAAN: str, + postfix: str, + threshold: int = Query(default=5, ge=2, description="Min group size to trigger condensation"), + max_member_ids: int = Query(default=0, ge=0, description="Max member IDs per group (0=unlimited)"), + force: bool = Query(default=False, description="Force re-condensation if one already exists"), +): + """Trigger async condensation of an RO-Crate. Resolves cross-crate + references from MongoDB and collapses repetitive provenance into + DatasetGroup summary nodes.""" + ark_id = f"ark:{NAAN}/{postfix}" + + entity = _flexible_find(ark_id) + if not entity: + return JSONResponse( + status_code=404, + content={"error": f"Entity {ark_id} not found"} + ) + + entity_type = entity.get("@type", []) + if isinstance(entity_type, str): + entity_type = [entity_type] + is_rocrate = any("ROCrate" in str(t) for t in entity_type) + if not is_rocrate: + return JSONResponse( + status_code=400, + content={"error": "Entity is not an RO-Crate"} + ) + + # Handle existing condensed ROCrate + if entity.get("metadata", {}).get("hasCondensedROCrate") and not force: + condensed_ref = entity["metadata"]["hasCondensedROCrate"] + return JSONResponse( + status_code=200, + content={ + "message": "Condensed ROCrate already exists. Use force=true to re-condense.", + "condensed_id": condensed_ref.get("@id"), + } + ) + + # If force, delete existing condensed ROCrate first + if force and entity.get("metadata", {}).get("hasCondensedROCrate"): + from fairscape_mds.crud.condensation import FairscapeCondensationRequest + condensation_req = FairscapeCondensationRequest(appConfig) + condensation_req.delete_condensed_rocrate(ark_id) + + # Check for in-progress task + task_doc = appConfig.asyncCollection.find_one({ + "task_type": "CondensedROCrateBuild", + "rocrate_id": ark_id, + "status": {"$in": ["PENDING", "PROCESSING"]} + }, {"_id": 0}) + + if task_doc: + return JSONResponse( + status_code=202, + content={ + "message": "Condensation already in progress", + "task_id": task_doc["guid"], + "status": task_doc["status"], + "status_endpoint": f"/rocrate/condense/status/{task_doc['guid']}" + } + ) + + # Create async task + task_guid = str(uuid.uuid4()) + task_data = { + "guid": task_guid, + "task_type": "CondensedROCrateBuild", + "rocrate_id": ark_id, + "owner_email": currentUser.email, + "threshold": threshold, + "max_member_ids": max_member_ids, + "status": "PENDING", + "time_created": datetime.datetime.utcnow(), + } + + appConfig.asyncCollection.insert_one(task_data) + + condense_rocrate_task.delay( + task_guid=task_guid, + rocrate_id=ark_id, + threshold=threshold, + max_member_ids=max_member_ids, + user_email=currentUser.email, + ) + + return JSONResponse( + status_code=202, + content={ + "message": "Condensation initiated", + "task_id": task_guid, + "status_endpoint": f"/rocrate/condense/status/{task_guid}" + } + ) + + +@rocrateRouter.get( + "/rocrate/condense/status/{task_id}", + summary="Get status of condensation task", +) +def get_condense_status(task_id: str): + """Check the status of an async condensation task.""" + task_doc = appConfig.asyncCollection.find_one( + {"guid": task_id}, {"_id": 0} + ) + + if not task_doc: + return JSONResponse( + status_code=404, + content={"error": "Task not found"} + ) + + return JSONResponse(status_code=200, content=task_doc) + + +@rocrateRouter.get( + "/rocrate/condensed/ark:/{NAAN}/{postfix}", + summary="Get condensed ROCrate (auto-triggers if none exists)", +) +@rocrateRouter.get( + "/rocrate/condensed/ark:{NAAN}/{postfix}", + summary="Get condensed ROCrate (auto-triggers if none exists)", +) +def get_condensed_rocrate( + NAAN: str, + postfix: str, + threshold: int = Query(default=5, ge=2, description="Threshold if auto-triggering"), +): + """Return the condensed ROCrate if it exists. If not, auto-trigger + condensation and return 202 with the task ID.""" + ark_id = f"ark:{NAAN}/{postfix}" + + entity = _flexible_find(ark_id) + if not entity: + return JSONResponse( + status_code=404, + content={"error": f"Entity {ark_id} not found"} + ) + + entity_type = entity.get("@type", []) + if isinstance(entity_type, str): + entity_type = [entity_type] + is_rocrate = any("ROCrate" in str(t) for t in entity_type) + if not is_rocrate: + return JSONResponse( + status_code=400, + content={"error": "Entity is not an RO-Crate"} + ) + + # If condensed version exists, return it + condensed_ref = entity.get("metadata", {}).get("hasCondensedROCrate") + if condensed_ref: + condensed_id = condensed_ref.get("@id") + condensed_doc = _flexible_find(condensed_id) + if condensed_doc: + # Return the condensed @graph from metadata + metadata = condensed_doc.get("metadata", {}) + condensed_graph = metadata.get("@graph", []) + return JSONResponse( + status_code=200, + content={ + "@context": {"@vocab": "https://schema.org/"}, + "@graph": condensed_graph, + "evi:condensationStats": metadata.get("evi:condensationStats"), + "evi:sourceROCrate": metadata.get("evi:sourceROCrate"), + } + ) + + # Check for in-progress task + task_doc = appConfig.asyncCollection.find_one({ + "task_type": "CondensedROCrateBuild", + "rocrate_id": ark_id, + "status": {"$in": ["PENDING", "PROCESSING"]} + }, {"_id": 0}) + + if task_doc: + return JSONResponse( + status_code=202, + content={ + "message": "Condensation in progress", + "task_id": task_doc["guid"], + "status": task_doc["status"], + "status_endpoint": f"/rocrate/condense/status/{task_doc['guid']}" + } + ) + + # Auto-trigger condensation + task_guid = str(uuid.uuid4()) + task_data = { + "guid": task_guid, + "task_type": "CondensedROCrateBuild", + "rocrate_id": ark_id, + "owner_email": "system@fairscape.org", + "threshold": threshold, + "max_member_ids": 0, + "status": "PENDING", + "time_created": datetime.datetime.utcnow(), + } + + appConfig.asyncCollection.insert_one(task_data) + + condense_rocrate_task.delay( + task_guid=task_guid, + rocrate_id=ark_id, + threshold=threshold, + max_member_ids=0, + user_email="system@fairscape.org", + ) + + return JSONResponse( + status_code=202, + content={ + "message": "Condensation auto-triggered", + "task_id": task_guid, + "status_endpoint": f"/rocrate/condense/status/{task_guid}" + } ) \ No newline at end of file diff --git a/mds/src/fairscape_mds/worker.py b/mds/src/fairscape_mds/worker.py index ae778f1..301abf6 100644 --- a/mds/src/fairscape_mds/worker.py +++ b/mds/src/fairscape_mds/worker.py @@ -16,6 +16,7 @@ from fairscape_mds.crud.evidence_graph import FairscapeEvidenceGraphRequest from fairscape_mds.crud.AIReady import FairscapeAIReadyScoreRequest from fairscape_mds.crud.llm_assist import FairscapeLLMAssistRequest +from fairscape_mds.crud.condensation import FairscapeCondensationRequest from fairscape_models.conversion.models.AIReady import AIReadyScore from fairscape_models.conversion.mapping.AIReady import ( @@ -28,6 +29,7 @@ evidenceGraphRequests = FairscapeEvidenceGraphRequest(appConfig) llmAssistRequests = FairscapeLLMAssistRequest(appConfig) identifierRequestFactory = IdentifierRequest(appConfig) +condensationRequests = FairscapeCondensationRequest(appConfig) # add support for logfire worker token @worker_init.connect() @@ -263,6 +265,66 @@ def score_ai_ready_task(self, task_guid: str, rocrate_id: str): ) return {"status": "FAILURE", "error": {"message": "An unexpected server error occurred"}} +@celeryApp.task(name='fairscape_mds.worker.condense_rocrate_task', bind=True) +def condense_rocrate_task(self, task_guid: str, rocrate_id: str, threshold: int = 5, max_member_ids: int = 0, user_email: str = "system@fairscape.org"): + print(f"Starting Condensation Task: {task_guid} for {rocrate_id}") + + try: + appConfig.asyncCollection.update_one( + {"guid": task_guid}, + {"$set": { + "status": "PROCESSING", + "time_started": datetime.datetime.utcnow() + }} + ) + + response = condensationRequests.condense_rocrate( + rocrate_id=rocrate_id, + threshold=threshold, + max_member_ids=max_member_ids, + owner_email=user_email, + ) + + if response.success: + result = response.model if isinstance(response.model, dict) else {"condensed_id": str(response.model)} + print(f"Successfully condensed ROCrate for Task GUID {task_guid}") + appConfig.asyncCollection.update_one( + {"guid": task_guid}, + {"$set": { + "status": "SUCCESS", + "result": result, + "time_finished": datetime.datetime.utcnow() + }} + ) + return {"status": "SUCCESS", "result": result} + else: + error_detail = response.error if isinstance(response.error, dict) else {"message": str(response.error)} + print(f"Failed to condense ROCrate for Task GUID {task_guid}: {error_detail}") + appConfig.asyncCollection.update_one( + {"guid": task_guid}, + {"$set": { + "status": "FAILURE", + "error": error_detail, + "time_finished": datetime.datetime.utcnow() + }} + ) + return {"status": "FAILURE", "error": error_detail} + + except Exception as e: + import traceback + error_msg = f"Unexpected error in condense_rocrate_task: {str(e)}" + print(error_msg) + traceback.print_exc() + appConfig.asyncCollection.update_one( + {"guid": task_guid}, + {"$set": { + "status": "FAILURE", + "error": {"message": "An unexpected error occurred", "details": str(e)}, + "time_finished": datetime.datetime.utcnow() + }} + ) + return {"status": "FAILURE", "error": {"message": "An unexpected server error occurred"}} + @celeryApp.task(name='fairscape_mds.worker.process_llm_assist_task', bind=True) def process_llm_assist_task(self, task_guid: str): print(f"Starting LLM Assist Processing Task: {task_guid}") From 952f93bb37023d9a34b2b9de077c6b6d88da69f2 Mon Sep 17 00:00:00 2001 From: jniestroy Date: Thu, 12 Mar 2026 10:04:50 -0400 Subject: [PATCH 02/27] fix structure --- mds/src/fairscape_mds/crud/condensation.py | 70 +++++++++++++++------- mds/src/fairscape_mds/routers/rocrate.py | 11 +--- 2 files changed, 52 insertions(+), 29 deletions(-) diff --git a/mds/src/fairscape_mds/crud/condensation.py b/mds/src/fairscape_mds/crud/condensation.py index 03ce695..1d26d6b 100644 --- a/mds/src/fairscape_mds/crud/condensation.py +++ b/mds/src/fairscape_mds/crud/condensation.py @@ -779,28 +779,56 @@ def condense_rocrate( else: rocrate_name = rocrate_doc.get("name", rocrate_id) - # Build the condensed ROCrate document as metadata - # Store the condensed @graph and stats as the metadata payload + # Build the condensed ROCrate as a proper RO-Crate structure: + # @graph[0] = ROCrateMetadataFileElem ("about" this crate) + # @graph[1] = ROCrateMetadataElem (the root crate node — already in condensed_graph) + # @graph[2..] = rest of condensed entities + + # The condensed_graph already contains the root ROCrateMetadataElem + # (with evi:condensed=True set by condense_graph). We need to: + # 1. Find it and update its @id to the condensed_id + # 2. Prepend the ROCrateMetadataFileElem + + # Find the root crate element in the condensed graph + root_idx = None + for idx, node in enumerate(condensed_graph): + if is_rocrate_root(node): + root_idx = idx + break + + if root_idx is not None: + # Update the root element with condensed-specific metadata + root_node = dict(condensed_graph[root_idx]) + root_node["evi:sourceROCrate"] = {"@id": rocrate_id} + root_node["evi:condensationStats"] = stats + # Keep original @id so provenance refs stay valid, + # but add the condensed_id as an alternate + condensed_graph[root_idx] = root_node + + # Build the ro-crate-metadata.json file descriptor element + file_elem = { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "conformsTo": {"@id": "https://w3id.org/ro/crate/1.2-DRAFT"}, + "about": {"@id": rocrate_id}, + } + + # Assemble the @graph: file descriptor first, then rest + # Remove the root from its current position and place it second + ordered_graph = [file_elem] + if root_idx is not None: + ordered_graph.append(condensed_graph[root_idx]) + for idx, node in enumerate(condensed_graph): + if idx == root_idx: + continue + # Skip any existing file descriptor from the source graph + if node.get("@id") == "ro-crate-metadata.json": + continue + ordered_graph.append(node) + condensed_metadata = { - "@type": ["https://w3id.org/EVI#Dataset", "https://w3id.org/EVI#ROCrate"], - "@id": condensed_id, - "name": f"Condensed: {rocrate_name}", - "description": ( - f"Condensed version of {rocrate_name}. " - f"{stats['datasetGroupCount']} dataset group(s), " - f"reduced from {stats['originalEntityCount']} to " - f"{stats['condensedEntityCount']} entities." - ), - "evi:condensed": True, - "evi:sourceROCrate": {"@id": rocrate_id}, - "evi:condensationThreshold": threshold, - "evi:condensationDate": str(datetime.date.today()), - "evi:condensationStats": stats, - "keywords": ["condensed", "provenance-summary"], - "version": "1.0", - "author": "fairscape-server", - "hasPart": [], - "@graph": condensed_graph, + "@context": {"@vocab": "https://schema.org/"}, + "@graph": ordered_graph, } now = datetime.datetime.utcnow() diff --git a/mds/src/fairscape_mds/routers/rocrate.py b/mds/src/fairscape_mds/routers/rocrate.py index e73b925..dc5ff5b 100644 --- a/mds/src/fairscape_mds/routers/rocrate.py +++ b/mds/src/fairscape_mds/routers/rocrate.py @@ -683,17 +683,12 @@ def get_condensed_rocrate( condensed_id = condensed_ref.get("@id") condensed_doc = _flexible_find(condensed_id) if condensed_doc: - # Return the condensed @graph from metadata + # Return the full RO-Crate structure stored in metadata + # (has @context and @graph with proper file descriptor + root + entities) metadata = condensed_doc.get("metadata", {}) - condensed_graph = metadata.get("@graph", []) return JSONResponse( status_code=200, - content={ - "@context": {"@vocab": "https://schema.org/"}, - "@graph": condensed_graph, - "evi:condensationStats": metadata.get("evi:condensationStats"), - "evi:sourceROCrate": metadata.get("evi:sourceROCrate"), - } + content=metadata ) # Check for in-progress task From bd9cbd9efb02a9fe169c551c441e8a8fddb6af8c Mon Sep 17 00:00:00 2001 From: jniestroy Date: Thu, 12 Mar 2026 10:09:42 -0400 Subject: [PATCH 03/27] don't grab stored identifier --- mds/src/fairscape_mds/crud/condensation.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/mds/src/fairscape_mds/crud/condensation.py b/mds/src/fairscape_mds/crud/condensation.py index 1d26d6b..31f2cf9 100644 --- a/mds/src/fairscape_mds/crud/condensation.py +++ b/mds/src/fairscape_mds/crud/condensation.py @@ -632,15 +632,23 @@ def condense_graph( def _flatten_metadata(doc: dict) -> dict: - """Flatten a StoredIdentifier document's metadata sub-dict to top level. + """Flatten a StoredIdentifier document's metadata sub-dict to top level, + stripping server-internal StoredIdentifier wrapper fields. - MongoDB stores entities as {"@id": ..., "@type": ..., "metadata": {...}, ...}. - The condensation algorithm expects flat RO-Crate nodes like + MongoDB stores entities as {"@id": ..., "@type": ..., "metadata": {...}, + "permissions": ..., "distribution": ..., ...}. + The condensation algorithm expects clean RO-Crate nodes like {"@id": ..., "@type": ..., "name": ..., "generatedBy": [...]}. """ - flattened = {k: v for k, v in doc.items() if k != "metadata"} + flattened = {} + # Start with the metadata sub-dict (the actual RO-Crate fields) if "metadata" in doc and isinstance(doc["metadata"], dict): flattened.update(doc["metadata"]) + # Ensure @id and @type are present (from top-level StoredIdentifier) + if "@id" in doc: + flattened["@id"] = doc["@id"] + if "@type" in doc and "@type" not in flattened: + flattened["@type"] = doc["@type"] return flattened From 17d7afab7126dc9ae31b2ce49b6499fadd555d7a Mon Sep 17 00:00:00 2001 From: jniestroy Date: Thu, 12 Mar 2026 12:14:44 -0400 Subject: [PATCH 04/27] more --- mds/src/fairscape_mds/crud/interpretation.py | 787 ++++++++++++++++++ mds/src/fairscape_mds/main.py | 2 + .../fairscape_mds/models/interpretation.py | 47 ++ .../fairscape_mds/routers/interpretation.py | 204 +++++ .../tests/crud/test_interpretation.py | 412 +++++++++ mds/src/fairscape_mds/worker.py | 30 + pyproject.toml | 1 + 7 files changed, 1483 insertions(+) create mode 100644 mds/src/fairscape_mds/crud/interpretation.py create mode 100644 mds/src/fairscape_mds/models/interpretation.py create mode 100644 mds/src/fairscape_mds/routers/interpretation.py create mode 100644 mds/src/fairscape_mds/tests/crud/test_interpretation.py diff --git a/mds/src/fairscape_mds/crud/interpretation.py b/mds/src/fairscape_mds/crud/interpretation.py new file mode 100644 index 0000000..e29bc21 --- /dev/null +++ b/mds/src/fairscape_mds/crud/interpretation.py @@ -0,0 +1,787 @@ +""" +interpretation.py -- AI-Mediated Interpretation Pipeline + +Takes an RO-Crate, ensures it is condensed, finds all Computation nodes, +pre-fetches software source code from GitHub, prompts an LLM per computation +(in parallel via ThreadPoolExecutor), synthesizes a graph-level summary, +and stores the resulting AnnotatedEvidenceGraph. +""" + +import datetime +import re +import uuid +import logging +from concurrent.futures import ThreadPoolExecutor, as_completed +from typing import Any, Dict, List, Optional, Tuple + +import httpx +from pydantic_ai import Agent + +from fairscape_models.annotated_computation import AnnotatedComputation, CodeAnalysis, DatasetSummary +from fairscape_models.annotated_evidence_graph import AnnotatedEvidenceGraph + +from fairscape_mds.crud.fairscape_request import FairscapeRequest +from fairscape_mds.crud.fairscape_response import FairscapeResponse +from fairscape_mds.crud.condensation import FairscapeCondensationRequest +from fairscape_mds.models.identifier import ( + MetadataTypeEnum, + StoredIdentifier, + PublicationStatusEnum, +) +from fairscape_mds.models.user import Permissions + +logger = logging.getLogger(__name__) + +# --------------------------------------------------------------------------- +# Constants +# --------------------------------------------------------------------------- + +MAX_SOFTWARE_BYTES = 50_000 # 50KB per software entity +CODE_EXTENSIONS = {".py", ".r", ".R", ".sh", ".pl", ".java", ".scala", ".jl", ".m", ".cpp", ".go", ".rs", ".ipynb"} +GITHUB_REPO_PATTERN = re.compile(r"https?://github\.com/([^/]+)/([^/]+?)(?:\.git)?(?:/.*)?$") +GITHUB_FILE_PATTERN = re.compile( + r"https?://github\.com/([^/]+)/([^/]+)/blob/([^/]+)/(.*)" +) + +# --------------------------------------------------------------------------- +# System Prompt -- Data Science Persona +# --------------------------------------------------------------------------- + +DATASCI_SYSTEM_PROMPT = """You are a senior data scientist and methodologist analyzing a single computation step from a scientific provenance graph (RO-Crate). + +You will receive: +- The computation's metadata (name, description, command) +- Input dataset metadata (names, descriptions, formats) +- Software source code used by the computation +- Output dataset metadata (names, descriptions, formats) + +Your task is to produce a structured annotation of this computation step. + +## Analysis Guidelines + +### Code Analysis (Deep) +- Libraries and frameworks: Note specific libraries and whether they are standard choices +- Data transformations: Trace transformations applied. Flag hidden assumptions +- Statistical methods: Evaluate appropriateness for the data type and research question +- Hardcoded values: Flag magic numbers and parameters that should be configurable +- Error handling: Note whether edge cases are handled + +### Methodology Assessment +- Data leakage: Check for test/validation information leaking into training +- Selection bias: Consider whether filtering steps introduce bias +- Reproducibility: Random seeds, version pinning, parameter documentation + +### Output Requirements +Return a structured annotation with: +- stepSummary: A clear description of what this computation step does and why +- codeAnalysis: For each software entity, provide a summary, key functions, and concerns +- inputSummaries: For each input dataset, describe its role +- outputSummaries: For each output dataset, describe what it contains +- concerns: List any methodological, statistical, or reproducibility concerns + +Be precise and evidence-based. Reference specific function names and parameter values. +Distinguish critical issues from minor improvements. +Acknowledge when methods are well-chosen.""" + + +SYNTHESIS_SYSTEM_PROMPT = """You are a senior data scientist synthesizing annotations from all computation steps in a scientific provenance graph (RO-Crate) into a graph-level summary. + +You will receive: +- The RO-Crate name and description +- Step-by-step annotations for each computation in the pipeline +- The pipeline's final outputs + +Your task is to produce: +1. executiveSummary: 3-5 sentences covering what the pipeline does, its analytical approach, and the most important observation +2. narrativeSummary: A forward-chronological story of the entire pipeline, starting from origin data and ending at final outputs +3. keyFindings: Bulleted list of important observations, prioritized by severity +4. concerns: Bulleted list of methodological, statistical, or reproducibility concerns across the whole pipeline + +Write as if explaining to a colleague. Be precise and evidence-based.""" + + +# --------------------------------------------------------------------------- +# Graph-level synthesis result model (just the fields we need from LLM) +# --------------------------------------------------------------------------- + +from pydantic import BaseModel, Field as PydanticField + + +class GraphSynthesisResult(BaseModel): + """Result model for the graph-level synthesis LLM call.""" + executiveSummary: str + narrativeSummary: str + keyFindings: List[str] = [] + concerns: List[str] = [] + + +# --------------------------------------------------------------------------- +# Helper functions +# --------------------------------------------------------------------------- + +def _extract_short_types(node: dict) -> list: + """Extract short EVI type names from a node's @type field.""" + raw = node.get("@type", []) + if isinstance(raw, str): + raw = [raw] + shorts = [] + for t in raw: + short = t.split("#")[-1] if "#" in t else t.split(":")[-1] if ":" in t else t + shorts.append(short) + return shorts + + +def _is_computation(node: dict) -> bool: + return "Computation" in _extract_short_types(node) + + +def _is_rocrate_root(node: dict) -> bool: + return "ROCrate" in _extract_short_types(node) + + +def _resolve_refs(field) -> list: + """Normalize a reference field to a list of @id strings.""" + if field is None: + return [] + if isinstance(field, str): + return [field] + if isinstance(field, dict): + return [field.get("@id", "")] if "@id" in field else [] + if isinstance(field, list): + result = [] + for item in field: + if isinstance(item, str): + result.append(item) + elif isinstance(item, dict) and "@id" in item: + result.append(item["@id"]) + return result + return [] + + +def _build_index(graph: list) -> dict: + """Build {`@id` -> node} index from @graph array.""" + index = {} + for node in graph: + node_id = node.get("@id") + if node_id: + index[node_id] = node + return index + + +# --------------------------------------------------------------------------- +# GitHub source code fetching +# --------------------------------------------------------------------------- + +def _fetch_github_file(owner: str, repo: str, branch: str, path: str) -> str: + """Fetch a single file from GitHub via raw URL.""" + raw_url = f"https://raw.githubusercontent.com/{owner}/{repo}/{branch}/{path}" + try: + resp = httpx.get(raw_url, timeout=15, follow_redirects=True) + resp.raise_for_status() + return resp.text + except Exception as e: + logger.warning(f"Failed to fetch {raw_url}: {e}") + return "" + + +def _fetch_github_repo_code(owner: str, repo: str, max_bytes: int = MAX_SOFTWARE_BYTES) -> str: + """Fetch code files from a GitHub repo up to max_bytes total.""" + try: + # Get default branch + api_url = f"https://api.github.com/repos/{owner}/{repo}" + resp = httpx.get(api_url, timeout=15, follow_redirects=True) + resp.raise_for_status() + default_branch = resp.json().get("default_branch", "main") + + # Get tree + tree_url = f"https://api.github.com/repos/{owner}/{repo}/git/trees/{default_branch}?recursive=1" + resp = httpx.get(tree_url, timeout=15, follow_redirects=True) + resp.raise_for_status() + tree = resp.json().get("tree", []) + + # Filter for code files + code_files = [] + for item in tree: + if item.get("type") != "blob": + continue + path = item.get("path", "") + ext = "." + path.rsplit(".", 1)[-1] if "." in path else "" + if ext.lower() in {e.lower() for e in CODE_EXTENSIONS}: + code_files.append(path) + + # Sort: prefer top-level files, then by name + code_files.sort(key=lambda p: (p.count("/"), p)) + + # Fetch files up to limit + collected = [] + total_bytes = 0 + for path in code_files: + if total_bytes >= max_bytes: + collected.append(f"\n--- [Truncated: reached {max_bytes} byte limit] ---\n") + break + content = _fetch_github_file(owner, repo, default_branch, path) + if content: + header = f"\n{'='*60}\n# FILE: {path}\n{'='*60}\n" + collected.append(header + content) + total_bytes += len(content.encode("utf-8")) + + return "\n".join(collected) + + except Exception as e: + logger.warning(f"Failed to fetch repo {owner}/{repo}: {e}") + return f"[Could not fetch repository code: {e}]" + + +def prefetch_software_code(content_url: str) -> str: + """Fetch source code from a contentUrl, handling GitHub URLs.""" + if not content_url: + return "[No contentUrl available]" + + # Check for GitHub file URL: github.com/owner/repo/blob/branch/path + file_match = GITHUB_FILE_PATTERN.match(content_url) + if file_match: + owner, repo, branch, path = file_match.groups() + code = _fetch_github_file(owner, repo, branch, path) + return code if code else f"[Could not fetch file from {content_url}]" + + # Check for GitHub repo URL: github.com/owner/repo + repo_match = GITHUB_REPO_PATTERN.match(content_url) + if repo_match: + owner, repo = repo_match.groups() + return _fetch_github_repo_code(owner, repo) + + # Non-GitHub HTTP URL + if content_url.startswith("http"): + return f"[External URL, not fetched: {content_url}]" + + return f"[Local/relative path: {content_url}]" + + +# --------------------------------------------------------------------------- +# CRUD Class +# --------------------------------------------------------------------------- + +class FairscapeInterpretationRequest(FairscapeRequest): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._condensation = FairscapeCondensationRequest(self.config) + + def _update_task(self, task_guid: str, updates: dict): + """Update the async task document in MongoDB.""" + self.config.asyncCollection.update_one( + {"guid": task_guid}, + {"$set": updates} + ) + + # ------------------------------------------------------------------ + # Step 1: Ensure condensed + # ------------------------------------------------------------------ + + def ensure_condensed(self, task_guid: str, rocrate_id: str) -> Tuple[list, str, dict]: + """Ensure the RO-Crate is condensed. Returns (graph_list, condensed_id, root_node). + + Checks: + 1. metadata.hasCondensedROCrate pointer -> fetch that doc + 2. evi:condensed: true on the crate itself -> it IS condensed + 3. Otherwise, trigger condensation synchronously + """ + self._update_task(task_guid, { + "current_step": "CONDENSING", + "status": "CONDENSING", + }) + + # Fetch the RO-Crate from MongoDB + entity = self.flexibleFind(rocrate_id) + if not entity: + raise ValueError(f"RO-Crate {rocrate_id} not found") + + metadata = entity.get("metadata", {}) + + # Case 1: Has pointer to condensed version + condensed_ref = metadata.get("hasCondensedROCrate") + if condensed_ref: + condensed_id = condensed_ref.get("@id") if isinstance(condensed_ref, dict) else condensed_ref + condensed_doc = self.flexibleFind(condensed_id) + if condensed_doc: + condensed_metadata = condensed_doc.get("metadata", {}) + graph = condensed_metadata.get("@graph", []) + if isinstance(graph, dict): + graph = list(graph.values()) + index = _build_index(graph) + root = next((n for n in graph if _is_rocrate_root(n)), {}) + return graph, condensed_id, root + + # Case 2: The crate itself is condensed + # Check the root node in the graph for evi:condensed + graph = metadata.get("@graph", []) + if isinstance(graph, dict): + graph = list(graph.values()) + + for node in graph: + if _is_rocrate_root(node): + if node.get("evi:condensed") is True: + return graph, rocrate_id, node + + # Case 3: Need to condense + logger.info(f"Condensing RO-Crate {rocrate_id}") + response = self._condensation.condense_rocrate( + rocrate_id=rocrate_id, + threshold=5, + max_member_ids=0, + owner_email="system@fairscape.org", + ) + + if not response.success: + raise RuntimeError(f"Condensation failed: {response.error}") + + # Fetch the newly created condensed doc + condensed_id = f"{rocrate_id}-condensed" + condensed_doc = self.flexibleFind(condensed_id) + if not condensed_doc: + raise RuntimeError(f"Condensed RO-Crate {condensed_id} not found after condensation") + + condensed_metadata = condensed_doc.get("metadata", {}) + graph = condensed_metadata.get("@graph", []) + if isinstance(graph, dict): + graph = list(graph.values()) + root = next((n for n in graph if _is_rocrate_root(n)), {}) + + self._update_task(task_guid, {"condensed_rocrate_id": condensed_id}) + return graph, condensed_id, root + + # ------------------------------------------------------------------ + # Step 2: Find computations + # ------------------------------------------------------------------ + + def find_computations(self, task_guid: str, graph: list) -> Tuple[List[dict], dict]: + """Find all Computation nodes in the graph. Returns (computations, index).""" + self._update_task(task_guid, { + "current_step": "TRAVERSING", + "status": "TRAVERSING", + }) + + index = _build_index(graph) + computations = [node for node in graph if _is_computation(node)] + + # Initialize computation_details for progress tracking + comp_details = [ + {"computation_id": c.get("@id", ""), "name": c.get("name", ""), "status": "pending"} + for c in computations + ] + self._update_task(task_guid, { + "total_computations": len(computations), + "computation_details": comp_details, + }) + + logger.info(f"Found {len(computations)} computation(s) in graph") + return computations, index + + # ------------------------------------------------------------------ + # Step 3: Pre-fetch software + # ------------------------------------------------------------------ + + def prefetch_all_software(self, task_guid: str, computations: list, index: dict) -> Dict[str, str]: + """Pre-fetch source code for all software referenced by computations. + Returns {software_id: source_code_text}. + """ + self._update_task(task_guid, { + "current_step": "PREFETCHING", + "status": "PREFETCHING", + }) + + software_cache: Dict[str, str] = {} + for comp in computations: + software_refs = _resolve_refs(comp.get("usedSoftware")) + for sw_id in software_refs: + if sw_id in software_cache: + continue + sw_node = index.get(sw_id, {}) + content_url = sw_node.get("contentUrl", "") + code = prefetch_software_code(content_url) + software_cache[sw_id] = code + logger.info(f"Pre-fetched software {sw_id}: {len(code)} chars") + + return software_cache + + # ------------------------------------------------------------------ + # Step 4: Annotate single computation + # ------------------------------------------------------------------ + + def _build_computation_prompt(self, computation: dict, software_cache: dict, index: dict) -> str: + """Build the prompt for a single computation annotation.""" + parts = [] + + # Computation metadata + parts.append("## Computation") + parts.append(f"**ID:** {computation.get('@id', 'unknown')}") + parts.append(f"**Name:** {computation.get('name', 'unnamed')}") + parts.append(f"**Description:** {computation.get('description', 'No description')}") + parts.append(f"**Command:** {computation.get('command', 'N/A')}") + parts.append(f"**Run By:** {computation.get('runBy', 'unknown')}") + parts.append(f"**Date Created:** {computation.get('dateCreated', 'unknown')}") + parts.append("") + + # Input datasets + input_refs = _resolve_refs(computation.get("usedDataset")) + if input_refs: + parts.append("## Input Datasets") + for ds_id in input_refs: + ds_node = index.get(ds_id, {}) + parts.append(f"- **{ds_node.get('name', ds_id)}** ({ds_node.get('format', 'unknown format')})") + parts.append(f" ID: {ds_id}") + parts.append(f" Description: {ds_node.get('description', 'No description')}") + if ds_node.get("keywords"): + parts.append(f" Keywords: {', '.join(ds_node['keywords']) if isinstance(ds_node['keywords'], list) else ds_node['keywords']}") + parts.append("") + + # Software and source code + software_refs = _resolve_refs(computation.get("usedSoftware")) + if software_refs: + parts.append("## Software") + for sw_id in software_refs: + sw_node = index.get(sw_id, {}) + parts.append(f"### {sw_node.get('name', sw_id)}") + parts.append(f"**ID:** {sw_id}") + parts.append(f"**Description:** {sw_node.get('description', 'No description')}") + parts.append(f"**Content URL:** {sw_node.get('contentUrl', 'N/A')}") + code = software_cache.get(sw_id, "") + if code and not code.startswith("["): + parts.append(f"\n**Source Code:**\n```\n{code}\n```") + else: + parts.append(f"\n**Source Code:** {code}") + parts.append("") + + # Output datasets + output_refs = _resolve_refs(computation.get("generated")) + if output_refs: + parts.append("## Output Datasets") + for ds_id in output_refs: + ds_node = index.get(ds_id, {}) + parts.append(f"- **{ds_node.get('name', ds_id)}** ({ds_node.get('format', 'unknown format')})") + parts.append(f" ID: {ds_id}") + parts.append(f" Description: {ds_node.get('description', 'No description')}") + parts.append("") + + return "\n".join(parts) + + def _annotate_single_computation( + self, + computation: dict, + software_cache: dict, + index: dict, + llm_model: str, + temperature: float, + ) -> AnnotatedComputation: + """Annotate a single computation using PydanticAI. Runs synchronously.""" + prompt = self._build_computation_prompt(computation, software_cache, index) + + agent = Agent( + llm_model, + result_type=AnnotatedComputation, + system_prompt=DATASCI_SYSTEM_PROMPT, + retries=2, + ) + + # Build the required fields that PydanticAI can't infer + comp_id = computation.get("@id", f"ark:59853/computation-{uuid.uuid4()}") + annotation_id = f"ark:59853/annotated-computation-{uuid.uuid4()}" + + # Run the agent synchronously + result = agent.run_sync(prompt) + annotated: AnnotatedComputation = result.data + + # Ensure required fields are set correctly + if not annotated.guid: + annotated.guid = annotation_id + if not annotated.annotates: + annotated.annotates = {"@id": comp_id} + annotated.llmModel = llm_model + annotated.llmTemperature = temperature + annotated.dateCreated = datetime.datetime.utcnow().isoformat() + + return annotated + + # ------------------------------------------------------------------ + # Step 4b: Parallel computation processing + # ------------------------------------------------------------------ + + def annotate_computations_parallel( + self, + task_guid: str, + computations: list, + software_cache: dict, + index: dict, + llm_model: str, + temperature: float, + max_workers: int = 4, + ) -> List[AnnotatedComputation]: + """Annotate all computations in parallel using ThreadPoolExecutor.""" + self._update_task(task_guid, { + "current_step": "PROMPTING", + "status": "PROMPTING", + }) + + results: List[AnnotatedComputation] = [] + errors: List[dict] = [] + + with ThreadPoolExecutor(max_workers=max_workers) as executor: + futures = { + executor.submit( + self._annotate_single_computation, + comp, software_cache, index, llm_model, temperature, + ): comp + for comp in computations + } + + for future in as_completed(futures): + comp = futures[future] + comp_id = comp.get("@id", "unknown") + + try: + annotated = future.result() + results.append(annotated) + + # Update per-computation status + self.config.asyncCollection.update_one( + {"guid": task_guid, "computation_details.computation_id": comp_id}, + {"$set": {"computation_details.$.status": "done"}} + ) + except Exception as e: + logger.error(f"Failed to annotate computation {comp_id}: {e}") + errors.append({"computation_id": comp_id, "error": str(e)}) + + self.config.asyncCollection.update_one( + {"guid": task_guid, "computation_details.computation_id": comp_id}, + {"$set": { + "computation_details.$.status": "error", + "computation_details.$.error": str(e), + }} + ) + + # Increment completed count + self.config.asyncCollection.update_one( + {"guid": task_guid}, + {"$inc": {"completed_computations": 1}} + ) + + if errors and not results: + raise RuntimeError(f"All {len(errors)} computation annotations failed. First error: {errors[0]['error']}") + + if errors: + logger.warning(f"{len(errors)} of {len(computations)} computation annotations failed") + + return results + + # ------------------------------------------------------------------ + # Step 5: Graph-level synthesis + # ------------------------------------------------------------------ + + def synthesize_graph( + self, + task_guid: str, + root_node: dict, + step_annotations: List[AnnotatedComputation], + llm_model: str, + temperature: float, + ) -> GraphSynthesisResult: + """Synthesize graph-level summary from all step annotations.""" + self._update_task(task_guid, { + "current_step": "SYNTHESIZING", + "status": "SYNTHESIZING", + }) + + # Build synthesis prompt + parts = [] + parts.append("## RO-Crate Overview") + parts.append(f"**Name:** {root_node.get('name', 'Unknown')}") + parts.append(f"**Description:** {root_node.get('description', 'No description')}") + parts.append(f"**Author:** {root_node.get('author', 'Unknown')}") + parts.append(f"**Keywords:** {root_node.get('keywords', '')}") + parts.append("") + + parts.append("## Step Annotations") + for i, ann in enumerate(step_annotations, 1): + parts.append(f"### Step {i}: {ann.annotates}") + parts.append(f"**Summary:** {ann.stepSummary}") + if ann.concerns: + parts.append(f"**Concerns:** {'; '.join(ann.concerns)}") + if ann.codeAnalysis: + for ca in ann.codeAnalysis: + parts.append(f"**Code ({ca.name or ca.software}):** {ca.summary}") + parts.append("") + + prompt = "\n".join(parts) + + agent = Agent( + llm_model, + result_type=GraphSynthesisResult, + system_prompt=SYNTHESIS_SYSTEM_PROMPT, + retries=2, + ) + + result = agent.run_sync(prompt) + return result.data + + # ------------------------------------------------------------------ + # Step 6: Build and store AnnotatedEvidenceGraph + # ------------------------------------------------------------------ + + def build_and_store( + self, + task_guid: str, + rocrate_id: str, + condensed_id: str, + graph: list, + step_annotations: List[AnnotatedComputation], + synthesis: GraphSynthesisResult, + llm_model: str, + temperature: float, + ) -> str: + """Assemble, validate, and store the AnnotatedEvidenceGraph.""" + self._update_task(task_guid, { + "current_step": "STORING", + "status": "STORING", + }) + + # Build the @graph dict: original entities + annotated computations + graph_dict = {} + for node in graph: + node_id = node.get("@id") + if node_id: + graph_dict[node_id] = node + + # Add annotated computations to graph + for ann in step_annotations: + ann_dict = ann.model_dump(by_alias=True, exclude_none=True, mode="json") + graph_dict[ann.guid] = ann_dict + + # Build step annotation refs + step_ann_refs = [{"@id": ann.guid} for ann in step_annotations] + + # Extract NAAN from rocrate_id for the new identifier + ark_match = re.match(r"ark:(\d+)/(.*)", rocrate_id) + if ark_match: + naan = ark_match.group(1) + postfix = ark_match.group(2) + aeg_id = f"ark:{naan}/annotated-eg-{postfix}" + else: + aeg_id = f"ark:59853/annotated-eg-{uuid.uuid4()}" + + now = datetime.datetime.utcnow().isoformat() + + # Build the AnnotatedEvidenceGraph + aeg_data = { + "@id": aeg_id, + "name": f"Annotated Evidence Graph: {graph_dict.get(rocrate_id, {}).get('name', rocrate_id)}", + "description": f"AI-mediated interpretation of RO-Crate {rocrate_id}", + "author": llm_model, + "evi:annotates": {"@id": rocrate_id}, + "@graph": graph_dict, + "evi:executiveSummary": synthesis.executiveSummary, + "evi:narrativeSummary": synthesis.narrativeSummary, + "evi:keyFindings": synthesis.keyFindings, + "evi:concerns": synthesis.concerns, + "evi:stepAnnotations": step_ann_refs, + "evi:llmModel": llm_model, + "evi:llmTemperature": temperature, + "dateCreated": now, + } + + aeg = AnnotatedEvidenceGraph.model_validate(aeg_data) + + # Store as StoredIdentifier + permissions = Permissions(owner="system@fairscape.org", group="", acl=[]) + stored = StoredIdentifier.model_validate({ + "@id": aeg_id, + "@type": "AnnotatedEvidenceGraph", + "metadata": aeg.model_dump(by_alias=True, mode="json"), + "permissions": permissions.model_dump(), + "publicationStatus": PublicationStatusEnum.DRAFT, + "dateCreated": datetime.datetime.utcnow(), + "dateModified": datetime.datetime.utcnow(), + "distribution": None, + }) + + self.config.identifierCollection.insert_one( + stored.model_dump(by_alias=True, mode="json") + ) + + # Update original RO-Crate with pointer + self.config.identifierCollection.update_one( + {"@id": rocrate_id}, + {"$set": {"metadata.hasAnnotatedEvidenceGraph": {"@id": aeg_id}}} + ) + + self._update_task(task_guid, {"annotated_evidence_graph_id": aeg_id}) + logger.info(f"Stored AnnotatedEvidenceGraph {aeg_id}") + return aeg_id + + # ------------------------------------------------------------------ + # Main orchestrator + # ------------------------------------------------------------------ + + def interpret_rocrate(self, task_guid: str) -> str: + """Main pipeline orchestrator. Returns the AnnotatedEvidenceGraph @id.""" + # Load task config + task_doc = self.config.asyncCollection.find_one({"guid": task_guid}) + if not task_doc: + raise ValueError(f"Task {task_guid} not found") + + rocrate_id = task_doc["rocrate_id"] + llm_model = task_doc.get("llm_model", "google-gla:gemini-2.5-flash") + temperature = task_doc.get("llm_temperature", 0.2) + + self._update_task(task_guid, { + "status": "PROCESSING", + "time_started": datetime.datetime.utcnow(), + }) + + try: + # Step 1: Ensure condensed + graph, condensed_id, root_node = self.ensure_condensed(task_guid, rocrate_id) + + # Step 2: Find computations + computations, index = self.find_computations(task_guid, graph) + + if not computations: + raise ValueError(f"No Computation nodes found in RO-Crate {rocrate_id}") + + # Step 3: Pre-fetch software + software_cache = self.prefetch_all_software(task_guid, computations, index) + + # Step 4: Annotate computations in parallel + step_annotations = self.annotate_computations_parallel( + task_guid, computations, software_cache, index, llm_model, temperature, + ) + + # Step 5: Graph-level synthesis + synthesis = self.synthesize_graph( + task_guid, root_node, step_annotations, llm_model, temperature, + ) + + # Step 6: Build and store + aeg_id = self.build_and_store( + task_guid, rocrate_id, condensed_id, graph, + step_annotations, synthesis, llm_model, temperature, + ) + + # Success + self._update_task(task_guid, { + "status": "SUCCESS", + "current_step": "COMPLETE", + "time_finished": datetime.datetime.utcnow(), + }) + + return aeg_id + + except Exception as e: + import traceback + logger.error(f"Interpretation failed for task {task_guid}: {e}") + traceback.print_exc() + self._update_task(task_guid, { + "status": "FAILURE", + "error": {"message": str(e), "error_type": type(e).__name__}, + "time_finished": datetime.datetime.utcnow(), + }) + raise diff --git a/mds/src/fairscape_mds/main.py b/mds/src/fairscape_mds/main.py index 1343840..99bb64e 100644 --- a/mds/src/fairscape_mds/main.py +++ b/mds/src/fairscape_mds/main.py @@ -13,6 +13,7 @@ from fairscape_mds.routers.llm_assist import router as llm_assist_router from fairscape_mds.routers.github import router as github_router from fairscape_mds.routers.mlmodel import mlModelRouter +from fairscape_mds.routers.interpretation import router as interpretation_router from fairscape_mds.core.logging import requestLogger from fairscape_mds.core.config import settings @@ -77,6 +78,7 @@ def LogRequestMiddleware( app.include_router(mlModelRouter) app.include_router(llm_assist_router) app.include_router(github_router) +app.include_router(interpretation_router) @app.get("/healthz") diff --git a/mds/src/fairscape_mds/models/interpretation.py b/mds/src/fairscape_mds/models/interpretation.py new file mode 100644 index 0000000..d640ace --- /dev/null +++ b/mds/src/fairscape_mds/models/interpretation.py @@ -0,0 +1,47 @@ +from pydantic import BaseModel, Field +from typing import Optional, Dict, Any, List +import datetime + + +class ComputationProgress(BaseModel): + """Track progress of a single computation annotation.""" + computation_id: str + name: str = "" + status: str = "pending" # pending, processing, done, error + error: Optional[str] = None + + +class InterpretationTask(BaseModel): + guid: str = Field(alias="guid") + task_type: str = Field(default="InterpretROCrate") + rocrate_id: str + owner_email: str + status: str = Field(default="PENDING") + + # Pipeline progress + current_step: str = Field(default="PENDING") + # Steps: CONDENSING, TRAVERSING, PREFETCHING, PROMPTING, SYNTHESIZING, STORING + + # Computation-level progress (for parallel step) + total_computations: int = 0 + completed_computations: int = 0 + computation_details: List[Dict[str, Any]] = Field(default_factory=list) + + # Results + condensed_rocrate_id: Optional[str] = None + annotated_evidence_graph_id: Optional[str] = None + + # Timing + time_created: datetime.datetime = Field(default_factory=datetime.datetime.utcnow) + time_started: Optional[datetime.datetime] = None + time_finished: Optional[datetime.datetime] = None + + # Config + llm_model: str = "gemini-2.5-flash" + llm_temperature: float = 0.2 + persona: str = "datasci" + + error: Optional[Dict[str, Any]] = None + + class Config: + populate_by_name = True diff --git a/mds/src/fairscape_mds/routers/interpretation.py b/mds/src/fairscape_mds/routers/interpretation.py new file mode 100644 index 0000000..08c6724 --- /dev/null +++ b/mds/src/fairscape_mds/routers/interpretation.py @@ -0,0 +1,204 @@ +"""Router for AI-mediated interpretation of RO-Crates.""" + +from typing import Annotated +from fastapi import APIRouter, Depends, Query +from fastapi.responses import JSONResponse + +import uuid +import datetime + +from fairscape_mds.models.user import UserWriteModel +from fairscape_mds.core.config import appConfig +from fairscape_mds.deps import getCurrentUser +from fairscape_mds.crud.fairscape_request import flexible_ark_query + +router = APIRouter( + prefix="/interpretation", + tags=["Interpretation"], +) + + +def _flexible_find(ark_id: str): + """Look up an entity by ARK, tolerating dash/slash variants.""" + result = appConfig.identifierCollection.find_one({"@id": ark_id}, {"_id": False}) + if result: + return result + query = flexible_ark_query(ark_id) + if query: + result = appConfig.identifierCollection.find_one(query, {"_id": False}) + return result + + +# --------------------------------------------------------------------------- +# POST /interpretation/ark:{NAAN}/{postfix} -- trigger interpretation +# --------------------------------------------------------------------------- + +@router.post( + "/ark:/{NAAN}/{postfix}", + summary="Trigger AI-mediated interpretation of an RO-Crate", + status_code=202, +) +@router.post( + "/ark:{NAAN}/{postfix}", + summary="Trigger AI-mediated interpretation of an RO-Crate", + status_code=202, +) +def trigger_interpretation( + currentUser: Annotated[UserWriteModel, Depends(getCurrentUser)], + NAAN: str, + postfix: str, + llm_model: str = Query(default="google-gla:gemini-2.5-flash", description="PydanticAI model string"), + temperature: float = Query(default=0.2, ge=0.0, le=2.0, description="LLM temperature"), + persona: str = Query(default="datasci", description="Interpretation persona"), + force: bool = Query(default=False, description="Force re-interpretation if one already exists"), +): + """Trigger async AI-mediated interpretation of an RO-Crate. + Ensures condensation, annotates each computation, synthesizes a graph-level + summary, and stores the resulting AnnotatedEvidenceGraph.""" + + from fairscape_mds.worker import interpret_rocrate_task + + ark_id = f"ark:{NAAN}/{postfix}" + + entity = _flexible_find(ark_id) + if not entity: + return JSONResponse(status_code=404, content={"error": f"Entity {ark_id} not found"}) + + entity_type = entity.get("@type", []) + if isinstance(entity_type, str): + entity_type = [entity_type] + is_rocrate = any("ROCrate" in str(t) for t in entity_type) + if not is_rocrate: + return JSONResponse(status_code=400, content={"error": "Entity is not an RO-Crate"}) + + # Check for existing AnnotatedEvidenceGraph + if not force: + existing_aeg = entity.get("metadata", {}).get("hasAnnotatedEvidenceGraph") + if existing_aeg: + return JSONResponse( + status_code=200, + content={ + "message": "Annotated Evidence Graph already exists. Use force=true to re-interpret.", + "annotated_evidence_graph_id": existing_aeg.get("@id"), + } + ) + + # Check for in-progress task + task_doc = appConfig.asyncCollection.find_one({ + "task_type": "InterpretROCrate", + "rocrate_id": ark_id, + "status": {"$nin": ["SUCCESS", "FAILURE"]}, + }, {"_id": 0}) + + if task_doc: + return JSONResponse( + status_code=202, + content={ + "message": "Interpretation already in progress", + "task_id": task_doc["guid"], + "status": task_doc.get("status"), + "current_step": task_doc.get("current_step"), + "status_endpoint": f"/interpretation/status/{task_doc['guid']}", + } + ) + + # Create async task + task_guid = str(uuid.uuid4()) + task_data = { + "guid": task_guid, + "task_type": "InterpretROCrate", + "rocrate_id": ark_id, + "owner_email": currentUser.email, + "status": "PENDING", + "current_step": "PENDING", + "total_computations": 0, + "completed_computations": 0, + "computation_details": [], + "condensed_rocrate_id": None, + "annotated_evidence_graph_id": None, + "llm_model": llm_model, + "llm_temperature": temperature, + "persona": persona, + "time_created": datetime.datetime.utcnow(), + "error": None, + } + + appConfig.asyncCollection.insert_one(task_data) + + interpret_rocrate_task.delay( + task_guid=task_guid, + rocrate_id=ark_id, + llm_model=llm_model, + temperature=temperature, + ) + + return JSONResponse( + status_code=202, + content={ + "message": "Interpretation initiated", + "task_id": task_guid, + "status_endpoint": f"/interpretation/status/{task_guid}", + } + ) + + +# --------------------------------------------------------------------------- +# GET /interpretation/status/{task_id} -- check progress +# --------------------------------------------------------------------------- + +@router.get( + "/status/{task_id}", + summary="Get status of an interpretation task", +) +def get_interpretation_status(task_id: str): + """Check the status of an async interpretation task. + Includes granular step tracking and per-computation progress.""" + task_doc = appConfig.asyncCollection.find_one( + {"guid": task_id}, {"_id": 0} + ) + + if not task_doc: + return JSONResponse(status_code=404, content={"error": "Task not found"}) + + # Serialize datetimes for JSON + for key in ("time_created", "time_started", "time_finished"): + if task_doc.get(key) and hasattr(task_doc[key], "isoformat"): + task_doc[key] = task_doc[key].isoformat() + + return JSONResponse(status_code=200, content=task_doc) + + +# --------------------------------------------------------------------------- +# GET /interpretation/result/ark:{NAAN}/{postfix} -- get result +# --------------------------------------------------------------------------- + +@router.get( + "/result/ark:/{NAAN}/{postfix}", + summary="Get the AnnotatedEvidenceGraph for an RO-Crate", +) +@router.get( + "/result/ark:{NAAN}/{postfix}", + summary="Get the AnnotatedEvidenceGraph for an RO-Crate", +) +def get_interpretation_result(NAAN: str, postfix: str): + """Return the AnnotatedEvidenceGraph if it exists.""" + ark_id = f"ark:{NAAN}/{postfix}" + + entity = _flexible_find(ark_id) + if not entity: + return JSONResponse(status_code=404, content={"error": f"Entity {ark_id} not found"}) + + aeg_ref = entity.get("metadata", {}).get("hasAnnotatedEvidenceGraph") + if not aeg_ref: + return JSONResponse( + status_code=404, + content={"error": "No AnnotatedEvidenceGraph exists for this RO-Crate. Trigger interpretation first."} + ) + + aeg_id = aeg_ref.get("@id") if isinstance(aeg_ref, dict) else aeg_ref + aeg_doc = _flexible_find(aeg_id) + if not aeg_doc: + return JSONResponse(status_code=404, content={"error": f"AnnotatedEvidenceGraph {aeg_id} not found"}) + + metadata = aeg_doc.get("metadata", {}) + return JSONResponse(status_code=200, content=metadata) diff --git a/mds/src/fairscape_mds/tests/crud/test_interpretation.py b/mds/src/fairscape_mds/tests/crud/test_interpretation.py new file mode 100644 index 0000000..f546d39 --- /dev/null +++ b/mds/src/fairscape_mds/tests/crud/test_interpretation.py @@ -0,0 +1,412 @@ +"""Tests for the AI-mediated interpretation pipeline. + +Uses a minimal condensed RO-Crate fixture, mongomock for DB isolation, +and PydanticAI TestModel to avoid real LLM calls. +""" + +import datetime +import pytest +from unittest.mock import patch, MagicMock + +from fairscape_mds.crud.interpretation import ( + FairscapeInterpretationRequest, + _build_index, + _is_computation, + _is_rocrate_root, + _resolve_refs, + prefetch_software_code, + GraphSynthesisResult, +) +from fairscape_models.annotated_computation import AnnotatedComputation +from fairscape_models.annotated_evidence_graph import AnnotatedEvidenceGraph + + +# --------------------------------------------------------------------------- +# Minimal condensed RO-Crate fixture +# --------------------------------------------------------------------------- + +MINIMAL_CONDENSED_GRAPH = [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "conformsTo": {"@id": "https://w3id.org/ro/crate/1.2-DRAFT"}, + "about": {"@id": "ark:59853/rocrate-test-pipeline"}, + }, + { + "@id": "ark:59853/rocrate-test-pipeline", + "@type": ["Dataset", "https://w3id.org/EVI#ROCrate"], + "name": "Test Pipeline RO-Crate", + "description": "A minimal test pipeline for interpretation tests", + "author": "Test Author", + "keywords": ["test", "pipeline"], + "evi:condensed": True, + "evi:condensationThreshold": 5, + "evi:originalEntityCount": 20, + "evi:condensedEntityCount": 8, + "evi:outputs": [{"@id": "ark:59853/dataset-output-final"}], + "hasPart": [ + {"@id": "ark:59853/dataset-input-raw"}, + {"@id": "ark:59853/software-preprocess"}, + {"@id": "ark:59853/computation-step1"}, + {"@id": "ark:59853/dataset-intermediate"}, + {"@id": "ark:59853/software-analyze"}, + {"@id": "ark:59853/computation-step2"}, + {"@id": "ark:59853/dataset-output-final"}, + ], + }, + { + "@id": "ark:59853/dataset-input-raw", + "@type": ["Dataset", "https://w3id.org/EVI#Dataset"], + "name": "Raw Input Data", + "description": "Raw experimental measurements", + "format": ".csv", + "author": "Jane Scientist", + }, + { + "@id": "ark:59853/software-preprocess", + "@type": ["SoftwareSourceCode", "https://w3id.org/EVI#Software"], + "name": "preprocess.py", + "description": "Data preprocessing script", + "contentUrl": "https://github.com/test-org/test-repo/blob/main/preprocess.py", + "format": ".py", + }, + { + "@id": "ark:59853/computation-step1", + "@type": ["https://w3id.org/EVI#Computation"], + "name": "Preprocessing Step", + "description": "Clean and normalize raw data", + "command": "python preprocess.py", + "runBy": "researcher@example.org", + "dateCreated": "2025-01-15T10:00:00", + "usedSoftware": [{"@id": "ark:59853/software-preprocess"}], + "usedDataset": [{"@id": "ark:59853/dataset-input-raw"}], + "generated": [{"@id": "ark:59853/dataset-intermediate"}], + }, + { + "@id": "ark:59853/dataset-intermediate", + "@type": ["Dataset", "https://w3id.org/EVI#Dataset"], + "name": "Preprocessed Data", + "description": "Cleaned and normalized dataset", + "format": ".parquet", + "generatedBy": [{"@id": "ark:59853/computation-step1"}], + }, + { + "@id": "ark:59853/software-analyze", + "@type": ["SoftwareSourceCode", "https://w3id.org/EVI#Software"], + "name": "analyze.py", + "description": "Statistical analysis script", + "contentUrl": "https://github.com/test-org/test-repo/blob/main/analyze.py", + "format": ".py", + }, + { + "@id": "ark:59853/computation-step2", + "@type": ["https://w3id.org/EVI#Computation"], + "name": "Analysis Step", + "description": "Perform statistical analysis on preprocessed data", + "command": "python analyze.py", + "runBy": "researcher@example.org", + "dateCreated": "2025-01-15T11:00:00", + "usedSoftware": [{"@id": "ark:59853/software-analyze"}], + "usedDataset": [{"@id": "ark:59853/dataset-intermediate"}], + "generated": [{"@id": "ark:59853/dataset-output-final"}], + }, + { + "@id": "ark:59853/dataset-output-final", + "@type": ["Dataset", "https://w3id.org/EVI#Dataset"], + "name": "Final Analysis Results", + "description": "Statistical analysis output", + "format": ".csv", + "generatedBy": [{"@id": "ark:59853/computation-step2"}], + }, +] + + +# --------------------------------------------------------------------------- +# Helper tests +# --------------------------------------------------------------------------- + + +class TestHelpers: + def test_build_index(self): + index = _build_index(MINIMAL_CONDENSED_GRAPH) + assert "ark:59853/rocrate-test-pipeline" in index + assert "ark:59853/computation-step1" in index + assert len(index) == 9 # including ro-crate-metadata.json + + def test_is_computation(self): + index = _build_index(MINIMAL_CONDENSED_GRAPH) + assert _is_computation(index["ark:59853/computation-step1"]) + assert _is_computation(index["ark:59853/computation-step2"]) + assert not _is_computation(index["ark:59853/dataset-input-raw"]) + assert not _is_computation(index["ark:59853/rocrate-test-pipeline"]) + + def test_is_rocrate_root(self): + index = _build_index(MINIMAL_CONDENSED_GRAPH) + assert _is_rocrate_root(index["ark:59853/rocrate-test-pipeline"]) + assert not _is_rocrate_root(index["ark:59853/computation-step1"]) + + def test_resolve_refs_list_of_dicts(self): + refs = _resolve_refs([{"@id": "ark:1"}, {"@id": "ark:2"}]) + assert refs == ["ark:1", "ark:2"] + + def test_resolve_refs_single_dict(self): + refs = _resolve_refs({"@id": "ark:1"}) + assert refs == ["ark:1"] + + def test_resolve_refs_none(self): + assert _resolve_refs(None) == [] + + def test_resolve_refs_string(self): + assert _resolve_refs("ark:1") == ["ark:1"] + + +class TestFindComputations: + """Test computation finding without DB.""" + + def test_finds_all_computations(self): + index = _build_index(MINIMAL_CONDENSED_GRAPH) + computations = [node for node in MINIMAL_CONDENSED_GRAPH if _is_computation(node)] + assert len(computations) == 2 + comp_ids = {c["@id"] for c in computations} + assert "ark:59853/computation-step1" in comp_ids + assert "ark:59853/computation-step2" in comp_ids + + +class TestPromptBuilding: + """Test prompt construction for computation annotation.""" + + def setup_method(self): + """Create a mock config for FairscapeInterpretationRequest.""" + self.mock_config = MagicMock() + self.mock_config.identifierCollection = MagicMock() + self.mock_config.asyncCollection = MagicMock() + self.request = FairscapeInterpretationRequest(self.mock_config) + + def test_build_computation_prompt(self): + index = _build_index(MINIMAL_CONDENSED_GRAPH) + computation = index["ark:59853/computation-step1"] + software_cache = {"ark:59853/software-preprocess": "import pandas as pd\ndf = pd.read_csv('input.csv')"} + + prompt = self.request._build_computation_prompt(computation, software_cache, index) + + assert "Preprocessing Step" in prompt + assert "python preprocess.py" in prompt + assert "Raw Input Data" in prompt + assert "import pandas" in prompt + assert "Preprocessed Data" in prompt + + def test_build_prompt_missing_software(self): + index = _build_index(MINIMAL_CONDENSED_GRAPH) + computation = index["ark:59853/computation-step1"] + software_cache = {} # empty + + prompt = self.request._build_computation_prompt(computation, software_cache, index) + + # Should still work, just no source code section + assert "Preprocessing Step" in prompt + + +class TestEnsureCondensed: + """Test the ensure_condensed logic.""" + + def setup_method(self): + self.mock_config = MagicMock() + self.mock_config.identifierCollection = MagicMock() + self.mock_config.asyncCollection = MagicMock() + self.request = FairscapeInterpretationRequest(self.mock_config) + + def test_already_condensed_crate(self): + """When the crate itself has evi:condensed: true.""" + self.mock_config.identifierCollection.find_one.return_value = { + "@id": "ark:59853/rocrate-test", + "@type": ["Dataset", "https://w3id.org/EVI#ROCrate"], + "metadata": { + "@graph": MINIMAL_CONDENSED_GRAPH, + } + } + + graph, condensed_id, root = self.request.ensure_condensed("task-123", "ark:59853/rocrate-test") + + assert len(graph) == len(MINIMAL_CONDENSED_GRAPH) + assert condensed_id == "ark:59853/rocrate-test" + assert root.get("evi:condensed") is True + + def test_has_condensed_pointer(self): + """When the crate has hasCondensedROCrate pointer.""" + # First call: get original crate + # Second call: get condensed crate + self.mock_config.identifierCollection.find_one.side_effect = [ + { + "@id": "ark:59853/rocrate-test", + "@type": ["Dataset", "https://w3id.org/EVI#ROCrate"], + "metadata": { + "hasCondensedROCrate": {"@id": "ark:59853/rocrate-test-condensed"}, + "@graph": [], + } + }, + { + "@id": "ark:59853/rocrate-test-condensed", + "metadata": { + "@graph": MINIMAL_CONDENSED_GRAPH, + } + }, + ] + + graph, condensed_id, root = self.request.ensure_condensed("task-123", "ark:59853/rocrate-test") + + assert len(graph) == len(MINIMAL_CONDENSED_GRAPH) + + def test_not_found_raises(self): + """When the RO-Crate doesn't exist.""" + self.mock_config.identifierCollection.find_one.return_value = None + + with pytest.raises(ValueError, match="not found"): + self.request.ensure_condensed("task-123", "ark:59853/nonexistent") + + +class TestPrefetchSoftware: + """Test software pre-fetching with mocked HTTP.""" + + @patch("fairscape_mds.crud.interpretation.httpx.get") + def test_github_file_url(self, mock_get): + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.text = "import pandas as pd\nprint('hello')" + mock_response.raise_for_status = MagicMock() + mock_get.return_value = mock_response + + code = prefetch_software_code("https://github.com/owner/repo/blob/main/script.py") + assert "import pandas" in code + + def test_no_content_url(self): + code = prefetch_software_code("") + assert "No contentUrl" in code + + def test_non_github_http(self): + code = prefetch_software_code("https://example.com/script.py") + assert "External URL" in code + + def test_local_path(self): + code = prefetch_software_code("scripts/preprocess.py") + assert "Local/relative" in code + + +class TestAnnotatedComputationModel: + """Test that AnnotatedComputation can be constructed correctly.""" + + def test_create_annotated_computation(self): + ac = AnnotatedComputation.model_validate({ + "@id": "ark:59853/annotated-computation-test", + "name": "Test Annotation", + "description": "Test annotation description", + "author": "gemini-2.5-flash", + "evi:annotates": {"@id": "ark:59853/computation-step1"}, + "evi:stepSummary": "This step preprocesses raw data by cleaning and normalizing it.", + "evi:codeAnalysis": [ + { + "software": {"@id": "ark:59853/software-preprocess"}, + "name": "preprocess.py", + "summary": "Reads CSV, drops NaN, normalizes columns", + "keyFunctions": ["clean_data", "normalize"], + "concerns": ["No logging of dropped rows"], + } + ], + "evi:inputSummaries": [ + { + "dataset": {"@id": "ark:59853/dataset-input-raw"}, + "name": "Raw Input Data", + "role": "Primary input", + "description": "Raw experimental measurements", + } + ], + "evi:outputSummaries": [ + { + "dataset": {"@id": "ark:59853/dataset-intermediate"}, + "name": "Preprocessed Data", + "role": "Cleaned output", + } + ], + "evi:concerns": ["No random seed set"], + "evi:llmModel": "gemini-2.5-flash", + "evi:llmTemperature": 0.2, + "dateCreated": "2025-01-15T12:00:00", + }) + + assert ac.stepSummary == "This step preprocesses raw data by cleaning and normalizing it." + assert len(ac.codeAnalysis) == 1 + assert ac.wasDerivedFrom == [{"@id": "ark:59853/computation-step1"}] + assert ac.wasAttributedTo == ["gemini-2.5-flash"] + + +class TestAnnotatedEvidenceGraphModel: + """Test that AnnotatedEvidenceGraph can be constructed correctly.""" + + def test_create_annotated_evidence_graph(self): + graph_dict = {node["@id"]: node for node in MINIMAL_CONDENSED_GRAPH if "@id" in node} + + aeg = AnnotatedEvidenceGraph.model_validate({ + "@id": "ark:59853/annotated-eg-test", + "name": "Test Annotated Evidence Graph", + "description": "Test AEG", + "author": "gemini-2.5-flash", + "evi:annotates": {"@id": "ark:59853/rocrate-test-pipeline"}, + "@graph": graph_dict, + "evi:executiveSummary": "This pipeline preprocesses and analyzes experimental data.", + "evi:narrativeSummary": "The pipeline starts with raw data and produces analysis results.", + "evi:keyFindings": ["Data is properly normalized", "No random seeds used"], + "evi:concerns": ["Reproducibility could be improved"], + "evi:stepAnnotations": [ + {"@id": "ark:59853/annotated-computation-1"}, + {"@id": "ark:59853/annotated-computation-2"}, + ], + "evi:llmModel": "gemini-2.5-flash", + "evi:llmTemperature": 0.2, + "dateCreated": "2025-01-15T12:00:00", + }) + + assert aeg.executiveSummary.startswith("This pipeline") + assert len(aeg.keyFindings) == 2 + assert len(aeg.graph) == len(graph_dict) + assert aeg.wasDerivedFrom == [{"@id": "ark:59853/rocrate-test-pipeline"}] + + +class TestGraphSynthesisResult: + """Test the synthesis result model.""" + + def test_create_synthesis_result(self): + result = GraphSynthesisResult( + executiveSummary="The pipeline processes data.", + narrativeSummary="Starting from raw data, the pipeline...", + keyFindings=["Finding 1", "Finding 2"], + concerns=["Concern 1"], + ) + assert result.executiveSummary == "The pipeline processes data." + assert len(result.keyFindings) == 2 + + +class TestStatusTracking: + """Test that status updates are called correctly.""" + + def setup_method(self): + self.mock_config = MagicMock() + self.mock_config.identifierCollection = MagicMock() + self.mock_config.asyncCollection = MagicMock() + self.request = FairscapeInterpretationRequest(self.mock_config) + + def test_update_task(self): + self.request._update_task("task-123", {"status": "PROCESSING", "current_step": "CONDENSING"}) + self.mock_config.asyncCollection.update_one.assert_called_once_with( + {"guid": "task-123"}, + {"$set": {"status": "PROCESSING", "current_step": "CONDENSING"}} + ) + + def test_find_computations_updates_status(self): + self.request.find_computations("task-123", MINIMAL_CONDENSED_GRAPH) + + # Should have been called to update TRAVERSING status and computation details + calls = self.mock_config.asyncCollection.update_one.call_args_list + assert len(calls) >= 2 # at least status update + computation details update + + # Check that total_computations was set + last_call_args = calls[-1][0][1]["$set"] + assert last_call_args["total_computations"] == 2 diff --git a/mds/src/fairscape_mds/worker.py b/mds/src/fairscape_mds/worker.py index 301abf6..29feee2 100644 --- a/mds/src/fairscape_mds/worker.py +++ b/mds/src/fairscape_mds/worker.py @@ -17,6 +17,7 @@ from fairscape_mds.crud.AIReady import FairscapeAIReadyScoreRequest from fairscape_mds.crud.llm_assist import FairscapeLLMAssistRequest from fairscape_mds.crud.condensation import FairscapeCondensationRequest +from fairscape_mds.crud.interpretation import FairscapeInterpretationRequest from fairscape_models.conversion.models.AIReady import AIReadyScore from fairscape_models.conversion.mapping.AIReady import ( @@ -30,6 +31,7 @@ llmAssistRequests = FairscapeLLMAssistRequest(appConfig) identifierRequestFactory = IdentifierRequest(appConfig) condensationRequests = FairscapeCondensationRequest(appConfig) +interpretationRequests = FairscapeInterpretationRequest(appConfig) # add support for logfire worker token @worker_init.connect() @@ -351,6 +353,34 @@ def process_llm_assist_task(self, task_guid: str): ) return {"status": "FAILURE", "error": error_msg} +@celeryApp.task(name='fairscape_mds.worker.interpret_rocrate_task', bind=True) +def interpret_rocrate_task(self, task_guid: str, rocrate_id: str, llm_model: str = "google-gla:gemini-2.5-flash", temperature: float = 0.2): + print(f"Starting Interpretation Task: {task_guid} for {rocrate_id}") + + try: + aeg_id = interpretationRequests.interpret_rocrate(task_guid) + + print(f"Successfully interpreted ROCrate for Task GUID {task_guid}: {aeg_id}") + return {"status": "SUCCESS", "annotated_evidence_graph_id": aeg_id} + + except Exception as e: + import traceback + error_msg = f"Unexpected error in interpret_rocrate_task: {str(e)}" + print(error_msg) + traceback.print_exc() + + # The CRUD class already updates the task to FAILURE, but ensure it's set + appConfig.asyncCollection.update_one( + {"guid": task_guid}, + {"$set": { + "status": "FAILURE", + "error": {"message": "An unexpected error occurred", "details": str(e)}, + "time_finished": datetime.datetime.utcnow() + }} + ) + return {"status": "FAILURE", "error": {"message": "An unexpected server error occurred"}} + + if __name__ == '__main__': args = ['worker', '--loglevel=INFO'] celeryApp.worker_main(argv=args) \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 4dc0a7e..6054f6a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,6 +44,7 @@ dependencies = [ "logfire[celery,fastapi]>=4.15.1", "PyYaml==6.0.1", "PyGithub", + "pydantic-ai[google]>=0.1.0", ] requires-python = ">=3.9" From 3908679870459b904ec8f0232350739d07ec04ed Mon Sep 17 00:00:00 2001 From: jniestroy Date: Thu, 12 Mar 2026 12:53:54 -0400 Subject: [PATCH 05/27] copy models --- mds/src/fairscape_mds/crud/interpretation.py | 4 +- .../models/annotated_computation.py | 78 +++++++++++++++++++ .../models/annotated_evidence_graph.py | 62 +++++++++++++++ .../tests/crud/test_interpretation.py | 4 +- pyproject.toml | 2 +- 5 files changed, 145 insertions(+), 5 deletions(-) create mode 100644 mds/src/fairscape_mds/models/annotated_computation.py create mode 100644 mds/src/fairscape_mds/models/annotated_evidence_graph.py diff --git a/mds/src/fairscape_mds/crud/interpretation.py b/mds/src/fairscape_mds/crud/interpretation.py index e29bc21..649217f 100644 --- a/mds/src/fairscape_mds/crud/interpretation.py +++ b/mds/src/fairscape_mds/crud/interpretation.py @@ -17,8 +17,8 @@ import httpx from pydantic_ai import Agent -from fairscape_models.annotated_computation import AnnotatedComputation, CodeAnalysis, DatasetSummary -from fairscape_models.annotated_evidence_graph import AnnotatedEvidenceGraph +from fairscape_mds.models.annotated_computation import AnnotatedComputation, CodeAnalysis, DatasetSummary +from fairscape_mds.models.annotated_evidence_graph import AnnotatedEvidenceGraph from fairscape_mds.crud.fairscape_request import FairscapeRequest from fairscape_mds.crud.fairscape_response import FairscapeResponse diff --git a/mds/src/fairscape_mds/models/annotated_computation.py b/mds/src/fairscape_mds/models/annotated_computation.py new file mode 100644 index 0000000..2801262 --- /dev/null +++ b/mds/src/fairscape_mds/models/annotated_computation.py @@ -0,0 +1,78 @@ +"""AnnotatedComputation model -- local copy for Docker builds that use +an older fairscape_models release without this module. + +Copied from fairscape_models/fairscape_models/annotated_computation.py +with imports adjusted to use fairscape_models base classes that ARE +present in the published package. +""" + +from pydantic import BaseModel, Field, ConfigDict, model_validator +from typing import Optional, List, Union, Dict, Any + +# These base classes exist in the published fairscape_models package +from fairscape_models.fairscape_base import IdentifierValue +from fairscape_models.digital_object import DigitalObject + +ANNOTATED_COMPUTATION_TYPE = "AnnotatedComputation" + + +class CodeAnalysis(BaseModel): + """Analysis of a software entity used in the computation.""" + model_config = ConfigDict(extra="allow", populate_by_name=True) + + software: IdentifierValue + name: Optional[str] = Field(default=None) + summary: str + keyFunctions: Optional[List[str]] = Field(default=None) + concerns: Optional[List[str]] = Field(default=None) + + +class DatasetSummary(BaseModel): + """Summary of a dataset's role in the computation.""" + model_config = ConfigDict(extra="allow", populate_by_name=True) + + dataset: IdentifierValue + name: Optional[str] = Field(default=None) + role: Optional[str] = Field(default=None) + description: Optional[str] = Field(default=None) + + +class AnnotatedComputation(DigitalObject): + """LLM-generated annotation of a single evi:Computation step. + + A DigitalObject (Document) that annotates an evi:Computation. + The original Computation stays in the graph in its original form; + this annotation points to it via evi:annotates. + """ + metadataType: Optional[Union[List[str], str]] = Field( + default=[ + 'prov:Entity', + "https://w3id.org/EVI#Annotation", + "https://w3id.org/EVI#AnnotatedComputation", + ], + alias="@type", + ) + additionalType: Optional[str] = Field(default=ANNOTATED_COMPUTATION_TYPE) + + # Points to the original Computation this annotates + annotates: IdentifierValue = Field(..., alias="evi:annotates") + + # LLM-generated content + stepSummary: str = Field(..., alias="evi:stepSummary") + codeAnalysis: Optional[List[CodeAnalysis]] = Field(default=[], alias="evi:codeAnalysis") + inputSummaries: Optional[List[DatasetSummary]] = Field(default=[], alias="evi:inputSummaries") + outputSummaries: Optional[List[DatasetSummary]] = Field(default=[], alias="evi:outputSummaries") + concerns: Optional[List[str]] = Field(default=[], alias="evi:concerns") + + # Provenance of the annotation itself + llmModel: str = Field(alias="evi:llmModel") + llmTemperature: Optional[float] = Field(default=None, alias="evi:llmTemperature") + dateCreated: str + interpreterVersion: Optional[str] = Field(default=None, alias="evi:interpreterVersion") + + @model_validator(mode='after') + def populate_prov_fields(self): + """Auto-populate PROV-O fields.""" + self.wasDerivedFrom = [self.annotates] + self.wasAttributedTo = [self.llmModel] + return self diff --git a/mds/src/fairscape_mds/models/annotated_evidence_graph.py b/mds/src/fairscape_mds/models/annotated_evidence_graph.py new file mode 100644 index 0000000..046decb --- /dev/null +++ b/mds/src/fairscape_mds/models/annotated_evidence_graph.py @@ -0,0 +1,62 @@ +"""AnnotatedEvidenceGraph model -- local copy for Docker builds that use +an older fairscape_models release without this module. + +Copied from fairscape_models/fairscape_models/annotated_evidence_graph.py +with imports adjusted to use fairscape_models base classes that ARE +present in the published package. +""" + +from pydantic import Field, model_validator +from typing import Optional, List, Union, Dict, Any + +from fairscape_models.fairscape_base import IdentifierValue +from fairscape_models.digital_object import DigitalObject + +ANNOTATED_EVIDENCE_GRAPH_TYPE = "AnnotatedEvidenceGraph" + + +class AnnotatedEvidenceGraph(DigitalObject): + """Full annotated condensed evidence graph -- the graph-level LLM output. + + Contains all original crate entities plus AnnotatedComputation nodes + in a flat dict keyed by @id. Computation nodes are replaced by their + annotated supersets. DAG is reconstructable from cross-references + (generatedBy, usedDataset, evi:annotates, etc.). + """ + metadataType: Optional[Union[List[str], str]] = Field( + default=[ + 'prov:Entity', + "https://w3id.org/EVI#EvidenceGraph", + "https://w3id.org/EVI#AnnotatedEvidenceGraph", + ], + alias="@type", + ) + additionalType: Optional[str] = Field(default=ANNOTATED_EVIDENCE_GRAPH_TYPE) + + # Reference to the original evidence graph or RO-Crate root + annotates: IdentifierValue = Field(..., alias="evi:annotates") + + # Flat entity lookup -- all entities keyed by ARK @id + graph: Dict[str, Any] = Field(..., alias="@graph") + + # Graph-level LLM outputs + executiveSummary: str = Field(..., alias="evi:executiveSummary") + narrativeSummary: str = Field(..., alias="evi:narrativeSummary") + keyFindings: Optional[List[str]] = Field(default=[], alias="evi:keyFindings") + concerns: Optional[List[str]] = Field(default=[], alias="evi:concerns") + + # Quick index of all AnnotatedComputation @ids in the graph + stepAnnotations: Optional[List[IdentifierValue]] = Field(default=[], alias="evi:stepAnnotations") + + # Provenance of the graph-level analysis + llmModel: str = Field(alias="evi:llmModel") + llmTemperature: Optional[float] = Field(default=None, alias="evi:llmTemperature") + dateCreated: str + interpreterVersion: Optional[str] = Field(default=None, alias="evi:interpreterVersion") + + @model_validator(mode='after') + def populate_prov_fields(self): + """Auto-populate PROV-O fields.""" + self.wasDerivedFrom = [self.annotates] + self.wasAttributedTo = [self.llmModel] + return self diff --git a/mds/src/fairscape_mds/tests/crud/test_interpretation.py b/mds/src/fairscape_mds/tests/crud/test_interpretation.py index f546d39..a46fceb 100644 --- a/mds/src/fairscape_mds/tests/crud/test_interpretation.py +++ b/mds/src/fairscape_mds/tests/crud/test_interpretation.py @@ -17,8 +17,8 @@ prefetch_software_code, GraphSynthesisResult, ) -from fairscape_models.annotated_computation import AnnotatedComputation -from fairscape_models.annotated_evidence_graph import AnnotatedEvidenceGraph +from fairscape_mds.models.annotated_computation import AnnotatedComputation +from fairscape_mds.models.annotated_evidence_graph import AnnotatedEvidenceGraph # --------------------------------------------------------------------------- diff --git a/pyproject.toml b/pyproject.toml index 6054f6a..d5bace8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,7 +42,7 @@ dependencies = [ "pandas>=2.3.3", "pandasql>=0.7.3", "logfire[celery,fastapi]>=4.15.1", - "PyYaml==6.0.1", + "PyYaml>=6.0.2", "PyGithub", "pydantic-ai[google]>=0.1.0", ] From 6cc36ce35faa28f188abe0284c22df69a4a9ac94 Mon Sep 17 00:00:00 2001 From: jniestroy Date: Mon, 16 Mar 2026 13:29:55 -0400 Subject: [PATCH 06/27] more --- deploy/docker_compose.env | 1 + mds/src/fairscape_mds/core/config.py | 8 +- mds/src/fairscape_mds/crud/condensation.py | 10 - mds/src/fairscape_mds/crud/evidence_graph.py | 1 - mds/src/fairscape_mds/crud/identifier.py | 18 +- mds/src/fairscape_mds/crud/interpretation.py | 270 +- mds/src/fairscape_mds/crud/rocrate.py | 162 +- mds/src/fairscape_mds/crud/software.py | 57 + mds/src/fairscape_mds/crud/statistics.py | 58 +- .../models/annotated_computation.py | 30 + mds/src/fairscape_mds/models/identifier.py | 9 +- .../fairscape_mds/routers/interpretation.py | 9 +- mds/src/fairscape_mds/routers/software.py | 41 +- mds/src/fairscape_mds/worker.py | 4 +- uv.lock | 2249 ++++++++++++++++- 15 files changed, 2758 insertions(+), 169 deletions(-) diff --git a/deploy/docker_compose.env b/deploy/docker_compose.env index 1f6191d..1db44f1 100644 --- a/deploy/docker_compose.env +++ b/deploy/docker_compose.env @@ -34,6 +34,7 @@ export FAIRSCAPE_REDIS_PORT="6379" export FAIRSCAPE_REDIS_JOB_DATABASE="0" export FAIRSCAPE_REDIS_RESULT_DATABASE="1" export FAIRSCAPE_BASE_URL="http://localhost:8080/api" +export FAIRSCAPE_INTERNAL_URL="http://fairscape-api:8080/api" # auth config export FAIRSCAPE_ADMIN_GROUP="admin" diff --git a/mds/src/fairscape_mds/core/config.py b/mds/src/fairscape_mds/core/config.py index 0352d97..97c4e90 100644 --- a/mds/src/fairscape_mds/core/config.py +++ b/mds/src/fairscape_mds/core/config.py @@ -46,6 +46,7 @@ class Settings(BaseSettings): FAIRSCAPE_ADMIN_GROUP: str FAIRSCAPE_BASE_URL: str + FAIRSCAPE_INTERNAL_URL: Optional[str] = Field(default=None) FAIRSCAPE_DESCRIPTIVE_STATISTICS_MAX_COLUMNS: int = 100 FAIRSCAPE_LOGFIRE_ENV: Optional[str] = Field(default=None) @@ -67,7 +68,8 @@ def __init__( tokensCollection, jwtSecret: str, adminGroup: str, - baseUrl: str + baseUrl: str, + internalUrl: Optional[str] = None ): self.minioClient=minioClient self.minioBucket=minioBucket @@ -80,6 +82,7 @@ def __init__( self.jwtSecret = jwtSecret self.adminGroup = adminGroup self.baseUrl = baseUrl + self.internalUrl = internalUrl @@ -172,5 +175,6 @@ def _add_header(request, **kwargs): tokensCollection=tokensCollection, jwtSecret=settings.FAIRSCAPE_JWT_SECRET, adminGroup=settings.FAIRSCAPE_ADMIN_GROUP, - baseUrl=settings.FAIRSCAPE_BASE_URL + baseUrl=settings.FAIRSCAPE_BASE_URL, + internalUrl=settings.FAIRSCAPE_INTERNAL_URL ) \ No newline at end of file diff --git a/mds/src/fairscape_mds/crud/condensation.py b/mds/src/fairscape_mds/crud/condensation.py index 31f2cf9..a413c52 100644 --- a/mds/src/fairscape_mds/crud/condensation.py +++ b/mds/src/fairscape_mds/crud/condensation.py @@ -620,11 +620,8 @@ def condense_graph( "generatedBy", "prov:wasGeneratedBy", "usedDataset", "usedSoftware", "usedMLModel", "derivedFrom", "prov:wasDerivedFrom", - "evi:Schema", "usedSample", "usedInstrument", "usedTreatment", "usedStain", "generated", "prov:used", - "usedByComputation", - "isPartOf", ) # Batch size for MongoDB $in queries @@ -768,13 +765,6 @@ def condense_rocrate( # Run condensation condensed_graph, stats = condense_graph(graph, threshold, max_member_ids) - if not stats.get("condensed"): - return FairscapeResponse( - success=True, - statusCode=200, - model={"message": "Nothing to condense", "stats": stats} - ) - # Find context from the original ROCrate root doc rocrate_doc = self.config.identifierCollection.find_one( {"@id": rocrate_id}, {"_id": 0} diff --git a/mds/src/fairscape_mds/crud/evidence_graph.py b/mds/src/fairscape_mds/crud/evidence_graph.py index 0d2946a..fdf3489 100644 --- a/mds/src/fairscape_mds/crud/evidence_graph.py +++ b/mds/src/fairscape_mds/crud/evidence_graph.py @@ -33,7 +33,6 @@ def create_evidence_graph( } try: - print(evidence_graph_data) evidence_graph = EvidenceGraph.model_validate(evidence_graph_data) default_permissions = Permissions( diff --git a/mds/src/fairscape_mds/crud/identifier.py b/mds/src/fairscape_mds/crud/identifier.py index 305b575..1cdaede 100644 --- a/mds/src/fairscape_mds/crud/identifier.py +++ b/mds/src/fairscape_mds/crud/identifier.py @@ -15,7 +15,8 @@ CategoricalStatistics ) from fairscape_mds.crud.statistics import ( - generateSummaryStatistics + generateSummaryStatistics, + generateSplitStatistics ) from fairscape_models import IdentifierValue from fairscape_models.model_card import ModelCard @@ -151,11 +152,24 @@ def generateStatistics( summaryStatistics = generateSummaryStatistics(dataframe) + # check for splits on the dataset metadata + identifier = self.getIdentifier(guid) + splits = getattr(identifier.metadata, 'splits', None) + + splitStats = None + if splits: + splitDicts = [s.model_dump() if hasattr(s, 'model_dump') else s for s in splits] + splitStats = generateSplitStatistics(dataframe, splitDicts) + # update identifier + updateFields = {"descriptiveStatistics": summaryStatistics} + if splitStats: + updateFields["splitStatistics"] = splitStats + updateOperation = self.config.identifierCollection.update_one( {"@id": guid}, { - "$set" : {"descriptiveStatistics": summaryStatistics} + "$set" : updateFields } ) diff --git a/mds/src/fairscape_mds/crud/interpretation.py b/mds/src/fairscape_mds/crud/interpretation.py index 649217f..d6aec7f 100644 --- a/mds/src/fairscape_mds/crud/interpretation.py +++ b/mds/src/fairscape_mds/crud/interpretation.py @@ -7,17 +7,20 @@ and stores the resulting AnnotatedEvidenceGraph. """ +import asyncio import datetime import re import uuid import logging -from concurrent.futures import ThreadPoolExecutor, as_completed from typing import Any, Dict, List, Optional, Tuple import httpx from pydantic_ai import Agent -from fairscape_mds.models.annotated_computation import AnnotatedComputation, CodeAnalysis, DatasetSummary +from fairscape_mds.models.annotated_computation import ( + AnnotatedComputation, CodeAnalysis, DatasetSummary, + LLMComputationAnnotation, LLMCodeAnalysis, LLMDatasetSummary, +) from fairscape_mds.models.annotated_evidence_graph import AnnotatedEvidenceGraph from fairscape_mds.crud.fairscape_request import FairscapeRequest @@ -32,6 +35,25 @@ logger = logging.getLogger(__name__) +# --------------------------------------------------------------------------- +# Global Event Loop for Worker +# --------------------------------------------------------------------------- + +_worker_loop = None + + +def run_async(coro): + """Run an async coroutine using a single, persistent event loop per worker process. + + This prevents 'RuntimeError: bound to a different event loop' caused by + PydanticAI's globally cached HTTP client. + """ + global _worker_loop + if _worker_loop is None or _worker_loop.is_closed(): + _worker_loop = asyncio.new_event_loop() + asyncio.set_event_loop(_worker_loop) + return _worker_loop.run_until_complete(coro) + # --------------------------------------------------------------------------- # Constants # --------------------------------------------------------------------------- @@ -274,6 +296,17 @@ def _update_task(self, task_guid: str, updates: dict): {"$set": updates} ) + def _save_llm_result(self, task_guid: str, label: str, raw_output: dict): + """Append a raw LLM result to the task document for debugging.""" + self.config.asyncCollection.update_one( + {"guid": task_guid}, + {"$push": {"llm_results": { + "label": label, + "timestamp": datetime.datetime.utcnow().isoformat(), + "output": raw_output, + }}} + ) + # ------------------------------------------------------------------ # Step 1: Ensure condensed # ------------------------------------------------------------------ @@ -381,7 +414,7 @@ def find_computations(self, task_guid: str, graph: list) -> Tuple[List[dict], di # Step 3: Pre-fetch software # ------------------------------------------------------------------ - def prefetch_all_software(self, task_guid: str, computations: list, index: dict) -> Dict[str, str]: + def prefetch_all_software(self, task_guid: str, computations: list, index: dict, user_token: str = "") -> Dict[str, str]: """Pre-fetch source code for all software referenced by computations. Returns {software_id: source_code_text}. """ @@ -398,6 +431,30 @@ def prefetch_all_software(self, task_guid: str, computations: list, index: dict) continue sw_node = index.get(sw_id, {}) content_url = sw_node.get("contentUrl", "") + + # Fairscape-hosted software: call download endpoint with user auth + if "/software/download/" in content_url and user_token: + try: + # Rewrite external URLs to internal service URL if configured + internal_url = content_url + if self.config.internalUrl and self.config.baseUrl: + internal_url = content_url.replace(self.config.baseUrl, self.config.internalUrl) + resp = httpx.get( + internal_url, + headers={"Authorization": f"Bearer {user_token}"}, + timeout=30.0 + ) + resp.raise_for_status() + code = resp.text + if len(code.encode("utf-8")) > MAX_SOFTWARE_BYTES: + code = code[:MAX_SOFTWARE_BYTES] + "\n[...truncated...]" + software_cache[sw_id] = code + logger.info(f"Pre-fetched software {sw_id} from download endpoint: {len(code)} chars") + continue + except Exception as e: + logger.warning(f"Failed to fetch software {sw_id} from download endpoint: {e}") + + # Fall back to GitHub/external URL fetching code = prefetch_software_code(content_url) software_cache[sw_id] = code logger.info(f"Pre-fetched software {sw_id}: {len(code)} chars") @@ -465,48 +522,103 @@ def _build_computation_prompt(self, computation: dict, software_cache: dict, ind return "\n".join(parts) - def _annotate_single_computation( + def _llm_to_annotated( + self, + llm_result: LLMComputationAnnotation, + comp_id: str, + llm_model: str, + temperature: float, + ) -> AnnotatedComputation: + """Convert lightweight LLM output into a full AnnotatedComputation.""" + annotation_id = f"{comp_id}-annotation" + now = datetime.datetime.utcnow().isoformat() + + # Convert LLM code analyses -> CodeAnalysis with IdentifierValue + code_analyses = [ + CodeAnalysis( + software={"@id": ca.software_id}, + name=ca.name, + summary=ca.summary, + keyFunctions=ca.keyFunctions, + concerns=ca.concerns, + ) + for ca in (llm_result.codeAnalysis or []) + ] + + # Convert LLM dataset summaries -> DatasetSummary with IdentifierValue + input_summaries = [ + DatasetSummary( + dataset={"@id": ds.dataset_id}, + name=ds.name, + role=ds.role, + description=ds.description, + ) + for ds in (llm_result.inputSummaries or []) + ] + output_summaries = [ + DatasetSummary( + dataset={"@id": ds.dataset_id}, + name=ds.name, + role=ds.role, + description=ds.description, + ) + for ds in (llm_result.outputSummaries or []) + ] + + return AnnotatedComputation.model_validate({ + "@id": annotation_id, + "name": f"Annotation of {comp_id}", + "author": llm_model, + "description": llm_result.stepSummary[:200] if len(llm_result.stepSummary) >= 10 else llm_result.stepSummary + " " * (10 - len(llm_result.stepSummary)), + "evi:annotates": {"@id": comp_id}, + "evi:stepSummary": llm_result.stepSummary, + "evi:codeAnalysis": [ca.model_dump(by_alias=True) for ca in code_analyses], + "evi:inputSummaries": [ds.model_dump(by_alias=True) for ds in input_summaries], + "evi:outputSummaries": [ds.model_dump(by_alias=True) for ds in output_summaries], + "evi:concerns": llm_result.concerns or [], + "evi:llmModel": llm_model, + "evi:llmTemperature": temperature, + "dateCreated": now, + }) + + async def _annotate_single_computation( self, + task_guid: str, computation: dict, software_cache: dict, index: dict, llm_model: str, temperature: float, ) -> AnnotatedComputation: - """Annotate a single computation using PydanticAI. Runs synchronously.""" + """Annotate a single computation using PydanticAI.""" prompt = self._build_computation_prompt(computation, software_cache, index) + comp_id = computation.get("@id", f"ark:59853/computation-{uuid.uuid4()}") agent = Agent( llm_model, - result_type=AnnotatedComputation, + output_type=LLMComputationAnnotation, system_prompt=DATASCI_SYSTEM_PROMPT, - retries=2, + retries=3, ) - # Build the required fields that PydanticAI can't infer - comp_id = computation.get("@id", f"ark:59853/computation-{uuid.uuid4()}") - annotation_id = f"ark:59853/annotated-computation-{uuid.uuid4()}" - - # Run the agent synchronously - result = agent.run_sync(prompt) - annotated: AnnotatedComputation = result.data + result = await agent.run(prompt) + llm_output: LLMComputationAnnotation = result.output - # Ensure required fields are set correctly - if not annotated.guid: - annotated.guid = annotation_id - if not annotated.annotates: - annotated.annotates = {"@id": comp_id} - annotated.llmModel = llm_model - annotated.llmTemperature = temperature - annotated.dateCreated = datetime.datetime.utcnow().isoformat() + # Persist raw LLM output for debugging + self._save_llm_result( + task_guid, + f"computation:{comp_id}", + llm_output.model_dump(mode="json"), + ) - return annotated + # Convert lightweight LLM output -> full AnnotatedComputation + return self._llm_to_annotated(llm_output, comp_id, llm_model, temperature) # ------------------------------------------------------------------ # Step 4b: Parallel computation processing # ------------------------------------------------------------------ - def annotate_computations_parallel( + async def _annotate_computations_async( self, task_guid: str, computations: list, @@ -516,41 +628,28 @@ def annotate_computations_parallel( temperature: float, max_workers: int = 4, ) -> List[AnnotatedComputation]: - """Annotate all computations in parallel using ThreadPoolExecutor.""" + """Annotate all computations concurrently via asyncio.gather.""" self._update_task(task_guid, { "current_step": "PROMPTING", "status": "PROMPTING", }) - results: List[AnnotatedComputation] = [] - errors: List[dict] = [] - - with ThreadPoolExecutor(max_workers=max_workers) as executor: - futures = { - executor.submit( - self._annotate_single_computation, - comp, software_cache, index, llm_model, temperature, - ): comp - for comp in computations - } - - for future in as_completed(futures): - comp = futures[future] - comp_id = comp.get("@id", "unknown") + sem = asyncio.Semaphore(max_workers) + async def _annotate_one(comp): + comp_id = comp.get("@id", "unknown") + async with sem: try: - annotated = future.result() - results.append(annotated) - - # Update per-computation status + annotated = await self._annotate_single_computation( + task_guid, comp, software_cache, index, llm_model, temperature, + ) self.config.asyncCollection.update_one( {"guid": task_guid, "computation_details.computation_id": comp_id}, {"$set": {"computation_details.$.status": "done"}} ) + return ("ok", comp_id, annotated) except Exception as e: logger.error(f"Failed to annotate computation {comp_id}: {e}") - errors.append({"computation_id": comp_id, "error": str(e)}) - self.config.asyncCollection.update_one( {"guid": task_guid, "computation_details.computation_id": comp_id}, {"$set": { @@ -558,21 +657,40 @@ def annotate_computations_parallel( "computation_details.$.error": str(e), }} ) + return ("error", comp_id, str(e)) + finally: + self.config.asyncCollection.update_one( + {"guid": task_guid}, + {"$inc": {"completed_computations": 1}} + ) + + outcomes = await asyncio.gather(*[_annotate_one(c) for c in computations]) - # Increment completed count - self.config.asyncCollection.update_one( - {"guid": task_guid}, - {"$inc": {"completed_computations": 1}} - ) + results = [o[2] for o in outcomes if o[0] == "ok"] + errors = [{"computation_id": o[1], "error": o[2]} for o in outcomes if o[0] == "error"] if errors and not results: raise RuntimeError(f"All {len(errors)} computation annotations failed. First error: {errors[0]['error']}") - if errors: logger.warning(f"{len(errors)} of {len(computations)} computation annotations failed") return results + def annotate_computations_parallel( + self, + task_guid: str, + computations: list, + software_cache: dict, + index: dict, + llm_model: str, + temperature: float, + max_workers: int = 4, + ) -> List[AnnotatedComputation]: + """Annotate computations concurrently using async I/O.""" + return run_async(self._annotate_computations_async( + task_guid, computations, software_cache, index, llm_model, temperature, max_workers, + )) + # ------------------------------------------------------------------ # Step 5: Graph-level synthesis # ------------------------------------------------------------------ @@ -613,15 +731,26 @@ def synthesize_graph( prompt = "\n".join(parts) - agent = Agent( - llm_model, - result_type=GraphSynthesisResult, - system_prompt=SYNTHESIS_SYSTEM_PROMPT, - retries=2, + async def _run_synthesis(): + agent = Agent( + llm_model, + output_type=GraphSynthesisResult, + system_prompt=SYNTHESIS_SYSTEM_PROMPT, + retries=2, + ) + result = await agent.run(prompt) + return result.output + + synthesis = run_async(_run_synthesis()) + + # Persist raw LLM output for debugging + self._save_llm_result( + task_guid, + "synthesis", + synthesis.model_dump(mode="json"), ) - result = agent.run_sync(prompt) - return result.data + return synthesis # ------------------------------------------------------------------ # Step 6: Build and store AnnotatedEvidenceGraph @@ -651,11 +780,22 @@ def build_and_store( if node_id: graph_dict[node_id] = node - # Add annotated computations to graph + # Add annotated computations to graph and back-link computations for ann in step_annotations: ann_dict = ann.model_dump(by_alias=True, exclude_none=True, mode="json") graph_dict[ann.guid] = ann_dict + # Add evi:annotatedBy reverse link on the computation node + comp_id = ann.annotates.guid if hasattr(ann.annotates, 'guid') else str(ann.annotates) + if comp_id and comp_id in graph_dict: + existing = graph_dict[comp_id].get("evi:annotatedBy", []) + if isinstance(existing, dict): + existing = [existing] + elif not isinstance(existing, list): + existing = [] + existing.append({"@id": ann.guid}) + graph_dict[comp_id]["evi:annotatedBy"] = existing + # Build step annotation refs step_ann_refs = [{"@id": ann.guid} for ann in step_annotations] @@ -694,7 +834,7 @@ def build_and_store( permissions = Permissions(owner="system@fairscape.org", group="", acl=[]) stored = StoredIdentifier.model_validate({ "@id": aeg_id, - "@type": "AnnotatedEvidenceGraph", + "@type": ["prov:Entity", "https://w3id.org/EVI#EvidenceGraph", "https://w3id.org/EVI#AnnotatedEvidenceGraph"], "metadata": aeg.model_dump(by_alias=True, mode="json"), "permissions": permissions.model_dump(), "publicationStatus": PublicationStatusEnum.DRAFT, @@ -721,7 +861,7 @@ def build_and_store( # Main orchestrator # ------------------------------------------------------------------ - def interpret_rocrate(self, task_guid: str) -> str: + def interpret_rocrate(self, task_guid: str, user_token: str = "") -> str: """Main pipeline orchestrator. Returns the AnnotatedEvidenceGraph @id.""" # Load task config task_doc = self.config.asyncCollection.find_one({"guid": task_guid}) @@ -729,7 +869,7 @@ def interpret_rocrate(self, task_guid: str) -> str: raise ValueError(f"Task {task_guid} not found") rocrate_id = task_doc["rocrate_id"] - llm_model = task_doc.get("llm_model", "google-gla:gemini-2.5-flash") + llm_model = task_doc.get("llm_model", "google-gla:gemini-2.5-flash-lite") temperature = task_doc.get("llm_temperature", 0.2) self._update_task(task_guid, { @@ -748,7 +888,9 @@ def interpret_rocrate(self, task_guid: str) -> str: raise ValueError(f"No Computation nodes found in RO-Crate {rocrate_id}") # Step 3: Pre-fetch software - software_cache = self.prefetch_all_software(task_guid, computations, index) + # Use token from argument, or fall back to task doc + effective_token = user_token or task_doc.get("user_token", "") + software_cache = self.prefetch_all_software(task_guid, computations, index, user_token=effective_token) # Step 4: Annotate computations in parallel step_annotations = self.annotate_computations_parallel( diff --git a/mds/src/fairscape_mds/crud/rocrate.py b/mds/src/fairscape_mds/crud/rocrate.py index 845f244..679e876 100644 --- a/mds/src/fairscape_mds/crud/rocrate.py +++ b/mds/src/fairscape_mds/crud/rocrate.py @@ -431,18 +431,15 @@ def processTaskWriteMLModels( }) ] - # if "file:///" in modelElem.contentUrl: - modelDistribution = DatasetDistribution.model_validate({}) - - contentUrlKey = modelElem.contentUrl.lstrip("file:///") + contentUrlKey = modelElem.contentUrl.removeprefix("file:///") # TODO format for mlmodels # Keras -> HDF5 # PyTorch -> .pt/.pth/.zip/.tar # Pickle -> .pkl # Numpy Arrays -> .npy - modelMimetype, _ = mimetypes.guess_type(contentUrlKey) + modelMimetype, _ = mimetypes.guess_type(contentUrlKey) modelElem.format = modelMimetype # if file in rocrate zip, inspect the object @@ -453,28 +450,22 @@ def processTaskWriteMLModels( try: response = self.config.minioClient.head_object( - Bucket= self.config.minioBucket, + Bucket=self.config.minioBucket, Key=objectKey ) - - except botocore.exceptions.ClientError as e: + + except botocore.exceptions.ClientError as e: raise Exception(f"message: Object Key Not Found\tkey: {objectKey}\tbucket: {self.config.minioBucket}") objectSize = response.get("ContentLength") modelElem.size = objectSize modelElem.contentUrl = f"{self.config.baseUrl}/download/{modelElem.guid}" - distribution = DatasetDistribution.model_validate({ + modelDistribution = DatasetDistribution.model_validate({ "distributionType": "minio", - "location": {"Path": objectKey} + "location": {"path": objectKey} }) - elif "https://" in modelElem.contentUrl or "http://" in modelElem.contentUrl: - modelDistribution = DatasetDistribution.model_validate({ - "distributionType": "url", - "location": {"uri": modelElem.contentUrl} - }) - elif "https://" in modelElem.contentUrl or "http://" in modelElem.contentUrl: modelDistribution = DatasetDistribution.model_validate({ "distributionType": "url", @@ -508,6 +499,128 @@ def processTaskWriteMLModels( pass + + def processTaskWriteSoftware( + self, + userInstance: UserWriteModel, + rocrateInstance: ROCrateV1_2, + uploadPath: str, + includeStem: bool, + stem: Optional[str] = None + ): + """ Write ROCrate metadata for all software elements with proper distributions. + + Args: + userInstance (UserWriteModel): User Record for the user inserting the metadata + rocrateInstance (fairscape_models.rocrate.ROCratev1_2): ROCrate Metadata as a pydantic model + uploadPath (str): Minio path where the zip was uploaded + includeStem (bool): look for object keys at the stemmed path + stem (str): the stem of the folder path + + Returns: + List[str]: List of all Software ARKs minted + """ + baseUrl = self.config.baseUrl + permissionsSet = userInstance.getPermissions() + now = datetime.datetime.now() + + rocrateElem = rocrateInstance.getCrateMetadata() + rocrateGUID = rocrateElem.guid + rocrateName = rocrateElem.name + + softwareList = [] + for elem in rocrateInstance.metadataGraph: + if isinstance(elem, Software): + softwareList.append(elem) + elif isinstance(elem, GenericMetadataElem) and 'Software' in elem.metadataType: + softwareList.append(elem) + + guidList = [] + + for softwareElem in softwareList: + # set isPartOf on the software element + softwareElem.isPartOf = [ + IdentifierValue.model_validate({ + "@id": rocrateGUID, + "@type": MetadataTypeEnum.ROCRATE, + "name": rocrateName + }) + ] + + distribution = None + + if not softwareElem.contentUrl or softwareElem.contentUrl == 'Embargoed': + distribution = None + + elif 'ftp://' in softwareElem.contentUrl: + distribution = DatasetDistribution.model_validate({ + "distributionType": 'ftp', + "location": {"uri": softwareElem.contentUrl} + }) + + elif 'http://' in softwareElem.contentUrl or 'https://' in softwareElem.contentUrl: + distribution = DatasetDistribution.model_validate({ + "distributionType": 'url', + "location": {"uri": softwareElem.contentUrl} + }) + + elif 'file:///' in softwareElem.contentUrl: + contentUrlKey = softwareElem.contentUrl.removeprefix("file:///") + + # detect MIME type + softwareMimetype, _ = mimetypes.guess_type(contentUrlKey) + softwareElem.format = softwareMimetype + + # build Minio object key + if includeStem: + objectKey = f"{uploadPath}/{stem}/{contentUrlKey}" + else: + objectKey = f"{uploadPath}/{contentUrlKey}" + + try: + response = self.config.minioClient.head_object( + Bucket=self.config.minioBucket, + Key=objectKey + ) + except botocore.exceptions.ClientError as e: + raise Exception(f"message: Object Key Not Found\tkey: {objectKey}\tbucket: {self.config.minioBucket}") + + objectSize = response.get("ContentLength") + softwareElem.size = objectSize + + # update contentUrl to download endpoint + softwareElem.contentUrl = f"{baseUrl}/software/download/{softwareElem.guid}" + + distribution = DatasetDistribution.model_validate({ + "distributionType": 'minio', + "location": {"path": objectKey} + }) + + storedSoftware = StoredIdentifier.model_validate({ + "@id": softwareElem.guid, + "@type": MetadataTypeEnum.SOFTWARE, + "metadata": softwareElem, + "permissions": permissionsSet, + "distribution": distribution, + "publicationStatus": PublicationStatusEnum.DRAFT, + "dateCreated": now, + "dateModified": now, + }) + + insertResult = self.config.identifierCollection.insert_one( + storedSoftware.model_dump(by_alias=True, mode='json') + ) + + if not insertResult.inserted_id: + raise Exception( + f"Writing Identifier To Mongo Failed id: {storedSoftware.guid}" + ) + + guidList.append(softwareElem.guid) + + return guidList + + def processTaskWriteMetadataElements( self, userInstance, @@ -538,7 +651,7 @@ def processTaskWriteMetadataElements( # mint all metadata elements for metadataModel in rocrateInstance.metadataGraph: metadataTypeList = metadataModel.metadataType if isinstance(metadataModel.metadataType, list) else [metadataModel.metadataType] - if any('ROCrate' in t or 'Dataset' in t for t in metadataTypeList) or metadataModel.guid == 'ro-crate-metadata.json': + if any('ROCrate' in t or 'Dataset' in t or 'Software' in t or 'MLModel' in t for t in metadataTypeList) or metadataModel.guid == 'ro-crate-metadata.json': continue else: @@ -875,13 +988,22 @@ def processROCrate(self, transactionGUID: str): ) self.processTaskWriteMLModels( - foundUser, - roCrateModel, + foundUser, + roCrateModel, uploadInstance.uploadPath, includeStem, stem ) - + + # write software records with distributions + self.processTaskWriteSoftware( + foundUser, + roCrateModel, + uploadInstance.uploadPath, + includeStem, + stem + ) + # write metadata elements nonDatasetGUIDS = self.processTaskWriteMetadataElements( foundUser, diff --git a/mds/src/fairscape_mds/crud/software.py b/mds/src/fairscape_mds/crud/software.py index 2ed1168..44cef15 100644 --- a/mds/src/fairscape_mds/crud/software.py +++ b/mds/src/fairscape_mds/crud/software.py @@ -5,6 +5,8 @@ from fairscape_mds.models.user import UserWriteModel, Permissions, checkPermissions from fairscape_mds.models.software import SoftwareWriteModel +from fairscape_mds.models.identifier import StoredIdentifier +from fairscape_mds.models.dataset import DistributionTypeEnum from fairscape_models.software import Software class FairscapeSoftwareRequest(FairscapeRequest): @@ -32,3 +34,58 @@ def createSoftware( def getSoftware(self, guid: str): return getMetadata(self.config.identifierCollection, Software, guid) + + def getSoftwareContent( + self, + userInstance: UserWriteModel, + softwareGUID: str, + ) -> FairscapeResponse: + softwareMetadata = self.config.identifierCollection.find_one( + {"@id": softwareGUID}, + projection={"_id": False} + ) + + if not softwareMetadata: + return FairscapeResponse( + success=False, + statusCode=404, + jsonResponse={"error": "Software not found"} + ) + + storedSoftware = StoredIdentifier.model_validate(softwareMetadata) + + if not checkPermissions(storedSoftware.permissions, userInstance): + return FairscapeResponse( + success=False, + statusCode=401, + jsonResponse={"error": "user unauthorized"} + ) + + if storedSoftware.distribution: + if storedSoftware.distribution.distributionType == DistributionTypeEnum.MINIO: + objectKey = storedSoftware.distribution.location.path + + response = self.config.minioClient.get_object( + Bucket=self.config.minioBucket, + Key=objectKey + ) + + return FairscapeResponse( + success=True, + statusCode=200, + fileResponse=response, + model=storedSoftware + ) + else: + return FairscapeResponse( + success=False, + statusCode=400, + jsonResponse={"error": "Software Not Stored Locally"} + ) + + else: + return FairscapeResponse( + success=False, + statusCode=400, + jsonResponse={"error": "Software Not Stored Locally"} + ) diff --git a/mds/src/fairscape_mds/crud/statistics.py b/mds/src/fairscape_mds/crud/statistics.py index 43550cd..09b4dbe 100644 --- a/mds/src/fairscape_mds/crud/statistics.py +++ b/mds/src/fairscape_mds/crud/statistics.py @@ -1,6 +1,6 @@ import pandas import numpy -from typing import Dict +from typing import Dict, List, Optional from fairscape_mds.core.config import descriptiveStatisticsMaxCols from fairscape_mds.models.statistics import ( DescriptiveStatistics, @@ -8,6 +8,11 @@ NumericalStatistics ) +try: + import pandasql +except ImportError: + pandasql = None + def generateNumericalStatistics(series) -> DescriptiveStatistics: @@ -70,4 +75,53 @@ def generateSummaryStatistics(dataframe)-> Dict[str, DescriptiveStatistics]: statistics[summaryStats.columnName] = summaryStats.model_dump(mode='json', by_alias=False) - return statistics \ No newline at end of file + return statistics + + +def applyQuery(dataframe: pandas.DataFrame, query: str, queryType: str) -> Optional[pandas.DataFrame]: + queryType = queryType.upper() if queryType else None + + if queryType == "SQL": + if pandasql is None: + raise ImportError("pandasql is required for SQL split queries. Install it with: pip install pandasql") + dataset = dataframe # noqa: F841 — referenced by SQL query + return pandasql.sqldf(query, locals()) + + elif queryType == "PANDAS": + # Extract the condition from queries like "split == 'train'" + return dataframe.query(query) + + else: + return None + + +def generateSplitStatistics( + dataframe: pandas.DataFrame, + splits: List +) -> Dict[str, Dict]: + splitStats = {} + + for split in splits: + _get = lambda k: split.get(k) if isinstance(split, dict) else getattr(split, k, None) + query = _get("query") + queryType = _get("queryType") + name = _get("name") + description = _get("description") + + if not query or not queryType or not name: + continue + + try: + subset = applyQuery(dataframe, query, queryType) + if subset is not None and not subset.empty: + splitStats[name] = { + "query": query, + "queryType": queryType, + "description": description, + "statistics": generateSummaryStatistics(subset) + } + except Exception: + # Skip splits that fail to query + continue + + return splitStats \ No newline at end of file diff --git a/mds/src/fairscape_mds/models/annotated_computation.py b/mds/src/fairscape_mds/models/annotated_computation.py index 2801262..406201f 100644 --- a/mds/src/fairscape_mds/models/annotated_computation.py +++ b/mds/src/fairscape_mds/models/annotated_computation.py @@ -37,6 +37,36 @@ class DatasetSummary(BaseModel): description: Optional[str] = Field(default=None) +# --------------------------------------------------------------------------- +# Lightweight LLM output models (no infrastructure fields the LLM can't know) +# --------------------------------------------------------------------------- + +class LLMCodeAnalysis(BaseModel): + """What the LLM returns for a software entity analysis.""" + software_id: str = Field(description="The @id of the software entity being analyzed") + name: Optional[str] = Field(default=None) + summary: str + keyFunctions: Optional[List[str]] = Field(default=None) + concerns: Optional[List[str]] = Field(default=None) + + +class LLMDatasetSummary(BaseModel): + """What the LLM returns for a dataset summary.""" + dataset_id: str = Field(description="The @id of the dataset") + name: Optional[str] = Field(default=None) + role: Optional[str] = Field(default=None) + description: Optional[str] = Field(default=None) + + +class LLMComputationAnnotation(BaseModel): + """Lightweight model for LLM output — only the fields the LLM should fill.""" + stepSummary: str + codeAnalysis: Optional[List[LLMCodeAnalysis]] = Field(default=[]) + inputSummaries: Optional[List[LLMDatasetSummary]] = Field(default=[]) + outputSummaries: Optional[List[LLMDatasetSummary]] = Field(default=[]) + concerns: Optional[List[str]] = Field(default=[]) + + class AnnotatedComputation(DigitalObject): """LLM-generated annotation of a single evi:Computation step. diff --git a/mds/src/fairscape_mds/models/identifier.py b/mds/src/fairscape_mds/models/identifier.py index 6507d11..86e4aea 100644 --- a/mds/src/fairscape_mds/models/identifier.py +++ b/mds/src/fairscape_mds/models/identifier.py @@ -22,6 +22,7 @@ from fairscape_models.conversion.models.AIReady import AIReadyScore from fairscape_models.model_card import ModelCard from fairscape_models.fairscape_base import IdentifierValue +from fairscape_mds.models.annotated_evidence_graph import AnnotatedEvidenceGraph import datetime @@ -53,6 +54,8 @@ class MetadataTypeEnum(Enum): ML_MODEL = ["prov:Entity","https://w3id.org/EVI#MLModel"] ANNOTATION = "https://w3id.org/EVI#Annotation" EVIDENCE_GRAPH = "evi:EvidenceGraph" + ANNOTATED_EVIDENCE_GRAPH = ["prov:Entity", "https://w3id.org/EVI#EvidenceGraph", "https://w3id.org/EVI#AnnotatedEvidenceGraph"] + ANNOTATED_COMPUTATION = ["prov:Entity", "https://w3id.org/EVI#Annotation", "https://w3id.org/EVI#AnnotatedComputation"] AI_READY_SCORE = "evi:AIReadyScore" @@ -60,6 +63,7 @@ class MetadataTypeEnum(Enum): Dataset, Software, Computation, + AnnotatedEvidenceGraph, ROCrateV1_2, ROCrateMetadataElem, Schema, @@ -85,6 +89,7 @@ class StoredIdentifier(BaseModel): permissions: Permissions distribution: Optional[DatasetDistribution] descriptiveStatistics: Optional[Dict[str, DescriptiveStatistics]] = Field(default = {}) + splitStatistics: Optional[Dict[str, Dict]] = Field(default=None) contentSummary: Optional[Dict] = Field(default=None) dateCreated: datetime.datetime dateModified: datetime.datetime @@ -104,11 +109,13 @@ def validate_metadata_type(cls, data): MetadataTypeEnum.EVIDENCE_GRAPH: EvidenceGraph, 'evi:AIReadyScore': AIReadyScore, MetadataTypeEnum.AI_READY_SCORE: AIReadyScore, + MetadataTypeEnum.ANNOTATED_EVIDENCE_GRAPH: AnnotatedEvidenceGraph, + tuple(MetadataTypeEnum.ANNOTATED_EVIDENCE_GRAPH.value): AnnotatedEvidenceGraph, } lookup_key = tuple(metadata_type) if isinstance(metadata_type, list) else metadata_type if lookup_key in type_map: - model_class = type_map[metadata_type] + model_class = type_map[lookup_key] data['metadata'] = model_class.model_validate(metadata_dict) return data diff --git a/mds/src/fairscape_mds/routers/interpretation.py b/mds/src/fairscape_mds/routers/interpretation.py index 08c6724..ce40709 100644 --- a/mds/src/fairscape_mds/routers/interpretation.py +++ b/mds/src/fairscape_mds/routers/interpretation.py @@ -9,7 +9,7 @@ from fairscape_mds.models.user import UserWriteModel from fairscape_mds.core.config import appConfig -from fairscape_mds.deps import getCurrentUser +from fairscape_mds.deps import getCurrentUser, OAuthScheme from fairscape_mds.crud.fairscape_request import flexible_ark_query router = APIRouter( @@ -45,12 +45,13 @@ def _flexible_find(ark_id: str): ) def trigger_interpretation( currentUser: Annotated[UserWriteModel, Depends(getCurrentUser)], + token: Annotated[str, Depends(OAuthScheme)], NAAN: str, postfix: str, - llm_model: str = Query(default="google-gla:gemini-2.5-flash", description="PydanticAI model string"), + llm_model: str = Query(default="google-gla:gemini-2.5-flash-lite", description="PydanticAI model string"), temperature: float = Query(default=0.2, ge=0.0, le=2.0, description="LLM temperature"), persona: str = Query(default="datasci", description="Interpretation persona"), - force: bool = Query(default=False, description="Force re-interpretation if one already exists"), + force: bool = Query(default=True, description="Force re-interpretation if one already exists"), ): """Trigger async AI-mediated interpretation of an RO-Crate. Ensures condensation, annotates each computation, synthesizes a graph-level @@ -109,6 +110,7 @@ def trigger_interpretation( "task_type": "InterpretROCrate", "rocrate_id": ark_id, "owner_email": currentUser.email, + "user_token": token, "status": "PENDING", "current_step": "PENDING", "total_computations": 0, @@ -130,6 +132,7 @@ def trigger_interpretation( rocrate_id=ark_id, llm_model=llm_model, temperature=temperature, + user_token=token, ) return JSONResponse( diff --git a/mds/src/fairscape_mds/routers/software.py b/mds/src/fairscape_mds/routers/software.py index c939cf2..4c6fb4b 100644 --- a/mds/src/fairscape_mds/routers/software.py +++ b/mds/src/fairscape_mds/routers/software.py @@ -6,7 +6,8 @@ from typing import Annotated from fastapi import APIRouter, Depends -from fastapi.responses import JSONResponse +from fastapi.responses import JSONResponse, StreamingResponse +import mimetypes softwareRequest = FairscapeSoftwareRequest(appConfig) @@ -32,6 +33,44 @@ def createSoftware( ) +@softwareRouter.get("/software/download/ark:{naan}/{postfix}") +def getSoftwareContent( + naan: str, + postfix: str, + currentUser: Annotated[UserWriteModel, Depends(getCurrentUser)] +): + softwareGUID = f"ark:{naan}/{postfix}" + softwareResponse = softwareRequest.getSoftwareContent( + userInstance=currentUser, + softwareGUID=softwareGUID + ) + + if softwareResponse.success: + software_instance = softwareResponse.model + object_key = software_instance.distribution.location.path + filename = object_key.split("/")[-1] + + content_type, _ = mimetypes.guess_type(filename) + if content_type is None: + content_type = "application/octet-stream" + + download_headers = { + "Content-Type": content_type, + "Content-Disposition": f'attachment; filename="{filename}"' + } + + return StreamingResponse( + softwareResponse.fileResponse['Body'], + headers=download_headers + ) + + else: + return JSONResponse( + status_code=softwareResponse.statusCode, + content=softwareResponse.error + ) + + @softwareRouter.get("/software/ark:/{NAAN}/{postfix}") @softwareRouter.get("/software/ark:{NAAN}/{postfix}") def getSoftware( diff --git a/mds/src/fairscape_mds/worker.py b/mds/src/fairscape_mds/worker.py index 29feee2..4640ec3 100644 --- a/mds/src/fairscape_mds/worker.py +++ b/mds/src/fairscape_mds/worker.py @@ -354,11 +354,11 @@ def process_llm_assist_task(self, task_guid: str): return {"status": "FAILURE", "error": error_msg} @celeryApp.task(name='fairscape_mds.worker.interpret_rocrate_task', bind=True) -def interpret_rocrate_task(self, task_guid: str, rocrate_id: str, llm_model: str = "google-gla:gemini-2.5-flash", temperature: float = 0.2): +def interpret_rocrate_task(self, task_guid: str, rocrate_id: str, llm_model: str = "google-gla:gemini-2.5-flash", temperature: float = 0.2, user_token: str = ""): print(f"Starting Interpretation Task: {task_guid} for {rocrate_id}") try: - aeg_id = interpretationRequests.interpret_rocrate(task_guid) + aeg_id = interpretationRequests.interpret_rocrate(task_guid, user_token=user_token) print(f"Successfully interpreted ROCrate for Task GUID {task_guid}: {aeg_id}") return {"status": "SUCCESS", "annotated_evidence_graph_id": aeg_id} diff --git a/uv.lock b/uv.lock index 9424c75..90ca869 100644 --- a/uv.lock +++ b/uv.lock @@ -15,6 +15,30 @@ resolution-markers = [ "python_full_version < '3.10'", ] +[[package]] +name = "ag-ui-protocol" +version = "0.1.13" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/b5/fc0b65b561d00d88811c8a7d98ee735833f81554be244340950e7b65820c/ag_ui_protocol-0.1.13.tar.gz", hash = "sha256:811d7d7dcce4783dec252918f40b717ebfa559399bf6b071c4ba47c0c1e21bcb", size = 5671, upload-time = "2026-02-19T18:40:38.602Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cd/9f/b833c1ab1999da35ebad54841ae85d2c2764c931da9a6f52d8541b6901b2/ag_ui_protocol-0.1.13-py3-none-any.whl", hash = "sha256:1393fa894c1e8416efe184168a50689e760d05b32f4646eebb8ff423dddf8e8f", size = 8053, upload-time = "2026-02-19T18:40:37.27Z" }, +] + +[[package]] +name = "aiofile" +version = "3.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "caio", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/67/e2/d7cb819de8df6b5c1968a2756c3cb4122d4fa2b8fc768b53b7c9e5edb646/aiofile-3.9.0.tar.gz", hash = "sha256:e5ad718bb148b265b6df1b3752c4d1d83024b93da9bd599df74b9d9ffcf7919b", size = 17943, upload-time = "2024-10-08T10:39:35.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/25/da1f0b4dd970e52bf5a36c204c107e11a0c6d3ed195eba0bfbc664c312b2/aiofile-3.9.0-py3-none-any.whl", hash = "sha256:ce2f6c1571538cbdfa0143b04e16b208ecb0e9cb4148e528af8a640ed51cc8aa", size = 19539, upload-time = "2024-10-08T10:39:32.955Z" }, +] + [[package]] name = "aiohappyeyeballs" version = "2.6.1" @@ -204,6 +228,25 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, ] +[[package]] +name = "anthropic" +version = "0.84.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "docstring-parser" }, + { name = "httpx" }, + { name = "jiter" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/ea/0869d6df9ef83dcf393aeefc12dd81677d091c6ffc86f783e51cf44062f2/anthropic-0.84.0.tar.gz", hash = "sha256:72f5f90e5aebe62dca316cb013629cfa24996b0f5a4593b8c3d712bc03c43c37", size = 539457, upload-time = "2026-02-25T05:22:38.54Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/ca/218fa25002a332c0aa149ba18ffc0543175998b1f65de63f6d106689a345/anthropic-0.84.0-py3-none-any.whl", hash = "sha256:861c4c50f91ca45f942e091d83b60530ad6d4f98733bfe648065364da05d29e7", size = 455156, upload-time = "2026-02-25T05:22:40.468Z" }, +] + [[package]] name = "anyio" version = "4.12.1" @@ -227,6 +270,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/81/29/5ecc3a15d5a33e31b26c11426c45c501e439cb865d0bff96315d86443b78/appnope-0.1.4-py2.py3-none-any.whl", hash = "sha256:502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c", size = 4321, upload-time = "2024-02-06T09:43:09.663Z" }, ] +[[package]] +name = "argcomplete" +version = "3.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/38/61/0b9ae6399dd4a58d8c1b1dc5a27d6f2808023d0b5dd3104bb99f45a33ff6/argcomplete-3.6.3.tar.gz", hash = "sha256:62e8ed4fd6a45864acc8235409461b72c9a28ee785a2011cc5eb78318786c89c", size = 73754, upload-time = "2025-10-20T03:33:34.741Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/74/f5/9373290775639cb67a2fce7f629a1c240dce9f12fe927bc32b2736e16dfc/argcomplete-3.6.3-py3-none-any.whl", hash = "sha256:f5007b3a600ccac5d25bbce33089211dfd49eab4a7718da3f10e3082525a92ce", size = 43846, upload-time = "2025-10-20T03:33:33.021Z" }, +] + [[package]] name = "argon2-cffi" version = "25.1.0" @@ -314,6 +366,36 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, ] +[[package]] +name = "authlib" +version = "1.6.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/af/98/00d3dd826d46959ad8e32af2dbb2398868fd9fd0683c26e56d0789bd0e68/authlib-1.6.9.tar.gz", hash = "sha256:d8f2421e7e5980cc1ddb4e32d3f5fa659cfaf60d8eaf3281ebed192e4ab74f04", size = 165134, upload-time = "2026-03-02T07:44:01.998Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/53/23/b65f568ed0c22f1efacb744d2db1a33c8068f384b8c9b482b52ebdbc3ef6/authlib-1.6.9-py2.py3-none-any.whl", hash = "sha256:f08b4c14e08f0861dc18a32357b33fbcfd2ea86cfe3fe149484b4d764c4a0ac3", size = 244197, upload-time = "2026-03-02T07:44:00.307Z" }, +] + +[[package]] +name = "backports-tarfile" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/86/72/cd9b395f25e290e633655a100af28cb253e4393396264a98bd5f5951d50f/backports_tarfile-1.2.0.tar.gz", hash = "sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991", size = 86406, upload-time = "2024-05-28T17:01:54.731Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl", hash = "sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34", size = 30181, upload-time = "2024-05-28T17:01:53.112Z" }, +] + +[[package]] +name = "beartype" +version = "0.22.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/94/1009e248bbfbab11397abca7193bea6626806be9a327d399810d523a07cb/beartype-0.22.9.tar.gz", hash = "sha256:8f82b54aa723a2848a56008d18875f91c1db02c32ef6a62319a002e3e25a975f", size = 1608866, upload-time = "2025-12-13T06:50:30.72Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/cc/18245721fa7747065ab478316c7fea7c74777d07f37ae60db2e84f8172e8/beartype-0.22.9-py3-none-any.whl", hash = "sha256:d16c9bbc61ea14637596c5f6fbff2ee99cbe3573e46a716401734ef50c3060c2", size = 1333658, upload-time = "2025-12-13T06:50:28.266Z" }, +] + [[package]] name = "billiard" version = "4.2.4" @@ -352,6 +434,44 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e6/51/aac7e419521d5519e13087a7198623655648c939822bd7f4bdc9ccbe07f9/botocore-1.42.42-py3-none-any.whl", hash = "sha256:1c9df5fc31e9073a9aa956271c4007d72f5d342cafca5f4154ea099bc6f83085", size = 14600186, upload-time = "2026-02-04T20:28:29.268Z" }, ] +[[package]] +name = "cachetools" +version = "7.0.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/dd/57fe3fdb6e65b25a5987fd2cdc7e22db0aef508b91634d2e57d22928d41b/cachetools-7.0.5.tar.gz", hash = "sha256:0cd042c24377200c1dcd225f8b7b12b0ca53cc2c961b43757e774ebe190fd990", size = 37367, upload-time = "2026-03-09T20:51:29.451Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/f3/39cf3367b8107baa44f861dc802cbf16263c945b62d8265d36034fc07bea/cachetools-7.0.5-py3-none-any.whl", hash = "sha256:46bc8ebefbe485407621d0a4264b23c080cedd913921bad7ac3ed2f26c183114", size = 13918, upload-time = "2026-03-09T20:51:27.33Z" }, +] + +[[package]] +name = "caio" +version = "0.9.25" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/92/88/b8527e1b00c1811db339a1df8bd1ae49d146fcea9d6a5c40e3a80aaeb38d/caio-0.9.25.tar.gz", hash = "sha256:16498e7f81d1d0f5a4c0ad3f2540e65fe25691376e0a5bd367f558067113ed10", size = 26781, upload-time = "2025-12-26T15:21:36.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/80/ea4ead0c5d52a9828692e7df20f0eafe8d26e671ce4883a0a146bb91049e/caio-0.9.25-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ca6c8ecda611478b6016cb94d23fd3eb7124852b985bdec7ecaad9f3116b9619", size = 36836, upload-time = "2025-12-26T15:22:04.662Z" }, + { url = "https://files.pythonhosted.org/packages/17/b9/36715c97c873649d1029001578f901b50250916295e3dddf20c865438865/caio-0.9.25-cp310-cp310-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:db9b5681e4af8176159f0d6598e73b2279bb661e718c7ac23342c550bd78c241", size = 79695, upload-time = "2025-12-26T15:22:18.818Z" }, + { url = "https://files.pythonhosted.org/packages/0b/ab/07080ecb1adb55a02cbd8ec0126aa8e43af343ffabb6a71125b42670e9a1/caio-0.9.25-cp310-cp310-manylinux_2_34_aarch64.whl", hash = "sha256:bf61d7d0c4fd10ffdd98ca47f7e8db4d7408e74649ffaf4bef40b029ada3c21b", size = 79457, upload-time = "2026-03-04T22:08:16.024Z" }, + { url = "https://files.pythonhosted.org/packages/88/95/dd55757bb671eb4c376e006c04e83beb413486821f517792ea603ef216e9/caio-0.9.25-cp310-cp310-manylinux_2_34_x86_64.whl", hash = "sha256:ab52e5b643f8bbd64a0605d9412796cd3464cb8ca88593b13e95a0f0b10508ae", size = 77705, upload-time = "2026-03-04T22:08:17.202Z" }, + { url = "https://files.pythonhosted.org/packages/ec/90/543f556fcfcfa270713eef906b6352ab048e1e557afec12925c991dc93c2/caio-0.9.25-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d6956d9e4a27021c8bd6c9677f3a59eb1d820cc32d0343cea7961a03b1371965", size = 36839, upload-time = "2025-12-26T15:21:40.267Z" }, + { url = "https://files.pythonhosted.org/packages/51/3b/36f3e8ec38dafe8de4831decd2e44c69303d2a3892d16ceda42afed44e1b/caio-0.9.25-cp311-cp311-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bf84bfa039f25ad91f4f52944452a5f6f405e8afab4d445450978cd6241d1478", size = 80255, upload-time = "2025-12-26T15:22:20.271Z" }, + { url = "https://files.pythonhosted.org/packages/df/ce/65e64867d928e6aff1b4f0e12dba0ef6d5bf412c240dc1df9d421ac10573/caio-0.9.25-cp311-cp311-manylinux_2_34_aarch64.whl", hash = "sha256:ae3d62587332bce600f861a8de6256b1014d6485cfd25d68c15caf1611dd1f7c", size = 80052, upload-time = "2026-03-04T22:08:20.402Z" }, + { url = "https://files.pythonhosted.org/packages/46/90/e278863c47e14ec58309aa2e38a45882fbe67b4cc29ec9bc8f65852d3e45/caio-0.9.25-cp311-cp311-manylinux_2_34_x86_64.whl", hash = "sha256:fc220b8533dcf0f238a6b1a4a937f92024c71e7b10b5a2dfc1c73604a25709bc", size = 78273, upload-time = "2026-03-04T22:08:21.368Z" }, + { url = "https://files.pythonhosted.org/packages/d3/25/79c98ebe12df31548ba4eaf44db11b7cad6b3e7b4203718335620939083c/caio-0.9.25-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fb7ff95af4c31ad3f03179149aab61097a71fd85e05f89b4786de0359dffd044", size = 36983, upload-time = "2025-12-26T15:21:36.075Z" }, + { url = "https://files.pythonhosted.org/packages/a3/2b/21288691f16d479945968a0a4f2856818c1c5be56881d51d4dac9b255d26/caio-0.9.25-cp312-cp312-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:97084e4e30dfa598449d874c4d8e0c8d5ea17d2f752ef5e48e150ff9d240cd64", size = 82012, upload-time = "2025-12-26T15:22:20.983Z" }, + { url = "https://files.pythonhosted.org/packages/03/c4/8a1b580875303500a9c12b9e0af58cb82e47f5bcf888c2457742a138273c/caio-0.9.25-cp312-cp312-manylinux_2_34_aarch64.whl", hash = "sha256:4fa69eba47e0f041b9d4f336e2ad40740681c43e686b18b191b6c5f4c5544bfb", size = 81502, upload-time = "2026-03-04T22:08:22.381Z" }, + { url = "https://files.pythonhosted.org/packages/d1/1c/0fe770b8ffc8362c48134d1592d653a81a3d8748d764bec33864db36319d/caio-0.9.25-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:6bebf6f079f1341d19f7386db9b8b1f07e8cc15ae13bfdaff573371ba0575d69", size = 80200, upload-time = "2026-03-04T22:08:23.382Z" }, + { url = "https://files.pythonhosted.org/packages/31/57/5e6ff127e6f62c9f15d989560435c642144aa4210882f9494204bc892305/caio-0.9.25-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d6c2a3411af97762a2b03840c3cec2f7f728921ff8adda53d7ea2315a8563451", size = 36979, upload-time = "2025-12-26T15:21:35.484Z" }, + { url = "https://files.pythonhosted.org/packages/a3/9f/f21af50e72117eb528c422d4276cbac11fb941b1b812b182e0a9c70d19c5/caio-0.9.25-cp313-cp313-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0998210a4d5cd5cb565b32ccfe4e53d67303f868a76f212e002a8554692870e6", size = 81900, upload-time = "2025-12-26T15:22:21.919Z" }, + { url = "https://files.pythonhosted.org/packages/9c/12/c39ae2a4037cb10ad5eb3578eb4d5f8c1a2575c62bba675f3406b7ef0824/caio-0.9.25-cp313-cp313-manylinux_2_34_aarch64.whl", hash = "sha256:1a177d4777141b96f175fe2c37a3d96dec7911ed9ad5f02bac38aaa1c936611f", size = 81523, upload-time = "2026-03-04T22:08:25.187Z" }, + { url = "https://files.pythonhosted.org/packages/22/59/f8f2e950eb4f1a5a3883e198dca514b9d475415cb6cd7b78b9213a0dd45a/caio-0.9.25-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:9ed3cfb28c0e99fec5e208c934e5c157d0866aa9c32aa4dc5e9b6034af6286b7", size = 80243, upload-time = "2026-03-04T22:08:26.449Z" }, + { url = "https://files.pythonhosted.org/packages/69/ca/a08fdc7efdcc24e6a6131a93c85be1f204d41c58f474c42b0670af8c016b/caio-0.9.25-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fab6078b9348e883c80a5e14b382e6ad6aabbc4429ca034e76e730cf464269db", size = 36978, upload-time = "2025-12-26T15:21:41.055Z" }, + { url = "https://files.pythonhosted.org/packages/5e/6c/d4d24f65e690213c097174d26eda6831f45f4734d9d036d81790a27e7b78/caio-0.9.25-cp314-cp314-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:44a6b58e52d488c75cfaa5ecaa404b2b41cc965e6c417e03251e868ecd5b6d77", size = 81832, upload-time = "2025-12-26T15:22:22.757Z" }, + { url = "https://files.pythonhosted.org/packages/87/a4/e534cf7d2d0e8d880e25dd61e8d921ffcfe15bd696734589826f5a2df727/caio-0.9.25-cp314-cp314-manylinux_2_34_aarch64.whl", hash = "sha256:628a630eb7fb22381dd8e3c8ab7f59e854b9c806639811fc3f4310c6bd711d79", size = 81565, upload-time = "2026-03-04T22:08:27.483Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ed/bf81aeac1d290017e5e5ac3e880fd56ee15e50a6d0353986799d1bc5cfd5/caio-0.9.25-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:0ba16aa605ccb174665357fc729cf500679c2d94d5f1458a6f0d5ca48f2060a7", size = 80071, upload-time = "2026-03-04T22:08:28.751Z" }, + { url = "https://files.pythonhosted.org/packages/86/93/1f76c8d1bafe3b0614e06b2195784a3765bbf7b0a067661af9e2dd47fc33/caio-0.9.25-py3-none-any.whl", hash = "sha256:06c0bb02d6b929119b1cfbe1ca403c768b2013a369e2db46bfa2a5761cf82e40", size = 19087, upload-time = "2025-12-26T15:22:00.221Z" }, +] + [[package]] name = "celery" version = "5.6.2" @@ -667,6 +787,26 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/52/40/9d857001228658f0d59e97ebd4c346fe73e138c6de1bce61dc568a57c7f8/click_repl-0.3.0-py3-none-any.whl", hash = "sha256:fb7e06deb8da8de86180a33a9da97ac316751c094c6899382da7feeeeb51b812", size = 10289, upload-time = "2023-06-15T12:43:48.626Z" }, ] +[[package]] +name = "cohere" +version = "5.20.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fastavro", marker = "python_full_version < '3.11' or sys_platform != 'emscripten'" }, + { name = "httpx", marker = "python_full_version < '3.11' or sys_platform != 'emscripten'" }, + { name = "pydantic", marker = "python_full_version < '3.11' or sys_platform != 'emscripten'" }, + { name = "pydantic-core", marker = "python_full_version < '3.11' or sys_platform != 'emscripten'" }, + { name = "requests", marker = "python_full_version < '3.11' or sys_platform != 'emscripten'" }, + { name = "tokenizers", marker = "python_full_version < '3.11' or sys_platform != 'emscripten'" }, + { name = "types-requests", version = "2.31.0.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "types-requests", version = "2.32.4.20260107", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.10' and sys_platform != 'emscripten') or (python_full_version == '3.10.*' and sys_platform == 'emscripten')" }, + { name = "typing-extensions", marker = "python_full_version < '3.11' or sys_platform != 'emscripten'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/44/0b/96e2b55a0114ed9d69b3154565f54b764e7530735426290b000f467f4c0f/cohere-5.20.7.tar.gz", hash = "sha256:997ed85fabb3a1e4a4c036fdb520382e7bfa670db48eb59a026803b6f7061dbb", size = 184986, upload-time = "2026-02-25T01:22:18.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/86/dc991a75e3b9c2007b90dbfaf7f36fdb2457c216f799e26ce0474faf0c1f/cohere-5.20.7-py3-none-any.whl", hash = "sha256:043fef2a12c30c07e9b2c1f0b869fd66ffd911f58d1492f87e901c4190a65914", size = 323389, upload-time = "2026-02-25T01:22:16.902Z" }, +] + [[package]] name = "colorama" version = "0.4.6" @@ -745,6 +885,23 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/79/f4/9ceb90cfd6a3847069b0b0b353fd3075dc69b49defc70182d8af0c4ca390/cryptography-46.0.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:be8c01a7d5a55f9a47d1888162b76c8f49d62b234d88f0ff91a9fbebe32ffbc3", size = 3406043, upload-time = "2026-01-28T00:24:32.236Z" }, ] +[[package]] +name = "cyclopts" +version = "4.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs", marker = "python_full_version >= '3.10'" }, + { name = "docstring-parser", marker = "python_full_version >= '3.10'" }, + { name = "rich", marker = "python_full_version >= '3.10'" }, + { name = "rich-rst", marker = "python_full_version >= '3.10'" }, + { name = "tomli", marker = "python_full_version == '3.10.*'" }, + { name = "typing-extensions", marker = "python_full_version == '3.10.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/33/7a/3c3623755561c7f283dd769470e99ae36c46810bf3b3f264d69006f6c97a/cyclopts-4.8.0.tar.gz", hash = "sha256:92cc292d18d8be372e58d8bce1aa966d30f819a5fb3fee02bd2ad4a6bb403f29", size = 164066, upload-time = "2026-03-07T19:39:18.122Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/01/6ec7210775ea5e4989a10d89eda6c5ea7ff06caa614231ad533d74fecac8/cyclopts-4.8.0-py3-none-any.whl", hash = "sha256:ef353da05fec36587d4ebce7a6e4b27515d775d184a23bab4b01426f93ddc8d4", size = 201948, upload-time = "2026-03-07T19:39:19.307Z" }, +] + [[package]] name = "debugpy" version = "1.8.20" @@ -844,12 +1001,52 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e3/26/57c6fb270950d476074c087527a558ccb6f4436657314bfb6cdf484114c4/docker-7.1.0-py3-none-any.whl", hash = "sha256:c96b93b7f0a746f9e77d325bcfb87422a3d8bd4f03136ae8a85b37f1898d5fc0", size = 147774, upload-time = "2024-05-23T11:13:55.01Z" }, ] +[[package]] +name = "docstring-parser" +version = "0.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/9d/c3b43da9515bd270df0f80548d9944e389870713cc1fe2b8fb35fe2bcefd/docstring_parser-0.17.0.tar.gz", hash = "sha256:583de4a309722b3315439bb31d64ba3eebada841f2e2cee23b99df001434c912", size = 27442, upload-time = "2025-07-21T07:35:01.868Z" } +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 = "docutils" +version = "0.22.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/b6/03bb70946330e88ffec97aefd3ea75ba575cb2e762061e0e62a213befee8/docutils-0.22.4.tar.gz", hash = "sha256:4db53b1fde9abecbb74d91230d32ab626d94f6badfc575d6db9194a49df29968", size = 2291750, upload-time = "2025-12-18T19:00:26.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl", hash = "sha256:d0013f540772d1420576855455d050a2180186c91c15779301ac2ccb3eeb68de", size = 633196, upload-time = "2025-12-18T19:00:18.077Z" }, +] + +[[package]] +name = "email-validator" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dnspython", version = "2.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "idna", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/22/900cb125c76b7aaa450ce02fd727f452243f2e91a61af068b40adba60ea9/email_validator-2.3.0.tar.gz", hash = "sha256:9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426", size = 51238, upload-time = "2025-08-26T13:09:06.831Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl", hash = "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4", size = 35604, upload-time = "2025-08-26T13:09:05.858Z" }, +] + +[[package]] +name = "eval-type-backport" +version = "0.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fb/a3/cafafb4558fd638aadfe4121dc6cefb8d743368c085acb2f521df0f3d9d7/eval_type_backport-0.3.1.tar.gz", hash = "sha256:57e993f7b5b69d271e37482e62f74e76a0276c82490cf8e4f0dffeb6b332d5ed", size = 9445, upload-time = "2025-12-02T11:51:42.987Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cf/22/fdc2e30d43ff853720042fa15baa3e6122722be1a7950a98233ebb55cd71/eval_type_backport-0.3.1-py3-none-any.whl", hash = "sha256:279ab641905e9f11129f56a8a78f493518515b83402b860f6f06dd7c011fdfa8", size = 6063, upload-time = "2025-12-02T11:51:41.665Z" }, +] + [[package]] 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 = [ @@ -887,6 +1084,8 @@ dependencies = [ { name = "pandas", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "pandasql" }, { name = "pydantic" }, + { name = "pydantic-ai", version = "0.8.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pydantic-ai", version = "1.67.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "pydantic-settings", version = "2.11.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "pydantic-settings", version = "2.12.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "pygithub" }, @@ -923,6 +1122,7 @@ requires-dist = [ { name = "pandas", specifier = ">=2.3.3" }, { name = "pandasql", specifier = ">=0.7.3" }, { name = "pydantic", specifier = ">=2.5.1" }, + { name = "pydantic-ai", extras = ["google"], specifier = ">=0.1.0" }, { name = "pydantic-settings", specifier = ">=2.8.1" }, { name = "pygithub" }, { name = "pyjwt", specifier = ">=2.8.0" }, @@ -930,7 +1130,7 @@ requires-dist = [ { name = "pytest", specifier = ">=8.3.5" }, { name = "python-dotenv", specifier = ">=1.0.1" }, { name = "python-multipart", specifier = ">=0.0.6" }, - { name = "pyyaml", specifier = "==6.0.1" }, + { name = "pyyaml", specifier = ">=6.0.2" }, { name = "rdflib", specifier = ">=6.3.2" }, { name = "requests", specifier = ">=2.31.0" }, { name = "requests-toolbelt", specifier = ">=1.0.0" }, @@ -970,6 +1170,124 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1a/08/3953db1979ea131c68279b997c6465080118b407f0800445b843f8e164b3/fastapi-0.128.1-py3-none-any.whl", hash = "sha256:ee82146bbf91ea5bbf2bb8629e4c6e056c4fbd997ea6068501b11b15260b50fb", size = 103810, upload-time = "2026-02-04T17:35:08.02Z" }, ] +[[package]] +name = "fastavro" +version = "1.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/8b/fa2d3287fd2267be6261d0177c6809a7fa12c5600ddb33490c8dc29e77b2/fastavro-1.12.1.tar.gz", hash = "sha256:2f285be49e45bc047ab2f6bed040bb349da85db3f3c87880e4b92595ea093b2b", size = 1025661, upload-time = "2025-10-10T15:40:55.41Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/a0/077fd7cbfc143152cb96780cb592ed6cb6696667d8bc1b977745eb2255a8/fastavro-1.12.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:00650ca533907361edda22e6ffe8cf87ab2091c5d8aee5c8000b0f2dcdda7ed3", size = 1000335, upload-time = "2025-10-10T15:40:59.834Z" }, + { url = "https://files.pythonhosted.org/packages/a0/ae/a115e027f3a75df237609701b03ecba0b7f0aa3d77fe0161df533fde1eb7/fastavro-1.12.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac76d6d95f909c72ee70d314b460b7e711d928845771531d823eb96a10952d26", size = 3221067, upload-time = "2025-10-10T15:41:04.399Z" }, + { url = "https://files.pythonhosted.org/packages/94/4e/c4991c3eec0175af9a8a0c161b88089cb7bf7fe353b3e3be1bc4cf9036b2/fastavro-1.12.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f55eef18c41d4476bd32a82ed5dd86aabc3f614e1b66bdb09ffa291612e1670", size = 3228979, upload-time = "2025-10-10T15:41:06.738Z" }, + { url = "https://files.pythonhosted.org/packages/21/0c/f2afb8eaea38799ccb1ed07d68bf2659f2e313f1902bbd36774cf6a1bef9/fastavro-1.12.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:81563e1f93570e6565487cdb01ba241a36a00e58cff9c5a0614af819d1155d8f", size = 3160740, upload-time = "2025-10-10T15:41:08.731Z" }, + { url = "https://files.pythonhosted.org/packages/0d/1a/f4d367924b40b86857862c1fa65f2afba94ddadf298b611e610a676a29e5/fastavro-1.12.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bec207360f76f0b3de540758a297193c5390e8e081c43c3317f610b1414d8c8f", size = 3235787, upload-time = "2025-10-10T15:41:10.869Z" }, + { url = "https://files.pythonhosted.org/packages/90/ec/8db9331896e3dfe4f71b2b3c23f2e97fbbfd90129777467ca9f8bafccb74/fastavro-1.12.1-cp310-cp310-win_amd64.whl", hash = "sha256:c0390bfe4a9f8056a75ac6785fbbff8f5e317f5356481d2e29ec980877d2314b", size = 449350, upload-time = "2025-10-10T15:41:12.104Z" }, + { url = "https://files.pythonhosted.org/packages/a0/e9/31c64b47cefc0951099e7c0c8c8ea1c931edd1350f34d55c27cbfbb08df1/fastavro-1.12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6b632b713bc5d03928a87d811fa4a11d5f25cd43e79c161e291c7d3f7aa740fd", size = 1016585, upload-time = "2025-10-10T15:41:13.717Z" }, + { url = "https://files.pythonhosted.org/packages/10/76/111560775b548f5d8d828c1b5285ff90e2d2745643fb80ecbf115344eea4/fastavro-1.12.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa7ab3769beadcebb60f0539054c7755f63bd9cf7666e2c15e615ab605f89a8", size = 3404629, upload-time = "2025-10-10T15:41:15.642Z" }, + { url = "https://files.pythonhosted.org/packages/b0/07/6bb93cb963932146c2b6c5c765903a0a547ad9f0f8b769a4a9aad8c06369/fastavro-1.12.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:123fb221df3164abd93f2d042c82f538a1d5a43ce41375f12c91ce1355a9141e", size = 3428594, upload-time = "2025-10-10T15:41:17.779Z" }, + { url = "https://files.pythonhosted.org/packages/d1/67/8115ec36b584197ea737ec79e3499e1f1b640b288d6c6ee295edd13b80f6/fastavro-1.12.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:632a4e3ff223f834ddb746baae0cc7cee1068eb12c32e4d982c2fee8a5b483d0", size = 3344145, upload-time = "2025-10-10T15:41:19.89Z" }, + { url = "https://files.pythonhosted.org/packages/9e/9e/a7cebb3af967e62539539897c10138fa0821668ec92525d1be88a9cd3ee6/fastavro-1.12.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:83e6caf4e7a8717d932a3b1ff31595ad169289bbe1128a216be070d3a8391671", size = 3431942, upload-time = "2025-10-10T15:41:22.076Z" }, + { url = "https://files.pythonhosted.org/packages/c0/d1/7774ddfb8781c5224294c01a593ebce2ad3289b948061c9701bd1903264d/fastavro-1.12.1-cp311-cp311-win_amd64.whl", hash = "sha256:b91a0fe5a173679a6c02d53ca22dcaad0a2c726b74507e0c1c2e71a7c3f79ef9", size = 450542, upload-time = "2025-10-10T15:41:23.333Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f0/10bd1a3d08667fa0739e2b451fe90e06df575ec8b8ba5d3135c70555c9bd/fastavro-1.12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:509818cb24b98a804fc80be9c5fed90f660310ae3d59382fc811bfa187122167", size = 1009057, upload-time = "2025-10-10T15:41:24.556Z" }, + { url = "https://files.pythonhosted.org/packages/78/ad/0d985bc99e1fa9e74c636658000ba38a5cd7f5ab2708e9c62eaf736ecf1a/fastavro-1.12.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:089e155c0c76e0d418d7e79144ce000524dd345eab3bc1e9c5ae69d500f71b14", size = 3391866, upload-time = "2025-10-10T15:41:26.882Z" }, + { url = "https://files.pythonhosted.org/packages/0d/9e/b4951dc84ebc34aac69afcbfbb22ea4a91080422ec2bfd2c06076ff1d419/fastavro-1.12.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44cbff7518901c91a82aab476fcab13d102e4999499df219d481b9e15f61af34", size = 3458005, upload-time = "2025-10-10T15:41:29.017Z" }, + { url = "https://files.pythonhosted.org/packages/af/f8/5a8df450a9f55ca8441f22ea0351d8c77809fc121498b6970daaaf667a21/fastavro-1.12.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a275e48df0b1701bb764b18a8a21900b24cf882263cb03d35ecdba636bbc830b", size = 3295258, upload-time = "2025-10-10T15:41:31.564Z" }, + { url = "https://files.pythonhosted.org/packages/99/b2/40f25299111d737e58b85696e91138a66c25b7334f5357e7ac2b0e8966f8/fastavro-1.12.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2de72d786eb38be6b16d556b27232b1bf1b2797ea09599507938cdb7a9fe3e7c", size = 3430328, upload-time = "2025-10-10T15:41:33.689Z" }, + { url = "https://files.pythonhosted.org/packages/e0/07/85157a7c57c5f8b95507d7829b5946561e5ee656ff80e9dd9a757f53ddaf/fastavro-1.12.1-cp312-cp312-win_amd64.whl", hash = "sha256:9090f0dee63fe022ee9cc5147483366cc4171c821644c22da020d6b48f576b4f", size = 444140, upload-time = "2025-10-10T15:41:34.902Z" }, + { url = "https://files.pythonhosted.org/packages/bb/57/26d5efef9182392d5ac9f253953c856ccb66e4c549fd3176a1e94efb05c9/fastavro-1.12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:78df838351e4dff9edd10a1c41d1324131ffecbadefb9c297d612ef5363c049a", size = 1000599, upload-time = "2025-10-10T15:41:36.554Z" }, + { url = "https://files.pythonhosted.org/packages/33/cb/8ab55b21d018178eb126007a56bde14fd01c0afc11d20b5f2624fe01e698/fastavro-1.12.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:780476c23175d2ae457c52f45b9ffa9d504593499a36cd3c1929662bf5b7b14b", size = 3335933, upload-time = "2025-10-10T15:41:39.07Z" }, + { url = "https://files.pythonhosted.org/packages/fe/03/9c94ec9bf873eb1ffb0aa694f4e71940154e6e9728ddfdc46046d7e8ced4/fastavro-1.12.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0714b285160fcd515eb0455540f40dd6dac93bdeacdb03f24e8eac3d8aa51f8d", size = 3402066, upload-time = "2025-10-10T15:41:41.608Z" }, + { url = "https://files.pythonhosted.org/packages/75/c8/cb472347c5a584ccb8777a649ebb28278fccea39d005fc7df19996f41df8/fastavro-1.12.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a8bc2dcec5843d499f2489bfe0747999108f78c5b29295d877379f1972a3d41a", size = 3240038, upload-time = "2025-10-10T15:41:43.743Z" }, + { url = "https://files.pythonhosted.org/packages/e1/77/569ce9474c40304b3a09e109494e020462b83e405545b78069ddba5f614e/fastavro-1.12.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3b1921ac35f3d89090a5816b626cf46e67dbecf3f054131f84d56b4e70496f45", size = 3369398, upload-time = "2025-10-10T15:41:45.719Z" }, + { url = "https://files.pythonhosted.org/packages/4a/1f/9589e35e9ea68035385db7bdbf500d36b8891db474063fb1ccc8215ee37c/fastavro-1.12.1-cp313-cp313-win_amd64.whl", hash = "sha256:5aa777b8ee595b50aa084104cd70670bf25a7bbb9fd8bb5d07524b0785ee1699", size = 444220, upload-time = "2025-10-10T15:41:47.39Z" }, + { url = "https://files.pythonhosted.org/packages/6c/d2/78435fe737df94bd8db2234b2100f5453737cffd29adee2504a2b013de84/fastavro-1.12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:c3d67c47f177e486640404a56f2f50b165fe892cc343ac3a34673b80cc7f1dd6", size = 1086611, upload-time = "2025-10-10T15:41:48.818Z" }, + { url = "https://files.pythonhosted.org/packages/b6/be/428f99b10157230ddac77ec8cc167005b29e2bd5cbe228345192bb645f30/fastavro-1.12.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5217f773492bac43dae15ff2931432bce2d7a80be7039685a78d3fab7df910bd", size = 3541001, upload-time = "2025-10-10T15:41:50.871Z" }, + { url = "https://files.pythonhosted.org/packages/16/08/a2eea4f20b85897740efe44887e1ac08f30dfa4bfc3de8962bdcbb21a5a1/fastavro-1.12.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:469fecb25cba07f2e1bfa4c8d008477cd6b5b34a59d48715e1b1a73f6160097d", size = 3432217, upload-time = "2025-10-10T15:41:53.149Z" }, + { url = "https://files.pythonhosted.org/packages/87/bb/b4c620b9eb6e9838c7f7e4b7be0762834443adf9daeb252a214e9ad3178c/fastavro-1.12.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d71c8aa841ef65cfab709a22bb887955f42934bced3ddb571e98fdbdade4c609", size = 3366742, upload-time = "2025-10-10T15:41:55.237Z" }, + { url = "https://files.pythonhosted.org/packages/3d/d1/e69534ccdd5368350646fea7d93be39e5f77c614cca825c990bd9ca58f67/fastavro-1.12.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:b81fc04e85dfccf7c028e0580c606e33aa8472370b767ef058aae2c674a90746", size = 3383743, upload-time = "2025-10-10T15:41:57.68Z" }, + { url = "https://files.pythonhosted.org/packages/58/54/b7b4a0c3fb5fcba38128542da1b26c4e6d69933c923f493548bdfd63ab6a/fastavro-1.12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:9445da127751ba65975d8e4bdabf36bfcfdad70fc35b2d988e3950cce0ec0e7c", size = 1001377, upload-time = "2025-10-10T15:41:59.241Z" }, + { url = "https://files.pythonhosted.org/packages/1e/4f/0e589089c7df0d8f57d7e5293fdc34efec9a3b758a0d4d0c99a7937e2492/fastavro-1.12.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ed924233272719b5d5a6a0b4d80ef3345fc7e84fc7a382b6232192a9112d38a6", size = 3320401, upload-time = "2025-10-10T15:42:01.682Z" }, + { url = "https://files.pythonhosted.org/packages/f9/19/260110d56194ae29d7e423a336fccea8bcd103196d00f0b364b732bdb84e/fastavro-1.12.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3616e2f0e1c9265e92954fa099db79c6e7817356d3ff34f4bcc92699ae99697c", size = 3350894, upload-time = "2025-10-10T15:42:04.073Z" }, + { url = "https://files.pythonhosted.org/packages/d0/96/58b0411e8be9694d5972bee3167d6c1fd1fdfdf7ce253c1a19a327208f4f/fastavro-1.12.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:cb0337b42fd3c047fcf0e9b7597bd6ad25868de719f29da81eabb6343f08d399", size = 3229644, upload-time = "2025-10-10T15:42:06.221Z" }, + { url = "https://files.pythonhosted.org/packages/5b/db/38660660eac82c30471d9101f45b3acfdcbadfe42d8f7cdb129459a45050/fastavro-1.12.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:64961ab15b74b7c168717bbece5660e0f3d457837c3cc9d9145181d011199fa7", size = 3329704, upload-time = "2025-10-10T15:42:08.384Z" }, + { url = "https://files.pythonhosted.org/packages/9d/a9/1672910f458ecb30b596c9e59e41b7c00309b602a0494341451e92e62747/fastavro-1.12.1-cp314-cp314-win_amd64.whl", hash = "sha256:792356d320f6e757e89f7ac9c22f481e546c886454a6709247f43c0dd7058004", size = 452911, upload-time = "2025-10-10T15:42:09.795Z" }, + { url = "https://files.pythonhosted.org/packages/dc/8d/2e15d0938ded1891b33eff252e8500605508b799c2e57188a933f0bd744c/fastavro-1.12.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:120aaf82ac19d60a1016afe410935fe94728752d9c2d684e267e5b7f0e70f6d9", size = 3541999, upload-time = "2025-10-10T15:42:11.794Z" }, + { url = "https://files.pythonhosted.org/packages/a7/1c/6dfd082a205be4510543221b734b1191299e6a1810c452b6bc76dfa6968e/fastavro-1.12.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6a3462934b20a74f9ece1daa49c2e4e749bd9a35fa2657b53bf62898fba80f5", size = 3433972, upload-time = "2025-10-10T15:42:14.485Z" }, + { url = "https://files.pythonhosted.org/packages/24/90/9de694625a1a4b727b1ad0958d220cab25a9b6cf7f16a5c7faa9ea7b2261/fastavro-1.12.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1f81011d54dd47b12437b51dd93a70a9aa17b61307abf26542fc3c13efbc6c51", size = 3368752, upload-time = "2025-10-10T15:42:16.618Z" }, + { url = "https://files.pythonhosted.org/packages/fa/93/b44f67589e4d439913dab6720f7e3507b0fa8b8e56d06f6fc875ced26afb/fastavro-1.12.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:43ded16b3f4a9f1a42f5970c2aa618acb23ea59c4fcaa06680bdf470b255e5a8", size = 3386636, upload-time = "2025-10-10T15:42:18.974Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b8/67f5682cd59cb59ec6eecb5479b132caaf69709d1e1ceda4f92d8c69d7f1/fastavro-1.12.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:02281432dcb11c78b3280da996eff61ee0eff39c5de06c6e0fbf19275093e6d4", size = 1002311, upload-time = "2025-10-10T15:42:20.415Z" }, + { url = "https://files.pythonhosted.org/packages/93/38/2f15a7ad24e4b6e0239016c068f142358732bf8ead0315ee926b88634bec/fastavro-1.12.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4128978b930aaf930332db4b3acc290783183f3be06a241ae4a482f3ed8ce892", size = 3195871, upload-time = "2025-10-10T15:42:22.675Z" }, + { url = "https://files.pythonhosted.org/packages/c1/e5/6fc0250b3006b1b42c1ab9f0511ccd44e1aeb15a63a77fc780ee97f58797/fastavro-1.12.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:546ffffda6610fca672f0ed41149808e106d8272bb246aa7539fa8bb6f117f17", size = 3204127, upload-time = "2025-10-10T15:42:24.855Z" }, + { url = "https://files.pythonhosted.org/packages/c6/43/1f3909eb096eb1066e416f0875abe783b73fabe823ad616f6956b3e80e84/fastavro-1.12.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:a7d840ccd9aacada3ddc80fbcc4ea079b658107fe62e9d289a0de9d54e95d366", size = 3135604, upload-time = "2025-10-10T15:42:28.899Z" }, + { url = "https://files.pythonhosted.org/packages/e6/61/ec083a3a5d7c2b97d0e2c9e137a6e667583afe884af0e49318f7ee7eaa5a/fastavro-1.12.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:3100ad643e7fa658469a2a2db229981c1a000ff16b8037c0b58ce3ec4d2107e8", size = 3210932, upload-time = "2025-10-10T15:42:30.891Z" }, + { url = "https://files.pythonhosted.org/packages/9e/ba/b814fb09b32c8f3059451c33bb11d55eb23188e6a499f5dde628d13bc076/fastavro-1.12.1-cp39-cp39-win_amd64.whl", hash = "sha256:a38607444281619eda3a9c1be9f5397634012d1b237142eee1540e810b30ac8b", size = 510328, upload-time = "2025-10-10T15:42:32.415Z" }, +] + +[[package]] +name = "fastmcp" +version = "3.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "authlib", marker = "python_full_version >= '3.10'" }, + { name = "cyclopts", marker = "python_full_version >= '3.10'" }, + { name = "exceptiongroup", marker = "python_full_version >= '3.10'" }, + { name = "httpx", marker = "python_full_version >= '3.10'" }, + { name = "jsonref", marker = "python_full_version >= '3.10'" }, + { name = "jsonschema-path", marker = "python_full_version >= '3.10'" }, + { name = "mcp", marker = "python_full_version >= '3.10'" }, + { name = "openapi-pydantic", marker = "python_full_version >= '3.10'" }, + { name = "opentelemetry-api", marker = "python_full_version >= '3.10'" }, + { name = "packaging", version = "25.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "platformdirs", version = "4.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "py-key-value-aio", extra = ["filetree", "keyring", "memory"], marker = "python_full_version >= '3.10'" }, + { name = "pydantic", extra = ["email"], marker = "python_full_version >= '3.10'" }, + { name = "pyperclip", marker = "python_full_version >= '3.10'" }, + { name = "python-dotenv", marker = "python_full_version >= '3.10'" }, + { name = "pyyaml", marker = "python_full_version >= '3.10'" }, + { name = "rich", marker = "python_full_version >= '3.10'" }, + { name = "uncalled-for", marker = "python_full_version >= '3.10'" }, + { name = "uvicorn", version = "0.40.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "watchfiles", marker = "python_full_version >= '3.10'" }, + { name = "websockets", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0a/70/862026c4589441f86ad3108f05bfb2f781c6b322ad60a982f40b303b47d7/fastmcp-3.1.0.tar.gz", hash = "sha256:e25264794c734b9977502a51466961eeecff92a0c2f3b49c40c070993628d6d0", size = 17347083, upload-time = "2026-03-03T02:43:11.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/07/516f5b20d88932e5a466c2216b628e5358a71b3a9f522215607c3281de05/fastmcp-3.1.0-py3-none-any.whl", hash = "sha256:b1f73b56fd3b0cb2bd9e2a144fc650d5cc31587ed129d996db7710e464ae8010", size = 633749, upload-time = "2026-03-03T02:43:09.06Z" }, +] + +[[package]] +name = "filelock" +version = "3.19.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/40/bb/0ab3e58d22305b6f5440629d20683af28959bf793d98d11950e305c1c326/filelock-3.19.1.tar.gz", hash = "sha256:66eda1888b0171c998b35be2bcc0f6d75c388a7ce20c3f3f37aa8e96c2dddf58", size = 17687, upload-time = "2025-08-14T16:56:03.016Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/14/42b2651a2f46b022ccd948bca9f2d5af0fd8929c4eec235b8d6d844fbe67/filelock-3.19.1-py3-none-any.whl", hash = "sha256:d38e30481def20772f5baf097c122c3babc4fcdb7e14e57049eb9d88c6dc017d", size = 15988, upload-time = "2025-08-14T16:56:01.633Z" }, +] + +[[package]] +name = "filelock" +version = "3.25.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'emscripten'", + "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'emscripten'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.10.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/94/b8/00651a0f559862f3bb7d6f7477b192afe3f583cc5e26403b44e59a55ab34/filelock-3.25.2.tar.gz", hash = "sha256:b64ece2b38f4ca29dd3e810287aa8c48182bbecd1ae6e9ae126c9b35f1382694", size = 40480, upload-time = "2026-03-11T20:45:38.487Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/a5/842ae8f0c08b61d6484b52f99a03510a3a72d23141942d216ebe81fefbce/filelock-3.25.2-py3-none-any.whl", hash = "sha256:ca8afb0da15f229774c9ad1b455ed96e85a81373065fb10446672f64444ddf70", size = 26759, upload-time = "2026-03-11T20:45:37.437Z" }, +] + [[package]] name = "frozenlist" version = "1.8.0" @@ -1107,6 +1425,53 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, ] +[[package]] +name = "fsspec" +version = "2025.10.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/24/7f/2747c0d332b9acfa75dc84447a066fdf812b5a6b8d30472b74d309bfe8cb/fsspec-2025.10.0.tar.gz", hash = "sha256:b6789427626f068f9a83ca4e8a3cc050850b6c0f71f99ddb4f542b8266a26a59", size = 309285, upload-time = "2025-10-30T14:58:44.036Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/02/a6b21098b1d5d6249b7c5ab69dde30108a71e4e819d4a9778f1de1d5b70d/fsspec-2025.10.0-py3-none-any.whl", hash = "sha256:7c7712353ae7d875407f97715f0e1ffcc21e33d5b24556cb1e090ae9409ec61d", size = 200966, upload-time = "2025-10-30T14:58:42.53Z" }, +] + +[[package]] +name = "fsspec" +version = "2026.2.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'emscripten'", + "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'emscripten'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.10.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/51/7c/f60c259dcbf4f0c47cc4ddb8f7720d2dcdc8888c8e5ad84c73ea4531cc5b/fsspec-2026.2.0.tar.gz", hash = "sha256:6544e34b16869f5aacd5b90bdf1a71acb37792ea3ddf6125ee69a22a53fb8bff", size = 313441, upload-time = "2026-02-05T21:50:53.743Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/ab/fb21f4c939bb440104cc2b396d3be1d9b7a9fd3c6c2a53d98c45b3d7c954/fsspec-2026.2.0-py3-none-any.whl", hash = "sha256:98de475b5cb3bd66bedd5c4679e87b4fdfe1a3bf4d707b151b3c07e58c9a2437", size = 202505, upload-time = "2026-02-05T21:50:51.819Z" }, +] + +[[package]] +name = "genai-prices" +version = "0.0.55" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "eval-type-backport", marker = "python_full_version < '3.10'" }, + { name = "httpx" }, + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/77/67/de9d9be180db6d80b298c281dff71502095c0776d7cc9286f486f667f61a/genai_prices-0.0.55.tar.gz", hash = "sha256:8692c65d0deefe2ad0680d71841eb12822a35945a6060d2b6adbcbdf4945e1cb", size = 59987, upload-time = "2026-02-26T17:56:41.467Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/98/66a06b82a5c840f896490d5ef9c7691776b147589f2e8d2fa66c67a3db9c/genai_prices-0.0.55-py3-none-any.whl", hash = "sha256:ccd795c90c926b3c71066bf5656f14c67fc11fdba6d71e072c7fb4fa311e1b12", size = 62603, upload-time = "2026-02-26T17:56:40.502Z" }, +] + [[package]] name = "google-auth" version = "2.48.0" @@ -1330,6 +1695,146 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e1/2b/98c7f93e6db9977aaee07eb1e51ca63bd5f779b900d362791d3252e60558/greenlet-3.3.1-cp314-cp314t-win_amd64.whl", hash = "sha256:301860987846c24cb8964bdec0e31a96ad4a2a801b41b4ef40963c1b44f33451", size = 233181, upload-time = "2026-01-23T15:33:00.29Z" }, ] +[[package]] +name = "griffe" +version = "1.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ec/d7/6c09dd7ce4c7837e4cdb11dce980cb45ae3cd87677298dc3b781b6bce7d3/griffe-1.14.0.tar.gz", hash = "sha256:9d2a15c1eca966d68e00517de5d69dd1bc5c9f2335ef6c1775362ba5b8651a13", size = 424684, upload-time = "2025-09-05T15:02:29.167Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/b1/9ff6578d789a89812ff21e4e0f80ffae20a65d5dd84e7a17873fe3b365be/griffe-1.14.0-py3-none-any.whl", hash = "sha256:0e9d52832cccf0f7188cfe585ba962d2674b241c01916d780925df34873bceb0", size = 144439, upload-time = "2025-09-05T15:02:27.511Z" }, +] + +[[package]] +name = "griffelib" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/51/c936033e16d12b627ea334aaaaf42229c37620d0f15593456ab69ab48161/griffelib-2.0.0-py3-none-any.whl", hash = "sha256:01284878c966508b6d6f1dbff9b6fa607bc062d8261c5c7253cb285b06422a7f", size = 142004, upload-time = "2026-02-09T19:09:40.561Z" }, +] + +[[package]] +name = "groq" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "anyio", marker = "python_full_version < '3.10'" }, + { name = "distro", marker = "python_full_version < '3.10'" }, + { name = "httpx", marker = "python_full_version < '3.10'" }, + { name = "pydantic", marker = "python_full_version < '3.10'" }, + { name = "sniffio", marker = "python_full_version < '3.10'" }, + { name = "typing-extensions", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3f/12/f4099a141677fcd2ed79dcc1fcec431e60c52e0e90c9c5d935f0ffaf8c0e/groq-1.0.0.tar.gz", hash = "sha256:66cb7bb729e6eb644daac7ce8efe945e99e4eb33657f733ee6f13059ef0c25a9", size = 146068, upload-time = "2025-12-17T23:34:23.115Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/88/3175759d2ef30406ea721f4d837bfa1ba4339fde3b81ba8c5640a96ed231/groq-1.0.0-py3-none-any.whl", hash = "sha256:6e22bf92ffad988f01d2d4df7729add66b8fd5dbfb2154b5bbf3af245b72c731", size = 138292, upload-time = "2025-12-17T23:34:21.957Z" }, +] + +[[package]] +name = "groq" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'emscripten'", + "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'emscripten'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "anyio", marker = "python_full_version >= '3.10'" }, + { name = "distro", marker = "python_full_version >= '3.10'" }, + { name = "httpx", marker = "python_full_version >= '3.10'" }, + { name = "pydantic", marker = "python_full_version >= '3.10'" }, + { name = "sniffio", marker = "python_full_version >= '3.10'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/bc/7ad1d9967c58b21cdec0c94f26f40fc37b07ba60715d6cbc7c7ef775d927/groq-1.1.1.tar.gz", hash = "sha256:ea971eca72d88e875a78567904bfb46a2f2e43907bfe400fc36a81150a4066d8", size = 150783, upload-time = "2026-03-11T09:11:32.027Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/1d/0749c5f0ed76693f6a3a40e2b0c40201fa23e1ccb00e69d5aa63e3f5b0ff/groq-1.1.1-py3-none-any.whl", hash = "sha256:6b7932c0fd3189ad1842fbc294f57fbf014713e01f72037451cb60a138c4b846", size = 139650, upload-time = "2026-03-11T09:11:29.87Z" }, +] + +[[package]] +name = "grpcio" +version = "1.78.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/8a/3d098f35c143a89520e568e6539cc098fcd294495910e359889ce8741c84/grpcio-1.78.0.tar.gz", hash = "sha256:7382b95189546f375c174f53a5fa873cef91c4b8005faa05cc5b3beea9c4f1c5", size = 12852416, upload-time = "2026-02-06T09:57:18.093Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/a8/690a085b4d1fe066130de97a87de32c45062cf2ecd218df9675add895550/grpcio-1.78.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:7cc47943d524ee0096f973e1081cb8f4f17a4615f2116882a5f1416e4cfe92b5", size = 5946986, upload-time = "2026-02-06T09:54:34.043Z" }, + { url = "https://files.pythonhosted.org/packages/c7/1b/e5213c5c0ced9d2d92778d30529ad5bb2dcfb6c48c4e2d01b1f302d33d64/grpcio-1.78.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:c3f293fdc675ccba4db5a561048cca627b5e7bd1c8a6973ffedabe7d116e22e2", size = 11816533, upload-time = "2026-02-06T09:54:37.04Z" }, + { url = "https://files.pythonhosted.org/packages/18/37/1ba32dccf0a324cc5ace744c44331e300b000a924bf14840f948c559ede7/grpcio-1.78.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:10a9a644b5dd5aec3b82b5b0b90d41c0fa94c85ef42cb42cf78a23291ddb5e7d", size = 6519964, upload-time = "2026-02-06T09:54:40.268Z" }, + { url = "https://files.pythonhosted.org/packages/ed/f5/c0e178721b818072f2e8b6fde13faaba942406c634009caf065121ce246b/grpcio-1.78.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:4c5533d03a6cbd7f56acfc9cfb44ea64f63d29091e40e44010d34178d392d7eb", size = 7198058, upload-time = "2026-02-06T09:54:42.389Z" }, + { url = "https://files.pythonhosted.org/packages/5b/b2/40d43c91ae9cd667edc960135f9f08e58faa1576dc95af29f66ec912985f/grpcio-1.78.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ff870aebe9a93a85283837801d35cd5f8814fe2ad01e606861a7fb47c762a2b7", size = 6727212, upload-time = "2026-02-06T09:54:44.91Z" }, + { url = "https://files.pythonhosted.org/packages/ed/88/9da42eed498f0efcfcd9156e48ae63c0cde3bea398a16c99fb5198c885b6/grpcio-1.78.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:391e93548644e6b2726f1bb84ed60048d4bcc424ce5e4af0843d28ca0b754fec", size = 7300845, upload-time = "2026-02-06T09:54:47.562Z" }, + { url = "https://files.pythonhosted.org/packages/23/3f/1c66b7b1b19a8828890e37868411a6e6925df5a9030bfa87ab318f34095d/grpcio-1.78.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:df2c8f3141f7cbd112a6ebbd760290b5849cda01884554f7c67acc14e7b1758a", size = 8284605, upload-time = "2026-02-06T09:54:50.475Z" }, + { url = "https://files.pythonhosted.org/packages/94/c4/ca1bd87394f7b033e88525384b4d1e269e8424ab441ea2fba1a0c5b50986/grpcio-1.78.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bd8cb8026e5f5b50498a3c4f196f57f9db344dad829ffae16b82e4fdbaea2813", size = 7726672, upload-time = "2026-02-06T09:54:53.11Z" }, + { url = "https://files.pythonhosted.org/packages/41/09/f16e487d4cc65ccaf670f6ebdd1a17566b965c74fc3d93999d3b2821e052/grpcio-1.78.0-cp310-cp310-win32.whl", hash = "sha256:f8dff3d9777e5d2703a962ee5c286c239bf0ba173877cc68dc02c17d042e29de", size = 4076715, upload-time = "2026-02-06T09:54:55.549Z" }, + { url = "https://files.pythonhosted.org/packages/2a/32/4ce60d94e242725fd3bcc5673c04502c82a8e87b21ea411a63992dc39f8f/grpcio-1.78.0-cp310-cp310-win_amd64.whl", hash = "sha256:94f95cf5d532d0e717eed4fc1810e8e6eded04621342ec54c89a7c2f14b581bf", size = 4799157, upload-time = "2026-02-06T09:54:59.838Z" }, + { url = "https://files.pythonhosted.org/packages/86/c7/d0b780a29b0837bf4ca9580904dfb275c1fc321ded7897d620af7047ec57/grpcio-1.78.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:2777b783f6c13b92bd7b716667452c329eefd646bfb3f2e9dabea2e05dbd34f6", size = 5951525, upload-time = "2026-02-06T09:55:01.989Z" }, + { url = "https://files.pythonhosted.org/packages/c5/b1/96920bf2ee61df85a9503cb6f733fe711c0ff321a5a697d791b075673281/grpcio-1.78.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:9dca934f24c732750389ce49d638069c3892ad065df86cb465b3fa3012b70c9e", size = 11830418, upload-time = "2026-02-06T09:55:04.462Z" }, + { url = "https://files.pythonhosted.org/packages/83/0c/7c1528f098aeb75a97de2bae18c530f56959fb7ad6c882db45d9884d6edc/grpcio-1.78.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:459ab414b35f4496138d0ecd735fed26f1318af5e52cb1efbc82a09f0d5aa911", size = 6524477, upload-time = "2026-02-06T09:55:07.111Z" }, + { url = "https://files.pythonhosted.org/packages/8d/52/e7c1f3688f949058e19a011c4e0dec973da3d0ae5e033909677f967ae1f4/grpcio-1.78.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:082653eecbdf290e6e3e2c276ab2c54b9e7c299e07f4221872380312d8cf395e", size = 7198266, upload-time = "2026-02-06T09:55:10.016Z" }, + { url = "https://files.pythonhosted.org/packages/e5/61/8ac32517c1e856677282c34f2e7812d6c328fa02b8f4067ab80e77fdc9c9/grpcio-1.78.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:85f93781028ec63f383f6bc90db785a016319c561cc11151fbb7b34e0d012303", size = 6730552, upload-time = "2026-02-06T09:55:12.207Z" }, + { url = "https://files.pythonhosted.org/packages/bd/98/b8ee0158199250220734f620b12e4a345955ac7329cfd908d0bf0fda77f0/grpcio-1.78.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f12857d24d98441af6a1d5c87442d624411db486f7ba12550b07788f74b67b04", size = 7304296, upload-time = "2026-02-06T09:55:15.044Z" }, + { url = "https://files.pythonhosted.org/packages/bd/0f/7b72762e0d8840b58032a56fdbd02b78fc645b9fa993d71abf04edbc54f4/grpcio-1.78.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5397fff416b79e4b284959642a4e95ac4b0f1ece82c9993658e0e477d40551ec", size = 8288298, upload-time = "2026-02-06T09:55:17.276Z" }, + { url = "https://files.pythonhosted.org/packages/24/ae/ae4ce56bc5bb5caa3a486d60f5f6083ac3469228faa734362487176c15c5/grpcio-1.78.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:fbe6e89c7ffb48518384068321621b2a69cab509f58e40e4399fdd378fa6d074", size = 7730953, upload-time = "2026-02-06T09:55:19.545Z" }, + { url = "https://files.pythonhosted.org/packages/b5/6e/8052e3a28eb6a820c372b2eb4b5e32d195c661e137d3eca94d534a4cfd8a/grpcio-1.78.0-cp311-cp311-win32.whl", hash = "sha256:6092beabe1966a3229f599d7088b38dfc8ffa1608b5b5cdda31e591e6500f856", size = 4076503, upload-time = "2026-02-06T09:55:21.521Z" }, + { url = "https://files.pythonhosted.org/packages/08/62/f22c98c5265dfad327251fa2f840b591b1df5f5e15d88b19c18c86965b27/grpcio-1.78.0-cp311-cp311-win_amd64.whl", hash = "sha256:1afa62af6e23f88629f2b29ec9e52ec7c65a7176c1e0a83292b93c76ca882558", size = 4799767, upload-time = "2026-02-06T09:55:24.107Z" }, + { url = "https://files.pythonhosted.org/packages/4e/f4/7384ed0178203d6074446b3c4f46c90a22ddf7ae0b3aee521627f54cfc2a/grpcio-1.78.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:f9ab915a267fc47c7e88c387a3a28325b58c898e23d4995f765728f4e3dedb97", size = 5913985, upload-time = "2026-02-06T09:55:26.832Z" }, + { url = "https://files.pythonhosted.org/packages/81/ed/be1caa25f06594463f685b3790b320f18aea49b33166f4141bfdc2bfb236/grpcio-1.78.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3f8904a8165ab21e07e58bf3e30a73f4dffc7a1e0dbc32d51c61b5360d26f43e", size = 11811853, upload-time = "2026-02-06T09:55:29.224Z" }, + { url = "https://files.pythonhosted.org/packages/24/a7/f06d151afc4e64b7e3cc3e872d331d011c279aaab02831e40a81c691fb65/grpcio-1.78.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:859b13906ce098c0b493af92142ad051bf64c7870fa58a123911c88606714996", size = 6475766, upload-time = "2026-02-06T09:55:31.825Z" }, + { url = "https://files.pythonhosted.org/packages/8a/a8/4482922da832ec0082d0f2cc3a10976d84a7424707f25780b82814aafc0a/grpcio-1.78.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b2342d87af32790f934a79c3112641e7b27d63c261b8b4395350dad43eff1dc7", size = 7170027, upload-time = "2026-02-06T09:55:34.7Z" }, + { url = "https://files.pythonhosted.org/packages/54/bf/f4a3b9693e35d25b24b0b39fa46d7d8a3c439e0a3036c3451764678fec20/grpcio-1.78.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:12a771591ae40bc65ba67048fa52ef4f0e6db8279e595fd349f9dfddeef571f9", size = 6690766, upload-time = "2026-02-06T09:55:36.902Z" }, + { url = "https://files.pythonhosted.org/packages/c7/b9/521875265cc99fe5ad4c5a17010018085cae2810a928bf15ebe7d8bcd9cc/grpcio-1.78.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:185dea0d5260cbb2d224c507bf2a5444d5abbb1fa3594c1ed7e4c709d5eb8383", size = 7266161, upload-time = "2026-02-06T09:55:39.824Z" }, + { url = "https://files.pythonhosted.org/packages/05/86/296a82844fd40a4ad4a95f100b55044b4f817dece732bf686aea1a284147/grpcio-1.78.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:51b13f9aed9d59ee389ad666b8c2214cc87b5de258fa712f9ab05f922e3896c6", size = 8253303, upload-time = "2026-02-06T09:55:42.353Z" }, + { url = "https://files.pythonhosted.org/packages/f3/e4/ea3c0caf5468537f27ad5aab92b681ed7cc0ef5f8c9196d3fd42c8c2286b/grpcio-1.78.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fd5f135b1bd58ab088930b3c613455796dfa0393626a6972663ccdda5b4ac6ce", size = 7698222, upload-time = "2026-02-06T09:55:44.629Z" }, + { url = "https://files.pythonhosted.org/packages/d7/47/7f05f81e4bb6b831e93271fb12fd52ba7b319b5402cbc101d588f435df00/grpcio-1.78.0-cp312-cp312-win32.whl", hash = "sha256:94309f498bcc07e5a7d16089ab984d42ad96af1d94b5a4eb966a266d9fcabf68", size = 4066123, upload-time = "2026-02-06T09:55:47.644Z" }, + { url = "https://files.pythonhosted.org/packages/ad/e7/d6914822c88aa2974dbbd10903d801a28a19ce9cd8bad7e694cbbcf61528/grpcio-1.78.0-cp312-cp312-win_amd64.whl", hash = "sha256:9566fe4ababbb2610c39190791e5b829869351d14369603702e890ef3ad2d06e", size = 4797657, upload-time = "2026-02-06T09:55:49.86Z" }, + { url = "https://files.pythonhosted.org/packages/05/a9/8f75894993895f361ed8636cd9237f4ab39ef87fd30db17467235ed1c045/grpcio-1.78.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:ce3a90455492bf8bfa38e56fbbe1dbd4f872a3d8eeaf7337dc3b1c8aa28c271b", size = 5920143, upload-time = "2026-02-06T09:55:52.035Z" }, + { url = "https://files.pythonhosted.org/packages/55/06/0b78408e938ac424100100fd081189451b472236e8a3a1f6500390dc4954/grpcio-1.78.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:2bf5e2e163b356978b23652c4818ce4759d40f4712ee9ec5a83c4be6f8c23a3a", size = 11803926, upload-time = "2026-02-06T09:55:55.494Z" }, + { url = "https://files.pythonhosted.org/packages/88/93/b59fe7832ff6ae3c78b813ea43dac60e295fa03606d14d89d2e0ec29f4f3/grpcio-1.78.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8f2ac84905d12918e4e55a16da17939eb63e433dc11b677267c35568aa63fc84", size = 6478628, upload-time = "2026-02-06T09:55:58.533Z" }, + { url = "https://files.pythonhosted.org/packages/ed/df/e67e3734527f9926b7d9c0dde6cd998d1d26850c3ed8eeec81297967ac67/grpcio-1.78.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b58f37edab4a3881bc6c9bca52670610e0c9ca14e2ea3cf9debf185b870457fb", size = 7173574, upload-time = "2026-02-06T09:56:01.786Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/cc03fffb07bfba982a9ec097b164e8835546980aec25ecfa5f9c1a47e022/grpcio-1.78.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:735e38e176a88ce41840c21bb49098ab66177c64c82426e24e0082500cc68af5", size = 6692639, upload-time = "2026-02-06T09:56:04.529Z" }, + { url = "https://files.pythonhosted.org/packages/bf/9a/289c32e301b85bdb67d7ec68b752155e674ee3ba2173a1858f118e399ef3/grpcio-1.78.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2045397e63a7a0ee7957c25f7dbb36ddc110e0cfb418403d110c0a7a68a844e9", size = 7268838, upload-time = "2026-02-06T09:56:08.397Z" }, + { url = "https://files.pythonhosted.org/packages/0e/79/1be93f32add280461fa4773880196572563e9c8510861ac2da0ea0f892b6/grpcio-1.78.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a9f136fbafe7ccf4ac7e8e0c28b31066e810be52d6e344ef954a3a70234e1702", size = 8251878, upload-time = "2026-02-06T09:56:10.914Z" }, + { url = "https://files.pythonhosted.org/packages/65/65/793f8e95296ab92e4164593674ae6291b204bb5f67f9d4a711489cd30ffa/grpcio-1.78.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:748b6138585379c737adc08aeffd21222abbda1a86a0dca2a39682feb9196c20", size = 7695412, upload-time = "2026-02-06T09:56:13.593Z" }, + { url = "https://files.pythonhosted.org/packages/1c/9f/1e233fe697ecc82845942c2822ed06bb522e70d6771c28d5528e4c50f6a4/grpcio-1.78.0-cp313-cp313-win32.whl", hash = "sha256:271c73e6e5676afe4fc52907686670c7cea22ab2310b76a59b678403ed40d670", size = 4064899, upload-time = "2026-02-06T09:56:15.601Z" }, + { url = "https://files.pythonhosted.org/packages/4d/27/d86b89e36de8a951501fb06a0f38df19853210f341d0b28f83f4aa0ffa08/grpcio-1.78.0-cp313-cp313-win_amd64.whl", hash = "sha256:f2d4e43ee362adfc05994ed479334d5a451ab7bc3f3fee1b796b8ca66895acb4", size = 4797393, upload-time = "2026-02-06T09:56:17.882Z" }, + { url = "https://files.pythonhosted.org/packages/29/f2/b56e43e3c968bfe822fa6ce5bca10d5c723aa40875b48791ce1029bb78c7/grpcio-1.78.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:e87cbc002b6f440482b3519e36e1313eb5443e9e9e73d6a52d43bd2004fcfd8e", size = 5920591, upload-time = "2026-02-06T09:56:20.758Z" }, + { url = "https://files.pythonhosted.org/packages/5d/81/1f3b65bd30c334167bfa8b0d23300a44e2725ce39bba5b76a2460d85f745/grpcio-1.78.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:c41bc64626db62e72afec66b0c8a0da76491510015417c127bfc53b2fe6d7f7f", size = 11813685, upload-time = "2026-02-06T09:56:24.315Z" }, + { url = "https://files.pythonhosted.org/packages/0e/1c/bbe2f8216a5bd3036119c544d63c2e592bdf4a8ec6e4a1867592f4586b26/grpcio-1.78.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8dfffba826efcf366b1e3ccc37e67afe676f290e13a3b48d31a46739f80a8724", size = 6487803, upload-time = "2026-02-06T09:56:27.367Z" }, + { url = "https://files.pythonhosted.org/packages/16/5c/a6b2419723ea7ddce6308259a55e8e7593d88464ce8db9f4aa857aba96fa/grpcio-1.78.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:74be1268d1439eaaf552c698cdb11cd594f0c49295ae6bb72c34ee31abbe611b", size = 7173206, upload-time = "2026-02-06T09:56:29.876Z" }, + { url = "https://files.pythonhosted.org/packages/df/1e/b8801345629a415ea7e26c83d75eb5dbe91b07ffe5210cc517348a8d4218/grpcio-1.78.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:be63c88b32e6c0f1429f1398ca5c09bc64b0d80950c8bb7807d7d7fb36fb84c7", size = 6693826, upload-time = "2026-02-06T09:56:32.305Z" }, + { url = "https://files.pythonhosted.org/packages/34/84/0de28eac0377742679a510784f049738a80424b17287739fc47d63c2439e/grpcio-1.78.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:3c586ac70e855c721bda8f548d38c3ca66ac791dc49b66a8281a1f99db85e452", size = 7277897, upload-time = "2026-02-06T09:56:34.915Z" }, + { url = "https://files.pythonhosted.org/packages/ca/9c/ad8685cfe20559a9edb66f735afdcb2b7d3de69b13666fdfc542e1916ebd/grpcio-1.78.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:35eb275bf1751d2ffbd8f57cdbc46058e857cf3971041521b78b7db94bdaf127", size = 8252404, upload-time = "2026-02-06T09:56:37.553Z" }, + { url = "https://files.pythonhosted.org/packages/3c/05/33a7a4985586f27e1de4803887c417ec7ced145ebd069bc38a9607059e2b/grpcio-1.78.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:207db540302c884b8848036b80db352a832b99dfdf41db1eb554c2c2c7800f65", size = 7696837, upload-time = "2026-02-06T09:56:40.173Z" }, + { url = "https://files.pythonhosted.org/packages/73/77/7382241caf88729b106e49e7d18e3116216c778e6a7e833826eb96de22f7/grpcio-1.78.0-cp314-cp314-win32.whl", hash = "sha256:57bab6deef2f4f1ca76cc04565df38dc5713ae6c17de690721bdf30cb1e0545c", size = 4142439, upload-time = "2026-02-06T09:56:43.258Z" }, + { url = "https://files.pythonhosted.org/packages/48/b2/b096ccce418882fbfda4f7496f9357aaa9a5af1896a9a7f60d9f2b275a06/grpcio-1.78.0-cp314-cp314-win_amd64.whl", hash = "sha256:dce09d6116df20a96acfdbf85e4866258c3758180e8c49845d6ba8248b6d0bbb", size = 4929852, upload-time = "2026-02-06T09:56:45.885Z" }, + { url = "https://files.pythonhosted.org/packages/58/6c/40a4bba2c753ea8eeb8d776a31e9c54f4e506edf36db93a3db5456725294/grpcio-1.78.0-cp39-cp39-linux_armv7l.whl", hash = "sha256:86f85dd7c947baa707078a236288a289044836d4b640962018ceb9cd1f899af5", size = 5947902, upload-time = "2026-02-06T09:56:48.469Z" }, + { url = "https://files.pythonhosted.org/packages/c0/4c/ed7664a37a7008be41204c77e0d88bbc4ac531bcf0c27668cd066f9ff6e2/grpcio-1.78.0-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:de8cb00d1483a412a06394b8303feec5dcb3b55f81d83aa216dbb6a0b86a94f5", size = 11824772, upload-time = "2026-02-06T09:56:51.264Z" }, + { url = "https://files.pythonhosted.org/packages/9a/5b/45a5c23ba3c4a0f51352366d9b25369a2a51163ab1c93482cb8408726617/grpcio-1.78.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e888474dee2f59ff68130f8a397792d8cb8e17e6b3434339657ba4ee90845a8c", size = 6521579, upload-time = "2026-02-06T09:56:54.967Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e3/392e647d918004231e3d1c780ed125c48939bfc8f845adb8b5820410da3e/grpcio-1.78.0-cp39-cp39-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:86ce2371bfd7f212cf60d8517e5e854475c2c43ce14aa910e136ace72c6db6c1", size = 7199330, upload-time = "2026-02-06T09:56:57.611Z" }, + { url = "https://files.pythonhosted.org/packages/68/2f/42a52d78bdbdb3f1310ed690a3511cd004740281ca75d300b7bd6d9d3de3/grpcio-1.78.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b0c689c02947d636bc7fab3e30cc3a3445cca99c834dfb77cd4a6cabfc1c5597", size = 6726696, upload-time = "2026-02-06T09:57:00.357Z" }, + { url = "https://files.pythonhosted.org/packages/0f/83/b3d932a4fbb2dce3056f6df2926fc2d3ddc5d5acbafbec32c84033cf3f23/grpcio-1.78.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:ce7599575eeb25c0f4dc1be59cada6219f3b56176f799627f44088b21381a28a", size = 7299076, upload-time = "2026-02-06T09:57:04.124Z" }, + { url = "https://files.pythonhosted.org/packages/ba/d9/70ea1be55efaf91fd19f7258b1292772a8226cf1b0e237717fba671073cb/grpcio-1.78.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:684083fd383e9dc04c794adb838d4faea08b291ce81f64ecd08e4577c7398adf", size = 8284493, upload-time = "2026-02-06T09:57:06.746Z" }, + { url = "https://files.pythonhosted.org/packages/d0/2f/3dddccf49e3e75564655b84175fca092d3efd81d2979fc89c4b1c1d879dc/grpcio-1.78.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:ab399ef5e3cd2a721b1038a0f3021001f19c5ab279f145e1146bb0b9f1b2b12c", size = 7724340, upload-time = "2026-02-06T09:57:09.453Z" }, + { url = "https://files.pythonhosted.org/packages/79/ae/dfdb3183141db787a9363078a98764675996a7c2448883153091fd7c8527/grpcio-1.78.0-cp39-cp39-win32.whl", hash = "sha256:f3d6379493e18ad4d39537a82371c5281e153e963cecb13f953ebac155756525", size = 4077641, upload-time = "2026-02-06T09:57:11.881Z" }, + { url = "https://files.pythonhosted.org/packages/aa/aa/694b2f505345cfdd234cffb2525aa379a81695e6c02fd40d7e9193e871c6/grpcio-1.78.0-cp39-cp39-win_amd64.whl", hash = "sha256:5361a0630a7fdb58a6a97638ab70e1dae2893c4d08d7aba64ded28bb9e7a29df", size = 4799428, upload-time = "2026-02-06T09:57:14.493Z" }, +] + [[package]] name = "h11" version = "0.16.0" @@ -1339,6 +1844,38 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, ] +[[package]] +name = "hf-xet" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/68/01/928fd82663fb0ab455551a178303a2960e65029da66b21974594f3a20a94/hf_xet-1.4.0.tar.gz", hash = "sha256:48e6ba7422b0885c9bbd8ac8fdf5c4e1306c3499b82d489944609cc4eae8ecbd", size = 660350, upload-time = "2026-03-11T18:50:03.354Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/4b/2351e30dddc6f3b47b3da0a0693ec1e82f8303b1a712faa299cf3552002b/hf_xet-1.4.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:76725fcbc5f59b23ac778f097d3029d6623e3cf6f4057d99d1fce1a7e3cff8fc", size = 3796397, upload-time = "2026-03-11T18:49:47.382Z" }, + { url = "https://files.pythonhosted.org/packages/48/3d/3db90ec0afb4e26e3330b1346b89fe0e9a3b7bfc2d6a2b2262787790d25f/hf_xet-1.4.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:76f1f73bee81a6e6f608b583908aa24c50004965358ac92c1dc01080a21bcd09", size = 3556235, upload-time = "2026-03-11T18:49:45.785Z" }, + { url = "https://files.pythonhosted.org/packages/57/6e/2a662af2cbc6c0a64ebe9fcdb8faf05b5205753d45a75a3011bb2209d0b4/hf_xet-1.4.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1818c2e5d6f15354c595d5111c6eb0e5a30a6c5c1a43eeaec20f19607cff0b34", size = 4213145, upload-time = "2026-03-11T18:49:38.009Z" }, + { url = "https://files.pythonhosted.org/packages/b9/4a/47c129affb540767e0e3e101039a95f4a73a292ec689c26e8f0c5b633f9d/hf_xet-1.4.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:70764d295f485db9cc9a6af76634ea00ec4f96311be7485f8f2b6144739b4ccf", size = 3991951, upload-time = "2026-03-11T18:49:36.396Z" }, + { url = "https://files.pythonhosted.org/packages/76/81/ec516cfc6281cfeef027b0919166b2fe11ab61fbe6131a2c43fafbed8b68/hf_xet-1.4.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9d3bd2a1e289f772c715ca88cdca8ceb3d8b5c9186534d5925410e531d849a3e", size = 4193205, upload-time = "2026-03-11T18:49:54.415Z" }, + { url = "https://files.pythonhosted.org/packages/49/48/0945b5e542ed6c6ce758b589b27895a449deab630dfcdee5a6ee0f699d21/hf_xet-1.4.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:06da3797f1fdd9a8f8dbc8c1bddfa0b914789b14580c375d29c32ee35c2c66ca", size = 4431022, upload-time = "2026-03-11T18:49:55.677Z" }, + { url = "https://files.pythonhosted.org/packages/e8/ad/a4859c55ab4b67a4fde2849be8bde81917f54062050419b821071f199a9c/hf_xet-1.4.0-cp313-cp313t-win_amd64.whl", hash = "sha256:30b9d8f384ccec848124d51d883e91f3c88d430589e02a7b6d867730ab8d53ac", size = 3674977, upload-time = "2026-03-11T18:50:06.369Z" }, + { url = "https://files.pythonhosted.org/packages/4b/17/5bf3791e3a53e597913c2a775a48a98aaded9c2ddb5d1afaedabb55e2ed8/hf_xet-1.4.0-cp313-cp313t-win_arm64.whl", hash = "sha256:07ffdbf7568fa3245b24d949f0f3790b5276fb7293a5554ac4ec02e5f7e2b38d", size = 3536778, upload-time = "2026-03-11T18:50:04.974Z" }, + { url = "https://files.pythonhosted.org/packages/3f/a1/05a7f9d6069bf78405d3fc2464b6c76b167128501e13b4f1d6266e1d1f54/hf_xet-1.4.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:e2731044f3a18442f9f7a3dcf03b96af13dee311f03846a1df1f0553a3ea0fc6", size = 3796727, upload-time = "2026-03-11T18:49:52.889Z" }, + { url = "https://files.pythonhosted.org/packages/ac/8a/67abc642c2b32efcb7a257cdad8555c2904e23f18a1b4fec3aef1ebfe0fc/hf_xet-1.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b6f3729335fbc4baef60fe14fe32ef13ac9d377bdc898148c541e20c6056b504", size = 3555869, upload-time = "2026-03-11T18:49:51.313Z" }, + { url = "https://files.pythonhosted.org/packages/19/3d/4765367c64ee70db15fa771d5b94bf12540b85076a1d3210ebbfec42d477/hf_xet-1.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9c0c9f052738a024073d332c573275c8e33697a3ef3f5dd2fb4ef98216e1e74a", size = 4212980, upload-time = "2026-03-11T18:49:44.21Z" }, + { url = "https://files.pythonhosted.org/packages/0e/bf/6ad99ee0e7ca2318f912a87318e493d82d8f9aace6be81f774bd14b996df/hf_xet-1.4.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:f44b2324be75bfa399735996ac299fd478684c48ce47d12a42b5f24b1a99ccb8", size = 3991136, upload-time = "2026-03-11T18:49:42.512Z" }, + { url = "https://files.pythonhosted.org/packages/50/aa/932e25c69699076088f57e3c14f83ccae87bac25e755994f3362acc908d5/hf_xet-1.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:01de78b1ceddf8b38da001f7cc728b3bc3eb956948b18e8a1997ad6fc80fbe9d", size = 4192676, upload-time = "2026-03-11T18:50:00.216Z" }, + { url = "https://files.pythonhosted.org/packages/5c/0a/5e41339a294fd3450948989a47ecba9824d5bc1950cf767f928ecaf53a55/hf_xet-1.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cac8616e7a974105c3494735313f5ab0fb79b5accadec1a7a992859a15536a9", size = 4430729, upload-time = "2026-03-11T18:50:01.923Z" }, + { url = "https://files.pythonhosted.org/packages/9c/c1/c3d8ed9b7118e9166b0cf71dfd501da82f1abe306387e34e0f3ee59553ec/hf_xet-1.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:3a5d9cb25095ceb3beab4843ae2d1b3e5746371ddbf2e5849f7be6a7d6f44df4", size = 3674989, upload-time = "2026-03-11T18:50:12.633Z" }, + { url = "https://files.pythonhosted.org/packages/65/bc/ea26cf774063cb09d7aaaa6cba9d341fb72b42ea99b8a94ca254dbafbbb0/hf_xet-1.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:9b777674499dc037317db372c90a2dd91329b5f1ee93c645bb89155bb974f5bf", size = 3536805, upload-time = "2026-03-11T18:50:11.082Z" }, + { url = "https://files.pythonhosted.org/packages/9f/f9/a0b01945726aea81d2f213457cd5f5102a51e6fd1ca9f9769f561fb57501/hf_xet-1.4.0-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:981d2b5222c3baadf9567c135cf1d1073786f546b7745686978d46b5df179e16", size = 3799223, upload-time = "2026-03-11T18:49:49.884Z" }, + { url = "https://files.pythonhosted.org/packages/5d/30/ee62b0c00412f49a7e6f509f0104ee8808692278d247234336df48029349/hf_xet-1.4.0-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:cc8bd050349d0d7995ce7b3a3a18732a2a8062ce118a82431602088abb373428", size = 3560682, upload-time = "2026-03-11T18:49:48.633Z" }, + { url = "https://files.pythonhosted.org/packages/93/d0/0fe5c44dbced465a651a03212e1135d0d7f95d19faada692920cb56f8e38/hf_xet-1.4.0-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5d0c38d2a280d814280b8c15eead4a43c9781e7bf6fc37843cffab06dcdc76b9", size = 4218323, upload-time = "2026-03-11T18:49:40.921Z" }, + { url = "https://files.pythonhosted.org/packages/73/df/7b3c99a4e50442039eae498e5c23db634538eb3e02214109880cf1165d4c/hf_xet-1.4.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6a883f0250682ea888a1bd0af0631feda377e59ad7aae6fb75860ecee7ae0f93", size = 3997156, upload-time = "2026-03-11T18:49:39.634Z" }, + { url = "https://files.pythonhosted.org/packages/a9/26/47dfedf271c21d95346660ae1698e7ece5ab10791fa6c4f20c59f3713083/hf_xet-1.4.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:99e1d9255fe8ecdf57149bb0543d49e7b7bd8d491ddf431eb57e114253274df5", size = 4199052, upload-time = "2026-03-11T18:49:57.097Z" }, + { url = "https://files.pythonhosted.org/packages/8d/c0/346b9aad1474e881e65f998d5c1981695f0af045bc7a99204d9d86759a89/hf_xet-1.4.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b25f06ce42bd2d5f2e79d4a2d72f783d3ac91827c80d34a38cf8e5290dd717b0", size = 4434346, upload-time = "2026-03-11T18:49:58.67Z" }, + { url = "https://files.pythonhosted.org/packages/2e/d6/88ce9d6caa397c3b935263d5bcbe3ebf6c443f7c76098b8c523d206116b9/hf_xet-1.4.0-cp37-abi3-win_amd64.whl", hash = "sha256:8d6d7816d01e0fa33f315c8ca21b05eca0ce4cdc314f13b81d953e46cc6db11d", size = 3678921, upload-time = "2026-03-11T18:50:09.496Z" }, + { url = "https://files.pythonhosted.org/packages/65/eb/17d99ed253b28a9550ca479867c66a8af4c9bcd8cdc9a26b0c8007c2000a/hf_xet-1.4.0-cp37-abi3-win_arm64.whl", hash = "sha256:cb8d9549122b5b42f34b23b14c6b662a88a586a919d418c774d8dbbc4b3ce2aa", size = 3541054, upload-time = "2026-03-11T18:50:07.963Z" }, +] + [[package]] name = "httpcore" version = "1.0.9" @@ -1367,6 +1904,39 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, ] +[[package]] +name = "httpx-sse" +version = "0.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/4c/751061ffa58615a32c31b2d82e8482be8dd4a89154f003147acee90f2be9/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d", size = 15943, upload-time = "2025-10-10T21:48:22.271Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" }, +] + +[[package]] +name = "huggingface-hub" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock", version = "3.19.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "filelock", version = "3.25.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "fsspec", version = "2025.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "fsspec", version = "2026.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "hf-xet", marker = "platform_machine == 'AMD64' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, + { name = "httpx" }, + { name = "packaging", version = "25.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "packaging", version = "26.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pyyaml" }, + { name = "tqdm" }, + { name = "typer", version = "0.23.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "typer", version = "0.24.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d5/7a/304cec37112382c4fe29a43bcb0d5891f922785d18745883d2aa4eb74e4b/huggingface_hub-1.6.0.tar.gz", hash = "sha256:d931ddad8ba8dfc1e816bf254810eb6f38e5c32f60d4184b5885662a3b167325", size = 717071, upload-time = "2026-03-06T14:19:18.524Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/e3/e3a44f54c8e2f28983fcf07f13d4260b37bd6a0d3a081041bc60b91d230e/huggingface_hub-1.6.0-py3-none-any.whl", hash = "sha256:ef40e2d5cb85e48b2c067020fa5142168342d5108a1b267478ed384ecbf18961", size = 612874, upload-time = "2026-03-06T14:19:16.844Z" }, +] + [[package]] name = "idna" version = "3.11" @@ -1421,6 +1991,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, ] +[[package]] +name = "invoke" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/de/bd/b461d3424a24c80490313fd77feeb666ca4f6a28c7e72713e3d9095719b4/invoke-2.2.1.tar.gz", hash = "sha256:515bf49b4a48932b79b024590348da22f39c4942dff991ad1fb8b8baea1be707", size = 304762, upload-time = "2025-10-11T00:36:35.172Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/4b/b99e37f88336009971405cbb7630610322ed6fbfa31e1d7ab3fbf3049a2d/invoke-2.2.1-py3-none-any.whl", hash = "sha256:2413bc441b376e5cd3f55bb5d364f973ad8bdd7bf87e53c79de3c11bf3feecc8", size = 160287, upload-time = "2025-10-11T00:36:33.703Z" }, +] + [[package]] name = "ipykernel" version = "6.31.0" @@ -1437,7 +2016,7 @@ dependencies = [ { name = "jupyter-core", version = "5.8.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "matplotlib-inline", marker = "python_full_version < '3.10'" }, { name = "nest-asyncio", marker = "python_full_version < '3.10'" }, - { name = "packaging", marker = "python_full_version < '3.10'" }, + { name = "packaging", version = "26.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "psutil", marker = "python_full_version < '3.10'" }, { name = "pyzmq", marker = "python_full_version < '3.10'" }, { name = "tornado", marker = "python_full_version < '3.10'" }, @@ -1474,7 +2053,7 @@ dependencies = [ { name = "jupyter-core", version = "5.9.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "matplotlib-inline", marker = "python_full_version >= '3.10'" }, { name = "nest-asyncio", marker = "python_full_version >= '3.10'" }, - { name = "packaging", marker = "python_full_version >= '3.10'" }, + { name = "packaging", version = "25.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "psutil", marker = "python_full_version >= '3.10'" }, { name = "pyzmq", marker = "python_full_version >= '3.10'" }, { name = "tornado", marker = "python_full_version >= '3.10'" }, @@ -1589,6 +2168,42 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/15/aa/0aca39a37d3c7eb941ba736ede56d689e7be91cab5d9ca846bde3999eba6/isodate-0.7.2-py3-none-any.whl", hash = "sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15", size = 22320, upload-time = "2024-10-08T23:04:09.501Z" }, ] +[[package]] +name = "jaraco-classes" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", size = 11780, upload-time = "2024-03-31T07:27:36.643Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790", size = 6777, upload-time = "2024-03-31T07:27:34.792Z" }, +] + +[[package]] +name = "jaraco-context" +version = "6.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "backports-tarfile", marker = "python_full_version >= '3.10' and python_full_version < '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/27/7b/c3081ff1af947915503121c649f26a778e1a2101fd525f74aef997d75b7e/jaraco_context-6.1.1.tar.gz", hash = "sha256:bc046b2dc94f1e5532bd02402684414575cc11f565d929b6563125deb0a6e581", size = 15832, upload-time = "2026-03-07T15:46:04.63Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/49/c152890d49102b280ecf86ba5f80a8c111c3a155dafa3bd24aeb64fde9e1/jaraco_context-6.1.1-py3-none-any.whl", hash = "sha256:0df6a0287258f3e364072c3e40d5411b20cafa30cb28c4839d24319cecf9f808", size = 7005, upload-time = "2026-03-07T15:46:03.515Z" }, +] + +[[package]] +name = "jaraco-functools" +version = "4.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0f/27/056e0638a86749374d6f57d0b0db39f29509cce9313cf91bdc0ac4d91084/jaraco_functools-4.4.0.tar.gz", hash = "sha256:da21933b0417b89515562656547a77b4931f98176eb173644c0d35032a33d6bb", size = 19943, upload-time = "2025-12-21T09:29:43.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/c4/813bb09f0985cb21e959f21f2464169eca882656849adf727ac7bb7e1767/jaraco_functools-4.4.0-py3-none-any.whl", hash = "sha256:9eec1e36f45c818d9bf307c8948eb03b2b56cd44087b3cdc989abca1f20b9176", size = 10481, upload-time = "2025-12-21T09:29:42.27Z" }, +] + [[package]] name = "jedi" version = "0.19.2" @@ -1601,6 +2216,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9", size = 1572278, upload-time = "2024-11-11T01:41:40.175Z" }, ] +[[package]] +name = "jeepney" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/6f/357efd7602486741aa73ffc0617fb310a29b588ed0fd69c2399acbb85b0c/jeepney-0.9.0.tar.gz", hash = "sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732", size = 106758, upload-time = "2025-02-27T18:51:01.684Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683", size = 49010, upload-time = "2025-02-27T18:51:00.104Z" }, +] + [[package]] name = "jinja2" version = "3.1.6" @@ -1613,6 +2237,115 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, ] +[[package]] +name = "jiter" +version = "0.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0d/5e/4ec91646aee381d01cdb9974e30882c9cd3b8c5d1079d6b5ff4af522439a/jiter-0.13.0.tar.gz", hash = "sha256:f2839f9c2c7e2dffc1bc5929a510e14ce0a946be9365fd1219e7ef342dae14f4", size = 164847, upload-time = "2026-02-02T12:37:56.441Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/5a/41da76c5ea07bec1b0472b6b2fdb1b651074d504b19374d7e130e0cdfb25/jiter-0.13.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2ffc63785fd6c7977defe49b9824ae6ce2b2e2b77ce539bdaf006c26da06342e", size = 311164, upload-time = "2026-02-02T12:35:17.688Z" }, + { url = "https://files.pythonhosted.org/packages/40/cb/4a1bf994a3e869f0d39d10e11efb471b76d0ad70ecbfb591427a46c880c2/jiter-0.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4a638816427006c1e3f0013eb66d391d7a3acda99a7b0cf091eff4497ccea33a", size = 320296, upload-time = "2026-02-02T12:35:19.828Z" }, + { url = "https://files.pythonhosted.org/packages/09/82/acd71ca9b50ecebadc3979c541cd717cce2fe2bc86236f4fa597565d8f1a/jiter-0.13.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19928b5d1ce0ff8c1ee1b9bdef3b5bfc19e8304f1b904e436caf30bc15dc6cf5", size = 352742, upload-time = "2026-02-02T12:35:21.258Z" }, + { url = "https://files.pythonhosted.org/packages/71/03/d1fc996f3aecfd42eb70922edecfb6dd26421c874503e241153ad41df94f/jiter-0.13.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:309549b778b949d731a2f0e1594a3f805716be704a73bf3ad9a807eed5eb5721", size = 363145, upload-time = "2026-02-02T12:35:24.653Z" }, + { url = "https://files.pythonhosted.org/packages/f1/61/a30492366378cc7a93088858f8991acd7d959759fe6138c12a4644e58e81/jiter-0.13.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bcdabaea26cb04e25df3103ce47f97466627999260290349a88c8136ecae0060", size = 487683, upload-time = "2026-02-02T12:35:26.162Z" }, + { url = "https://files.pythonhosted.org/packages/20/4e/4223cffa9dbbbc96ed821c5aeb6bca510848c72c02086d1ed3f1da3d58a7/jiter-0.13.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a3a377af27b236abbf665a69b2bdd680e3b5a0bd2af825cd3b81245279a7606c", size = 373579, upload-time = "2026-02-02T12:35:27.582Z" }, + { url = "https://files.pythonhosted.org/packages/fe/c9/b0489a01329ab07a83812d9ebcffe7820a38163c6d9e7da644f926ff877c/jiter-0.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe49d3ff6db74321f144dff9addd4a5874d3105ac5ba7c5b77fac099cfae31ae", size = 362904, upload-time = "2026-02-02T12:35:28.925Z" }, + { url = "https://files.pythonhosted.org/packages/05/af/53e561352a44afcba9a9bc67ee1d320b05a370aed8df54eafe714c4e454d/jiter-0.13.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2113c17c9a67071b0f820733c0893ed1d467b5fcf4414068169e5c2cabddb1e2", size = 392380, upload-time = "2026-02-02T12:35:30.385Z" }, + { url = "https://files.pythonhosted.org/packages/76/2a/dd805c3afb8ed5b326c5ae49e725d1b1255b9754b1b77dbecdc621b20773/jiter-0.13.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ab1185ca5c8b9491b55ebf6c1e8866b8f68258612899693e24a92c5fdb9455d5", size = 517939, upload-time = "2026-02-02T12:35:31.865Z" }, + { url = "https://files.pythonhosted.org/packages/20/2a/7b67d76f55b8fe14c937e7640389612f05f9a4145fc28ae128aaa5e62257/jiter-0.13.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:9621ca242547edc16400981ca3231e0c91c0c4c1ab8573a596cd9bb3575d5c2b", size = 551696, upload-time = "2026-02-02T12:35:33.306Z" }, + { url = "https://files.pythonhosted.org/packages/85/9c/57cdd64dac8f4c6ab8f994fe0eb04dc9fd1db102856a4458fcf8a99dfa62/jiter-0.13.0-cp310-cp310-win32.whl", hash = "sha256:a7637d92b1c9d7a771e8c56f445c7f84396d48f2e756e5978840ecba2fac0894", size = 204592, upload-time = "2026-02-02T12:35:34.58Z" }, + { url = "https://files.pythonhosted.org/packages/a7/38/f4f3ea5788b8a5bae7510a678cdc747eda0c45ffe534f9878ff37e7cf3b3/jiter-0.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:c1b609e5cbd2f52bb74fb721515745b407df26d7b800458bd97cb3b972c29e7d", size = 206016, upload-time = "2026-02-02T12:35:36.435Z" }, + { url = "https://files.pythonhosted.org/packages/71/29/499f8c9eaa8a16751b1c0e45e6f5f1761d180da873d417996cc7bddc8eef/jiter-0.13.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ea026e70a9a28ebbdddcbcf0f1323128a8db66898a06eaad3a4e62d2f554d096", size = 311157, upload-time = "2026-02-02T12:35:37.758Z" }, + { url = "https://files.pythonhosted.org/packages/50/f6/566364c777d2ab450b92100bea11333c64c38d32caf8dc378b48e5b20c46/jiter-0.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:66aa3e663840152d18cc8ff1e4faad3dd181373491b9cfdc6004b92198d67911", size = 319729, upload-time = "2026-02-02T12:35:39.246Z" }, + { url = "https://files.pythonhosted.org/packages/73/dd/560f13ec5e4f116d8ad2658781646cca91b617ae3b8758d4a5076b278f70/jiter-0.13.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3524798e70655ff19aec58c7d05adb1f074fecff62da857ea9be2b908b6d701", size = 354766, upload-time = "2026-02-02T12:35:40.662Z" }, + { url = "https://files.pythonhosted.org/packages/7c/0d/061faffcfe94608cbc28a0d42a77a74222bdf5055ccdbe5fd2292b94f510/jiter-0.13.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ec7e287d7fbd02cb6e22f9a00dd9c9cd504c40a61f2c61e7e1f9690a82726b4c", size = 362587, upload-time = "2026-02-02T12:35:42.025Z" }, + { url = "https://files.pythonhosted.org/packages/92/c9/c66a7864982fd38a9773ec6e932e0398d1262677b8c60faecd02ffb67bf3/jiter-0.13.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:47455245307e4debf2ce6c6e65a717550a0244231240dcf3b8f7d64e4c2f22f4", size = 487537, upload-time = "2026-02-02T12:35:43.459Z" }, + { url = "https://files.pythonhosted.org/packages/6c/86/84eb4352cd3668f16d1a88929b5888a3fe0418ea8c1dfc2ad4e7bf6e069a/jiter-0.13.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ee9da221dca6e0429c2704c1b3655fe7b025204a71d4d9b73390c759d776d165", size = 373717, upload-time = "2026-02-02T12:35:44.928Z" }, + { url = "https://files.pythonhosted.org/packages/6e/09/9fe4c159358176f82d4390407a03f506a8659ed13ca3ac93a843402acecf/jiter-0.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24ab43126d5e05f3d53a36a8e11eb2f23304c6c1117844aaaf9a0aa5e40b5018", size = 362683, upload-time = "2026-02-02T12:35:46.636Z" }, + { url = "https://files.pythonhosted.org/packages/c9/5e/85f3ab9caca0c1d0897937d378b4a515cae9e119730563572361ea0c48ae/jiter-0.13.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9da38b4fedde4fb528c740c2564628fbab737166a0e73d6d46cb4bb5463ff411", size = 392345, upload-time = "2026-02-02T12:35:48.088Z" }, + { url = "https://files.pythonhosted.org/packages/12/4c/05b8629ad546191939e6f0c2f17e29f542a398f4a52fb987bc70b6d1eb8b/jiter-0.13.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0b34c519e17658ed88d5047999a93547f8889f3c1824120c26ad6be5f27b6cf5", size = 517775, upload-time = "2026-02-02T12:35:49.482Z" }, + { url = "https://files.pythonhosted.org/packages/4d/88/367ea2eb6bc582c7052e4baf5ddf57ebe5ab924a88e0e09830dfb585c02d/jiter-0.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d2a6394e6af690d462310a86b53c47ad75ac8c21dc79f120714ea449979cb1d3", size = 551325, upload-time = "2026-02-02T12:35:51.104Z" }, + { url = "https://files.pythonhosted.org/packages/f3/12/fa377ffb94a2f28c41afaed093e0d70cfe512035d5ecb0cad0ae4792d35e/jiter-0.13.0-cp311-cp311-win32.whl", hash = "sha256:0f0c065695f616a27c920a56ad0d4fc46415ef8b806bf8fc1cacf25002bd24e1", size = 204709, upload-time = "2026-02-02T12:35:52.467Z" }, + { url = "https://files.pythonhosted.org/packages/cb/16/8e8203ce92f844dfcd3d9d6a5a7322c77077248dbb12da52d23193a839cd/jiter-0.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:0733312953b909688ae3c2d58d043aa040f9f1a6a75693defed7bc2cc4bf2654", size = 204560, upload-time = "2026-02-02T12:35:53.925Z" }, + { url = "https://files.pythonhosted.org/packages/44/26/97cc40663deb17b9e13c3a5cf29251788c271b18ee4d262c8f94798b8336/jiter-0.13.0-cp311-cp311-win_arm64.whl", hash = "sha256:5d9b34ad56761b3bf0fbe8f7e55468704107608512350962d3317ffd7a4382d5", size = 189608, upload-time = "2026-02-02T12:35:55.304Z" }, + { url = "https://files.pythonhosted.org/packages/2e/30/7687e4f87086829955013ca12a9233523349767f69653ebc27036313def9/jiter-0.13.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0a2bd69fc1d902e89925fc34d1da51b2128019423d7b339a45d9e99c894e0663", size = 307958, upload-time = "2026-02-02T12:35:57.165Z" }, + { url = "https://files.pythonhosted.org/packages/c3/27/e57f9a783246ed95481e6749cc5002a8a767a73177a83c63ea71f0528b90/jiter-0.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f917a04240ef31898182f76a332f508f2cc4b57d2b4d7ad2dbfebbfe167eb505", size = 318597, upload-time = "2026-02-02T12:35:58.591Z" }, + { url = "https://files.pythonhosted.org/packages/cf/52/e5719a60ac5d4d7c5995461a94ad5ef962a37c8bf5b088390e6fad59b2ff/jiter-0.13.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1e2b199f446d3e82246b4fd9236d7cb502dc2222b18698ba0d986d2fecc6152", size = 348821, upload-time = "2026-02-02T12:36:00.093Z" }, + { url = "https://files.pythonhosted.org/packages/61/db/c1efc32b8ba4c740ab3fc2d037d8753f67685f475e26b9d6536a4322bcdd/jiter-0.13.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:04670992b576fa65bd056dbac0c39fe8bd67681c380cb2b48efa885711d9d726", size = 364163, upload-time = "2026-02-02T12:36:01.937Z" }, + { url = "https://files.pythonhosted.org/packages/55/8a/fb75556236047c8806995671a18e4a0ad646ed255276f51a20f32dceaeec/jiter-0.13.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5a1aff1fbdb803a376d4d22a8f63f8e7ccbce0b4890c26cc7af9e501ab339ef0", size = 483709, upload-time = "2026-02-02T12:36:03.41Z" }, + { url = "https://files.pythonhosted.org/packages/7e/16/43512e6ee863875693a8e6f6d532e19d650779d6ba9a81593ae40a9088ff/jiter-0.13.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b3fb8c2053acaef8580809ac1d1f7481a0a0bdc012fd7f5d8b18fb696a5a089", size = 370480, upload-time = "2026-02-02T12:36:04.791Z" }, + { url = "https://files.pythonhosted.org/packages/f8/4c/09b93e30e984a187bc8aaa3510e1ec8dcbdcd71ca05d2f56aac0492453aa/jiter-0.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bdaba7d87e66f26a2c45d8cbadcbfc4bf7884182317907baf39cfe9775bb4d93", size = 360735, upload-time = "2026-02-02T12:36:06.994Z" }, + { url = "https://files.pythonhosted.org/packages/1a/1b/46c5e349019874ec5dfa508c14c37e29864ea108d376ae26d90bee238cd7/jiter-0.13.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7b88d649135aca526da172e48083da915ec086b54e8e73a425ba50999468cc08", size = 391814, upload-time = "2026-02-02T12:36:08.368Z" }, + { url = "https://files.pythonhosted.org/packages/15/9e/26184760e85baee7162ad37b7912797d2077718476bf91517641c92b3639/jiter-0.13.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e404ea551d35438013c64b4f357b0474c7abf9f781c06d44fcaf7a14c69ff9e2", size = 513990, upload-time = "2026-02-02T12:36:09.993Z" }, + { url = "https://files.pythonhosted.org/packages/e9/34/2c9355247d6debad57a0a15e76ab1566ab799388042743656e566b3b7de1/jiter-0.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1f4748aad1b4a93c8bdd70f604d0f748cdc0e8744c5547798acfa52f10e79228", size = 548021, upload-time = "2026-02-02T12:36:11.376Z" }, + { url = "https://files.pythonhosted.org/packages/ac/4a/9f2c23255d04a834398b9c2e0e665382116911dc4d06b795710503cdad25/jiter-0.13.0-cp312-cp312-win32.whl", hash = "sha256:0bf670e3b1445fc4d31612199f1744f67f889ee1bbae703c4b54dc097e5dd394", size = 203024, upload-time = "2026-02-02T12:36:12.682Z" }, + { url = "https://files.pythonhosted.org/packages/09/ee/f0ae675a957ae5a8f160be3e87acea6b11dc7b89f6b7ab057e77b2d2b13a/jiter-0.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:15db60e121e11fe186c0b15236bd5d18381b9ddacdcf4e659feb96fc6c969c92", size = 205424, upload-time = "2026-02-02T12:36:13.93Z" }, + { url = "https://files.pythonhosted.org/packages/1b/02/ae611edf913d3cbf02c97cdb90374af2082c48d7190d74c1111dde08bcdd/jiter-0.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:41f92313d17989102f3cb5dd533a02787cdb99454d494344b0361355da52fcb9", size = 186818, upload-time = "2026-02-02T12:36:15.308Z" }, + { url = "https://files.pythonhosted.org/packages/91/9c/7ee5a6ff4b9991e1a45263bfc46731634c4a2bde27dfda6c8251df2d958c/jiter-0.13.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1f8a55b848cbabf97d861495cd65f1e5c590246fabca8b48e1747c4dfc8f85bf", size = 306897, upload-time = "2026-02-02T12:36:16.748Z" }, + { url = "https://files.pythonhosted.org/packages/7c/02/be5b870d1d2be5dd6a91bdfb90f248fbb7dcbd21338f092c6b89817c3dbf/jiter-0.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f556aa591c00f2c45eb1b89f68f52441a016034d18b65da60e2d2875bbbf344a", size = 317507, upload-time = "2026-02-02T12:36:18.351Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/b25d2ec333615f5f284f3a4024f7ce68cfa0604c322c6808b2344c7f5d2b/jiter-0.13.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7e1d61da332ec412350463891923f960c3073cf1aae93b538f0bb4c8cd46efb", size = 350560, upload-time = "2026-02-02T12:36:19.746Z" }, + { url = "https://files.pythonhosted.org/packages/be/ec/74dcb99fef0aca9fbe56b303bf79f6bd839010cb18ad41000bf6cc71eec0/jiter-0.13.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3097d665a27bc96fd9bbf7f86178037db139f319f785e4757ce7ccbf390db6c2", size = 363232, upload-time = "2026-02-02T12:36:21.243Z" }, + { url = "https://files.pythonhosted.org/packages/1b/37/f17375e0bb2f6a812d4dd92d7616e41917f740f3e71343627da9db2824ce/jiter-0.13.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d01ecc3a8cbdb6f25a37bd500510550b64ddf9f7d64a107d92f3ccb25035d0f", size = 483727, upload-time = "2026-02-02T12:36:22.688Z" }, + { url = "https://files.pythonhosted.org/packages/77/d2/a71160a5ae1a1e66c1395b37ef77da67513b0adba73b993a27fbe47eb048/jiter-0.13.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ed9bbc30f5d60a3bdf63ae76beb3f9db280d7f195dfcfa61af792d6ce912d159", size = 370799, upload-time = "2026-02-02T12:36:24.106Z" }, + { url = "https://files.pythonhosted.org/packages/01/99/ed5e478ff0eb4e8aa5fd998f9d69603c9fd3f32de3bd16c2b1194f68361c/jiter-0.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98fbafb6e88256f4454de33c1f40203d09fc33ed19162a68b3b257b29ca7f663", size = 359120, upload-time = "2026-02-02T12:36:25.519Z" }, + { url = "https://files.pythonhosted.org/packages/16/be/7ffd08203277a813f732ba897352797fa9493faf8dc7995b31f3d9cb9488/jiter-0.13.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5467696f6b827f1116556cb0db620440380434591e93ecee7fd14d1a491b6daa", size = 390664, upload-time = "2026-02-02T12:36:26.866Z" }, + { url = "https://files.pythonhosted.org/packages/d1/84/e0787856196d6d346264d6dcccb01f741e5f0bd014c1d9a2ebe149caf4f3/jiter-0.13.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:2d08c9475d48b92892583df9da592a0e2ac49bcd41fae1fec4f39ba6cf107820", size = 513543, upload-time = "2026-02-02T12:36:28.217Z" }, + { url = "https://files.pythonhosted.org/packages/65/50/ecbd258181c4313cf79bca6c88fb63207d04d5bf5e4f65174114d072aa55/jiter-0.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:aed40e099404721d7fcaf5b89bd3b4568a4666358bcac7b6b15c09fb6252ab68", size = 547262, upload-time = "2026-02-02T12:36:29.678Z" }, + { url = "https://files.pythonhosted.org/packages/27/da/68f38d12e7111d2016cd198161b36e1f042bd115c169255bcb7ec823a3bf/jiter-0.13.0-cp313-cp313-win32.whl", hash = "sha256:36ebfbcffafb146d0e6ffb3e74d51e03d9c35ce7c625c8066cdbfc7b953bdc72", size = 200630, upload-time = "2026-02-02T12:36:31.808Z" }, + { url = "https://files.pythonhosted.org/packages/25/65/3bd1a972c9a08ecd22eb3b08a95d1941ebe6938aea620c246cf426ae09c2/jiter-0.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:8d76029f077379374cf0dbc78dbe45b38dec4a2eb78b08b5194ce836b2517afc", size = 202602, upload-time = "2026-02-02T12:36:33.679Z" }, + { url = "https://files.pythonhosted.org/packages/15/fe/13bd3678a311aa67686bb303654792c48206a112068f8b0b21426eb6851e/jiter-0.13.0-cp313-cp313-win_arm64.whl", hash = "sha256:bb7613e1a427cfcb6ea4544f9ac566b93d5bf67e0d48c787eca673ff9c9dff2b", size = 185939, upload-time = "2026-02-02T12:36:35.065Z" }, + { url = "https://files.pythonhosted.org/packages/49/19/a929ec002ad3228bc97ca01dbb14f7632fffdc84a95ec92ceaf4145688ae/jiter-0.13.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fa476ab5dd49f3bf3a168e05f89358c75a17608dbabb080ef65f96b27c19ab10", size = 316616, upload-time = "2026-02-02T12:36:36.579Z" }, + { url = "https://files.pythonhosted.org/packages/52/56/d19a9a194afa37c1728831e5fb81b7722c3de18a3109e8f282bfc23e587a/jiter-0.13.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ade8cb6ff5632a62b7dbd4757d8c5573f7a2e9ae285d6b5b841707d8363205ef", size = 346850, upload-time = "2026-02-02T12:36:38.058Z" }, + { url = "https://files.pythonhosted.org/packages/36/4a/94e831c6bf287754a8a019cb966ed39ff8be6ab78cadecf08df3bb02d505/jiter-0.13.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9950290340acc1adaded363edd94baebcee7dabdfa8bee4790794cd5cfad2af6", size = 358551, upload-time = "2026-02-02T12:36:39.417Z" }, + { url = "https://files.pythonhosted.org/packages/a2/ec/a4c72c822695fa80e55d2b4142b73f0012035d9fcf90eccc56bc060db37c/jiter-0.13.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2b4972c6df33731aac0742b64fd0d18e0a69bc7d6e03108ce7d40c85fd9e3e6d", size = 201950, upload-time = "2026-02-02T12:36:40.791Z" }, + { url = "https://files.pythonhosted.org/packages/b6/00/393553ec27b824fbc29047e9c7cd4a3951d7fbe4a76743f17e44034fa4e4/jiter-0.13.0-cp313-cp313t-win_arm64.whl", hash = "sha256:701a1e77d1e593c1b435315ff625fd071f0998c5f02792038a5ca98899261b7d", size = 185852, upload-time = "2026-02-02T12:36:42.077Z" }, + { url = "https://files.pythonhosted.org/packages/6e/f5/f1997e987211f6f9bd71b8083047b316208b4aca0b529bb5f8c96c89ef3e/jiter-0.13.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:cc5223ab19fe25e2f0bf2643204ad7318896fe3729bf12fde41b77bfc4fafff0", size = 308804, upload-time = "2026-02-02T12:36:43.496Z" }, + { url = "https://files.pythonhosted.org/packages/cd/8f/5482a7677731fd44881f0204981ce2d7175db271f82cba2085dd2212e095/jiter-0.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9776ebe51713acf438fd9b4405fcd86893ae5d03487546dae7f34993217f8a91", size = 318787, upload-time = "2026-02-02T12:36:45.071Z" }, + { url = "https://files.pythonhosted.org/packages/f3/b9/7257ac59778f1cd025b26a23c5520a36a424f7f1b068f2442a5b499b7464/jiter-0.13.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:879e768938e7b49b5e90b7e3fecc0dbec01b8cb89595861fb39a8967c5220d09", size = 353880, upload-time = "2026-02-02T12:36:47.365Z" }, + { url = "https://files.pythonhosted.org/packages/c3/87/719eec4a3f0841dad99e3d3604ee4cba36af4419a76f3cb0b8e2e691ad67/jiter-0.13.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:682161a67adea11e3aae9038c06c8b4a9a71023228767477d683f69903ebc607", size = 366702, upload-time = "2026-02-02T12:36:48.871Z" }, + { url = "https://files.pythonhosted.org/packages/d2/65/415f0a75cf6921e43365a1bc227c565cb949caca8b7532776e430cbaa530/jiter-0.13.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a13b68cd1cd8cc9de8f244ebae18ccb3e4067ad205220ef324c39181e23bbf66", size = 486319, upload-time = "2026-02-02T12:36:53.006Z" }, + { url = "https://files.pythonhosted.org/packages/54/a2/9e12b48e82c6bbc6081fd81abf915e1443add1b13d8fc586e1d90bb02bb8/jiter-0.13.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87ce0f14c6c08892b610686ae8be350bf368467b6acd5085a5b65441e2bf36d2", size = 372289, upload-time = "2026-02-02T12:36:54.593Z" }, + { url = "https://files.pythonhosted.org/packages/4e/c1/e4693f107a1789a239c759a432e9afc592366f04e901470c2af89cfd28e1/jiter-0.13.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c365005b05505a90d1c47856420980d0237adf82f70c4aff7aebd3c1cc143ad", size = 360165, upload-time = "2026-02-02T12:36:56.112Z" }, + { url = "https://files.pythonhosted.org/packages/17/08/91b9ea976c1c758240614bd88442681a87672eebc3d9a6dde476874e706b/jiter-0.13.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1317fdffd16f5873e46ce27d0e0f7f4f90f0cdf1d86bf6abeaea9f63ca2c401d", size = 389634, upload-time = "2026-02-02T12:36:57.495Z" }, + { url = "https://files.pythonhosted.org/packages/18/23/58325ef99390d6d40427ed6005bf1ad54f2577866594bcf13ce55675f87d/jiter-0.13.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:c05b450d37ba0c9e21c77fef1f205f56bcee2330bddca68d344baebfc55ae0df", size = 514933, upload-time = "2026-02-02T12:36:58.909Z" }, + { url = "https://files.pythonhosted.org/packages/5b/25/69f1120c7c395fd276c3996bb8adefa9c6b84c12bb7111e5c6ccdcd8526d/jiter-0.13.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:775e10de3849d0631a97c603f996f518159272db00fdda0a780f81752255ee9d", size = 548842, upload-time = "2026-02-02T12:37:00.433Z" }, + { url = "https://files.pythonhosted.org/packages/18/05/981c9669d86850c5fbb0d9e62bba144787f9fba84546ba43d624ee27ef29/jiter-0.13.0-cp314-cp314-win32.whl", hash = "sha256:632bf7c1d28421c00dd8bbb8a3bac5663e1f57d5cd5ed962bce3c73bf62608e6", size = 202108, upload-time = "2026-02-02T12:37:01.718Z" }, + { url = "https://files.pythonhosted.org/packages/8d/96/cdcf54dd0b0341db7d25413229888a346c7130bd20820530905fdb65727b/jiter-0.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:f22ef501c3f87ede88f23f9b11e608581c14f04db59b6a801f354397ae13739f", size = 204027, upload-time = "2026-02-02T12:37:03.075Z" }, + { url = "https://files.pythonhosted.org/packages/fb/f9/724bcaaab7a3cd727031fe4f6995cb86c4bd344909177c186699c8dec51a/jiter-0.13.0-cp314-cp314-win_arm64.whl", hash = "sha256:07b75fe09a4ee8e0c606200622e571e44943f47254f95e2436c8bdcaceb36d7d", size = 187199, upload-time = "2026-02-02T12:37:04.414Z" }, + { url = "https://files.pythonhosted.org/packages/62/92/1661d8b9fd6a3d7a2d89831db26fe3c1509a287d83ad7838831c7b7a5c7e/jiter-0.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:964538479359059a35fb400e769295d4b315ae61e4105396d355a12f7fef09f0", size = 318423, upload-time = "2026-02-02T12:37:05.806Z" }, + { url = "https://files.pythonhosted.org/packages/4f/3b/f77d342a54d4ebcd128e520fc58ec2f5b30a423b0fd26acdfc0c6fef8e26/jiter-0.13.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e104da1db1c0991b3eaed391ccd650ae8d947eab1480c733e5a3fb28d4313e40", size = 351438, upload-time = "2026-02-02T12:37:07.189Z" }, + { url = "https://files.pythonhosted.org/packages/76/b3/ba9a69f0e4209bd3331470c723c2f5509e6f0482e416b612431a5061ed71/jiter-0.13.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e3a5f0cde8ff433b8e88e41aa40131455420fb3649a3c7abdda6145f8cb7202", size = 364774, upload-time = "2026-02-02T12:37:08.579Z" }, + { url = "https://files.pythonhosted.org/packages/b3/16/6cdb31fa342932602458dbb631bfbd47f601e03d2e4950740e0b2100b570/jiter-0.13.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:57aab48f40be1db920a582b30b116fe2435d184f77f0e4226f546794cedd9cf0", size = 487238, upload-time = "2026-02-02T12:37:10.066Z" }, + { url = "https://files.pythonhosted.org/packages/ed/b1/956cc7abaca8d95c13aa8d6c9b3f3797241c246cd6e792934cc4c8b250d2/jiter-0.13.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7772115877c53f62beeb8fd853cab692dbc04374ef623b30f997959a4c0e7e95", size = 372892, upload-time = "2026-02-02T12:37:11.656Z" }, + { url = "https://files.pythonhosted.org/packages/26/c4/97ecde8b1e74f67b8598c57c6fccf6df86ea7861ed29da84629cdbba76c4/jiter-0.13.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1211427574b17b633cfceba5040de8081e5abf114f7a7602f73d2e16f9fdaa59", size = 360309, upload-time = "2026-02-02T12:37:13.244Z" }, + { url = "https://files.pythonhosted.org/packages/4b/d7/eabe3cf46715854ccc80be2cd78dd4c36aedeb30751dbf85a1d08c14373c/jiter-0.13.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7beae3a3d3b5212d3a55d2961db3c292e02e302feb43fce6a3f7a31b90ea6dfe", size = 389607, upload-time = "2026-02-02T12:37:14.881Z" }, + { url = "https://files.pythonhosted.org/packages/df/2d/03963fc0804e6109b82decfb9974eb92df3797fe7222428cae12f8ccaa0c/jiter-0.13.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:e5562a0f0e90a6223b704163ea28e831bd3a9faa3512a711f031611e6b06c939", size = 514986, upload-time = "2026-02-02T12:37:16.326Z" }, + { url = "https://files.pythonhosted.org/packages/f6/6c/8c83b45eb3eb1c1e18d841fe30b4b5bc5619d781267ca9bc03e005d8fd0a/jiter-0.13.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:6c26a424569a59140fb51160a56df13f438a2b0967365e987889186d5fc2f6f9", size = 548756, upload-time = "2026-02-02T12:37:17.736Z" }, + { url = "https://files.pythonhosted.org/packages/47/66/eea81dfff765ed66c68fd2ed8c96245109e13c896c2a5015c7839c92367e/jiter-0.13.0-cp314-cp314t-win32.whl", hash = "sha256:24dc96eca9f84da4131cdf87a95e6ce36765c3b156fc9ae33280873b1c32d5f6", size = 201196, upload-time = "2026-02-02T12:37:19.101Z" }, + { url = "https://files.pythonhosted.org/packages/ff/32/4ac9c7a76402f8f00d00842a7f6b83b284d0cf7c1e9d4227bc95aa6d17fa/jiter-0.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0a8d76c7524087272c8ae913f5d9d608bd839154b62c4322ef65723d2e5bb0b8", size = 204215, upload-time = "2026-02-02T12:37:20.495Z" }, + { url = "https://files.pythonhosted.org/packages/f9/8e/7def204fea9f9be8b3c21a6f2dd6c020cf56c7d5ff753e0e23ed7f9ea57e/jiter-0.13.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2c26cf47e2cad140fa23b6d58d435a7c0161f5c514284802f25e87fddfe11024", size = 187152, upload-time = "2026-02-02T12:37:22.124Z" }, + { url = "https://files.pythonhosted.org/packages/41/95/8e6611379c9ce8534ff94dd800c50d6d0061b2c9ae6386fbcd86c7386f0a/jiter-0.13.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:4397ee562b9f69d283e5674445551b47a5e8076fdde75e71bfac5891113dc543", size = 313635, upload-time = "2026-02-02T12:37:23.545Z" }, + { url = "https://files.pythonhosted.org/packages/70/ea/17db64dcaf84bbb187874232222030ea4d689e6008f93bda6e7c691bc4c7/jiter-0.13.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7f90023f8f672e13ea1819507d2d21b9d2d1c18920a3b3a5f1541955a85b5504", size = 309761, upload-time = "2026-02-02T12:37:25.075Z" }, + { url = "https://files.pythonhosted.org/packages/a3/36/b2e2a7b12b94ecc7248acf2a8fe6288be893d1ebb9728655ceada22f00ad/jiter-0.13.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed0240dd1536a98c3ab55e929c60dfff7c899fecafcb7d01161b21a99fc8c363", size = 355245, upload-time = "2026-02-02T12:37:26.646Z" }, + { url = "https://files.pythonhosted.org/packages/77/3f/5b159663c5be622daec20074c997bb66bc1fac63c167c02aef3df476fb32/jiter-0.13.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6207fc61c395b26fffdcf637a0b06b4326f35bfa93c6e92fe1a166a21aeb6731", size = 365842, upload-time = "2026-02-02T12:37:28.207Z" }, + { url = "https://files.pythonhosted.org/packages/98/30/76a68fa2c9c815c6b7802a92fc354080d66ffba9acc4690fd85622f77ad4/jiter-0.13.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00203f47c214156df427b5989de74cb340c65c8180d09be1bf9de81d0abad599", size = 489223, upload-time = "2026-02-02T12:37:29.571Z" }, + { url = "https://files.pythonhosted.org/packages/a3/39/7c5cb85ccd71241513c878054c26a55828ccded6567d931a23ea4be73787/jiter-0.13.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c26ad6967c9dcedf10c995a21539c3aa57d4abad7001b7a84f621a263a6b605", size = 375762, upload-time = "2026-02-02T12:37:31.186Z" }, + { url = "https://files.pythonhosted.org/packages/a8/6a/381cd18d050b0102e60324e8d3f51f37ef02c56e9f4e5f0b7d26ba18958d/jiter-0.13.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a576f5dce9ac7de5d350b8e2f552cf364f32975ed84717c35379a51c7cb198bd", size = 364996, upload-time = "2026-02-02T12:37:32.931Z" }, + { url = "https://files.pythonhosted.org/packages/37/1e/d66310f1f7085c13ea6f1119c9566ec5d2cfd1dc90df963118a6869247bb/jiter-0.13.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b22945be8425d161f2e536cdae66da300b6b000f1c0ba3ddf237d1bfd45d21b8", size = 395463, upload-time = "2026-02-02T12:37:34.446Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ab/06ae77cb293f860b152c356c635c15aaa800ce48772865a41704d9fac80d/jiter-0.13.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6eeb7db8bc77dc20476bc2f7407a23dbe3d46d9cc664b166e3d474e1c1de4baa", size = 520944, upload-time = "2026-02-02T12:37:35.987Z" }, + { url = "https://files.pythonhosted.org/packages/f1/8e/57b49b20361c42a80d455a6d83cb38626204508cab4298d6a22880205319/jiter-0.13.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:19cd6f85e1dc090277c3ce90a5b7d96f32127681d825e71c9dce28788e39fc0c", size = 554955, upload-time = "2026-02-02T12:37:37.656Z" }, + { url = "https://files.pythonhosted.org/packages/79/dd/113489973c3b4256e383321aea11bd57389e401912fa48eb145a99b38767/jiter-0.13.0-cp39-cp39-win32.whl", hash = "sha256:dc3ce84cfd4fa9628fe62c4f85d0d597a4627d4242cfafac32a12cc1455d00f7", size = 206876, upload-time = "2026-02-02T12:37:39.225Z" }, + { url = "https://files.pythonhosted.org/packages/6e/73/2bdfc7133c5ee0c8f18cfe4a7582f3cfbbf3ff672cec1b5f4ca67ff9d041/jiter-0.13.0-cp39-cp39-win_amd64.whl", hash = "sha256:9ffda299e417dc83362963966c50cb76d42da673ee140de8a8ac762d4bb2378b", size = 206404, upload-time = "2026-02-02T12:37:40.632Z" }, + { url = "https://files.pythonhosted.org/packages/79/b3/3c29819a27178d0e461a8571fb63c6ae38be6dc36b78b3ec2876bbd6a910/jiter-0.13.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b1cbfa133241d0e6bdab48dcdc2604e8ba81512f6bbd68ec3e8e1357dd3c316c", size = 307016, upload-time = "2026-02-02T12:37:42.755Z" }, + { url = "https://files.pythonhosted.org/packages/eb/ae/60993e4b07b1ac5ebe46da7aa99fdbb802eb986c38d26e3883ac0125c4e0/jiter-0.13.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:db367d8be9fad6e8ebbac4a7578b7af562e506211036cba2c06c3b998603c3d2", size = 305024, upload-time = "2026-02-02T12:37:44.774Z" }, + { url = "https://files.pythonhosted.org/packages/77/fa/2227e590e9cf98803db2811f172b2d6460a21539ab73006f251c66f44b14/jiter-0.13.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45f6f8efb2f3b0603092401dc2df79fa89ccbc027aaba4174d2d4133ed661434", size = 339337, upload-time = "2026-02-02T12:37:46.668Z" }, + { url = "https://files.pythonhosted.org/packages/2d/92/015173281f7eb96c0ef580c997da8ef50870d4f7f4c9e03c845a1d62ae04/jiter-0.13.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:597245258e6ad085d064780abfb23a284d418d3e61c57362d9449c6c7317ee2d", size = 346395, upload-time = "2026-02-02T12:37:48.09Z" }, + { url = "https://files.pythonhosted.org/packages/80/60/e50fa45dd7e2eae049f0ce964663849e897300433921198aef94b6ffa23a/jiter-0.13.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:3d744a6061afba08dd7ae375dcde870cffb14429b7477e10f67e9e6d68772a0a", size = 305169, upload-time = "2026-02-02T12:37:50.376Z" }, + { url = "https://files.pythonhosted.org/packages/d2/73/a009f41c5eed71c49bec53036c4b33555afcdee70682a18c6f66e396c039/jiter-0.13.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:ff732bd0a0e778f43d5009840f20b935e79087b4dc65bd36f1cd0f9b04b8ff7f", size = 303808, upload-time = "2026-02-02T12:37:52.092Z" }, + { url = "https://files.pythonhosted.org/packages/c4/10/528b439290763bff3d939268085d03382471b442f212dca4ff5f12802d43/jiter-0.13.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab44b178f7981fcaea7e0a5df20e773c663d06ffda0198f1a524e91b2fde7e59", size = 337384, upload-time = "2026-02-02T12:37:53.582Z" }, + { url = "https://files.pythonhosted.org/packages/67/8a/a342b2f0251f3dac4ca17618265d93bf244a2a4d089126e81e4c1056ac50/jiter-0.13.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bb00b6d26db67a05fe3e12c76edc75f32077fb51deed13822dc648fa373bc19", size = 343768, upload-time = "2026-02-02T12:37:55.055Z" }, +] + [[package]] name = "jmespath" version = "1.1.0" @@ -1622,6 +2355,56 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl", hash = "sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64", size = 20419, upload-time = "2026-01-22T16:35:24.919Z" }, ] +[[package]] +name = "jsonref" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/0d/c1f3277e90ccdb50d33ed5ba1ec5b3f0a242ed8c1b1a85d3afeb68464dca/jsonref-1.1.0.tar.gz", hash = "sha256:32fe8e1d85af0fdefbebce950af85590b22b60f9e95443176adbde4e1ecea552", size = 8814, upload-time = "2023-01-16T16:10:04.455Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/ec/e1db9922bceb168197a558a2b8c03a7963f1afe93517ddd3cf99f202f996/jsonref-1.1.0-py3-none-any.whl", hash = "sha256:590dc7773df6c21cbf948b5dac07a72a251db28b0238ceecce0a2abfa8ec30a9", size = 9425, upload-time = "2023-01-16T16:10:02.255Z" }, +] + +[[package]] +name = "jsonschema" +version = "4.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs", marker = "python_full_version >= '3.10'" }, + { name = "jsonschema-specifications", marker = "python_full_version >= '3.10'" }, + { name = "referencing", marker = "python_full_version >= '3.10'" }, + { name = "rpds-py", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, +] + +[[package]] +name = "jsonschema-path" +version = "0.4.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pathable", marker = "python_full_version >= '3.10'" }, + { name = "pyyaml", marker = "python_full_version >= '3.10'" }, + { name = "referencing", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/8a/7e6102f2b8bdc6705a9eb5294f8f6f9ccd3a8420e8e8e19671d1dd773251/jsonschema_path-0.4.5.tar.gz", hash = "sha256:c6cd7d577ae290c7defd4f4029e86fdb248ca1bd41a07557795b3c95e5144918", size = 15113, upload-time = "2026-03-03T09:56:46.87Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/d5/4e96c44f6c1ea3d812cf5391d81a4f5abaa540abf8d04ecd7f66e0ed11df/jsonschema_path-0.4.5-py3-none-any.whl", hash = "sha256:7d77a2c3f3ec569a40efe5c5f942c44c1af2a6f96fe0866794c9ef5b8f87fd65", size = 19368, upload-time = "2026-03-03T09:56:45.39Z" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, +] + [[package]] name = "jupyter-client" version = "8.6.3" @@ -1712,13 +2495,32 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl", hash = "sha256:ebf87fdc6073d142e114c72c9e29a9d7ca03fad818c5d300ce2adc1fb0743407", size = 29032, upload-time = "2025-10-16T19:19:16.783Z" }, ] +[[package]] +name = "keyring" +version = "25.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata", marker = "python_full_version >= '3.10' and python_full_version < '3.12'" }, + { name = "jaraco-classes", marker = "python_full_version >= '3.10'" }, + { name = "jaraco-context", marker = "python_full_version >= '3.10'" }, + { name = "jaraco-functools", marker = "python_full_version >= '3.10'" }, + { name = "jeepney", marker = "python_full_version >= '3.10' and sys_platform == 'linux'" }, + { name = "pywin32-ctypes", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, + { name = "secretstorage", marker = "python_full_version >= '3.10' and sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/4b/674af6ef2f97d56f0ab5153bf0bfa28ccb6c3ed4d1babf4305449668807b/keyring-25.7.0.tar.gz", hash = "sha256:fe01bd85eb3f8fb3dd0405defdeac9a5b4f6f0439edbb3149577f244a2e8245b", size = 63516, upload-time = "2025-11-16T16:26:09.482Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl", hash = "sha256:be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f", size = 39160, upload-time = "2025-11-16T16:26:08.402Z" }, +] + [[package]] name = "kombu" version = "5.6.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "amqp" }, - { name = "packaging" }, + { name = "packaging", version = "25.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "packaging", version = "26.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "tzdata" }, { name = "vine" }, ] @@ -1758,6 +2560,18 @@ celery = [ fastapi = [ { name = "opentelemetry-instrumentation-fastapi" }, ] +httpx = [ + { name = "opentelemetry-instrumentation-httpx", marker = "python_full_version >= '3.10'" }, +] + +[[package]] +name = "logfire-api" +version = "4.28.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f7/c9/f2deb23fd2ce59630dd42ea86cc6c3dc0b8e17692238406a19a220ddb5ed/logfire_api-4.28.0.tar.gz", hash = "sha256:03b7814aa792eb91498871b6a33bd912a0ed910d20d20d87131e56525cf4c221", size = 76410, upload-time = "2026-03-11T16:23:42.815Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/b0/b00af83be6fbdee98371802b23fa43f395fb973498c454dbd012e80f372d/logfire_api-4.28.0-py3-none-any.whl", hash = "sha256:d95714211673b64cf61f94b01704155033e6dc0f48b88b3cc312f94bbaaeeca6", size = 121457, upload-time = "2026-03-11T16:23:39.892Z" }, +] [[package]] name = "markdown-it-py" @@ -1906,6 +2720,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/af/33/ee4519fa02ed11a94aef9559552f3b17bb863f2ecfe1a35dc7f548cde231/matplotlib_inline-0.2.1-py3-none-any.whl", hash = "sha256:d56ce5156ba6085e00a9d54fead6ed29a9c47e215cd1bba2e976ef39f5710a76", size = 9516, upload-time = "2025-10-23T09:00:20.675Z" }, ] +[[package]] +name = "mcp" +version = "1.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio", marker = "python_full_version >= '3.10'" }, + { name = "httpx", marker = "python_full_version >= '3.10'" }, + { name = "httpx-sse", marker = "python_full_version >= '3.10'" }, + { name = "jsonschema", marker = "python_full_version >= '3.10'" }, + { name = "pydantic", marker = "python_full_version >= '3.10'" }, + { name = "pydantic-settings", version = "2.12.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pyjwt", extra = ["crypto"], marker = "python_full_version >= '3.10'" }, + { name = "python-multipart", version = "0.0.22", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pywin32", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, + { name = "sse-starlette", marker = "python_full_version >= '3.10'" }, + { name = "starlette", version = "0.50.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.10'" }, + { name = "typing-inspection", marker = "python_full_version >= '3.10'" }, + { name = "uvicorn", version = "0.40.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' and sys_platform != 'emscripten'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/6d/62e76bbb8144d6ed86e202b5edd8a4cb631e7c8130f3f4893c3f90262b10/mcp-1.26.0.tar.gz", hash = "sha256:db6e2ef491eecc1a0d93711a76f28dec2e05999f93afd48795da1c1137142c66", size = 608005, upload-time = "2026-01-24T19:40:32.468Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/d9/eaa1f80170d2b7c5ba23f3b59f766f3a0bb41155fbc32a69adfa1adaaef9/mcp-1.26.0-py3-none-any.whl", hash = "sha256:904a21c33c25aa98ddbeb47273033c435e595bbacfdb177f4bd87f6dceebe1ca", size = 233615, upload-time = "2026-01-24T19:40:30.652Z" }, +] + [[package]] name = "mdurl" version = "0.1.2" @@ -1932,12 +2771,64 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3e/9a/b697530a882588a84db616580f2ba5d1d515c815e11c30d219145afeec87/minio-7.2.20-py3-none-any.whl", hash = "sha256:eb33dd2fb80e04c3726a76b13241c6be3c4c46f8d81e1d58e757786f6501897e", size = 93751, upload-time = "2025-11-27T00:37:13.993Z" }, ] +[[package]] +name = "mistralai" +version = "1.9.11" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "eval-type-backport", marker = "python_full_version < '3.10'" }, + { name = "httpx", marker = "python_full_version < '3.10'" }, + { name = "invoke", marker = "python_full_version < '3.10'" }, + { name = "pydantic", marker = "python_full_version < '3.10'" }, + { name = "python-dateutil", marker = "python_full_version < '3.10'" }, + { name = "pyyaml", marker = "python_full_version < '3.10'" }, + { name = "typing-inspection", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/8d/d8b7af67a966b6f227024e1cb7287fc19901a434f87a5a391dcfe635d338/mistralai-1.9.11.tar.gz", hash = "sha256:3df9e403c31a756ec79e78df25ee73cea3eb15f86693773e16b16adaf59c9b8a", size = 208051, upload-time = "2025-10-02T15:53:40.473Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/76/4ce12563aea5a76016f8643eff30ab731e6656c845e9e4d090ef10c7b925/mistralai-1.9.11-py3-none-any.whl", hash = "sha256:7a3dc2b8ef3fceaa3582220234261b5c4e3e03a972563b07afa150e44a25a6d3", size = 442796, upload-time = "2025-10-02T15:53:39.134Z" }, +] + +[[package]] +name = "mistralai" +version = "2.0.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'emscripten'", + "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'emscripten'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "eval-type-backport", marker = "python_full_version >= '3.10'" }, + { name = "httpx", marker = "python_full_version >= '3.10'" }, + { name = "opentelemetry-api", marker = "python_full_version >= '3.10'" }, + { name = "opentelemetry-semantic-conventions", marker = "python_full_version >= '3.10'" }, + { name = "pydantic", marker = "python_full_version >= '3.10'" }, + { name = "python-dateutil", marker = "python_full_version >= '3.10'" }, + { name = "typing-inspection", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ef/7e/3f4d307ad43b37cf3d51b8237e8e45755b45c3b6c55c7ce24ad41b7eee37/mistralai-2.0.1.tar.gz", hash = "sha256:3f55dbe3c801d553379892e3c3700bafe53e5ff1e80c2e1345d8b95593ad0c0a", size = 318437, upload-time = "2026-03-12T08:00:03.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/3f/40c34a543a638f4f0740f7f9a413ca0245865e426ce3f98672c03a27c432/mistralai-2.0.1-py3-none-any.whl", hash = "sha256:7d51e309c34f5ea33dc00dccc9e8a13d09da3005e2104e3eba2af6ea013b8d0a", size = 713277, upload-time = "2026-03-12T08:00:01.459Z" }, +] + [[package]] name = "mongomock" version = "4.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "packaging" }, + { name = "packaging", version = "25.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "packaging", version = "26.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "pytz" }, { name = "sentinels" }, ] @@ -1946,6 +2837,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/94/4d/8bea712978e3aff017a2ab50f262c620e9239cc36f348aae45e48d6a4786/mongomock-4.3.0-py2.py3-none-any.whl", hash = "sha256:5ef86bd12fc8806c6e7af32f21266c61b6c4ba96096f85129852d1c4fec1327e", size = 64891, upload-time = "2024-11-16T11:23:24.748Z" }, ] +[[package]] +name = "more-itertools" +version = "10.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ea/5d/38b681d3fce7a266dd9ab73c66959406d565b3e85f21d5e66e1181d93721/more_itertools-10.8.0.tar.gz", hash = "sha256:f638ddf8a1a0d134181275fb5d58b086ead7c6a72429ad725c67503f13ba30bd", size = 137431, upload-time = "2025-09-02T15:23:11.018Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/8e/469e5a4a2f5855992e425f3cb33804cc07bf18d48f2db061aec61ce50270/more_itertools-10.8.0-py3-none-any.whl", hash = "sha256:52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b", size = 69667, upload-time = "2025-09-02T15:23:09.635Z" }, +] + [[package]] name = "multidict" version = "6.7.1" @@ -2111,6 +3011,45 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195, upload-time = "2024-01-21T14:25:17.223Z" }, ] +[[package]] +name = "nexus-rpc" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ef/66/540687556bd28cf1ec370cc6881456203dfddb9dab047b8979c6865b5984/nexus_rpc-1.1.0.tar.gz", hash = "sha256:d65ad6a2f54f14e53ebe39ee30555eaeb894102437125733fb13034a04a44553", size = 77383, upload-time = "2025-07-07T19:03:58.368Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/2f/9e9d0dcaa4c6ffa22b7aa31069a8a264c753ff8027b36af602cce038c92f/nexus_rpc-1.1.0-py3-none-any.whl", hash = "sha256:d1b007af2aba186a27e736f8eaae39c03aed05b488084ff6c3d1785c9ba2ad38", size = 27743, upload-time = "2025-07-07T19:03:57.556Z" }, +] + +[[package]] +name = "nexus-rpc" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'emscripten'", + "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'emscripten'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "typing-extensions", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/50/95d7bc91f900da5e22662c82d9bf0f72a4b01f2a552708bf2f43807707a1/nexus_rpc-1.2.0.tar.gz", hash = "sha256:b4ddaffa4d3996aaeadf49b80dfcdfbca48fe4cb616defaf3b3c5c2c8fc61890", size = 74142, upload-time = "2025-11-17T19:17:06.798Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/04/eaac430d0e6bf21265ae989427d37e94be5e41dc216879f1fbb6c5339942/nexus_rpc-1.2.0-py3-none-any.whl", hash = "sha256:977876f3af811ad1a09b2961d3d1ac9233bda43ff0febbb0c9906483b9d9f8a3", size = 28166, upload-time = "2025-11-17T19:17:05.64Z" }, +] + [[package]] name = "numpy" version = "2.0.2" @@ -2321,6 +3260,37 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/de/e5/b7d20451657664b07986c2f6e3be564433f5dcaf3482d68eaecd79afaf03/numpy-2.4.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:be71bf1edb48ebbbf7f6337b5bfd2f895d1902f6335a5830b20141fc126ffba0", size = 12502577, upload-time = "2026-01-31T23:13:07.08Z" }, ] +[[package]] +name = "openai" +version = "2.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "httpx" }, + { name = "jiter" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d7/91/2a06c4e9597c338cac1e5e5a8dd6f29e1836fc229c4c523529dca387fda8/openai-2.26.0.tar.gz", hash = "sha256:b41f37c140ae0034a6e92b0c509376d907f3a66109935fba2c1b471a7c05a8fb", size = 666702, upload-time = "2026-03-05T23:17:35.874Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/2e/3f73e8ca53718952222cacd0cf7eecc9db439d020f0c1fe7ae717e4e199a/openai-2.26.0-py3-none-any.whl", hash = "sha256:6151bf8f83802f036117f06cc8a57b3a4da60da9926826cc96747888b57f394f", size = 1136409, upload-time = "2026-03-05T23:17:34.072Z" }, +] + +[[package]] +name = "openapi-pydantic" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/2e/58d83848dd1a79cb92ed8e63f6ba901ca282c5f09d04af9423ec26c56fd7/openapi_pydantic-0.5.1.tar.gz", hash = "sha256:ff6835af6bde7a459fb93eb93bb92b8749b754fc6e51b2f1590a19dc3005ee0d", size = 60892, upload-time = "2025-01-08T19:29:27.083Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/cf/03675d8bd8ecbf4445504d8071adab19f5f993676795708e36402ab38263/openapi_pydantic-0.5.1-py3-none-any.whl", hash = "sha256:a3a09ef4586f5bd760a8df7f43028b60cafb6d9f61de2acba9574766255ab146", size = 96381, upload-time = "2025-01-08T19:29:25.275Z" }, +] + [[package]] name = "opentelemetry-api" version = "1.39.1" @@ -2371,7 +3341,8 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-semantic-conventions" }, - { name = "packaging" }, + { name = "packaging", version = "25.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "packaging", version = "26.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "wrapt" }, ] sdist = { url = "https://files.pythonhosted.org/packages/41/0f/7e6b713ac117c1f5e4e3300748af699b9902a2e5e34c9cf443dde25a01fa/opentelemetry_instrumentation-0.60b1.tar.gz", hash = "sha256:57ddc7974c6eb35865af0426d1a17132b88b2ed8586897fee187fd5b8944bd6a", size = 31706, upload-time = "2025-12-11T13:36:42.515Z" } @@ -2425,6 +3396,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7d/cc/6e808328ba54662e50babdcab21138eae4250bc0fddf67d55526a615a2ca/opentelemetry_instrumentation_fastapi-0.60b1-py3-none-any.whl", hash = "sha256:af94b7a239ad1085fc3a820ecf069f67f579d7faf4c085aaa7bd9b64eafc8eaf", size = 13478, upload-time = "2025-12-11T13:36:00.811Z" }, ] +[[package]] +name = "opentelemetry-instrumentation-httpx" +version = "0.60b1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api", marker = "python_full_version >= '3.10'" }, + { name = "opentelemetry-instrumentation", marker = "python_full_version >= '3.10'" }, + { name = "opentelemetry-semantic-conventions", marker = "python_full_version >= '3.10'" }, + { name = "opentelemetry-util-http", marker = "python_full_version >= '3.10'" }, + { name = "wrapt", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/86/08/11208bcfcab4fc2023252c3f322aa397fd9ad948355fea60f5fc98648603/opentelemetry_instrumentation_httpx-0.60b1.tar.gz", hash = "sha256:a506ebaf28c60112cbe70ad4f0338f8603f148938cb7b6794ce1051cd2b270ae", size = 20611, upload-time = "2025-12-11T13:37:01.661Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/59/b98e84eebf745ffc75397eaad4763795bff8a30cbf2373a50ed4e70646c5/opentelemetry_instrumentation_httpx-0.60b1-py3-none-any.whl", hash = "sha256:f37636dd742ad2af83d896ba69601ed28da51fa4e25d1ab62fde89ce413e275b", size = 15701, upload-time = "2025-12-11T13:36:04.56Z" }, +] + [[package]] name = "opentelemetry-proto" version = "1.39.1" @@ -2473,10 +3460,34 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/16/5c/d3f1733665f7cd582ef0842fb1d2ed0bc1fba10875160593342d22bba375/opentelemetry_util_http-0.60b1-py3-none-any.whl", hash = "sha256:66381ba28550c91bee14dcba8979ace443444af1ed609226634596b4b0faf199", size = 8947, upload-time = "2025-12-11T13:36:37.151Z" }, ] +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'emscripten'", + "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'emscripten'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.10.*'", +] +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" } +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" }, +] + [[package]] name = "packaging" version = "26.0" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] 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/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, @@ -2649,6 +3660,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/16/32/f8e3c85d1d5250232a5d3477a2a28cc291968ff175caeadaf3cc19ce0e4a/parso-0.8.5-py2.py3-none-any.whl", hash = "sha256:646204b5ee239c396d040b90f9e272e9a8017c630092bf59980beb62fd033887", size = 106668, upload-time = "2025-08-23T15:15:25.663Z" }, ] +[[package]] +name = "pathable" +version = "0.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/55/b748445cb4ea6b125626f15379be7c96d1035d4fa3e8fee362fa92298abf/pathable-0.5.0.tar.gz", hash = "sha256:d81938348a1cacb525e7c75166270644782c0fb9c8cecc16be033e71427e0ef1", size = 16655, upload-time = "2026-02-20T08:47:00.748Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/96/5a770e5c461462575474468e5af931cff9de036e7c2b4fea23c1c58d2cbe/pathable-0.5.0-py3-none-any.whl", hash = "sha256:646e3d09491a6351a0c82632a09c02cdf70a252e73196b36d8a15ba0a114f0a6", size = 16867, upload-time = "2026-02-20T08:46:59.536Z" }, +] + [[package]] name = "pexpect" version = "4.9.0" @@ -2906,6 +3926,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842, upload-time = "2024-07-21T12:58:20.04Z" }, ] +[[package]] +name = "py-key-value-aio" +version = "0.4.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beartype", marker = "python_full_version >= '3.10'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/3c/0397c072a38d4bc580994b42e0c90c5f44f679303489e4376289534735e5/py_key_value_aio-0.4.4.tar.gz", hash = "sha256:e3012e6243ed7cc09bb05457bd4d03b1ba5c2b1ca8700096b3927db79ffbbe55", size = 92300, upload-time = "2026-02-16T21:21:43.245Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/69/f1b537ee70b7def42d63124a539ed3026a11a3ffc3086947a1ca6e861868/py_key_value_aio-0.4.4-py3-none-any.whl", hash = "sha256:18e17564ecae61b987f909fc2cd41ee2012c84b4b1dcb8c055cf8b4bc1bf3f5d", size = 152291, upload-time = "2026-02-16T21:21:44.241Z" }, +] + +[package.optional-dependencies] +filetree = [ + { name = "aiofile", marker = "python_full_version >= '3.10'" }, + { name = "anyio", marker = "python_full_version >= '3.10'" }, +] +keyring = [ + { name = "keyring", marker = "python_full_version >= '3.10'" }, +] +memory = [ + { name = "cachetools", marker = "python_full_version >= '3.10'" }, +] + [[package]] name = "pyasn1" version = "0.6.2" @@ -3015,28 +4060,240 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, ] +[package.optional-dependencies] +email = [ + { name = "email-validator", marker = "python_full_version >= '3.10'" }, +] + [[package]] -name = "pydantic-core" -version = "2.41.5" +name = "pydantic-ai" +version = "0.8.1" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] dependencies = [ - { name = "typing-extensions" }, + { name = "pydantic-ai-slim", version = "0.8.1", source = { registry = "https://pypi.org/simple" }, extra = ["ag-ui", "anthropic", "bedrock", "cli", "cohere", "evals", "google", "groq", "huggingface", "mistral", "openai", "retries", "temporal", "vertexai"], marker = "python_full_version < '3.10'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } +sdist = { url = "https://files.pythonhosted.org/packages/56/d7/fcc18ce80008e888404a3615f973aa3f39b98384d61b03621144c9f4c2d4/pydantic_ai-0.8.1.tar.gz", hash = "sha256:05974382082ee4f3706909d06bdfcc5e95f39e29230cc4d00e47429080099844", size = 43772581, upload-time = "2025-08-29T14:46:23.201Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c6/90/32c9941e728d564b411d574d8ee0cf09b12ec978cb22b294995bae5549a5/pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146", size = 2107298, upload-time = "2025-11-04T13:39:04.116Z" }, - { url = "https://files.pythonhosted.org/packages/fb/a8/61c96a77fe28993d9a6fb0f4127e05430a267b235a124545d79fea46dd65/pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2", size = 1901475, upload-time = "2025-11-04T13:39:06.055Z" }, - { url = "https://files.pythonhosted.org/packages/5d/b6/338abf60225acc18cdc08b4faef592d0310923d19a87fba1faf05af5346e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97", size = 1918815, upload-time = "2025-11-04T13:39:10.41Z" }, - { url = "https://files.pythonhosted.org/packages/d1/1c/2ed0433e682983d8e8cba9c8d8ef274d4791ec6a6f24c58935b90e780e0a/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9", size = 2065567, upload-time = "2025-11-04T13:39:12.244Z" }, - { url = "https://files.pythonhosted.org/packages/b3/24/cf84974ee7d6eae06b9e63289b7b8f6549d416b5c199ca2d7ce13bbcf619/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52", size = 2230442, upload-time = "2025-11-04T13:39:13.962Z" }, - { url = "https://files.pythonhosted.org/packages/fd/21/4e287865504b3edc0136c89c9c09431be326168b1eb7841911cbc877a995/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941", size = 2350956, upload-time = "2025-11-04T13:39:15.889Z" }, - { url = "https://files.pythonhosted.org/packages/a8/76/7727ef2ffa4b62fcab916686a68a0426b9b790139720e1934e8ba797e238/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a", size = 2068253, upload-time = "2025-11-04T13:39:17.403Z" }, - { url = "https://files.pythonhosted.org/packages/d5/8c/a4abfc79604bcb4c748e18975c44f94f756f08fb04218d5cb87eb0d3a63e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c", size = 2177050, upload-time = "2025-11-04T13:39:19.351Z" }, - { url = "https://files.pythonhosted.org/packages/67/b1/de2e9a9a79b480f9cb0b6e8b6ba4c50b18d4e89852426364c66aa82bb7b3/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2", size = 2147178, upload-time = "2025-11-04T13:39:21Z" }, - { url = "https://files.pythonhosted.org/packages/16/c1/dfb33f837a47b20417500efaa0378adc6635b3c79e8369ff7a03c494b4ac/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556", size = 2341833, upload-time = "2025-11-04T13:39:22.606Z" }, - { url = "https://files.pythonhosted.org/packages/47/36/00f398642a0f4b815a9a558c4f1dca1b4020a7d49562807d7bc9ff279a6c/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49", size = 2321156, upload-time = "2025-11-04T13:39:25.843Z" }, - { url = "https://files.pythonhosted.org/packages/7e/70/cad3acd89fde2010807354d978725ae111ddf6d0ea46d1ea1775b5c1bd0c/pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba", size = 1989378, upload-time = "2025-11-04T13:39:27.92Z" }, - { url = "https://files.pythonhosted.org/packages/76/92/d338652464c6c367e5608e4488201702cd1cbb0f33f7b6a85a60fe5f3720/pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9", size = 2013622, upload-time = "2025-11-04T13:39:29.848Z" }, + { url = "https://files.pythonhosted.org/packages/f9/04/802b8cf834dffcda8baabb3b76c549243694a83346c3f54e47a3a4d519fb/pydantic_ai-0.8.1-py3-none-any.whl", hash = "sha256:5fa923097132aa69b4d6a310b462dc091009c7b87705edf4443d37b887d5ef9a", size = 10188, upload-time = "2025-08-29T14:46:11.137Z" }, +] + +[[package]] +name = "pydantic-ai" +version = "1.67.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'emscripten'", + "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'emscripten'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "pydantic-ai-slim", version = "1.67.0", source = { registry = "https://pypi.org/simple" }, extra = ["ag-ui", "anthropic", "bedrock", "cli", "cohere", "evals", "fastmcp", "google", "groq", "huggingface", "logfire", "mcp", "mistral", "openai", "retries", "temporal", "ui", "vertexai", "xai"], marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/99/c2/61c8423d4d7c3a7b9c402bdb9f78aea2d6174e8f778019738b35a563fcda/pydantic_ai-1.67.0.tar.gz", hash = "sha256:87e402dbc68b10f2b8494abce110f43c1f56817ebf2bcc4fe47698899e5e8516", size = 12142, upload-time = "2026-03-06T22:40:04.927Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/e3/2e6f8dad1f5f7c20d54dabb8f4b50505268093078a905bfb10b77ba204dc/pydantic_ai-1.67.0-py3-none-any.whl", hash = "sha256:bc1f3264f73658891ef023861944c567a5aee96fa45eeeb4c10286550ec71ade", size = 7227, upload-time = "2026-03-06T22:39:56.895Z" }, +] + +[[package]] +name = "pydantic-ai-slim" +version = "0.8.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "eval-type-backport", marker = "python_full_version < '3.10'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.10'" }, + { name = "genai-prices", marker = "python_full_version < '3.10'" }, + { name = "griffe", marker = "python_full_version < '3.10'" }, + { name = "httpx", marker = "python_full_version < '3.10'" }, + { name = "opentelemetry-api", marker = "python_full_version < '3.10'" }, + { name = "pydantic", marker = "python_full_version < '3.10'" }, + { name = "pydantic-graph", version = "0.8.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "typing-inspection", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a2/91/08137459b3745900501b3bd11852ced6c81b7ce6e628696d75b09bb786c5/pydantic_ai_slim-0.8.1.tar.gz", hash = "sha256:12ef3dcbe5e1dad195d5e256746ef960f6e59aeddda1a55bdd553ee375ff53ae", size = 218906, upload-time = "2025-08-29T14:46:27.517Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/ce/8dbadd04f578d02a9825a46e931005743fe223736296f30b55846c084fab/pydantic_ai_slim-0.8.1-py3-none-any.whl", hash = "sha256:fc7edc141b21fe42bc54a2d92c1127f8a75160c5e57a168dba154d3f4adb963f", size = 297821, upload-time = "2025-08-29T14:46:14.647Z" }, +] + +[package.optional-dependencies] +ag-ui = [ + { name = "ag-ui-protocol", marker = "python_full_version < '3.10'" }, + { name = "starlette", version = "0.49.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, +] +anthropic = [ + { name = "anthropic", marker = "python_full_version < '3.10'" }, +] +bedrock = [ + { name = "boto3", marker = "python_full_version < '3.10'" }, +] +cli = [ + { name = "argcomplete", marker = "python_full_version < '3.10'" }, + { name = "prompt-toolkit", marker = "python_full_version < '3.10'" }, + { name = "pyperclip", marker = "python_full_version < '3.10'" }, + { name = "rich", marker = "python_full_version < '3.10'" }, +] +cohere = [ + { name = "cohere", marker = "python_full_version < '3.10' and sys_platform != 'emscripten'" }, +] +evals = [ + { name = "pydantic-evals", version = "0.8.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, +] +google = [ + { name = "google-genai", version = "1.47.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, +] +groq = [ + { name = "groq", version = "1.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, +] +huggingface = [ + { name = "huggingface-hub", marker = "python_full_version < '3.10'" }, +] +mistral = [ + { name = "mistralai", version = "1.9.11", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, +] +openai = [ + { name = "openai", marker = "python_full_version < '3.10'" }, +] +retries = [ + { name = "tenacity", version = "9.1.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, +] +temporal = [ + { name = "temporalio", version = "1.16.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, +] +vertexai = [ + { name = "google-auth", marker = "python_full_version < '3.10'" }, + { name = "requests", marker = "python_full_version < '3.10'" }, +] + +[[package]] +name = "pydantic-ai-slim" +version = "1.67.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'emscripten'", + "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'emscripten'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version == '3.10.*'" }, + { name = "genai-prices", marker = "python_full_version >= '3.10'" }, + { name = "griffelib", marker = "python_full_version >= '3.10'" }, + { name = "httpx", marker = "python_full_version >= '3.10'" }, + { name = "opentelemetry-api", marker = "python_full_version >= '3.10'" }, + { name = "pydantic", marker = "python_full_version >= '3.10'" }, + { name = "pydantic-graph", version = "1.67.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "typing-inspection", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8c/51/c20e9aa4ee5f1d92a77dcb74741113e0d1838d63b65bb04ef16b9ba4b55f/pydantic_ai_slim-1.67.0.tar.gz", hash = "sha256:ce646e96aea775c00305d0c214080c970640cb4adf3bf58aa1296e186103e765", size = 436929, upload-time = "2026-03-06T22:40:07.061Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/bc/401bf11e4615a7adf8c00fb8f62313eb15ae7ee191fa77438cb9a27fa745/pydantic_ai_slim-1.67.0-py3-none-any.whl", hash = "sha256:241290e634298a38ed60c135eca2f9efbfa269811bf1e95aad33b234b6880945", size = 567858, upload-time = "2026-03-06T22:39:59.977Z" }, +] + +[package.optional-dependencies] +ag-ui = [ + { name = "ag-ui-protocol", marker = "python_full_version >= '3.10'" }, + { name = "starlette", version = "0.50.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +anthropic = [ + { name = "anthropic", marker = "python_full_version >= '3.10'" }, +] +bedrock = [ + { name = "boto3", marker = "python_full_version >= '3.10'" }, +] +cli = [ + { name = "argcomplete", marker = "python_full_version >= '3.10'" }, + { name = "prompt-toolkit", marker = "python_full_version >= '3.10'" }, + { name = "pyperclip", marker = "python_full_version >= '3.10'" }, + { name = "rich", marker = "python_full_version >= '3.10'" }, +] +cohere = [ + { name = "cohere", marker = "python_full_version >= '3.10' and sys_platform != 'emscripten'" }, +] +evals = [ + { name = "pydantic-evals", version = "1.67.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +fastmcp = [ + { name = "fastmcp", marker = "python_full_version >= '3.10'" }, +] +google = [ + { name = "google-genai", version = "1.64.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +groq = [ + { name = "groq", version = "1.1.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +huggingface = [ + { name = "huggingface-hub", marker = "python_full_version >= '3.10'" }, +] +logfire = [ + { name = "logfire", extra = ["httpx"], marker = "python_full_version >= '3.10'" }, +] +mcp = [ + { name = "mcp", marker = "python_full_version >= '3.10'" }, +] +mistral = [ + { name = "mistralai", version = "2.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +openai = [ + { name = "openai", marker = "python_full_version >= '3.10'" }, + { name = "tiktoken", marker = "python_full_version >= '3.10'" }, +] +retries = [ + { name = "tenacity", version = "9.1.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +temporal = [ + { name = "temporalio", version = "1.20.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +ui = [ + { name = "starlette", version = "0.50.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +vertexai = [ + { name = "google-auth", marker = "python_full_version >= '3.10'" }, + { name = "requests", marker = "python_full_version >= '3.10'" }, +] +xai = [ + { name = "xai-sdk", marker = "python_full_version >= '3.10'" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/90/32c9941e728d564b411d574d8ee0cf09b12ec978cb22b294995bae5549a5/pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146", size = 2107298, upload-time = "2025-11-04T13:39:04.116Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a8/61c96a77fe28993d9a6fb0f4127e05430a267b235a124545d79fea46dd65/pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2", size = 1901475, upload-time = "2025-11-04T13:39:06.055Z" }, + { url = "https://files.pythonhosted.org/packages/5d/b6/338abf60225acc18cdc08b4faef592d0310923d19a87fba1faf05af5346e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97", size = 1918815, upload-time = "2025-11-04T13:39:10.41Z" }, + { url = "https://files.pythonhosted.org/packages/d1/1c/2ed0433e682983d8e8cba9c8d8ef274d4791ec6a6f24c58935b90e780e0a/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9", size = 2065567, upload-time = "2025-11-04T13:39:12.244Z" }, + { url = "https://files.pythonhosted.org/packages/b3/24/cf84974ee7d6eae06b9e63289b7b8f6549d416b5c199ca2d7ce13bbcf619/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52", size = 2230442, upload-time = "2025-11-04T13:39:13.962Z" }, + { url = "https://files.pythonhosted.org/packages/fd/21/4e287865504b3edc0136c89c9c09431be326168b1eb7841911cbc877a995/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941", size = 2350956, upload-time = "2025-11-04T13:39:15.889Z" }, + { url = "https://files.pythonhosted.org/packages/a8/76/7727ef2ffa4b62fcab916686a68a0426b9b790139720e1934e8ba797e238/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a", size = 2068253, upload-time = "2025-11-04T13:39:17.403Z" }, + { url = "https://files.pythonhosted.org/packages/d5/8c/a4abfc79604bcb4c748e18975c44f94f756f08fb04218d5cb87eb0d3a63e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c", size = 2177050, upload-time = "2025-11-04T13:39:19.351Z" }, + { url = "https://files.pythonhosted.org/packages/67/b1/de2e9a9a79b480f9cb0b6e8b6ba4c50b18d4e89852426364c66aa82bb7b3/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2", size = 2147178, upload-time = "2025-11-04T13:39:21Z" }, + { url = "https://files.pythonhosted.org/packages/16/c1/dfb33f837a47b20417500efaa0378adc6635b3c79e8369ff7a03c494b4ac/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556", size = 2341833, upload-time = "2025-11-04T13:39:22.606Z" }, + { url = "https://files.pythonhosted.org/packages/47/36/00f398642a0f4b815a9a558c4f1dca1b4020a7d49562807d7bc9ff279a6c/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49", size = 2321156, upload-time = "2025-11-04T13:39:25.843Z" }, + { url = "https://files.pythonhosted.org/packages/7e/70/cad3acd89fde2010807354d978725ae111ddf6d0ea46d1ea1775b5c1bd0c/pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba", size = 1989378, upload-time = "2025-11-04T13:39:27.92Z" }, + { url = "https://files.pythonhosted.org/packages/76/92/d338652464c6c367e5608e4488201702cd1cbb0f33f7b6a85a60fe5f3720/pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9", size = 2013622, upload-time = "2025-11-04T13:39:29.848Z" }, { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" }, { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" }, { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" }, @@ -3146,6 +4403,101 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, ] +[[package]] +name = "pydantic-evals" +version = "0.8.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "anyio", marker = "python_full_version < '3.10'" }, + { name = "eval-type-backport", marker = "python_full_version < '3.10'" }, + { name = "logfire-api", marker = "python_full_version < '3.10'" }, + { name = "pydantic", marker = "python_full_version < '3.10'" }, + { name = "pydantic-ai-slim", version = "0.8.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pyyaml", marker = "python_full_version < '3.10'" }, + { name = "rich", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6c/9d/460a1f2c9f5f263e9d8e9661acbd654ccc81ad3373ea43048d914091a817/pydantic_evals-0.8.1.tar.gz", hash = "sha256:c398a623c31c19ce70e346ad75654fcb1517c3f6a821461f64fe5cbbe0813023", size = 43933, upload-time = "2025-08-29T14:46:28.903Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/f9/1d21c4687167c4fa76fd3b1ed47f9bc2d38fd94cbacd9aa3f19e82e59830/pydantic_evals-0.8.1-py3-none-any.whl", hash = "sha256:6c76333b1d79632f619eb58a24ac656e9f402c47c75ad750ba0230d7f5514344", size = 52602, upload-time = "2025-08-29T14:46:16.602Z" }, +] + +[[package]] +name = "pydantic-evals" +version = "1.67.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'emscripten'", + "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'emscripten'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "anyio", marker = "python_full_version >= '3.10'" }, + { name = "logfire-api", marker = "python_full_version >= '3.10'" }, + { name = "pydantic", marker = "python_full_version >= '3.10'" }, + { name = "pydantic-ai-slim", version = "1.67.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pyyaml", marker = "python_full_version >= '3.10'" }, + { name = "rich", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fd/fc/cb4c5ac7144e4506b582f44fe0a97e4b2f97327a3e6d2539b6669326e45f/pydantic_evals-1.67.0.tar.gz", hash = "sha256:62ddcf40665114b8d36ec54c6728afc2f8d861c749e53e78c0ed65d943706b8a", size = 56689, upload-time = "2026-03-06T22:40:08.407Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/a4/5cf9caf529a523d8039598988f9c0fce87c59d928abb5d42b7b3347f336e/pydantic_evals-1.67.0-py3-none-any.whl", hash = "sha256:aa1bb0e9c5f87901420a9c60d84bbabc56ee90bf243509a2a60027762414b031", size = 67604, upload-time = "2026-03-06T22:40:01.833Z" }, +] + +[[package]] +name = "pydantic-graph" +version = "0.8.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "httpx", marker = "python_full_version < '3.10'" }, + { name = "logfire-api", marker = "python_full_version < '3.10'" }, + { name = "pydantic", marker = "python_full_version < '3.10'" }, + { name = "typing-inspection", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bd/97/b35b7cb82d9f1bb6d5c6d21bba54f6196a3a5f593373f3a9c163a3821fd7/pydantic_graph-0.8.1.tar.gz", hash = "sha256:c61675a05c74f661d4ff38d04b74bd652c1e0959467801986f2f85dc7585410d", size = 21675, upload-time = "2025-08-29T14:46:29.839Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/e3/5908643b049bb2384d143885725cbeb0f53707d418357d4d1ac8d2c82629/pydantic_graph-0.8.1-py3-none-any.whl", hash = "sha256:f1dd5db0fe22f4e3323c04c65e2f0013846decc312b3efc3196666764556b765", size = 27239, upload-time = "2025-08-29T14:46:18.317Z" }, +] + +[[package]] +name = "pydantic-graph" +version = "1.67.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'emscripten'", + "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'emscripten'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "httpx", marker = "python_full_version >= '3.10'" }, + { name = "logfire-api", marker = "python_full_version >= '3.10'" }, + { name = "pydantic", marker = "python_full_version >= '3.10'" }, + { name = "typing-inspection", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/17/f9/a2ce0fb0bba88701b77cc49ca249e6b143bd8b2adcbfcdff7b7b9e3f689d/pydantic_graph-1.67.0.tar.gz", hash = "sha256:d7bb6bb95aa5f3808b10d3700235aa881feebc462736cb8a5b917ffb470da36b", size = 58527, upload-time = "2026-03-06T22:40:09.271Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/b8/fc47a5151b274c354c8e15a7a02fe306d0c4d6e050cdf0cc0060f36d1ac2/pydantic_graph-1.67.0-py3-none-any.whl", hash = "sha256:fd3ec24dcf1f93435c6ad8b2968f66d2a20505c488e6319f1d96cfac832322d5", size = 72352, upload-time = "2026-03-06T22:40:03.04Z" }, +] + [[package]] name = "pydantic-settings" version = "2.11.0" @@ -3367,6 +4719,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8e/5e/c86a5643653825d3c913719e788e41386bee415c2b87b4f955432f2de6b2/pypdf2-3.0.1-py3-none-any.whl", hash = "sha256:d16e4205cfee272fbdc0568b68d82be796540b1537508cef59388f839c191928", size = 232572, upload-time = "2022-12-31T10:36:10.327Z" }, ] +[[package]] +name = "pyperclip" +version = "1.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/52/d87eba7cb129b81563019d1679026e7a112ef76855d6159d24754dbd2a51/pyperclip-1.11.0.tar.gz", hash = "sha256:244035963e4428530d9e3a6101a1ef97209c6825edab1567beac148ccc1db1b6", size = 12185, upload-time = "2025-09-26T14:40:37.245Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/80/fc9d01d5ed37ba4c42ca2b55b4339ae6e200b456be3a1aaddf4a9fa99b8c/pyperclip-1.11.0-py3-none-any.whl", hash = "sha256:299403e9ff44581cb9ba2ffeed69c7aa96a008622ad0c46cb575ca75b5b84273", size = 11063, upload-time = "2025-09-26T14:40:36.069Z" }, +] + [[package]] name = "pytest" version = "8.4.2" @@ -3378,7 +4739,7 @@ dependencies = [ { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, { name = "exceptiongroup", marker = "python_full_version < '3.10'" }, { name = "iniconfig", version = "2.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "packaging", marker = "python_full_version < '3.10'" }, + { name = "packaging", version = "26.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "pluggy", marker = "python_full_version < '3.10'" }, { name = "pygments", marker = "python_full_version < '3.10'" }, { name = "tomli", marker = "python_full_version < '3.10'" }, @@ -3408,7 +4769,7 @@ dependencies = [ { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, { name = "exceptiongroup", marker = "python_full_version == '3.10.*'" }, { name = "iniconfig", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "packaging", marker = "python_full_version >= '3.10'" }, + { name = "packaging", version = "25.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "pluggy", marker = "python_full_version >= '3.10'" }, { name = "pygments", marker = "python_full_version >= '3.10'" }, { name = "tomli", marker = "python_full_version == '3.10.*'" }, @@ -3506,43 +4867,66 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/60/22/e0e8d802f124772cec9c75430b01a212f86f9de7546bda715e54140d5aeb/pywin32-311-cp39-cp39-win_arm64.whl", hash = "sha256:62ea666235135fee79bb154e695f3ff67370afefd71bd7fea7512fc70ef31e3d", size = 8778162, upload-time = "2025-07-14T20:13:03.544Z" }, ] +[[package]] +name = "pywin32-ctypes" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471, upload-time = "2024-08-14T10:15:34.626Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756, upload-time = "2024-08-14T10:15:33.187Z" }, +] + [[package]] name = "pyyaml" -version = "6.0.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cd/e5/af35f7ea75cf72f2cd079c95ee16797de7cd71f29ea7c68ae5ce7be1eda0/PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43", size = 125201, upload-time = "2023-07-18T00:00:23.308Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/96/06/4beb652c0fe16834032e54f0956443d4cc797fe645527acee59e7deaa0a2/PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a", size = 189447, upload-time = "2023-07-17T23:57:04.325Z" }, - { url = "https://files.pythonhosted.org/packages/5b/07/10033a403b23405a8fc48975444463d3d10a5c2736b7eb2550b07b367429/PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f", size = 169264, upload-time = "2023-07-17T23:57:07.787Z" }, - { url = "https://files.pythonhosted.org/packages/f1/26/55e4f21db1f72eaef092015d9017c11510e7e6301c62a6cfee91295d13c6/PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938", size = 677003, upload-time = "2023-07-17T23:57:13.144Z" }, - { url = "https://files.pythonhosted.org/packages/ba/91/090818dfa62e85181f3ae23dd1e8b7ea7f09684864a900cab72d29c57346/PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d", size = 699070, upload-time = "2023-07-17T23:57:19.402Z" }, - { url = "https://files.pythonhosted.org/packages/29/61/bf33c6c85c55bc45a29eee3195848ff2d518d84735eb0e2d8cb42e0d285e/PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515", size = 705525, upload-time = "2023-07-17T23:57:25.272Z" }, - { url = "https://files.pythonhosted.org/packages/07/91/45dfd0ef821a7f41d9d0136ea3608bb5b1653e42fd56a7970532cb5c003f/PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290", size = 707514, upload-time = "2023-08-28T18:43:20.945Z" }, - { url = "https://files.pythonhosted.org/packages/b6/a0/b6700da5d49e9fed49dc3243d3771b598dad07abb37cc32e524607f96adc/PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924", size = 130488, upload-time = "2023-07-17T23:57:28.144Z" }, - { url = "https://files.pythonhosted.org/packages/24/97/9b59b43431f98d01806b288532da38099cc6f2fea0f3d712e21e269c0279/PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d", size = 145338, upload-time = "2023-07-17T23:57:31.118Z" }, - { url = "https://files.pythonhosted.org/packages/ec/0d/26fb23e8863e0aeaac0c64e03fd27367ad2ae3f3cccf3798ee98ce160368/PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007", size = 187867, upload-time = "2023-07-17T23:57:34.35Z" }, - { url = "https://files.pythonhosted.org/packages/28/09/55f715ddbf95a054b764b547f617e22f1d5e45d83905660e9a088078fe67/PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab", size = 167530, upload-time = "2023-07-17T23:57:36.975Z" }, - { url = "https://files.pythonhosted.org/packages/5e/94/7d5ee059dfb92ca9e62f4057dcdec9ac08a9e42679644854dc01177f8145/PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d", size = 732244, upload-time = "2023-07-17T23:57:43.774Z" }, - { url = "https://files.pythonhosted.org/packages/06/92/e0224aa6ebf9dc54a06a4609da37da40bb08d126f5535d81bff6b417b2ae/PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc", size = 752871, upload-time = "2023-07-17T23:57:51.921Z" }, - { url = "https://files.pythonhosted.org/packages/7b/5e/efd033ab7199a0b2044dab3b9f7a4f6670e6a52c089de572e928d2873b06/PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673", size = 757729, upload-time = "2023-07-17T23:57:59.865Z" }, - { url = "https://files.pythonhosted.org/packages/03/5c/c4671451b2f1d76ebe352c0945d4cd13500adb5d05f5a51ee296d80152f7/PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b", size = 748528, upload-time = "2023-08-28T18:43:23.207Z" }, - { url = "https://files.pythonhosted.org/packages/73/9c/766e78d1efc0d1fca637a6b62cea1b4510a7fb93617eb805223294fef681/PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741", size = 130286, upload-time = "2023-07-17T23:58:02.964Z" }, - { url = "https://files.pythonhosted.org/packages/b3/34/65bb4b2d7908044963ebf614fe0fdb080773fc7030d7e39c8d3eddcd4257/PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34", size = 144699, upload-time = "2023-07-17T23:58:05.586Z" }, - { url = "https://files.pythonhosted.org/packages/bc/06/1b305bf6aa704343be85444c9d011f626c763abb40c0edc1cad13bfd7f86/PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28", size = 178692, upload-time = "2023-08-28T18:43:24.924Z" }, - { url = "https://files.pythonhosted.org/packages/84/02/404de95ced348b73dd84f70e15a41843d817ff8c1744516bf78358f2ffd2/PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9", size = 165622, upload-time = "2023-08-28T18:43:26.54Z" }, - { url = "https://files.pythonhosted.org/packages/c7/4c/4a2908632fc980da6d918b9de9c1d9d7d7e70b2672b1ad5166ed27841ef7/PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef", size = 696937, upload-time = "2024-01-18T20:40:22.92Z" }, - { url = "https://files.pythonhosted.org/packages/b4/33/720548182ffa8344418126017aa1d4ab4aeec9a2275f04ce3f3573d8ace8/PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0", size = 724969, upload-time = "2023-08-28T18:43:28.56Z" }, - { url = "https://files.pythonhosted.org/packages/4f/78/77b40157b6cb5f2d3d31a3d9b2efd1ba3505371f76730d267e8b32cf4b7f/PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4", size = 712604, upload-time = "2023-08-28T18:43:30.206Z" }, - { url = "https://files.pythonhosted.org/packages/2e/97/3e0e089ee85e840f4b15bfa00e4e63d84a3691ababbfea92d6f820ea6f21/PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54", size = 126098, upload-time = "2023-08-28T18:43:31.835Z" }, - { url = "https://files.pythonhosted.org/packages/2b/9f/fbade56564ad486809c27b322d0f7e6a89c01f6b4fe208402e90d4443a99/PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df", size = 138675, upload-time = "2023-08-28T18:43:33.613Z" }, - { url = "https://files.pythonhosted.org/packages/57/c5/5d09b66b41d549914802f482a2118d925d876dc2a35b2d127694c1345c34/PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8", size = 197846, upload-time = "2023-07-17T23:59:46.424Z" }, - { url = "https://files.pythonhosted.org/packages/0e/88/21b2f16cb2123c1e9375f2c93486e35fdc86e63f02e274f0e99c589ef153/PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859", size = 174396, upload-time = "2023-07-17T23:59:49.538Z" }, - { url = "https://files.pythonhosted.org/packages/ac/6c/967d91a8edf98d2b2b01d149bd9e51b8f9fb527c98d80ebb60c6b21d60c4/PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6", size = 731824, upload-time = "2023-07-17T23:59:58.111Z" }, - { url = "https://files.pythonhosted.org/packages/4a/4b/c71ef18ef83c82f99e6da8332910692af78ea32bd1d1d76c9787dfa36aea/PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0", size = 754777, upload-time = "2023-07-18T00:00:06.716Z" }, - { url = "https://files.pythonhosted.org/packages/7d/39/472f2554a0f1e825bd7c5afc11c817cd7a2f3657460f7159f691fbb37c51/PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c", size = 738883, upload-time = "2023-07-18T00:00:14.423Z" }, - { url = "https://files.pythonhosted.org/packages/40/da/a175a35cf5583580e90ac3e2a3dbca90e43011593ae62ce63f79d7b28d92/PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5", size = 750294, upload-time = "2023-08-28T18:43:37.153Z" }, - { url = "https://files.pythonhosted.org/packages/24/62/7fcc372442ec8ea331da18c24b13710e010c5073ab851ef36bf9dacb283f/PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c", size = 136936, upload-time = "2023-07-18T00:00:17.167Z" }, - { url = "https://files.pythonhosted.org/packages/84/4d/82704d1ab9290b03da94e6425f5e87396b999fd7eb8e08f3a92c158402bf/PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486", size = 152751, upload-time = "2023-07-18T00:00:19.939Z" }, +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199, upload-time = "2024-08-06T20:31:40.178Z" }, + { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758, upload-time = "2024-08-06T20:31:42.173Z" }, + { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463, upload-time = "2024-08-06T20:31:44.263Z" }, + { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280, upload-time = "2024-08-06T20:31:50.199Z" }, + { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239, upload-time = "2024-08-06T20:31:52.292Z" }, + { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802, upload-time = "2024-08-06T20:31:53.836Z" }, + { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527, upload-time = "2024-08-06T20:31:55.565Z" }, + { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052, upload-time = "2024-08-06T20:31:56.914Z" }, + { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774, upload-time = "2024-08-06T20:31:58.304Z" }, + { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612, upload-time = "2024-08-06T20:32:03.408Z" }, + { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040, upload-time = "2024-08-06T20:32:04.926Z" }, + { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829, upload-time = "2024-08-06T20:32:06.459Z" }, + { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167, upload-time = "2024-08-06T20:32:08.338Z" }, + { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952, upload-time = "2024-08-06T20:32:14.124Z" }, + { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301, upload-time = "2024-08-06T20:32:16.17Z" }, + { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638, upload-time = "2024-08-06T20:32:18.555Z" }, + { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850, upload-time = "2024-08-06T20:32:19.889Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980, upload-time = "2024-08-06T20:32:21.273Z" }, + { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873, upload-time = "2024-08-06T20:32:25.131Z" }, + { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302, upload-time = "2024-08-06T20:32:26.511Z" }, + { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154, upload-time = "2024-08-06T20:32:28.363Z" }, + { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223, upload-time = "2024-08-06T20:32:30.058Z" }, + { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542, upload-time = "2024-08-06T20:32:31.881Z" }, + { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164, upload-time = "2024-08-06T20:32:37.083Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611, upload-time = "2024-08-06T20:32:38.898Z" }, + { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591, upload-time = "2024-08-06T20:32:40.241Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338, upload-time = "2024-08-06T20:32:41.93Z" }, + { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" }, + { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" }, + { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" }, + { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload-time = "2024-08-06T20:32:51.188Z" }, + { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload-time = "2024-08-06T20:32:53.019Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload-time = "2024-08-06T20:32:54.708Z" }, + { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" }, + { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" }, + { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" }, + { url = "https://files.pythonhosted.org/packages/65/d8/b7a1db13636d7fb7d4ff431593c510c8b8fca920ade06ca8ef20015493c5/PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d", size = 184777, upload-time = "2024-08-06T20:33:25.896Z" }, + { url = "https://files.pythonhosted.org/packages/0a/02/6ec546cd45143fdf9840b2c6be8d875116a64076218b61d68e12548e5839/PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f", size = 172318, upload-time = "2024-08-06T20:33:27.212Z" }, + { url = "https://files.pythonhosted.org/packages/0e/9a/8cc68be846c972bda34f6c2a93abb644fb2476f4dcc924d52175786932c9/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290", size = 720891, upload-time = "2024-08-06T20:33:28.974Z" }, + { url = "https://files.pythonhosted.org/packages/e9/6c/6e1b7f40181bc4805e2e07f4abc10a88ce4648e7e95ff1abe4ae4014a9b2/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12", size = 722614, upload-time = "2024-08-06T20:33:34.157Z" }, + { url = "https://files.pythonhosted.org/packages/3d/32/e7bd8535d22ea2874cef6a81021ba019474ace0d13a4819c2a4bce79bd6a/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19", size = 737360, upload-time = "2024-08-06T20:33:35.84Z" }, + { url = "https://files.pythonhosted.org/packages/d7/12/7322c1e30b9be969670b672573d45479edef72c9a0deac3bb2868f5d7469/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e", size = 699006, upload-time = "2024-08-06T20:33:37.501Z" }, + { url = "https://files.pythonhosted.org/packages/82/72/04fcad41ca56491995076630c3ec1e834be241664c0c09a64c9a2589b507/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725", size = 723577, upload-time = "2024-08-06T20:33:39.389Z" }, + { url = "https://files.pythonhosted.org/packages/ed/5e/46168b1f2757f1fcd442bc3029cd8767d88a98c9c05770d8b420948743bb/PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631", size = 144593, upload-time = "2024-08-06T20:33:46.63Z" }, + { url = "https://files.pythonhosted.org/packages/19/87/5124b1c1f2412bb95c59ec481eaf936cd32f0fe2a7b16b97b81c4c017a6a/PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8", size = 162312, upload-time = "2024-08-06T20:33:49.073Z" }, ] [[package]] @@ -3658,6 +5042,141 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e8/02/89e2ed7e85db6c93dfa9e8f691c5087df4e3551ab39081a4d7c6d1f90e05/redis-6.4.0-py3-none-any.whl", hash = "sha256:f0544fa9604264e9464cdf4814e7d4830f74b165d52f2a330a760a88dd248b7f", size = 279847, upload-time = "2025-08-07T08:10:09.84Z" }, ] +[[package]] +name = "referencing" +version = "0.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs", marker = "python_full_version >= '3.10'" }, + { name = "rpds-py", marker = "python_full_version >= '3.10'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.10' and python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, +] + +[[package]] +name = "regex" +version = "2026.2.28" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8b/71/41455aa99a5a5ac1eaf311f5d8efd9ce6433c03ac1e0962de163350d0d97/regex-2026.2.28.tar.gz", hash = "sha256:a729e47d418ea11d03469f321aaf67cdee8954cde3ff2cf8403ab87951ad10f2", size = 415184, upload-time = "2026-02-28T02:19:42.792Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/b8/845a927e078f5e5cc55d29f57becbfde0003d52806544531ab3f2da4503c/regex-2026.2.28-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fc48c500838be6882b32748f60a15229d2dea96e59ef341eaa96ec83538f498d", size = 488461, upload-time = "2026-02-28T02:15:48.405Z" }, + { url = "https://files.pythonhosted.org/packages/32/f9/8a0034716684e38a729210ded6222249f29978b24b684f448162ef21f204/regex-2026.2.28-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2afa673660928d0b63d84353c6c08a8a476ddfc4a47e11742949d182e6863ce8", size = 290774, upload-time = "2026-02-28T02:15:51.738Z" }, + { url = "https://files.pythonhosted.org/packages/a6/ba/b27feefffbb199528dd32667cd172ed484d9c197618c575f01217fbe6103/regex-2026.2.28-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7ab218076eb0944549e7fe74cf0e2b83a82edb27e81cc87411f76240865e04d5", size = 288737, upload-time = "2026-02-28T02:15:53.534Z" }, + { url = "https://files.pythonhosted.org/packages/18/c5/65379448ca3cbfe774fcc33774dc8295b1ee97dc3237ae3d3c7b27423c9d/regex-2026.2.28-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94d63db12e45a9b9f064bfe4800cefefc7e5f182052e4c1b774d46a40ab1d9bb", size = 782675, upload-time = "2026-02-28T02:15:55.488Z" }, + { url = "https://files.pythonhosted.org/packages/aa/30/6fa55bef48090f900fbd4649333791fc3e6467380b9e775e741beeb3231f/regex-2026.2.28-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:195237dc327858a7721bf8b0bbbef797554bc13563c3591e91cd0767bacbe359", size = 850514, upload-time = "2026-02-28T02:15:57.509Z" }, + { url = "https://files.pythonhosted.org/packages/a9/28/9ca180fb3787a54150209754ac06a42409913571fa94994f340b3bba4e1e/regex-2026.2.28-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b387a0d092dac157fb026d737dde35ff3e49ef27f285343e7c6401851239df27", size = 896612, upload-time = "2026-02-28T02:15:59.682Z" }, + { url = "https://files.pythonhosted.org/packages/46/b5/f30d7d3936d6deecc3ea7bea4f7d3c5ee5124e7c8de372226e436b330a55/regex-2026.2.28-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3935174fa4d9f70525a4367aaff3cb8bc0548129d114260c29d9dfa4a5b41692", size = 791691, upload-time = "2026-02-28T02:16:01.752Z" }, + { url = "https://files.pythonhosted.org/packages/f5/34/96631bcf446a56ba0b2a7f684358a76855dfe315b7c2f89b35388494ede0/regex-2026.2.28-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2b2b23587b26496ff5fd40df4278becdf386813ec00dc3533fa43a4cf0e2ad3c", size = 783111, upload-time = "2026-02-28T02:16:03.651Z" }, + { url = "https://files.pythonhosted.org/packages/39/54/f95cb7a85fe284d41cd2f3625e0f2ae30172b55dfd2af1d9b4eaef6259d7/regex-2026.2.28-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3b24bd7e9d85dc7c6a8bd2aa14ecd234274a0248335a02adeb25448aecdd420d", size = 767512, upload-time = "2026-02-28T02:16:05.616Z" }, + { url = "https://files.pythonhosted.org/packages/3d/af/a650f64a79c02a97f73f64d4e7fc4cc1984e64affab14075e7c1f9a2db34/regex-2026.2.28-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:bd477d5f79920338107f04aa645f094032d9e3030cc55be581df3d1ef61aa318", size = 773920, upload-time = "2026-02-28T02:16:08.325Z" }, + { url = "https://files.pythonhosted.org/packages/72/f8/3f9c2c2af37aedb3f5a1e7227f81bea065028785260d9cacc488e43e6997/regex-2026.2.28-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:b49eb78048c6354f49e91e4b77da21257fecb92256b6d599ae44403cab30b05b", size = 846681, upload-time = "2026-02-28T02:16:10.381Z" }, + { url = "https://files.pythonhosted.org/packages/54/12/8db04a334571359f4d127d8f89550917ec6561a2fddfd69cd91402b47482/regex-2026.2.28-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:a25c7701e4f7a70021db9aaf4a4a0a67033c6318752146e03d1b94d32006217e", size = 755565, upload-time = "2026-02-28T02:16:11.972Z" }, + { url = "https://files.pythonhosted.org/packages/da/bc/91c22f384d79324121b134c267a86ca90d11f8016aafb1dc5bee05890ee3/regex-2026.2.28-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:9dd450db6458387167e033cfa80887a34c99c81d26da1bf8b0b41bf8c9cac88e", size = 835789, upload-time = "2026-02-28T02:16:14.036Z" }, + { url = "https://files.pythonhosted.org/packages/46/a7/4cc94fd3af01dcfdf5a9ed75c8e15fd80fcd62cc46da7592b1749e9c35db/regex-2026.2.28-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2954379dd20752e82d22accf3ff465311cbb2bac6c1f92c4afd400e1757f7451", size = 780094, upload-time = "2026-02-28T02:16:15.468Z" }, + { url = "https://files.pythonhosted.org/packages/3c/21/e5a38f420af3c77cab4a65f0c3a55ec02ac9babf04479cfd282d356988a6/regex-2026.2.28-cp310-cp310-win32.whl", hash = "sha256:1f8b17be5c27a684ea6759983c13506bd77bfc7c0347dff41b18ce5ddd2ee09a", size = 266025, upload-time = "2026-02-28T02:16:16.828Z" }, + { url = "https://files.pythonhosted.org/packages/4d/0a/205c4c1466a36e04d90afcd01d8908bac327673050c7fe316b2416d99d3d/regex-2026.2.28-cp310-cp310-win_amd64.whl", hash = "sha256:dd8847c4978bc3c7e6c826fb745f5570e518b8459ac2892151ce6627c7bc00d5", size = 277965, upload-time = "2026-02-28T02:16:18.752Z" }, + { url = "https://files.pythonhosted.org/packages/c3/4d/29b58172f954b6ec2c5ed28529a65e9026ab96b4b7016bcd3858f1c31d3c/regex-2026.2.28-cp310-cp310-win_arm64.whl", hash = "sha256:73cdcdbba8028167ea81490c7f45280113e41db2c7afb65a276f4711fa3bcbff", size = 270336, upload-time = "2026-02-28T02:16:20.735Z" }, + { url = "https://files.pythonhosted.org/packages/04/db/8cbfd0ba3f302f2d09dd0019a9fcab74b63fee77a76c937d0e33161fb8c1/regex-2026.2.28-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e621fb7c8dc147419b28e1702f58a0177ff8308a76fa295c71f3e7827849f5d9", size = 488462, upload-time = "2026-02-28T02:16:22.616Z" }, + { url = "https://files.pythonhosted.org/packages/5d/10/ccc22c52802223f2368731964ddd117799e1390ffc39dbb31634a83022ee/regex-2026.2.28-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0d5bef2031cbf38757a0b0bc4298bb4824b6332d28edc16b39247228fbdbad97", size = 290774, upload-time = "2026-02-28T02:16:23.993Z" }, + { url = "https://files.pythonhosted.org/packages/62/b9/6796b3bf3101e64117201aaa3a5a030ec677ecf34b3cd6141b5d5c6c67d5/regex-2026.2.28-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bcb399ed84eabf4282587ba151f2732ad8168e66f1d3f85b1d038868fe547703", size = 288724, upload-time = "2026-02-28T02:16:25.403Z" }, + { url = "https://files.pythonhosted.org/packages/9c/02/291c0ae3f3a10cea941d0f5366da1843d8d1fa8a25b0671e20a0e454bb38/regex-2026.2.28-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7c1b34dfa72f826f535b20712afa9bb3ba580020e834f3c69866c5bddbf10098", size = 791924, upload-time = "2026-02-28T02:16:26.863Z" }, + { url = "https://files.pythonhosted.org/packages/0f/57/f0235cc520d9672742196c5c15098f8f703f2758d48d5a7465a56333e496/regex-2026.2.28-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:851fa70df44325e1e4cdb79c5e676e91a78147b1b543db2aec8734d2add30ec2", size = 860095, upload-time = "2026-02-28T02:16:28.772Z" }, + { url = "https://files.pythonhosted.org/packages/b3/7c/393c94cbedda79a0f5f2435ebd01644aba0b338d327eb24b4aa5b8d6c07f/regex-2026.2.28-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:516604edd17b1c2c3e579cf4e9b25a53bf8fa6e7cedddf1127804d3e0140ca64", size = 906583, upload-time = "2026-02-28T02:16:30.977Z" }, + { url = "https://files.pythonhosted.org/packages/2c/73/a72820f47ca5abf2b5d911d0407ba5178fc52cf9780191ed3a54f5f419a2/regex-2026.2.28-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e7ce83654d1ab701cb619285a18a8e5a889c1216d746ddc710c914ca5fd71022", size = 800234, upload-time = "2026-02-28T02:16:32.55Z" }, + { url = "https://files.pythonhosted.org/packages/34/b3/6e6a4b7b31fa998c4cf159a12cbeaf356386fbd1a8be743b1e80a3da51e4/regex-2026.2.28-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f2791948f7c70bb9335a9102df45e93d428f4b8128020d85920223925d73b9e1", size = 772803, upload-time = "2026-02-28T02:16:34.029Z" }, + { url = "https://files.pythonhosted.org/packages/10/e7/5da0280c765d5a92af5e1cd324b3fe8464303189cbaa449de9a71910e273/regex-2026.2.28-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:03a83cc26aa2acda6b8b9dfe748cf9e84cbd390c424a1de34fdcef58961a297a", size = 781117, upload-time = "2026-02-28T02:16:36.253Z" }, + { url = "https://files.pythonhosted.org/packages/76/39/0b8d7efb256ae34e1b8157acc1afd8758048a1cf0196e1aec2e71fd99f4b/regex-2026.2.28-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:ec6f5674c5dc836994f50f1186dd1fafde4be0666aae201ae2fcc3d29d8adf27", size = 854224, upload-time = "2026-02-28T02:16:38.119Z" }, + { url = "https://files.pythonhosted.org/packages/21/ff/a96d483ebe8fe6d1c67907729202313895d8de8495569ec319c6f29d0438/regex-2026.2.28-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:50c2fc924749543e0eacc93ada6aeeb3ea5f6715825624baa0dccaec771668ae", size = 761898, upload-time = "2026-02-28T02:16:40.333Z" }, + { url = "https://files.pythonhosted.org/packages/89/bd/d4f2e75cb4a54b484e796017e37c0d09d8a0a837de43d17e238adf163f4e/regex-2026.2.28-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ba55c50f408fb5c346a3a02d2ce0ebc839784e24f7c9684fde328ff063c3cdea", size = 844832, upload-time = "2026-02-28T02:16:41.875Z" }, + { url = "https://files.pythonhosted.org/packages/8a/a7/428a135cf5e15e4e11d1e696eb2bf968362f8ea8a5f237122e96bc2ae950/regex-2026.2.28-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:edb1b1b3a5576c56f08ac46f108c40333f222ebfd5cf63afdfa3aab0791ebe5b", size = 788347, upload-time = "2026-02-28T02:16:43.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/59/68691428851cf9c9c3707217ab1d9b47cfeec9d153a49919e6c368b9e926/regex-2026.2.28-cp311-cp311-win32.whl", hash = "sha256:948c12ef30ecedb128903c2c2678b339746eb7c689c5c21957c4a23950c96d15", size = 266033, upload-time = "2026-02-28T02:16:45.094Z" }, + { url = "https://files.pythonhosted.org/packages/42/8b/1483de1c57024e89296cbcceb9cccb3f625d416ddb46e570be185c9b05a9/regex-2026.2.28-cp311-cp311-win_amd64.whl", hash = "sha256:fd63453f10d29097cc3dc62d070746523973fb5aa1c66d25f8558bebd47fed61", size = 277978, upload-time = "2026-02-28T02:16:46.75Z" }, + { url = "https://files.pythonhosted.org/packages/a4/36/abec45dc6e7252e3dbc797120496e43bb5730a7abf0d9cb69340696a2f2d/regex-2026.2.28-cp311-cp311-win_arm64.whl", hash = "sha256:00f2b8d9615aa165fdff0a13f1a92049bfad555ee91e20d246a51aa0b556c60a", size = 270340, upload-time = "2026-02-28T02:16:48.626Z" }, + { url = "https://files.pythonhosted.org/packages/07/42/9061b03cf0fc4b5fa2c3984cbbaed54324377e440a5c5a29d29a72518d62/regex-2026.2.28-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fcf26c3c6d0da98fada8ae4ef0aa1c3405a431c0a77eb17306d38a89b02adcd7", size = 489574, upload-time = "2026-02-28T02:16:50.455Z" }, + { url = "https://files.pythonhosted.org/packages/77/83/0c8a5623a233015595e3da499c5a1c13720ac63c107897a6037bb97af248/regex-2026.2.28-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:02473c954af35dd2defeb07e44182f5705b30ea3f351a7cbffa9177beb14da5d", size = 291426, upload-time = "2026-02-28T02:16:52.52Z" }, + { url = "https://files.pythonhosted.org/packages/9e/06/3ef1ac6910dc3295ebd71b1f9bfa737e82cfead211a18b319d45f85ddd09/regex-2026.2.28-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9b65d33a17101569f86d9c5966a8b1d7fbf8afdda5a8aa219301b0a80f58cf7d", size = 289200, upload-time = "2026-02-28T02:16:54.08Z" }, + { url = "https://files.pythonhosted.org/packages/dd/c9/8cc8d850b35ab5650ff6756a1cb85286e2000b66c97520b29c1587455344/regex-2026.2.28-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e71dcecaa113eebcc96622c17692672c2d104b1d71ddf7adeda90da7ddeb26fc", size = 796765, upload-time = "2026-02-28T02:16:55.905Z" }, + { url = "https://files.pythonhosted.org/packages/e9/5d/57702597627fc23278ebf36fbb497ac91c0ce7fec89ac6c81e420ca3e38c/regex-2026.2.28-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:481df4623fa4969c8b11f3433ed7d5e3dc9cec0f008356c3212b3933fb77e3d8", size = 863093, upload-time = "2026-02-28T02:16:58.094Z" }, + { url = "https://files.pythonhosted.org/packages/02/6d/f3ecad537ca2811b4d26b54ca848cf70e04fcfc138667c146a9f3157779c/regex-2026.2.28-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:64e7c6ad614573e0640f271e811a408d79a9e1fe62a46adb602f598df42a818d", size = 909455, upload-time = "2026-02-28T02:17:00.918Z" }, + { url = "https://files.pythonhosted.org/packages/9e/40/bb226f203caa22c1043c1ca79b36340156eca0f6a6742b46c3bb222a3a57/regex-2026.2.28-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6b08a06976ff4fb0d83077022fde3eca06c55432bb997d8c0495b9a4e9872f4", size = 802037, upload-time = "2026-02-28T02:17:02.842Z" }, + { url = "https://files.pythonhosted.org/packages/44/7c/c6d91d8911ac6803b45ca968e8e500c46934e58c0903cbc6d760ee817a0a/regex-2026.2.28-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:864cdd1a2ef5716b0ab468af40139e62ede1b3a53386b375ec0786bb6783fc05", size = 775113, upload-time = "2026-02-28T02:17:04.506Z" }, + { url = "https://files.pythonhosted.org/packages/dc/8d/4a9368d168d47abd4158580b8c848709667b1cd293ff0c0c277279543bd0/regex-2026.2.28-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:511f7419f7afab475fd4d639d4aedfc54205bcb0800066753ef68a59f0f330b5", size = 784194, upload-time = "2026-02-28T02:17:06.888Z" }, + { url = "https://files.pythonhosted.org/packages/cc/bf/2c72ab5d8b7be462cb1651b5cc333da1d0068740342f350fcca3bca31947/regex-2026.2.28-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b42f7466e32bf15a961cf09f35fa6323cc72e64d3d2c990b10de1274a5da0a59", size = 856846, upload-time = "2026-02-28T02:17:09.11Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f4/6b65c979bb6d09f51bb2d2a7bc85de73c01ec73335d7ddd202dcb8cd1c8f/regex-2026.2.28-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:8710d61737b0c0ce6836b1da7109f20d495e49b3809f30e27e9560be67a257bf", size = 763516, upload-time = "2026-02-28T02:17:11.004Z" }, + { url = "https://files.pythonhosted.org/packages/8e/32/29ea5e27400ee86d2cc2b4e80aa059df04eaf78b4f0c18576ae077aeff68/regex-2026.2.28-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4390c365fd2d45278f45afd4673cb90f7285f5701607e3ad4274df08e36140ae", size = 849278, upload-time = "2026-02-28T02:17:12.693Z" }, + { url = "https://files.pythonhosted.org/packages/1d/91/3233d03b5f865111cd517e1c95ee8b43e8b428d61fa73764a80c9bb6f537/regex-2026.2.28-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cb3b1db8ff6c7b8bf838ab05583ea15230cb2f678e569ab0e3a24d1e8320940b", size = 790068, upload-time = "2026-02-28T02:17:14.9Z" }, + { url = "https://files.pythonhosted.org/packages/76/92/abc706c1fb03b4580a09645b206a3fc032f5a9f457bc1a8038ac555658ab/regex-2026.2.28-cp312-cp312-win32.whl", hash = "sha256:f8ed9a5d4612df9d4de15878f0bc6aa7a268afbe5af21a3fdd97fa19516e978c", size = 266416, upload-time = "2026-02-28T02:17:17.15Z" }, + { url = "https://files.pythonhosted.org/packages/fa/06/2a6f7dff190e5fa9df9fb4acf2fdf17a1aa0f7f54596cba8de608db56b3a/regex-2026.2.28-cp312-cp312-win_amd64.whl", hash = "sha256:01d65fd24206c8e1e97e2e31b286c59009636c022eb5d003f52760b0f42155d4", size = 277297, upload-time = "2026-02-28T02:17:18.723Z" }, + { url = "https://files.pythonhosted.org/packages/b7/f0/58a2484851fadf284458fdbd728f580d55c1abac059ae9f048c63b92f427/regex-2026.2.28-cp312-cp312-win_arm64.whl", hash = "sha256:c0b5ccbb8ffb433939d248707d4a8b31993cb76ab1a0187ca886bf50e96df952", size = 270408, upload-time = "2026-02-28T02:17:20.328Z" }, + { url = "https://files.pythonhosted.org/packages/87/f6/dc9ef48c61b79c8201585bf37fa70cd781977da86e466cd94e8e95d2443b/regex-2026.2.28-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6d63a07e5ec8ce7184452cb00c41c37b49e67dc4f73b2955b5b8e782ea970784", size = 489311, upload-time = "2026-02-28T02:17:22.591Z" }, + { url = "https://files.pythonhosted.org/packages/95/c8/c20390f2232d3f7956f420f4ef1852608ad57aa26c3dd78516cb9f3dc913/regex-2026.2.28-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e59bc8f30414d283ae8ee1617b13d8112e7135cb92830f0ec3688cb29152585a", size = 291285, upload-time = "2026-02-28T02:17:24.355Z" }, + { url = "https://files.pythonhosted.org/packages/d2/a6/ba1068a631ebd71a230e7d8013fcd284b7c89c35f46f34a7da02082141b1/regex-2026.2.28-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:de0cf053139f96219ccfabb4a8dd2d217c8c82cb206c91d9f109f3f552d6b43d", size = 289051, upload-time = "2026-02-28T02:17:26.722Z" }, + { url = "https://files.pythonhosted.org/packages/1d/1b/7cc3b7af4c244c204b7a80924bd3d85aecd9ba5bc82b485c5806ee8cda9e/regex-2026.2.28-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb4db2f17e6484904f986c5a657cec85574c76b5c5e61c7aae9ffa1bc6224f95", size = 796842, upload-time = "2026-02-28T02:17:29.064Z" }, + { url = "https://files.pythonhosted.org/packages/24/87/26bd03efc60e0d772ac1e7b60a2e6325af98d974e2358f659c507d3c76db/regex-2026.2.28-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:52b017b35ac2214d0db5f4f90e303634dc44e4aba4bd6235a27f97ecbe5b0472", size = 863083, upload-time = "2026-02-28T02:17:31.363Z" }, + { url = "https://files.pythonhosted.org/packages/ae/54/aeaf4afb1aa0a65e40de52a61dc2ac5b00a83c6cb081c8a1d0dda74f3010/regex-2026.2.28-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:69fc560ccbf08a09dc9b52ab69cacfae51e0ed80dc5693078bdc97db2f91ae96", size = 909412, upload-time = "2026-02-28T02:17:33.248Z" }, + { url = "https://files.pythonhosted.org/packages/12/2f/049901def913954e640d199bbc6a7ca2902b6aeda0e5da9d17f114100ec2/regex-2026.2.28-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e61eea47230eba62a31f3e8a0e3164d0f37ef9f40529fb2c79361bc6b53d2a92", size = 802101, upload-time = "2026-02-28T02:17:35.053Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a5/512fb9ff7f5b15ea204bb1967ebb649059446decacccb201381f9fa6aad4/regex-2026.2.28-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4f5c0b182ad4269e7381b7c27fdb0408399881f7a92a4624fd5487f2971dfc11", size = 775260, upload-time = "2026-02-28T02:17:37.692Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a8/9a92935878aba19bd72706b9db5646a6f993d99b3f6ed42c02ec8beb1d61/regex-2026.2.28-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:96f6269a2882fbb0ee76967116b83679dc628e68eaea44e90884b8d53d833881", size = 784311, upload-time = "2026-02-28T02:17:39.855Z" }, + { url = "https://files.pythonhosted.org/packages/09/d3/fc51a8a738a49a6b6499626580554c9466d3ea561f2b72cfdc72e4149773/regex-2026.2.28-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b5acd4b6a95f37c3c3828e5d053a7d4edaedb85de551db0153754924cb7c83e3", size = 856876, upload-time = "2026-02-28T02:17:42.317Z" }, + { url = "https://files.pythonhosted.org/packages/08/b7/2e641f3d084b120ca4c52e8c762a78da0b32bf03ef546330db3e2635dc5f/regex-2026.2.28-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2234059cfe33d9813a3677ef7667999caea9eeaa83fef98eb6ce15c6cf9e0215", size = 763632, upload-time = "2026-02-28T02:17:45.073Z" }, + { url = "https://files.pythonhosted.org/packages/fe/6d/0009021d97e79ee99f3d8641f0a8d001eed23479ade4c3125a5480bf3e2d/regex-2026.2.28-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:c15af43c72a7fb0c97cbc66fa36a43546eddc5c06a662b64a0cbf30d6ac40944", size = 849320, upload-time = "2026-02-28T02:17:47.192Z" }, + { url = "https://files.pythonhosted.org/packages/05/7a/51cfbad5758f8edae430cb21961a9c8d04bce1dae4d2d18d4186eec7cfa1/regex-2026.2.28-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9185cc63359862a6e80fe97f696e04b0ad9a11c4ac0a4a927f979f611bfe3768", size = 790152, upload-time = "2026-02-28T02:17:49.067Z" }, + { url = "https://files.pythonhosted.org/packages/90/3d/a83e2b6b3daa142acb8c41d51de3876186307d5cb7490087031747662500/regex-2026.2.28-cp313-cp313-win32.whl", hash = "sha256:fb66e5245db9652abd7196ace599b04d9c0e4aa7c8f0e2803938377835780081", size = 266398, upload-time = "2026-02-28T02:17:50.744Z" }, + { url = "https://files.pythonhosted.org/packages/85/4f/16e9ebb1fe5425e11b9596c8d57bf8877dcb32391da0bfd33742e3290637/regex-2026.2.28-cp313-cp313-win_amd64.whl", hash = "sha256:71a911098be38c859ceb3f9a9ce43f4ed9f4c6720ad8684a066ea246b76ad9ff", size = 277282, upload-time = "2026-02-28T02:17:53.074Z" }, + { url = "https://files.pythonhosted.org/packages/07/b4/92851335332810c5a89723bf7a7e35c7209f90b7d4160024501717b28cc9/regex-2026.2.28-cp313-cp313-win_arm64.whl", hash = "sha256:39bb5727650b9a0275c6a6690f9bb3fe693a7e6cc5c3155b1240aedf8926423e", size = 270382, upload-time = "2026-02-28T02:17:54.888Z" }, + { url = "https://files.pythonhosted.org/packages/24/07/6c7e4cec1e585959e96cbc24299d97e4437a81173217af54f1804994e911/regex-2026.2.28-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:97054c55db06ab020342cc0d35d6f62a465fa7662871190175f1ad6c655c028f", size = 492541, upload-time = "2026-02-28T02:17:56.813Z" }, + { url = "https://files.pythonhosted.org/packages/7c/13/55eb22ada7f43d4f4bb3815b6132183ebc331c81bd496e2d1f3b8d862e0d/regex-2026.2.28-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0d25a10811de831c2baa6aef3c0be91622f44dd8d31dd12e69f6398efb15e48b", size = 292984, upload-time = "2026-02-28T02:17:58.538Z" }, + { url = "https://files.pythonhosted.org/packages/5b/11/c301f8cb29ce9644a5ef85104c59244e6e7e90994a0f458da4d39baa8e17/regex-2026.2.28-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d6cfe798d8da41bb1862ed6e0cba14003d387c3c0c4a5d45591076ae9f0ce2f8", size = 291509, upload-time = "2026-02-28T02:18:00.208Z" }, + { url = "https://files.pythonhosted.org/packages/b5/43/aabe384ec1994b91796e903582427bc2ffaed9c4103819ed3c16d8e749f3/regex-2026.2.28-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fd0ce43e71d825b7c0661f9c54d4d74bd97c56c3fd102a8985bcfea48236bacb", size = 809429, upload-time = "2026-02-28T02:18:02.328Z" }, + { url = "https://files.pythonhosted.org/packages/04/b8/8d2d987a816720c4f3109cee7c06a4b24ad0e02d4fc74919ab619e543737/regex-2026.2.28-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:00945d007fd74a9084d2ab79b695b595c6b7ba3698972fadd43e23230c6979c1", size = 869422, upload-time = "2026-02-28T02:18:04.23Z" }, + { url = "https://files.pythonhosted.org/packages/fc/ad/2c004509e763c0c3719f97c03eca26473bffb3868d54c5f280b8cd4f9e3d/regex-2026.2.28-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bec23c11cbbf09a4df32fe50d57cbdd777bc442269b6e39a1775654f1c95dee2", size = 915175, upload-time = "2026-02-28T02:18:06.791Z" }, + { url = "https://files.pythonhosted.org/packages/55/c2/fd429066da487ef555a9da73bf214894aec77fc8c66a261ee355a69871a8/regex-2026.2.28-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5cdcc17d935c8f9d3f4db5c2ebe2640c332e3822ad5d23c2f8e0228e6947943a", size = 812044, upload-time = "2026-02-28T02:18:08.736Z" }, + { url = "https://files.pythonhosted.org/packages/5b/ca/feedb7055c62a3f7f659971bf45f0e0a87544b6b0cf462884761453f97c5/regex-2026.2.28-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a448af01e3d8031c89c5d902040b124a5e921a25c4e5e07a861ca591ce429341", size = 782056, upload-time = "2026-02-28T02:18:10.777Z" }, + { url = "https://files.pythonhosted.org/packages/95/30/1aa959ed0d25c1dd7dd5047ea8ba482ceaef38ce363c401fd32a6b923e60/regex-2026.2.28-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:10d28e19bd4888e4abf43bd3925f3c134c52fdf7259219003588a42e24c2aa25", size = 798743, upload-time = "2026-02-28T02:18:13.025Z" }, + { url = "https://files.pythonhosted.org/packages/3b/1f/dadb9cf359004784051c897dcf4d5d79895f73a1bbb7b827abaa4814ae80/regex-2026.2.28-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:99985a2c277dcb9ccb63f937451af5d65177af1efdeb8173ac55b61095a0a05c", size = 864633, upload-time = "2026-02-28T02:18:16.84Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f1/b9a25eb24e1cf79890f09e6ec971ee5b511519f1851de3453bc04f6c902b/regex-2026.2.28-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:e1e7b24cb3ae9953a560c563045d1ba56ee4749fbd05cf21ba571069bd7be81b", size = 770862, upload-time = "2026-02-28T02:18:18.892Z" }, + { url = "https://files.pythonhosted.org/packages/02/9a/c5cb10b7aa6f182f9247a30cc9527e326601f46f4df864ac6db588d11fcd/regex-2026.2.28-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:d8511a01d0e4ee1992eb3ba19e09bc1866fe03f05129c3aec3fdc4cbc77aad3f", size = 854788, upload-time = "2026-02-28T02:18:21.475Z" }, + { url = "https://files.pythonhosted.org/packages/0a/50/414ba0731c4bd40b011fa4703b2cc86879ec060c64f2a906e65a56452589/regex-2026.2.28-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:aaffaecffcd2479ce87aa1e74076c221700b7c804e48e98e62500ee748f0f550", size = 800184, upload-time = "2026-02-28T02:18:23.492Z" }, + { url = "https://files.pythonhosted.org/packages/69/50/0c7290987f97e7e6830b0d853f69dc4dc5852c934aae63e7fdcd76b4c383/regex-2026.2.28-cp313-cp313t-win32.whl", hash = "sha256:ef77bdde9c9eba3f7fa5b58084b29bbcc74bcf55fdbeaa67c102a35b5bd7e7cc", size = 269137, upload-time = "2026-02-28T02:18:25.375Z" }, + { url = "https://files.pythonhosted.org/packages/68/80/ef26ff90e74ceb4051ad6efcbbb8a4be965184a57e879ebcbdef327d18fa/regex-2026.2.28-cp313-cp313t-win_amd64.whl", hash = "sha256:98adf340100cbe6fbaf8e6dc75e28f2c191b1be50ffefe292fb0e6f6eefdb0d8", size = 280682, upload-time = "2026-02-28T02:18:27.205Z" }, + { url = "https://files.pythonhosted.org/packages/69/8b/fbad9c52e83ffe8f97e3ed1aa0516e6dff6bb633a41da9e64645bc7efdc5/regex-2026.2.28-cp313-cp313t-win_arm64.whl", hash = "sha256:2fb950ac1d88e6b6a9414381f403797b236f9fa17e1eee07683af72b1634207b", size = 271735, upload-time = "2026-02-28T02:18:29.015Z" }, + { url = "https://files.pythonhosted.org/packages/cf/03/691015f7a7cb1ed6dacb2ea5de5682e4858e05a4c5506b2839cd533bbcd6/regex-2026.2.28-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:78454178c7df31372ea737996fb7f36b3c2c92cccc641d251e072478afb4babc", size = 489497, upload-time = "2026-02-28T02:18:30.889Z" }, + { url = "https://files.pythonhosted.org/packages/c6/ba/8db8fd19afcbfa0e1036eaa70c05f20ca8405817d4ad7a38a6b4c2f031ac/regex-2026.2.28-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:5d10303dd18cedfd4d095543998404df656088240bcfd3cd20a8f95b861f74bd", size = 291295, upload-time = "2026-02-28T02:18:33.426Z" }, + { url = "https://files.pythonhosted.org/packages/5a/79/9aa0caf089e8defef9b857b52fc53801f62ff868e19e5c83d4a96612eba1/regex-2026.2.28-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:19a9c9e0a8f24f39d575a6a854d516b48ffe4cbdcb9de55cb0570a032556ecff", size = 289275, upload-time = "2026-02-28T02:18:35.247Z" }, + { url = "https://files.pythonhosted.org/packages/eb/26/ee53117066a30ef9c883bf1127eece08308ccf8ccd45c45a966e7a665385/regex-2026.2.28-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09500be324f49b470d907b3ef8af9afe857f5cca486f853853f7945ddbf75911", size = 797176, upload-time = "2026-02-28T02:18:37.15Z" }, + { url = "https://files.pythonhosted.org/packages/05/1b/67fb0495a97259925f343ae78b5d24d4a6624356ae138b57f18bd43006e4/regex-2026.2.28-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fb1c4ff62277d87a7335f2c1ea4e0387b8f2b3ad88a64efd9943906aafad4f33", size = 863813, upload-time = "2026-02-28T02:18:39.478Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/93ac9bbafc53618091c685c7ed40239a90bf9f2a82c983f0baa97cb7ae07/regex-2026.2.28-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b8b3f1be1738feadc69f62daa250c933e85c6f34fa378f54a7ff43807c1b9117", size = 908678, upload-time = "2026-02-28T02:18:41.619Z" }, + { url = "https://files.pythonhosted.org/packages/c7/7a/a8f5e0561702b25239846a16349feece59712ae20598ebb205580332a471/regex-2026.2.28-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc8ed8c3f41c27acb83f7b6a9eb727a73fc6663441890c5cb3426a5f6a91ce7d", size = 801528, upload-time = "2026-02-28T02:18:43.624Z" }, + { url = "https://files.pythonhosted.org/packages/96/5d/ed6d4cbde80309854b1b9f42d9062fee38ade15f7eb4909f6ef2440403b5/regex-2026.2.28-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa539be029844c0ce1114762d2952ab6cfdd7c7c9bd72e0db26b94c3c36dcc5a", size = 775373, upload-time = "2026-02-28T02:18:46.102Z" }, + { url = "https://files.pythonhosted.org/packages/6a/e9/6e53c34e8068b9deec3e87210086ecb5b9efebdefca6b0d3fa43d66dcecb/regex-2026.2.28-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7900157786428a79615a8264dac1f12c9b02957c473c8110c6b1f972dcecaddf", size = 784859, upload-time = "2026-02-28T02:18:48.269Z" }, + { url = "https://files.pythonhosted.org/packages/48/3c/736e1c7ca7f0dcd2ae33819888fdc69058a349b7e5e84bc3e2f296bbf794/regex-2026.2.28-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:0b1d2b07614d95fa2bf8a63fd1e98bd8fa2b4848dc91b1efbc8ba219fdd73952", size = 857813, upload-time = "2026-02-28T02:18:50.576Z" }, + { url = "https://files.pythonhosted.org/packages/6e/7c/48c4659ad9da61f58e79dbe8c05223e0006696b603c16eb6b5cbfbb52c27/regex-2026.2.28-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:b389c61aa28a79c2e0527ac36da579869c2e235a5b208a12c5b5318cda2501d8", size = 763705, upload-time = "2026-02-28T02:18:52.59Z" }, + { url = "https://files.pythonhosted.org/packages/cf/a1/bc1c261789283128165f71b71b4b221dd1b79c77023752a6074c102f18d8/regex-2026.2.28-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f467cb602f03fbd1ab1908f68b53c649ce393fde056628dc8c7e634dab6bfc07", size = 848734, upload-time = "2026-02-28T02:18:54.595Z" }, + { url = "https://files.pythonhosted.org/packages/10/d8/979407faf1397036e25a5ae778157366a911c0f382c62501009f4957cf86/regex-2026.2.28-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e8c8cb2deba42f5ec1ede46374e990f8adc5e6456a57ac1a261b19be6f28e4e6", size = 789871, upload-time = "2026-02-28T02:18:57.34Z" }, + { url = "https://files.pythonhosted.org/packages/03/23/da716821277115fcb1f4e3de1e5dc5023a1e6533598c486abf5448612579/regex-2026.2.28-cp314-cp314-win32.whl", hash = "sha256:9036b400b20e4858d56d117108d7813ed07bb7803e3eed766675862131135ca6", size = 271825, upload-time = "2026-02-28T02:18:59.202Z" }, + { url = "https://files.pythonhosted.org/packages/91/ff/90696f535d978d5f16a52a419be2770a8d8a0e7e0cfecdbfc31313df7fab/regex-2026.2.28-cp314-cp314-win_amd64.whl", hash = "sha256:1d367257cd86c1cbb97ea94e77b373a0bbc2224976e247f173d19e8f18b4afa7", size = 280548, upload-time = "2026-02-28T02:19:01.049Z" }, + { url = "https://files.pythonhosted.org/packages/69/f9/5e1b5652fc0af3fcdf7677e7df3ad2a0d47d669b34ac29a63bb177bb731b/regex-2026.2.28-cp314-cp314-win_arm64.whl", hash = "sha256:5e68192bb3a1d6fb2836da24aa494e413ea65853a21505e142e5b1064a595f3d", size = 273444, upload-time = "2026-02-28T02:19:03.255Z" }, + { url = "https://files.pythonhosted.org/packages/d3/eb/8389f9e940ac89bcf58d185e230a677b4fd07c5f9b917603ad5c0f8fa8fe/regex-2026.2.28-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:a5dac14d0872eeb35260a8e30bac07ddf22adc1e3a0635b52b02e180d17c9c7e", size = 492546, upload-time = "2026-02-28T02:19:05.378Z" }, + { url = "https://files.pythonhosted.org/packages/7b/c7/09441d27ce2a6fa6a61ea3150ea4639c1dcda9b31b2ea07b80d6937b24dd/regex-2026.2.28-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:ec0c608b7a7465ffadb344ed7c987ff2f11ee03f6a130b569aa74d8a70e8333c", size = 292986, upload-time = "2026-02-28T02:19:07.24Z" }, + { url = "https://files.pythonhosted.org/packages/fb/69/4144b60ed7760a6bd235e4087041f487aa4aa62b45618ce018b0c14833ea/regex-2026.2.28-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c7815afb0ca45456613fdaf60ea9c993715511c8d53a83bc468305cbc0ee23c7", size = 291518, upload-time = "2026-02-28T02:19:09.698Z" }, + { url = "https://files.pythonhosted.org/packages/2d/be/77e5426cf5948c82f98c53582009ca9e94938c71f73a8918474f2e2990bb/regex-2026.2.28-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b059e71ec363968671693a78c5053bd9cb2fe410f9b8e4657e88377ebd603a2e", size = 809464, upload-time = "2026-02-28T02:19:12.494Z" }, + { url = "https://files.pythonhosted.org/packages/45/99/2c8c5ac90dc7d05c6e7d8e72c6a3599dc08cd577ac476898e91ca787d7f1/regex-2026.2.28-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b8cf76f1a29f0e99dcfd7aef1551a9827588aae5a737fe31442021165f1920dc", size = 869553, upload-time = "2026-02-28T02:19:15.151Z" }, + { url = "https://files.pythonhosted.org/packages/53/34/daa66a342f0271e7737003abf6c3097aa0498d58c668dbd88362ef94eb5d/regex-2026.2.28-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:180e08a435a0319e6a4821c3468da18dc7001987e1c17ae1335488dfe7518dd8", size = 915289, upload-time = "2026-02-28T02:19:17.331Z" }, + { url = "https://files.pythonhosted.org/packages/c5/c7/e22c2aaf0a12e7e22ab19b004bb78d32ca1ecc7ef245949935463c5567de/regex-2026.2.28-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1e496956106fd59ba6322a8ea17141a27c5040e5ee8f9433ae92d4e5204462a0", size = 812156, upload-time = "2026-02-28T02:19:20.011Z" }, + { url = "https://files.pythonhosted.org/packages/7f/bb/2dc18c1efd9051cf389cd0d7a3a4d90f6804b9fff3a51b5dc3c85b935f71/regex-2026.2.28-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bba2b18d70eeb7b79950f12f633beeecd923f7c9ad6f6bae28e59b4cb3ab046b", size = 782215, upload-time = "2026-02-28T02:19:22.047Z" }, + { url = "https://files.pythonhosted.org/packages/17/1e/9e4ec9b9013931faa32226ec4aa3c71fe664a6d8a2b91ac56442128b332f/regex-2026.2.28-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6db7bfae0f8a2793ff1f7021468ea55e2699d0790eb58ee6ab36ae43aa00bc5b", size = 798925, upload-time = "2026-02-28T02:19:24.173Z" }, + { url = "https://files.pythonhosted.org/packages/71/57/a505927e449a9ccb41e2cc8d735e2abe3444b0213d1cf9cb364a8c1f2524/regex-2026.2.28-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:d0b02e8b7e5874b48ae0f077ecca61c1a6a9f9895e9c6dfb191b55b242862033", size = 864701, upload-time = "2026-02-28T02:19:26.376Z" }, + { url = "https://files.pythonhosted.org/packages/a6/ad/c62cb60cdd93e13eac5b3d9d6bd5d284225ed0e3329426f94d2552dd7cca/regex-2026.2.28-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:25b6eb660c5cf4b8c3407a1ed462abba26a926cc9965e164268a3267bcc06a43", size = 770899, upload-time = "2026-02-28T02:19:29.38Z" }, + { url = "https://files.pythonhosted.org/packages/3c/5a/874f861f5c3d5ab99633e8030dee1bc113db8e0be299d1f4b07f5b5ec349/regex-2026.2.28-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:5a932ea8ad5d0430351ff9c76c8db34db0d9f53c1d78f06022a21f4e290c5c18", size = 854727, upload-time = "2026-02-28T02:19:31.494Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ca/d2c03b0efde47e13db895b975b2be6a73ed90b8ba963677927283d43bf74/regex-2026.2.28-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1c2c95e1a2b0f89d01e821ff4de1be4b5d73d1f4b0bf679fa27c1ad8d2327f1a", size = 800366, upload-time = "2026-02-28T02:19:34.248Z" }, + { url = "https://files.pythonhosted.org/packages/14/bd/ee13b20b763b8989f7c75d592bfd5de37dc1181814a2a2747fedcf97e3ba/regex-2026.2.28-cp314-cp314t-win32.whl", hash = "sha256:bbb882061f742eb5d46f2f1bd5304055be0a66b783576de3d7eef1bed4778a6e", size = 274936, upload-time = "2026-02-28T02:19:36.313Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e7/d8020e39414c93af7f0d8688eabcecece44abfd5ce314b21dfda0eebd3d8/regex-2026.2.28-cp314-cp314t-win_amd64.whl", hash = "sha256:6591f281cb44dc13de9585b552cec6fc6cf47fb2fe7a48892295ee9bc4a612f9", size = 284779, upload-time = "2026-02-28T02:19:38.625Z" }, + { url = "https://files.pythonhosted.org/packages/13/c0/ad225f4a405827486f1955283407cf758b6d2fb966712644c5f5aef33d1b/regex-2026.2.28-cp314-cp314t-win_arm64.whl", hash = "sha256:dee50f1be42222f89767b64b283283ef963189da0dda4a515aa54a5563c62dec", size = 275010, upload-time = "2026-02-28T02:19:40.65Z" }, +] + [[package]] name = "requests" version = "2.32.5" @@ -3700,6 +5219,141 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ef/45/615f5babd880b4bd7d405cc0dc348234c5ffb6ed1ea33e152ede08b2072d/rich-14.3.2-py3-none-any.whl", hash = "sha256:08e67c3e90884651da3239ea668222d19bea7b589149d8014a21c633420dbb69", size = 309963, upload-time = "2026-02-01T16:20:46.078Z" }, ] +[[package]] +name = "rich-rst" +version = "1.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils", marker = "python_full_version >= '3.10'" }, + { name = "rich", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bc/6d/a506aaa4a9eaa945ed8ab2b7347859f53593864289853c5d6d62b77246e0/rich_rst-1.3.2.tar.gz", hash = "sha256:a1196fdddf1e364b02ec68a05e8ff8f6914fee10fbca2e6b6735f166bb0da8d4", size = 14936, upload-time = "2025-10-14T16:49:45.332Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/2f/b4530fbf948867702d0a3f27de4a6aab1d156f406d72852ab902c4d04de9/rich_rst-1.3.2-py3-none-any.whl", hash = "sha256:a99b4907cbe118cf9d18b0b44de272efa61f15117c61e39ebdc431baf5df722a", size = 12567, upload-time = "2025-10-14T16:49:42.953Z" }, +] + +[[package]] +name = "rpds-py" +version = "0.30.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/0c/0c411a0ec64ccb6d104dcabe0e713e05e153a9a2c3c2bd2b32ce412166fe/rpds_py-0.30.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:679ae98e00c0e8d68a7fda324e16b90fd5260945b45d3b824c892cec9eea3288", size = 370490, upload-time = "2025-11-30T20:21:33.256Z" }, + { url = "https://files.pythonhosted.org/packages/19/6a/4ba3d0fb7297ebae71171822554abe48d7cab29c28b8f9f2c04b79988c05/rpds_py-0.30.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4cc2206b76b4f576934f0ed374b10d7ca5f457858b157ca52064bdfc26b9fc00", size = 359751, upload-time = "2025-11-30T20:21:34.591Z" }, + { url = "https://files.pythonhosted.org/packages/cd/7c/e4933565ef7f7a0818985d87c15d9d273f1a649afa6a52ea35ad011195ea/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:389a2d49eded1896c3d48b0136ead37c48e221b391c052fba3f4055c367f60a6", size = 389696, upload-time = "2025-11-30T20:21:36.122Z" }, + { url = "https://files.pythonhosted.org/packages/5e/01/6271a2511ad0815f00f7ed4390cf2567bec1d4b1da39e2c27a41e6e3b4de/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:32c8528634e1bf7121f3de08fa85b138f4e0dc47657866630611b03967f041d7", size = 403136, upload-time = "2025-11-30T20:21:37.728Z" }, + { url = "https://files.pythonhosted.org/packages/55/64/c857eb7cd7541e9b4eee9d49c196e833128a55b89a9850a9c9ac33ccf897/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f207f69853edd6f6700b86efb84999651baf3789e78a466431df1331608e5324", size = 524699, upload-time = "2025-11-30T20:21:38.92Z" }, + { url = "https://files.pythonhosted.org/packages/9c/ed/94816543404078af9ab26159c44f9e98e20fe47e2126d5d32c9d9948d10a/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:67b02ec25ba7a9e8fa74c63b6ca44cf5707f2fbfadae3ee8e7494297d56aa9df", size = 412022, upload-time = "2025-11-30T20:21:40.407Z" }, + { url = "https://files.pythonhosted.org/packages/61/b5/707f6cf0066a6412aacc11d17920ea2e19e5b2f04081c64526eb35b5c6e7/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0e95f6819a19965ff420f65578bacb0b00f251fefe2c8b23347c37174271f3", size = 390522, upload-time = "2025-11-30T20:21:42.17Z" }, + { url = "https://files.pythonhosted.org/packages/13/4e/57a85fda37a229ff4226f8cbcf09f2a455d1ed20e802ce5b2b4a7f5ed053/rpds_py-0.30.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:a452763cc5198f2f98898eb98f7569649fe5da666c2dc6b5ddb10fde5a574221", size = 404579, upload-time = "2025-11-30T20:21:43.769Z" }, + { url = "https://files.pythonhosted.org/packages/f9/da/c9339293513ec680a721e0e16bf2bac3db6e5d7e922488de471308349bba/rpds_py-0.30.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e0b65193a413ccc930671c55153a03ee57cecb49e6227204b04fae512eb657a7", size = 421305, upload-time = "2025-11-30T20:21:44.994Z" }, + { url = "https://files.pythonhosted.org/packages/f9/be/522cb84751114f4ad9d822ff5a1aa3c98006341895d5f084779b99596e5c/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:858738e9c32147f78b3ac24dc0edb6610000e56dc0f700fd5f651d0a0f0eb9ff", size = 572503, upload-time = "2025-11-30T20:21:46.91Z" }, + { url = "https://files.pythonhosted.org/packages/a2/9b/de879f7e7ceddc973ea6e4629e9b380213a6938a249e94b0cdbcc325bb66/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:da279aa314f00acbb803da1e76fa18666778e8a8f83484fba94526da5de2cba7", size = 598322, upload-time = "2025-11-30T20:21:48.709Z" }, + { url = "https://files.pythonhosted.org/packages/48/ac/f01fc22efec3f37d8a914fc1b2fb9bcafd56a299edbe96406f3053edea5a/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7c64d38fb49b6cdeda16ab49e35fe0da2e1e9b34bc38bd78386530f218b37139", size = 560792, upload-time = "2025-11-30T20:21:50.024Z" }, + { url = "https://files.pythonhosted.org/packages/e2/da/4e2b19d0f131f35b6146425f846563d0ce036763e38913d917187307a671/rpds_py-0.30.0-cp310-cp310-win32.whl", hash = "sha256:6de2a32a1665b93233cde140ff8b3467bdb9e2af2b91079f0333a0974d12d464", size = 221901, upload-time = "2025-11-30T20:21:51.32Z" }, + { url = "https://files.pythonhosted.org/packages/96/cb/156d7a5cf4f78a7cc571465d8aec7a3c447c94f6749c5123f08438bcf7bc/rpds_py-0.30.0-cp310-cp310-win_amd64.whl", hash = "sha256:1726859cd0de969f88dc8673bdd954185b9104e05806be64bcd87badbe313169", size = 235823, upload-time = "2025-11-30T20:21:52.505Z" }, + { url = "https://files.pythonhosted.org/packages/4d/6e/f964e88b3d2abee2a82c1ac8366da848fce1c6d834dc2132c3fda3970290/rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425", size = 370157, upload-time = "2025-11-30T20:21:53.789Z" }, + { url = "https://files.pythonhosted.org/packages/94/ba/24e5ebb7c1c82e74c4e4f33b2112a5573ddc703915b13a073737b59b86e0/rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d", size = 359676, upload-time = "2025-11-30T20:21:55.475Z" }, + { url = "https://files.pythonhosted.org/packages/84/86/04dbba1b087227747d64d80c3b74df946b986c57af0a9f0c98726d4d7a3b/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4", size = 389938, upload-time = "2025-11-30T20:21:57.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/bb/1463f0b1722b7f45431bdd468301991d1328b16cffe0b1c2918eba2c4eee/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f", size = 402932, upload-time = "2025-11-30T20:21:58.47Z" }, + { url = "https://files.pythonhosted.org/packages/99/ee/2520700a5c1f2d76631f948b0736cdf9b0acb25abd0ca8e889b5c62ac2e3/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4", size = 525830, upload-time = "2025-11-30T20:21:59.699Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ad/bd0331f740f5705cc555a5e17fdf334671262160270962e69a2bdef3bf76/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97", size = 412033, upload-time = "2025-11-30T20:22:00.991Z" }, + { url = "https://files.pythonhosted.org/packages/f8/1e/372195d326549bb51f0ba0f2ecb9874579906b97e08880e7a65c3bef1a99/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89", size = 390828, upload-time = "2025-11-30T20:22:02.723Z" }, + { url = "https://files.pythonhosted.org/packages/ab/2b/d88bb33294e3e0c76bc8f351a3721212713629ffca1700fa94979cb3eae8/rpds_py-0.30.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d", size = 404683, upload-time = "2025-11-30T20:22:04.367Z" }, + { url = "https://files.pythonhosted.org/packages/50/32/c759a8d42bcb5289c1fac697cd92f6fe01a018dd937e62ae77e0e7f15702/rpds_py-0.30.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038", size = 421583, upload-time = "2025-11-30T20:22:05.814Z" }, + { url = "https://files.pythonhosted.org/packages/2b/81/e729761dbd55ddf5d84ec4ff1f47857f4374b0f19bdabfcf929164da3e24/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7", size = 572496, upload-time = "2025-11-30T20:22:07.713Z" }, + { url = "https://files.pythonhosted.org/packages/14/f6/69066a924c3557c9c30baa6ec3a0aa07526305684c6f86c696b08860726c/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed", size = 598669, upload-time = "2025-11-30T20:22:09.312Z" }, + { url = "https://files.pythonhosted.org/packages/5f/48/905896b1eb8a05630d20333d1d8ffd162394127b74ce0b0784ae04498d32/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85", size = 561011, upload-time = "2025-11-30T20:22:11.309Z" }, + { url = "https://files.pythonhosted.org/packages/22/16/cd3027c7e279d22e5eb431dd3c0fbc677bed58797fe7581e148f3f68818b/rpds_py-0.30.0-cp311-cp311-win32.whl", hash = "sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c", size = 221406, upload-time = "2025-11-30T20:22:13.101Z" }, + { url = "https://files.pythonhosted.org/packages/fa/5b/e7b7aa136f28462b344e652ee010d4de26ee9fd16f1bfd5811f5153ccf89/rpds_py-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825", size = 236024, upload-time = "2025-11-30T20:22:14.853Z" }, + { url = "https://files.pythonhosted.org/packages/14/a6/364bba985e4c13658edb156640608f2c9e1d3ea3c81b27aa9d889fff0e31/rpds_py-0.30.0-cp311-cp311-win_arm64.whl", hash = "sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229", size = 229069, upload-time = "2025-11-30T20:22:16.577Z" }, + { url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086, upload-time = "2025-11-30T20:22:17.93Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053, upload-time = "2025-11-30T20:22:19.297Z" }, + { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763, upload-time = "2025-11-30T20:22:21.661Z" }, + { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951, upload-time = "2025-11-30T20:22:23.408Z" }, + { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622, upload-time = "2025-11-30T20:22:25.16Z" }, + { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492, upload-time = "2025-11-30T20:22:26.505Z" }, + { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080, upload-time = "2025-11-30T20:22:27.934Z" }, + { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680, upload-time = "2025-11-30T20:22:29.341Z" }, + { url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589, upload-time = "2025-11-30T20:22:31.469Z" }, + { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289, upload-time = "2025-11-30T20:22:32.997Z" }, + { url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737, upload-time = "2025-11-30T20:22:34.419Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120, upload-time = "2025-11-30T20:22:35.903Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782, upload-time = "2025-11-30T20:22:37.271Z" }, + { url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463, upload-time = "2025-11-30T20:22:39.021Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868, upload-time = "2025-11-30T20:22:40.493Z" }, + { url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887, upload-time = "2025-11-30T20:22:41.812Z" }, + { url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" }, + { url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783, upload-time = "2025-11-30T20:22:46.103Z" }, + { url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021, upload-time = "2025-11-30T20:22:47.458Z" }, + { url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589, upload-time = "2025-11-30T20:22:48.872Z" }, + { url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025, upload-time = "2025-11-30T20:22:50.196Z" }, + { url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895, upload-time = "2025-11-30T20:22:51.87Z" }, + { url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799, upload-time = "2025-11-30T20:22:53.341Z" }, + { url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731, upload-time = "2025-11-30T20:22:54.778Z" }, + { url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027, upload-time = "2025-11-30T20:22:56.212Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020, upload-time = "2025-11-30T20:22:58.2Z" }, + { url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139, upload-time = "2025-11-30T20:23:00.209Z" }, + { url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224, upload-time = "2025-11-30T20:23:02.008Z" }, + { url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645, upload-time = "2025-11-30T20:23:03.43Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443, upload-time = "2025-11-30T20:23:04.878Z" }, + { url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375, upload-time = "2025-11-30T20:23:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850, upload-time = "2025-11-30T20:23:07.825Z" }, + { url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812, upload-time = "2025-11-30T20:23:09.228Z" }, + { url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841, upload-time = "2025-11-30T20:23:11.186Z" }, + { url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149, upload-time = "2025-11-30T20:23:12.864Z" }, + { url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843, upload-time = "2025-11-30T20:23:14.638Z" }, + { url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507, upload-time = "2025-11-30T20:23:16.105Z" }, + { url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949, upload-time = "2025-11-30T20:23:17.539Z" }, + { url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790, upload-time = "2025-11-30T20:23:19.029Z" }, + { url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217, upload-time = "2025-11-30T20:23:20.885Z" }, + { url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" }, + { url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" }, + { url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099, upload-time = "2025-11-30T20:23:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192, upload-time = "2025-11-30T20:23:29.151Z" }, + { url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080, upload-time = "2025-11-30T20:23:30.785Z" }, + { url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841, upload-time = "2025-11-30T20:23:32.209Z" }, + { url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670, upload-time = "2025-11-30T20:23:33.742Z" }, + { url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005, upload-time = "2025-11-30T20:23:35.253Z" }, + { url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112, upload-time = "2025-11-30T20:23:36.842Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049, upload-time = "2025-11-30T20:23:38.343Z" }, + { url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661, upload-time = "2025-11-30T20:23:40.263Z" }, + { url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606, upload-time = "2025-11-30T20:23:42.186Z" }, + { url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126, upload-time = "2025-11-30T20:23:44.086Z" }, + { url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371, upload-time = "2025-11-30T20:23:46.004Z" }, + { url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298, upload-time = "2025-11-30T20:23:47.696Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604, upload-time = "2025-11-30T20:23:49.501Z" }, + { url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391, upload-time = "2025-11-30T20:23:50.96Z" }, + { url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868, upload-time = "2025-11-30T20:23:52.494Z" }, + { url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747, upload-time = "2025-11-30T20:23:54.036Z" }, + { url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795, upload-time = "2025-11-30T20:23:55.556Z" }, + { url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330, upload-time = "2025-11-30T20:23:57.033Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194, upload-time = "2025-11-30T20:23:58.637Z" }, + { url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340, upload-time = "2025-11-30T20:24:00.2Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765, upload-time = "2025-11-30T20:24:01.759Z" }, + { url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834, upload-time = "2025-11-30T20:24:03.687Z" }, + { url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470, upload-time = "2025-11-30T20:24:05.232Z" }, + { url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630, upload-time = "2025-11-30T20:24:06.878Z" }, + { url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148, upload-time = "2025-11-30T20:24:08.445Z" }, + { url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030, upload-time = "2025-11-30T20:24:10.956Z" }, + { url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" }, + { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" }, + { url = "https://files.pythonhosted.org/packages/69/71/3f34339ee70521864411f8b6992e7ab13ac30d8e4e3309e07c7361767d91/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58", size = 372292, upload-time = "2025-11-30T20:24:16.537Z" }, + { url = "https://files.pythonhosted.org/packages/57/09/f183df9b8f2d66720d2ef71075c59f7e1b336bec7ee4c48f0a2b06857653/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a", size = 362128, upload-time = "2025-11-30T20:24:18.086Z" }, + { url = "https://files.pythonhosted.org/packages/7a/68/5c2594e937253457342e078f0cc1ded3dd7b2ad59afdbf2d354869110a02/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb", size = 391542, upload-time = "2025-11-30T20:24:20.092Z" }, + { url = "https://files.pythonhosted.org/packages/49/5c/31ef1afd70b4b4fbdb2800249f34c57c64beb687495b10aec0365f53dfc4/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c", size = 404004, upload-time = "2025-11-30T20:24:22.231Z" }, + { url = "https://files.pythonhosted.org/packages/e3/63/0cfbea38d05756f3440ce6534d51a491d26176ac045e2707adc99bb6e60a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3", size = 527063, upload-time = "2025-11-30T20:24:24.302Z" }, + { url = "https://files.pythonhosted.org/packages/42/e6/01e1f72a2456678b0f618fc9a1a13f882061690893c192fcad9f2926553a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5", size = 413099, upload-time = "2025-11-30T20:24:25.916Z" }, + { url = "https://files.pythonhosted.org/packages/b8/25/8df56677f209003dcbb180765520c544525e3ef21ea72279c98b9aa7c7fb/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738", size = 392177, upload-time = "2025-11-30T20:24:27.834Z" }, + { url = "https://files.pythonhosted.org/packages/4a/b4/0a771378c5f16f8115f796d1f437950158679bcd2a7c68cf251cfb00ed5b/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f", size = 406015, upload-time = "2025-11-30T20:24:29.457Z" }, + { url = "https://files.pythonhosted.org/packages/36/d8/456dbba0af75049dc6f63ff295a2f92766b9d521fa00de67a2bd6427d57a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877", size = 423736, upload-time = "2025-11-30T20:24:31.22Z" }, + { url = "https://files.pythonhosted.org/packages/13/64/b4d76f227d5c45a7e0b796c674fd81b0a6c4fbd48dc29271857d8219571c/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a", size = 573981, upload-time = "2025-11-30T20:24:32.934Z" }, + { url = "https://files.pythonhosted.org/packages/20/91/092bacadeda3edf92bf743cc96a7be133e13a39cdbfd7b5082e7ab638406/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4", size = 599782, upload-time = "2025-11-30T20:24:35.169Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b7/b95708304cd49b7b6f82fdd039f1748b66ec2b21d6a45180910802f1abf1/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e", size = 562191, upload-time = "2025-11-30T20:24:36.853Z" }, +] + [[package]] name = "rsa" version = "4.9.1" @@ -3724,6 +5378,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fc/51/727abb13f44c1fcf6d145979e1535a35794db0f6e450a0cb46aa24732fe2/s3transfer-0.16.0-py3-none-any.whl", hash = "sha256:18e25d66fed509e3868dc1572b3f427ff947dd2c56f844a5bf09481ad3f3b2fe", size = 86830, upload-time = "2025-12-01T02:30:57.729Z" }, ] +[[package]] +name = "secretstorage" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography", marker = "(python_full_version == '3.10.*' and sys_platform == 'emscripten') or (python_full_version == '3.10.*' and sys_platform == 'win32') or (python_full_version >= '3.10' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "jeepney", marker = "(python_full_version == '3.10.*' and sys_platform == 'emscripten') or (python_full_version == '3.10.*' and sys_platform == 'win32') or (python_full_version >= '3.10' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1c/03/e834bcd866f2f8a49a85eaff47340affa3bfa391ee9912a952a1faa68c7b/secretstorage-3.5.0.tar.gz", hash = "sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be", size = 19884, upload-time = "2025-11-23T19:02:53.191Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl", hash = "sha256:0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137", size = 15554, upload-time = "2025-11-23T19:02:51.545Z" }, +] + [[package]] name = "sentinels" version = "1.1.1" @@ -3733,6 +5400,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/49/65/dea992c6a97074f6d8ff9eab34741298cac2ce23e2b6c74fb7d08afdf85c/sentinels-1.1.1-py3-none-any.whl", hash = "sha256:835d3b28f3b47f5284afa4bf2db6e00f2dc5f80f9923d4b7e7aeeeccf6146a11", size = 3744, upload-time = "2025-08-12T07:57:48.858Z" }, ] +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + [[package]] name = "six" version = "1.17.0" @@ -3824,6 +5500,19 @@ wheels = [ { 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]] +name = "sse-starlette" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio", marker = "python_full_version >= '3.10'" }, + { name = "starlette", version = "0.50.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/9f/c3695c2d2d4ef70072c3a06992850498b01c6bc9be531950813716b426fa/sse_starlette-3.3.2.tar.gz", hash = "sha256:678fca55a1945c734d8472a6cad186a55ab02840b4f6786f5ee8770970579dcd", size = 32326, upload-time = "2026-02-28T11:24:34.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/28/8cb142d3fe80c4a2d8af54ca0b003f47ce0ba920974e7990fa6e016402d1/sse_starlette-3.3.2-py3-none-any.whl", hash = "sha256:5c3ea3dad425c601236726af2f27689b74494643f57017cafcb6f8c9acfbb862", size = 14270, upload-time = "2026-02-28T11:24:32.984Z" }, +] + [[package]] name = "stack-data" version = "0.6.3" @@ -3879,6 +5568,60 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d9/52/1064f510b141bd54025f9b55105e26d1fa970b9be67ad766380a3c9b74b0/starlette-0.50.0-py3-none-any.whl", hash = "sha256:9e5391843ec9b6e472eed1365a78c8098cfceb7a74bfd4d6b1c0c0095efb3bca", size = 74033, upload-time = "2025-11-01T15:25:25.461Z" }, ] +[[package]] +name = "temporalio" +version = "1.16.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "nexus-rpc", version = "1.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "protobuf", marker = "python_full_version < '3.10'" }, + { name = "python-dateutil", marker = "python_full_version < '3.10'" }, + { name = "types-protobuf", version = "6.32.1.20251210", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "typing-extensions", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/32/375ab75d0ebb468cf9c8abbc450a03d3a8c66401fc320b338bd8c00d36b4/temporalio-1.16.0.tar.gz", hash = "sha256:dd926f3e30626fd4edf5e0ce596b75ecb5bbe0e4a0281e545ac91b5577967c91", size = 1733873, upload-time = "2025-08-21T22:12:50.879Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/36/12bb7234c83ddca4b8b032c8f1a9e07a03067c6ed6d2ddb39c770a4c87c6/temporalio-1.16.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:547c0853310350d3e5b5b9c806246cbf2feb523f685b05bf14ec1b0ece8a7bb6", size = 12540769, upload-time = "2025-08-21T22:11:24.551Z" }, + { url = "https://files.pythonhosted.org/packages/3c/16/a7d402435b8f994979abfeffd3f5ffcaaeada467ac16438e61c51c9f7abe/temporalio-1.16.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b05bb0d06025645aed6f936615311a6774eb8dc66280f32a810aac2283e1258", size = 12968631, upload-time = "2025-08-21T22:11:48.375Z" }, + { url = "https://files.pythonhosted.org/packages/11/6f/16663eef877b61faa5fd917b3a63497416ec4319195af75f6169a1594479/temporalio-1.16.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a08aed4e0f6c2b6bfc779b714e91dfe8c8491a0ddb4c4370627bb07f9bddcfd", size = 13164612, upload-time = "2025-08-21T22:12:16.366Z" }, + { url = "https://files.pythonhosted.org/packages/af/0e/8c6704ca7033aa09dc084f285d70481d758972cc341adc3c84d5f82f7b01/temporalio-1.16.0-cp39-abi3-win_amd64.whl", hash = "sha256:7c190362b0d7254f1f93fb71456063e7b299ac85a89f6227758af82c6a5aa65b", size = 13177058, upload-time = "2025-08-21T22:12:44.239Z" }, +] + +[[package]] +name = "temporalio" +version = "1.20.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'emscripten'", + "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'emscripten'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "nexus-rpc", version = "1.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "protobuf", marker = "python_full_version >= '3.10'" }, + { name = "python-dateutil", marker = "python_full_version == '3.10.*'" }, + { name = "types-protobuf", version = "6.32.1.20260221", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/21/db/7d5118d28b0918888e1ec98f56f659fdb006351e06d95f30f4274962a76f/temporalio-1.20.0.tar.gz", hash = "sha256:5a6a85b7d298b7359bffa30025f7deac83c74ac095a4c6952fbf06c249a2a67c", size = 1850498, upload-time = "2025-11-25T21:25:20.225Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/1b/e69052aa6003eafe595529485d9c62d1382dd5e671108f1bddf544fb6032/temporalio-1.20.0-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:fba70314b4068f8b1994bddfa0e2ad742483f0ae714d2ef52e63013ccfd7042e", size = 12061638, upload-time = "2025-11-25T21:24:57.918Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3b/3e8c67ed7f23bedfa231c6ac29a7a9c12b89881da7694732270f3ecd6b0c/temporalio-1.20.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:ffc5bb6cabc6ae67f0bfba44de6a9c121603134ae18784a2ff3a7f230ad99080", size = 11562603, upload-time = "2025-11-25T21:25:01.721Z" }, + { url = "https://files.pythonhosted.org/packages/6d/be/ed0cc11702210522a79e09703267ebeca06eb45832b873a58de3ca76b9d0/temporalio-1.20.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1e80c1e4cdf88fa8277177f563edc91466fe4dc13c0322f26e55c76b6a219e6", size = 11824016, upload-time = "2025-11-25T21:25:06.771Z" }, + { url = "https://files.pythonhosted.org/packages/9d/97/09c5cafabc80139d97338a2bdd8ec22e08817dfd2949ab3e5b73565006eb/temporalio-1.20.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba92d909188930860c9d89ca6d7a753bc5a67e4e9eac6cea351477c967355eed", size = 12189521, upload-time = "2025-11-25T21:25:12.091Z" }, + { url = "https://files.pythonhosted.org/packages/11/23/5689c014a76aff3b744b3ee0d80815f63b1362637814f5fbb105244df09b/temporalio-1.20.0-cp310-abi3-win_amd64.whl", hash = "sha256:eacfd571b653e0a0f4aa6593f4d06fc628797898f0900d400e833a1f40cad03a", size = 12745027, upload-time = "2025-11-25T21:25:16.827Z" }, +] + [[package]] name = "tenacity" version = "9.1.2" @@ -3912,6 +5655,108 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d7/c1/eb8f9debc45d3b7918a32ab756658a0904732f75e555402972246b0b8e71/tenacity-9.1.4-py3-none-any.whl", hash = "sha256:6095a360c919085f28c6527de529e76a06ad89b23659fa881ae0649b867a9d55", size = 28926, upload-time = "2026-02-07T10:45:32.24Z" }, ] +[[package]] +name = "tiktoken" +version = "0.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "regex", marker = "python_full_version >= '3.10'" }, + { name = "requests", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/ab/4d017d0f76ec3171d469d80fc03dfbb4e48a4bcaddaa831b31d526f05edc/tiktoken-0.12.0.tar.gz", hash = "sha256:b18ba7ee2b093863978fcb14f74b3707cdc8d4d4d3836853ce7ec60772139931", size = 37806, upload-time = "2025-10-06T20:22:45.419Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/b3/2cb7c17b6c4cf8ca983204255d3f1d95eda7213e247e6947a0ee2c747a2c/tiktoken-0.12.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3de02f5a491cfd179aec916eddb70331814bd6bf764075d39e21d5862e533970", size = 1051991, upload-time = "2025-10-06T20:21:34.098Z" }, + { url = "https://files.pythonhosted.org/packages/27/0f/df139f1df5f6167194ee5ab24634582ba9a1b62c6b996472b0277ec80f66/tiktoken-0.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b6cfb6d9b7b54d20af21a912bfe63a2727d9cfa8fbda642fd8322c70340aad16", size = 995798, upload-time = "2025-10-06T20:21:35.579Z" }, + { url = "https://files.pythonhosted.org/packages/ef/5d/26a691f28ab220d5edc09b9b787399b130f24327ef824de15e5d85ef21aa/tiktoken-0.12.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:cde24cdb1b8a08368f709124f15b36ab5524aac5fa830cc3fdce9c03d4fb8030", size = 1129865, upload-time = "2025-10-06T20:21:36.675Z" }, + { url = "https://files.pythonhosted.org/packages/b2/94/443fab3d4e5ebecac895712abd3849b8da93b7b7dec61c7db5c9c7ebe40c/tiktoken-0.12.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:6de0da39f605992649b9cfa6f84071e3f9ef2cec458d08c5feb1b6f0ff62e134", size = 1152856, upload-time = "2025-10-06T20:21:37.873Z" }, + { url = "https://files.pythonhosted.org/packages/54/35/388f941251b2521c70dd4c5958e598ea6d2c88e28445d2fb8189eecc1dfc/tiktoken-0.12.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6faa0534e0eefbcafaccb75927a4a380463a2eaa7e26000f0173b920e98b720a", size = 1195308, upload-time = "2025-10-06T20:21:39.577Z" }, + { url = "https://files.pythonhosted.org/packages/f8/00/c6681c7f833dd410576183715a530437a9873fa910265817081f65f9105f/tiktoken-0.12.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:82991e04fc860afb933efb63957affc7ad54f83e2216fe7d319007dab1ba5892", size = 1255697, upload-time = "2025-10-06T20:21:41.154Z" }, + { url = "https://files.pythonhosted.org/packages/5f/d2/82e795a6a9bafa034bf26a58e68fe9a89eeaaa610d51dbeb22106ba04f0a/tiktoken-0.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:6fb2995b487c2e31acf0a9e17647e3b242235a20832642bb7a9d1a181c0c1bb1", size = 879375, upload-time = "2025-10-06T20:21:43.201Z" }, + { url = "https://files.pythonhosted.org/packages/de/46/21ea696b21f1d6d1efec8639c204bdf20fde8bafb351e1355c72c5d7de52/tiktoken-0.12.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6e227c7f96925003487c33b1b32265fad2fbcec2b7cf4817afb76d416f40f6bb", size = 1051565, upload-time = "2025-10-06T20:21:44.566Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d9/35c5d2d9e22bb2a5f74ba48266fb56c63d76ae6f66e02feb628671c0283e/tiktoken-0.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c06cf0fcc24c2cb2adb5e185c7082a82cba29c17575e828518c2f11a01f445aa", size = 995284, upload-time = "2025-10-06T20:21:45.622Z" }, + { url = "https://files.pythonhosted.org/packages/01/84/961106c37b8e49b9fdcf33fe007bb3a8fdcc380c528b20cc7fbba80578b8/tiktoken-0.12.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:f18f249b041851954217e9fd8e5c00b024ab2315ffda5ed77665a05fa91f42dc", size = 1129201, upload-time = "2025-10-06T20:21:47.074Z" }, + { url = "https://files.pythonhosted.org/packages/6a/d0/3d9275198e067f8b65076a68894bb52fd253875f3644f0a321a720277b8a/tiktoken-0.12.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:47a5bc270b8c3db00bb46ece01ef34ad050e364b51d406b6f9730b64ac28eded", size = 1152444, upload-time = "2025-10-06T20:21:48.139Z" }, + { url = "https://files.pythonhosted.org/packages/78/db/a58e09687c1698a7c592e1038e01c206569b86a0377828d51635561f8ebf/tiktoken-0.12.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:508fa71810c0efdcd1b898fda574889ee62852989f7c1667414736bcb2b9a4bd", size = 1195080, upload-time = "2025-10-06T20:21:49.246Z" }, + { url = "https://files.pythonhosted.org/packages/9e/1b/a9e4d2bf91d515c0f74afc526fd773a812232dd6cda33ebea7f531202325/tiktoken-0.12.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a1af81a6c44f008cba48494089dd98cccb8b313f55e961a52f5b222d1e507967", size = 1255240, upload-time = "2025-10-06T20:21:50.274Z" }, + { url = "https://files.pythonhosted.org/packages/9d/15/963819345f1b1fb0809070a79e9dd96938d4ca41297367d471733e79c76c/tiktoken-0.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:3e68e3e593637b53e56f7237be560f7a394451cb8c11079755e80ae64b9e6def", size = 879422, upload-time = "2025-10-06T20:21:51.734Z" }, + { url = "https://files.pythonhosted.org/packages/a4/85/be65d39d6b647c79800fd9d29241d081d4eeb06271f383bb87200d74cf76/tiktoken-0.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b97f74aca0d78a1ff21b8cd9e9925714c15a9236d6ceacf5c7327c117e6e21e8", size = 1050728, upload-time = "2025-10-06T20:21:52.756Z" }, + { url = "https://files.pythonhosted.org/packages/4a/42/6573e9129bc55c9bf7300b3a35bef2c6b9117018acca0dc760ac2d93dffe/tiktoken-0.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2b90f5ad190a4bb7c3eb30c5fa32e1e182ca1ca79f05e49b448438c3e225a49b", size = 994049, upload-time = "2025-10-06T20:21:53.782Z" }, + { url = "https://files.pythonhosted.org/packages/66/c5/ed88504d2f4a5fd6856990b230b56d85a777feab84e6129af0822f5d0f70/tiktoken-0.12.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:65b26c7a780e2139e73acc193e5c63ac754021f160df919add909c1492c0fb37", size = 1129008, upload-time = "2025-10-06T20:21:54.832Z" }, + { url = "https://files.pythonhosted.org/packages/f4/90/3dae6cc5436137ebd38944d396b5849e167896fc2073da643a49f372dc4f/tiktoken-0.12.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:edde1ec917dfd21c1f2f8046b86348b0f54a2c0547f68149d8600859598769ad", size = 1152665, upload-time = "2025-10-06T20:21:56.129Z" }, + { url = "https://files.pythonhosted.org/packages/a3/fe/26df24ce53ffde419a42f5f53d755b995c9318908288c17ec3f3448313a3/tiktoken-0.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:35a2f8ddd3824608b3d650a000c1ef71f730d0c56486845705a8248da00f9fe5", size = 1194230, upload-time = "2025-10-06T20:21:57.546Z" }, + { url = "https://files.pythonhosted.org/packages/20/cc/b064cae1a0e9fac84b0d2c46b89f4e57051a5f41324e385d10225a984c24/tiktoken-0.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:83d16643edb7fa2c99eff2ab7733508aae1eebb03d5dfc46f5565862810f24e3", size = 1254688, upload-time = "2025-10-06T20:21:58.619Z" }, + { url = "https://files.pythonhosted.org/packages/81/10/b8523105c590c5b8349f2587e2fdfe51a69544bd5a76295fc20f2374f470/tiktoken-0.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:ffc5288f34a8bc02e1ea7047b8d041104791d2ddbf42d1e5fa07822cbffe16bd", size = 878694, upload-time = "2025-10-06T20:21:59.876Z" }, + { url = "https://files.pythonhosted.org/packages/00/61/441588ee21e6b5cdf59d6870f86beb9789e532ee9718c251b391b70c68d6/tiktoken-0.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:775c2c55de2310cc1bc9a3ad8826761cbdc87770e586fd7b6da7d4589e13dab3", size = 1050802, upload-time = "2025-10-06T20:22:00.96Z" }, + { url = "https://files.pythonhosted.org/packages/1f/05/dcf94486d5c5c8d34496abe271ac76c5b785507c8eae71b3708f1ad9b45a/tiktoken-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a01b12f69052fbe4b080a2cfb867c4de12c704b56178edf1d1d7b273561db160", size = 993995, upload-time = "2025-10-06T20:22:02.788Z" }, + { url = "https://files.pythonhosted.org/packages/a0/70/5163fe5359b943f8db9946b62f19be2305de8c3d78a16f629d4165e2f40e/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:01d99484dc93b129cd0964f9d34eee953f2737301f18b3c7257bf368d7615baa", size = 1128948, upload-time = "2025-10-06T20:22:03.814Z" }, + { url = "https://files.pythonhosted.org/packages/0c/da/c028aa0babf77315e1cef357d4d768800c5f8a6de04d0eac0f377cb619fa/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:4a1a4fcd021f022bfc81904a911d3df0f6543b9e7627b51411da75ff2fe7a1be", size = 1151986, upload-time = "2025-10-06T20:22:05.173Z" }, + { url = "https://files.pythonhosted.org/packages/a0/5a/886b108b766aa53e295f7216b509be95eb7d60b166049ce2c58416b25f2a/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:981a81e39812d57031efdc9ec59fa32b2a5a5524d20d4776574c4b4bd2e9014a", size = 1194222, upload-time = "2025-10-06T20:22:06.265Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f8/4db272048397636ac7a078d22773dd2795b1becee7bc4922fe6207288d57/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9baf52f84a3f42eef3ff4e754a0db79a13a27921b457ca9832cf944c6be4f8f3", size = 1255097, upload-time = "2025-10-06T20:22:07.403Z" }, + { url = "https://files.pythonhosted.org/packages/8e/32/45d02e2e0ea2be3a9ed22afc47d93741247e75018aac967b713b2941f8ea/tiktoken-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:b8a0cd0c789a61f31bf44851defbd609e8dd1e2c8589c614cc1060940ef1f697", size = 879117, upload-time = "2025-10-06T20:22:08.418Z" }, + { url = "https://files.pythonhosted.org/packages/ce/76/994fc868f88e016e6d05b0da5ac24582a14c47893f4474c3e9744283f1d5/tiktoken-0.12.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d5f89ea5680066b68bcb797ae85219c72916c922ef0fcdd3480c7d2315ffff16", size = 1050309, upload-time = "2025-10-06T20:22:10.939Z" }, + { url = "https://files.pythonhosted.org/packages/f6/b8/57ef1456504c43a849821920d582a738a461b76a047f352f18c0b26c6516/tiktoken-0.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b4e7ed1c6a7a8a60a3230965bdedba8cc58f68926b835e519341413370e0399a", size = 993712, upload-time = "2025-10-06T20:22:12.115Z" }, + { url = "https://files.pythonhosted.org/packages/72/90/13da56f664286ffbae9dbcfadcc625439142675845baa62715e49b87b68b/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:fc530a28591a2d74bce821d10b418b26a094bf33839e69042a6e86ddb7a7fb27", size = 1128725, upload-time = "2025-10-06T20:22:13.541Z" }, + { url = "https://files.pythonhosted.org/packages/05/df/4f80030d44682235bdaecd7346c90f67ae87ec8f3df4a3442cb53834f7e4/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:06a9f4f49884139013b138920a4c393aa6556b2f8f536345f11819389c703ebb", size = 1151875, upload-time = "2025-10-06T20:22:14.559Z" }, + { url = "https://files.pythonhosted.org/packages/22/1f/ae535223a8c4ef4c0c1192e3f9b82da660be9eb66b9279e95c99288e9dab/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:04f0e6a985d95913cabc96a741c5ffec525a2c72e9df086ff17ebe35985c800e", size = 1194451, upload-time = "2025-10-06T20:22:15.545Z" }, + { url = "https://files.pythonhosted.org/packages/78/a7/f8ead382fce0243cb625c4f266e66c27f65ae65ee9e77f59ea1653b6d730/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0ee8f9ae00c41770b5f9b0bb1235474768884ae157de3beb5439ca0fd70f3e25", size = 1253794, upload-time = "2025-10-06T20:22:16.624Z" }, + { url = "https://files.pythonhosted.org/packages/93/e0/6cc82a562bc6365785a3ff0af27a2a092d57c47d7a81d9e2295d8c36f011/tiktoken-0.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:dc2dd125a62cb2b3d858484d6c614d136b5b848976794edfb63688d539b8b93f", size = 878777, upload-time = "2025-10-06T20:22:18.036Z" }, + { url = "https://files.pythonhosted.org/packages/72/05/3abc1db5d2c9aadc4d2c76fa5640134e475e58d9fbb82b5c535dc0de9b01/tiktoken-0.12.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a90388128df3b3abeb2bfd1895b0681412a8d7dc644142519e6f0a97c2111646", size = 1050188, upload-time = "2025-10-06T20:22:19.563Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7b/50c2f060412202d6c95f32b20755c7a6273543b125c0985d6fa9465105af/tiktoken-0.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:da900aa0ad52247d8794e307d6446bd3cdea8e192769b56276695d34d2c9aa88", size = 993978, upload-time = "2025-10-06T20:22:20.702Z" }, + { url = "https://files.pythonhosted.org/packages/14/27/bf795595a2b897e271771cd31cb847d479073497344c637966bdf2853da1/tiktoken-0.12.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:285ba9d73ea0d6171e7f9407039a290ca77efcdb026be7769dccc01d2c8d7fff", size = 1129271, upload-time = "2025-10-06T20:22:22.06Z" }, + { url = "https://files.pythonhosted.org/packages/f5/de/9341a6d7a8f1b448573bbf3425fa57669ac58258a667eb48a25dfe916d70/tiktoken-0.12.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:d186a5c60c6a0213f04a7a802264083dea1bbde92a2d4c7069e1a56630aef830", size = 1151216, upload-time = "2025-10-06T20:22:23.085Z" }, + { url = "https://files.pythonhosted.org/packages/75/0d/881866647b8d1be4d67cb24e50d0c26f9f807f994aa1510cb9ba2fe5f612/tiktoken-0.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:604831189bd05480f2b885ecd2d1986dc7686f609de48208ebbbddeea071fc0b", size = 1194860, upload-time = "2025-10-06T20:22:24.602Z" }, + { url = "https://files.pythonhosted.org/packages/b3/1e/b651ec3059474dab649b8d5b69f5c65cd8fcd8918568c1935bd4136c9392/tiktoken-0.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8f317e8530bb3a222547b85a58583238c8f74fd7a7408305f9f63246d1a0958b", size = 1254567, upload-time = "2025-10-06T20:22:25.671Z" }, + { url = "https://files.pythonhosted.org/packages/80/57/ce64fd16ac390fafde001268c364d559447ba09b509181b2808622420eec/tiktoken-0.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:399c3dd672a6406719d84442299a490420b458c44d3ae65516302a99675888f3", size = 921067, upload-time = "2025-10-06T20:22:26.753Z" }, + { url = "https://files.pythonhosted.org/packages/ac/a4/72eed53e8976a099539cdd5eb36f241987212c29629d0a52c305173e0a68/tiktoken-0.12.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2c714c72bc00a38ca969dae79e8266ddec999c7ceccd603cc4f0d04ccd76365", size = 1050473, upload-time = "2025-10-06T20:22:27.775Z" }, + { url = "https://files.pythonhosted.org/packages/e6/d7/0110b8f54c008466b19672c615f2168896b83706a6611ba6e47313dbc6e9/tiktoken-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:cbb9a3ba275165a2cb0f9a83f5d7025afe6b9d0ab01a22b50f0e74fee2ad253e", size = 993855, upload-time = "2025-10-06T20:22:28.799Z" }, + { url = "https://files.pythonhosted.org/packages/5f/77/4f268c41a3957c418b084dd576ea2fad2e95da0d8e1ab705372892c2ca22/tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:dfdfaa5ffff8993a3af94d1125870b1d27aed7cb97aa7eb8c1cefdbc87dbee63", size = 1129022, upload-time = "2025-10-06T20:22:29.981Z" }, + { url = "https://files.pythonhosted.org/packages/4e/2b/fc46c90fe5028bd094cd6ee25a7db321cb91d45dc87531e2bdbb26b4867a/tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:584c3ad3d0c74f5269906eb8a659c8bfc6144a52895d9261cdaf90a0ae5f4de0", size = 1150736, upload-time = "2025-10-06T20:22:30.996Z" }, + { url = "https://files.pythonhosted.org/packages/28/c0/3c7a39ff68022ddfd7d93f3337ad90389a342f761c4d71de99a3ccc57857/tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:54c891b416a0e36b8e2045b12b33dd66fb34a4fe7965565f1b482da50da3e86a", size = 1194908, upload-time = "2025-10-06T20:22:32.073Z" }, + { url = "https://files.pythonhosted.org/packages/ab/0d/c1ad6f4016a3968c048545f5d9b8ffebf577774b2ede3e2e352553b685fe/tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5edb8743b88d5be814b1a8a8854494719080c28faaa1ccbef02e87354fe71ef0", size = 1253706, upload-time = "2025-10-06T20:22:33.385Z" }, + { url = "https://files.pythonhosted.org/packages/af/df/c7891ef9d2712ad774777271d39fdef63941ffba0a9d59b7ad1fd2765e57/tiktoken-0.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f61c0aea5565ac82e2ec50a05e02a6c44734e91b51c10510b084ea1b8e633a71", size = 920667, upload-time = "2025-10-06T20:22:34.444Z" }, + { url = "https://files.pythonhosted.org/packages/c7/d1/7507bfb9c2ceef52ae3ae813013215c185648e21127538aae66dedd3af9c/tiktoken-0.12.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:d51d75a5bffbf26f86554d28e78bfb921eae998edc2675650fd04c7e1f0cdc1e", size = 1053407, upload-time = "2025-10-06T20:22:35.492Z" }, + { url = "https://files.pythonhosted.org/packages/ee/4a/8ea1da602ac39dee4356b4cd6040a2325507482c36043044b6f581597b4f/tiktoken-0.12.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:09eb4eae62ae7e4c62364d9ec3a57c62eea707ac9a2b2c5d6bd05de6724ea179", size = 997150, upload-time = "2025-10-06T20:22:37.286Z" }, + { url = "https://files.pythonhosted.org/packages/2c/1a/62d1d36b167eccd441aff2f0091551ca834295541b949d161021aa658167/tiktoken-0.12.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:df37684ace87d10895acb44b7f447d4700349b12197a526da0d4a4149fde074c", size = 1131575, upload-time = "2025-10-06T20:22:39.023Z" }, + { url = "https://files.pythonhosted.org/packages/f7/16/544207d63c8c50edd2321228f21d236e4e49d235128bb7e3e0f69eed0807/tiktoken-0.12.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:4c9614597ac94bb294544345ad8cf30dac2129c05e2db8dc53e082f355857af7", size = 1154920, upload-time = "2025-10-06T20:22:40.175Z" }, + { url = "https://files.pythonhosted.org/packages/99/4c/0a3504157c81364fc0c64cada54efef0567961357e786706ea63bc8946e1/tiktoken-0.12.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:20cf97135c9a50de0b157879c3c4accbb29116bcf001283d26e073ff3b345946", size = 1196766, upload-time = "2025-10-06T20:22:41.365Z" }, + { url = "https://files.pythonhosted.org/packages/d4/46/8e6a258ae65447c75770fe5ea8968acab369e8c9f537f727c91f83772325/tiktoken-0.12.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:15d875454bbaa3728be39880ddd11a5a2a9e548c29418b41e8fd8a767172b5ec", size = 1258278, upload-time = "2025-10-06T20:22:42.846Z" }, + { url = "https://files.pythonhosted.org/packages/35/43/3b95de4f5e76f3cafc70dac9b1b9cfe759ff3bfd494ac91a280e93772e90/tiktoken-0.12.0-cp39-cp39-win_amd64.whl", hash = "sha256:2cff3688ba3c639ebe816f8d58ffbbb0aa7433e23e08ab1cade5d175fc973fb3", size = 881888, upload-time = "2025-10-06T20:22:44.059Z" }, +] + +[[package]] +name = "tokenizers" +version = "0.22.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub", marker = "python_full_version < '3.11' or sys_platform != 'emscripten'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/73/6f/f80cfef4a312e1fb34baf7d85c72d4411afde10978d4657f8cdd811d3ccc/tokenizers-0.22.2.tar.gz", hash = "sha256:473b83b915e547aa366d1eee11806deaf419e17be16310ac0a14077f1e28f917", size = 372115, upload-time = "2026-01-05T10:45:15.988Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/97/5dbfabf04c7e348e655e907ed27913e03db0923abb5dfdd120d7b25630e1/tokenizers-0.22.2-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:544dd704ae7238755d790de45ba8da072e9af3eea688f698b137915ae959281c", size = 3100275, upload-time = "2026-01-05T10:41:02.158Z" }, + { url = "https://files.pythonhosted.org/packages/2e/47/174dca0502ef88b28f1c9e06b73ce33500eedfac7a7692108aec220464e7/tokenizers-0.22.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1e418a55456beedca4621dbab65a318981467a2b188e982a23e117f115ce5001", size = 2981472, upload-time = "2026-01-05T10:41:00.276Z" }, + { url = "https://files.pythonhosted.org/packages/d6/84/7990e799f1309a8b87af6b948f31edaa12a3ed22d11b352eaf4f4b2e5753/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2249487018adec45d6e3554c71d46eb39fa8ea67156c640f7513eb26f318cec7", size = 3290736, upload-time = "2026-01-05T10:40:32.165Z" }, + { url = "https://files.pythonhosted.org/packages/78/59/09d0d9ba94dcd5f4f1368d4858d24546b4bdc0231c2354aa31d6199f0399/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25b85325d0815e86e0bac263506dd114578953b7b53d7de09a6485e4a160a7dd", size = 3168835, upload-time = "2026-01-05T10:40:38.847Z" }, + { url = "https://files.pythonhosted.org/packages/47/50/b3ebb4243e7160bda8d34b731e54dd8ab8b133e50775872e7a434e524c28/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfb88f22a209ff7b40a576d5324bf8286b519d7358663db21d6246fb17eea2d5", size = 3521673, upload-time = "2026-01-05T10:40:56.614Z" }, + { url = "https://files.pythonhosted.org/packages/e0/fa/89f4cb9e08df770b57adb96f8cbb7e22695a4cb6c2bd5f0c4f0ebcf33b66/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c774b1276f71e1ef716e5486f21e76333464f47bece56bbd554485982a9e03e", size = 3724818, upload-time = "2026-01-05T10:40:44.507Z" }, + { url = "https://files.pythonhosted.org/packages/64/04/ca2363f0bfbe3b3d36e95bf67e56a4c88c8e3362b658e616d1ac185d47f2/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df6c4265b289083bf710dff49bc51ef252f9d5be33a45ee2bed151114a56207b", size = 3379195, upload-time = "2026-01-05T10:40:51.139Z" }, + { url = "https://files.pythonhosted.org/packages/2e/76/932be4b50ef6ccedf9d3c6639b056a967a86258c6d9200643f01269211ca/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:369cc9fc8cc10cb24143873a0d95438bb8ee257bb80c71989e3ee290e8d72c67", size = 3274982, upload-time = "2026-01-05T10:40:58.331Z" }, + { url = "https://files.pythonhosted.org/packages/1d/28/5f9f5a4cc211b69e89420980e483831bcc29dade307955cc9dc858a40f01/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:29c30b83d8dcd061078b05ae0cb94d3c710555fbb44861139f9f83dcca3dc3e4", size = 9478245, upload-time = "2026-01-05T10:41:04.053Z" }, + { url = "https://files.pythonhosted.org/packages/6c/fb/66e2da4704d6aadebf8cb39f1d6d1957df667ab24cff2326b77cda0dcb85/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:37ae80a28c1d3265bb1f22464c856bd23c02a05bb211e56d0c5301a435be6c1a", size = 9560069, upload-time = "2026-01-05T10:45:10.673Z" }, + { url = "https://files.pythonhosted.org/packages/16/04/fed398b05caa87ce9b1a1bb5166645e38196081b225059a6edaff6440fac/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:791135ee325f2336f498590eb2f11dc5c295232f288e75c99a36c5dbce63088a", size = 9899263, upload-time = "2026-01-05T10:45:12.559Z" }, + { url = "https://files.pythonhosted.org/packages/05/a1/d62dfe7376beaaf1394917e0f8e93ee5f67fea8fcf4107501db35996586b/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38337540fbbddff8e999d59970f3c6f35a82de10053206a7562f1ea02d046fa5", size = 10033429, upload-time = "2026-01-05T10:45:14.333Z" }, + { url = "https://files.pythonhosted.org/packages/fd/18/a545c4ea42af3df6effd7d13d250ba77a0a86fb20393143bbb9a92e434d4/tokenizers-0.22.2-cp39-abi3-win32.whl", hash = "sha256:a6bf3f88c554a2b653af81f3204491c818ae2ac6fbc09e76ef4773351292bc92", size = 2502363, upload-time = "2026-01-05T10:45:20.593Z" }, + { url = "https://files.pythonhosted.org/packages/65/71/0670843133a43d43070abeb1949abfdef12a86d490bea9cd9e18e37c5ff7/tokenizers-0.22.2-cp39-abi3-win_amd64.whl", hash = "sha256:c9ea31edff2968b44a88f97d784c2f16dc0729b8b143ed004699ebca91f05c48", size = 2747786, upload-time = "2026-01-05T10:45:18.411Z" }, + { url = "https://files.pythonhosted.org/packages/72/f4/0de46cfa12cdcbcd464cc59fde36912af405696f687e53a091fb432f694c/tokenizers-0.22.2-cp39-abi3-win_arm64.whl", hash = "sha256:9ce725d22864a1e965217204946f830c37876eee3b2ba6fc6255e8e903d5fcbc", size = 2612133, upload-time = "2026-01-05T10:45:17.232Z" }, + { url = "https://files.pythonhosted.org/packages/84/04/655b79dbcc9b3ac5f1479f18e931a344af67e5b7d3b251d2dcdcd7558592/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:753d47ebd4542742ef9261d9da92cd545b2cacbb48349a1225466745bb866ec4", size = 3282301, upload-time = "2026-01-05T10:40:34.858Z" }, + { url = "https://files.pythonhosted.org/packages/46/cd/e4851401f3d8f6f45d8480262ab6a5c8cb9c4302a790a35aa14eeed6d2fd/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e10bf9113d209be7cd046d40fbabbaf3278ff6d18eb4da4c500443185dc1896c", size = 3161308, upload-time = "2026-01-05T10:40:40.737Z" }, + { url = "https://files.pythonhosted.org/packages/6f/6e/55553992a89982cd12d4a66dddb5e02126c58677ea3931efcbe601d419db/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:64d94e84f6660764e64e7e0b22baa72f6cd942279fdbb21d46abd70d179f0195", size = 3718964, upload-time = "2026-01-05T10:40:46.56Z" }, + { url = "https://files.pythonhosted.org/packages/59/8c/b1c87148aa15e099243ec9f0cf9d0e970cc2234c3257d558c25a2c5304e6/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f01a9c019878532f98927d2bacb79bbb404b43d3437455522a00a30718cdedb5", size = 3373542, upload-time = "2026-01-05T10:40:52.803Z" }, + { url = "https://files.pythonhosted.org/packages/27/46/8d7db1dff181be50b207ab0a7483a22d5c3a4f903a9afc7cf7e465ad8109/tokenizers-0.22.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:319f659ee992222f04e58f84cbf407cfa66a65fe3a8de44e8ad2bc53e7d99012", size = 3287784, upload-time = "2026-01-05T10:40:37.108Z" }, + { url = "https://files.pythonhosted.org/packages/5b/6e/3bc33cae8bf114afa5a98e35eb065c72b7c37d01d370906a893f33881767/tokenizers-0.22.2-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1e50f8554d504f617d9e9d6e4c2c2884a12b388a97c5c77f0bc6cf4cd032feee", size = 3164301, upload-time = "2026-01-05T10:40:42.367Z" }, + { url = "https://files.pythonhosted.org/packages/91/fc/6aa749d7d443aab4daa6f8bc00338389149fd2534e25b772285c3301993e/tokenizers-0.22.2-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a62ba2c5faa2dd175aaeed7b15abf18d20266189fb3406c5d0550dd34dd5f37", size = 3717771, upload-time = "2026-01-05T10:40:49.076Z" }, + { url = "https://files.pythonhosted.org/packages/fc/60/5b440d251863bd33f9b0a416c695b0309487b83abf6f2dafe9163a3aeac2/tokenizers-0.22.2-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:143b999bdc46d10febb15cbffb4207ddd1f410e2c755857b5a0797961bbdc113", size = 3377740, upload-time = "2026-01-05T10:40:54.859Z" }, +] + [[package]] name = "tomli" version = "2.4.0" @@ -3985,6 +5830,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/50/49/8dc3fd90902f70084bd2cd059d576ddb4f8bb44c2c7c0e33a11422acb17e/tornado-6.5.4-cp39-abi3-win_arm64.whl", hash = "sha256:053e6e16701eb6cbe641f308f4c1a9541f91b6261991160391bfc342e8a551a1", size = 445910, upload-time = "2025-12-15T19:21:02.571Z" }, ] +[[package]] +name = "tqdm" +version = "4.67.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" }, +] + [[package]] name = "traitlets" version = "5.14.3" @@ -3994,6 +5851,129 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359, upload-time = "2024-04-19T11:11:46.763Z" }, ] +[[package]] +name = "typer" +version = "0.23.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "annotated-doc", marker = "python_full_version < '3.10'" }, + { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "rich", marker = "python_full_version < '3.10'" }, + { name = "shellingham", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d3/ae/93d16574e66dfe4c2284ffdaca4b0320ade32858cb2cc586c8dd79f127c5/typer-0.23.2.tar.gz", hash = "sha256:a99706a08e54f1aef8bb6a8611503808188a4092808e86addff1828a208af0de", size = 120162, upload-time = "2026-02-16T18:52:40.354Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/2c/dee705c427875402200fe779eb8a3c00ccb349471172c41178336e9599cc/typer-0.23.2-py3-none-any.whl", hash = "sha256:e9c8dc380f82450b3c851a9b9d5a0edf95d1d6456ae70c517d8b06a50c7a9978", size = 56834, upload-time = "2026-02-16T18:52:39.308Z" }, +] + +[[package]] +name = "typer" +version = "0.24.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'emscripten'", + "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'emscripten'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "annotated-doc", marker = "python_full_version >= '3.10'" }, + { name = "click", version = "8.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "rich", marker = "python_full_version >= '3.10'" }, + { name = "shellingham", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/24/cb09efec5cc954f7f9b930bf8279447d24618bb6758d4f6adf2574c41780/typer-0.24.1.tar.gz", hash = "sha256:e39b4732d65fbdcde189ae76cf7cd48aeae72919dea1fdfc16593be016256b45", size = 118613, upload-time = "2026-02-21T16:54:40.609Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/91/48db081e7a63bb37284f9fbcefda7c44c277b18b0e13fbc36ea2335b71e6/typer-0.24.1-py3-none-any.whl", hash = "sha256:112c1f0ce578bfb4cab9ffdabc68f031416ebcc216536611ba21f04e9aa84c9e", size = 56085, upload-time = "2026-02-21T16:54:41.616Z" }, +] + +[[package]] +name = "types-protobuf" +version = "6.32.1.20251210" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/c2/59/c743a842911887cd96d56aa8936522b0cd5f7a7f228c96e81b59fced45be/types_protobuf-6.32.1.20251210.tar.gz", hash = "sha256:c698bb3f020274b1a2798ae09dc773728ce3f75209a35187bd11916ebfde6763", size = 63900, upload-time = "2025-12-10T03:14:25.451Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/aa/43/58e75bac4219cbafee83179505ff44cae3153ec279be0e30583a73b8f108/types_protobuf-6.32.1.20251210-py3-none-any.whl", hash = "sha256:2641f78f3696822a048cfb8d0ff42ccd85c25f12f871fbebe86da63793692140", size = 77921, upload-time = "2025-12-10T03:14:24.477Z" }, +] + +[[package]] +name = "types-protobuf" +version = "6.32.1.20260221" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'emscripten'", + "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'emscripten'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.10.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/5f/e2/9aa4a3b2469508bd7b4e2ae11cbedaf419222a09a1b94daffcd5efca4023/types_protobuf-6.32.1.20260221.tar.gz", hash = "sha256:6d5fb060a616bfb076cbb61b4b3c3969f5fc8bec5810f9a2f7e648ee5cbcbf6e", size = 64408, upload-time = "2026-02-21T03:55:13.916Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/e8/1fd38926f9cf031188fbc5a96694203ea6f24b0e34bd64a225ec6f6291ba/types_protobuf-6.32.1.20260221-py3-none-any.whl", hash = "sha256:da7cdd947975964a93c30bfbcc2c6841ee646b318d3816b033adc2c4eb6448e4", size = 77956, upload-time = "2026-02-21T03:55:12.894Z" }, +] + +[[package]] +name = "types-requests" +version = "2.31.0.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "types-urllib3", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f9/b8/c1e8d39996b4929b918aba10dba5de07a8b3f4c8487bb61bb79882544e69/types-requests-2.31.0.6.tar.gz", hash = "sha256:cd74ce3b53c461f1228a9b783929ac73a666658f223e28ed29753771477b3bd0", size = 15535, upload-time = "2023-09-27T06:19:38.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/a1/6f8dc74d9069e790d604ddae70cb46dcbac668f1bb08136e7b0f2f5cd3bf/types_requests-2.31.0.6-py3-none-any.whl", hash = "sha256:a2db9cb228a81da8348b49ad6db3f5519452dd20a9c1e1a868c83c5fe88fd1a9", size = 14516, upload-time = "2023-09-27T06:19:36.373Z" }, +] + +[[package]] +name = "types-requests" +version = "2.32.4.20260107" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "urllib3", version = "2.6.3", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.10' and sys_platform != 'emscripten') or (python_full_version == '3.10.*' and sys_platform == 'emscripten')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0f/f3/a0663907082280664d745929205a89d41dffb29e89a50f753af7d57d0a96/types_requests-2.32.4.20260107.tar.gz", hash = "sha256:018a11ac158f801bfa84857ddec1650750e393df8a004a8a9ae2a9bec6fcb24f", size = 23165, upload-time = "2026-01-07T03:20:54.091Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1c/12/709ea261f2bf91ef0a26a9eed20f2623227a8ed85610c1e54c5805692ecb/types_requests-2.32.4.20260107-py3-none-any.whl", hash = "sha256:b703fe72f8ce5b31ef031264fe9395cac8f46a04661a79f7ed31a80fb308730d", size = 20676, upload-time = "2026-01-07T03:20:52.929Z" }, +] + +[[package]] +name = "types-urllib3" +version = "1.26.25.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/73/de/b9d7a68ad39092368fb21dd6194b362b98a1daeea5dcfef5e1adb5031c7e/types-urllib3-1.26.25.14.tar.gz", hash = "sha256:229b7f577c951b8c1b92c1bc2b2fdb0b49847bd2af6d1cc2a2e3dd340f3bda8f", size = 11239, upload-time = "2023-07-20T15:19:31.307Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/7b/3fc711b2efea5e85a7a0bbfe269ea944aa767bbba5ec52f9ee45d362ccf3/types_urllib3-1.26.25.14-py3-none-any.whl", hash = "sha256:9683bbb7fb72e32bfe9d2be6e04875fbe1b3eeec3cbb4ea231435aa7fd6b4f0e", size = 15377, upload-time = "2023-07-20T15:19:30.379Z" }, +] + [[package]] name = "typing" version = "3.10.0.0" @@ -4045,6 +6025,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c2/14/e2a54fabd4f08cd7af1c07030603c3356b74da07f7cc056e600436edfa17/tzlocal-5.3.1-py3-none-any.whl", hash = "sha256:eb1a66c3ef5847adf7a834f1be0800581b683b5608e74f86ecbcef8ab91bb85d", size = 18026, upload-time = "2025-03-05T21:17:39.857Z" }, ] +[[package]] +name = "uncalled-for" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/02/7c/b5b7d8136f872e3f13b0584e576886de0489d7213a12de6bebf29ff6ebfc/uncalled_for-0.2.0.tar.gz", hash = "sha256:b4f8fdbcec328c5a113807d653e041c5094473dd4afa7c34599ace69ccb7e69f", size = 49488, upload-time = "2026-02-27T17:40:58.137Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/7f/4320d9ce3be404e6310b915c3629fe27bf1e2f438a1a7a3cb0396e32e9a9/uncalled_for-0.2.0-py3-none-any.whl", hash = "sha256:2c0bd338faff5f930918f79e7eb9ff48290df2cb05fcc0b40a7f334e55d4d85f", size = 11351, upload-time = "2026-02-27T17:40:56.804Z" }, +] + [[package]] name = "urllib3" version = "1.26.20" @@ -4130,6 +6119,125 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/03/ff/7c0c86c43b3cbb927e0ccc0255cb4057ceba4799cd44ae95174ce8e8b5b2/vine-5.1.0-py3-none-any.whl", hash = "sha256:40fdf3c48b2cfe1c38a49e9ae2da6fda88e4794c810050a728bd7413811fb1dc", size = 9636, upload-time = "2023-11-05T08:46:51.205Z" }, ] +[[package]] +name = "watchfiles" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440, upload-time = "2025-10-14T15:06:21.08Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/1a/206e8cf2dd86fddf939165a57b4df61607a1e0add2785f170a3f616b7d9f/watchfiles-1.1.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:eef58232d32daf2ac67f42dea51a2c80f0d03379075d44a587051e63cc2e368c", size = 407318, upload-time = "2025-10-14T15:04:18.753Z" }, + { url = "https://files.pythonhosted.org/packages/b3/0f/abaf5262b9c496b5dad4ed3c0e799cbecb1f8ea512ecb6ddd46646a9fca3/watchfiles-1.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:03fa0f5237118a0c5e496185cafa92878568b652a2e9a9382a5151b1a0380a43", size = 394478, upload-time = "2025-10-14T15:04:20.297Z" }, + { url = "https://files.pythonhosted.org/packages/b1/04/9cc0ba88697b34b755371f5ace8d3a4d9a15719c07bdc7bd13d7d8c6a341/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8ca65483439f9c791897f7db49202301deb6e15fe9f8fe2fed555bf986d10c31", size = 449894, upload-time = "2025-10-14T15:04:21.527Z" }, + { url = "https://files.pythonhosted.org/packages/d2/9c/eda4615863cd8621e89aed4df680d8c3ec3da6a4cf1da113c17decd87c7f/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f0ab1c1af0cb38e3f598244c17919fb1a84d1629cc08355b0074b6d7f53138ac", size = 459065, upload-time = "2025-10-14T15:04:22.795Z" }, + { url = "https://files.pythonhosted.org/packages/84/13/f28b3f340157d03cbc8197629bc109d1098764abe1e60874622a0be5c112/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bc570d6c01c206c46deb6e935a260be44f186a2f05179f52f7fcd2be086a94d", size = 488377, upload-time = "2025-10-14T15:04:24.138Z" }, + { url = "https://files.pythonhosted.org/packages/86/93/cfa597fa9389e122488f7ffdbd6db505b3b915ca7435ecd7542e855898c2/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e84087b432b6ac94778de547e08611266f1f8ffad28c0ee4c82e028b0fc5966d", size = 595837, upload-time = "2025-10-14T15:04:25.057Z" }, + { url = "https://files.pythonhosted.org/packages/57/1e/68c1ed5652b48d89fc24d6af905d88ee4f82fa8bc491e2666004e307ded1/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:620bae625f4cb18427b1bb1a2d9426dc0dd5a5ba74c7c2cdb9de405f7b129863", size = 473456, upload-time = "2025-10-14T15:04:26.497Z" }, + { url = "https://files.pythonhosted.org/packages/d5/dc/1a680b7458ffa3b14bb64878112aefc8f2e4f73c5af763cbf0bd43100658/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:544364b2b51a9b0c7000a4b4b02f90e9423d97fbbf7e06689236443ebcad81ab", size = 455614, upload-time = "2025-10-14T15:04:27.539Z" }, + { url = "https://files.pythonhosted.org/packages/61/a5/3d782a666512e01eaa6541a72ebac1d3aae191ff4a31274a66b8dd85760c/watchfiles-1.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:bbe1ef33d45bc71cf21364df962af171f96ecaeca06bd9e3d0b583efb12aec82", size = 630690, upload-time = "2025-10-14T15:04:28.495Z" }, + { url = "https://files.pythonhosted.org/packages/9b/73/bb5f38590e34687b2a9c47a244aa4dd50c56a825969c92c9c5fc7387cea1/watchfiles-1.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1a0bb430adb19ef49389e1ad368450193a90038b5b752f4ac089ec6942c4dff4", size = 622459, upload-time = "2025-10-14T15:04:29.491Z" }, + { url = "https://files.pythonhosted.org/packages/f1/ac/c9bb0ec696e07a20bd58af5399aeadaef195fb2c73d26baf55180fe4a942/watchfiles-1.1.1-cp310-cp310-win32.whl", hash = "sha256:3f6d37644155fb5beca5378feb8c1708d5783145f2a0f1c4d5a061a210254844", size = 272663, upload-time = "2025-10-14T15:04:30.435Z" }, + { url = "https://files.pythonhosted.org/packages/11/a0/a60c5a7c2ec59fa062d9a9c61d02e3b6abd94d32aac2d8344c4bdd033326/watchfiles-1.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:a36d8efe0f290835fd0f33da35042a1bb5dc0e83cbc092dcf69bce442579e88e", size = 287453, upload-time = "2025-10-14T15:04:31.53Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f8/2c5f479fb531ce2f0564eda479faecf253d886b1ab3630a39b7bf7362d46/watchfiles-1.1.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:f57b396167a2565a4e8b5e56a5a1c537571733992b226f4f1197d79e94cf0ae5", size = 406529, upload-time = "2025-10-14T15:04:32.899Z" }, + { url = "https://files.pythonhosted.org/packages/fe/cd/f515660b1f32f65df671ddf6f85bfaca621aee177712874dc30a97397977/watchfiles-1.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:421e29339983e1bebc281fab40d812742268ad057db4aee8c4d2bce0af43b741", size = 394384, upload-time = "2025-10-14T15:04:33.761Z" }, + { url = "https://files.pythonhosted.org/packages/7b/c3/28b7dc99733eab43fca2d10f55c86e03bd6ab11ca31b802abac26b23d161/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e43d39a741e972bab5d8100b5cdacf69db64e34eb19b6e9af162bccf63c5cc6", size = 448789, upload-time = "2025-10-14T15:04:34.679Z" }, + { url = "https://files.pythonhosted.org/packages/4a/24/33e71113b320030011c8e4316ccca04194bf0cbbaeee207f00cbc7d6b9f5/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f537afb3276d12814082a2e9b242bdcf416c2e8fd9f799a737990a1dbe906e5b", size = 460521, upload-time = "2025-10-14T15:04:35.963Z" }, + { url = "https://files.pythonhosted.org/packages/f4/c3/3c9a55f255aa57b91579ae9e98c88704955fa9dac3e5614fb378291155df/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2cd9e04277e756a2e2d2543d65d1e2166d6fd4c9b183f8808634fda23f17b14", size = 488722, upload-time = "2025-10-14T15:04:37.091Z" }, + { url = "https://files.pythonhosted.org/packages/49/36/506447b73eb46c120169dc1717fe2eff07c234bb3232a7200b5f5bd816e9/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f3f58818dc0b07f7d9aa7fe9eb1037aecb9700e63e1f6acfed13e9fef648f5d", size = 596088, upload-time = "2025-10-14T15:04:38.39Z" }, + { url = "https://files.pythonhosted.org/packages/82/ab/5f39e752a9838ec4d52e9b87c1e80f1ee3ccdbe92e183c15b6577ab9de16/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9bb9f66367023ae783551042d31b1d7fd422e8289eedd91f26754a66f44d5cff", size = 472923, upload-time = "2025-10-14T15:04:39.666Z" }, + { url = "https://files.pythonhosted.org/packages/af/b9/a419292f05e302dea372fa7e6fda5178a92998411f8581b9830d28fb9edb/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aebfd0861a83e6c3d1110b78ad54704486555246e542be3e2bb94195eabb2606", size = 456080, upload-time = "2025-10-14T15:04:40.643Z" }, + { url = "https://files.pythonhosted.org/packages/b0/c3/d5932fd62bde1a30c36e10c409dc5d54506726f08cb3e1d8d0ba5e2bc8db/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5fac835b4ab3c6487b5dbad78c4b3724e26bcc468e886f8ba8cc4306f68f6701", size = 629432, upload-time = "2025-10-14T15:04:41.789Z" }, + { url = "https://files.pythonhosted.org/packages/f7/77/16bddd9779fafb795f1a94319dc965209c5641db5bf1edbbccace6d1b3c0/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:399600947b170270e80134ac854e21b3ccdefa11a9529a3decc1327088180f10", size = 623046, upload-time = "2025-10-14T15:04:42.718Z" }, + { url = "https://files.pythonhosted.org/packages/46/ef/f2ecb9a0f342b4bfad13a2787155c6ee7ce792140eac63a34676a2feeef2/watchfiles-1.1.1-cp311-cp311-win32.whl", hash = "sha256:de6da501c883f58ad50db3a32ad397b09ad29865b5f26f64c24d3e3281685849", size = 271473, upload-time = "2025-10-14T15:04:43.624Z" }, + { url = "https://files.pythonhosted.org/packages/94/bc/f42d71125f19731ea435c3948cad148d31a64fccde3867e5ba4edee901f9/watchfiles-1.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:35c53bd62a0b885bf653ebf6b700d1bf05debb78ad9292cf2a942b23513dc4c4", size = 287598, upload-time = "2025-10-14T15:04:44.516Z" }, + { url = "https://files.pythonhosted.org/packages/57/c9/a30f897351f95bbbfb6abcadafbaca711ce1162f4db95fc908c98a9165f3/watchfiles-1.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:57ca5281a8b5e27593cb7d82c2ac927ad88a96ed406aa446f6344e4328208e9e", size = 277210, upload-time = "2025-10-14T15:04:45.883Z" }, + { url = "https://files.pythonhosted.org/packages/74/d5/f039e7e3c639d9b1d09b07ea412a6806d38123f0508e5f9b48a87b0a76cc/watchfiles-1.1.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8c89f9f2f740a6b7dcc753140dd5e1ab9215966f7a3530d0c0705c83b401bd7d", size = 404745, upload-time = "2025-10-14T15:04:46.731Z" }, + { url = "https://files.pythonhosted.org/packages/a5/96/a881a13aa1349827490dab2d363c8039527060cfcc2c92cc6d13d1b1049e/watchfiles-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bd404be08018c37350f0d6e34676bd1e2889990117a2b90070b3007f172d0610", size = 391769, upload-time = "2025-10-14T15:04:48.003Z" }, + { url = "https://files.pythonhosted.org/packages/4b/5b/d3b460364aeb8da471c1989238ea0e56bec24b6042a68046adf3d9ddb01c/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8526e8f916bb5b9a0a777c8317c23ce65de259422bba5b31325a6fa6029d33af", size = 449374, upload-time = "2025-10-14T15:04:49.179Z" }, + { url = "https://files.pythonhosted.org/packages/b9/44/5769cb62d4ed055cb17417c0a109a92f007114a4e07f30812a73a4efdb11/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2edc3553362b1c38d9f06242416a5d8e9fe235c204a4072e988ce2e5bb1f69f6", size = 459485, upload-time = "2025-10-14T15:04:50.155Z" }, + { url = "https://files.pythonhosted.org/packages/19/0c/286b6301ded2eccd4ffd0041a1b726afda999926cf720aab63adb68a1e36/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30f7da3fb3f2844259cba4720c3fc7138eb0f7b659c38f3bfa65084c7fc7abce", size = 488813, upload-time = "2025-10-14T15:04:51.059Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2b/8530ed41112dd4a22f4dcfdb5ccf6a1baad1ff6eed8dc5a5f09e7e8c41c7/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8979280bdafff686ba5e4d8f97840f929a87ed9cdf133cbbd42f7766774d2aa", size = 594816, upload-time = "2025-10-14T15:04:52.031Z" }, + { url = "https://files.pythonhosted.org/packages/ce/d2/f5f9fb49489f184f18470d4f99f4e862a4b3e9ac2865688eb2099e3d837a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dcc5c24523771db3a294c77d94771abcfcb82a0e0ee8efd910c37c59ec1b31bb", size = 475186, upload-time = "2025-10-14T15:04:53.064Z" }, + { url = "https://files.pythonhosted.org/packages/cf/68/5707da262a119fb06fbe214d82dd1fe4a6f4af32d2d14de368d0349eb52a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db5d7ae38ff20153d542460752ff397fcf5c96090c1230803713cf3147a6803", size = 456812, upload-time = "2025-10-14T15:04:55.174Z" }, + { url = "https://files.pythonhosted.org/packages/66/ab/3cbb8756323e8f9b6f9acb9ef4ec26d42b2109bce830cc1f3468df20511d/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:28475ddbde92df1874b6c5c8aaeb24ad5be47a11f87cde5a28ef3835932e3e94", size = 630196, upload-time = "2025-10-14T15:04:56.22Z" }, + { url = "https://files.pythonhosted.org/packages/78/46/7152ec29b8335f80167928944a94955015a345440f524d2dfe63fc2f437b/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:36193ed342f5b9842edd3532729a2ad55c4160ffcfa3700e0d54be496b70dd43", size = 622657, upload-time = "2025-10-14T15:04:57.521Z" }, + { url = "https://files.pythonhosted.org/packages/0a/bf/95895e78dd75efe9a7f31733607f384b42eb5feb54bd2eb6ed57cc2e94f4/watchfiles-1.1.1-cp312-cp312-win32.whl", hash = "sha256:859e43a1951717cc8de7f4c77674a6d389b106361585951d9e69572823f311d9", size = 272042, upload-time = "2025-10-14T15:04:59.046Z" }, + { url = "https://files.pythonhosted.org/packages/87/0a/90eb755f568de2688cb220171c4191df932232c20946966c27a59c400850/watchfiles-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:91d4c9a823a8c987cce8fa2690923b069966dabb196dd8d137ea2cede885fde9", size = 288410, upload-time = "2025-10-14T15:05:00.081Z" }, + { url = "https://files.pythonhosted.org/packages/36/76/f322701530586922fbd6723c4f91ace21364924822a8772c549483abed13/watchfiles-1.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:a625815d4a2bdca61953dbba5a39d60164451ef34c88d751f6c368c3ea73d404", size = 278209, upload-time = "2025-10-14T15:05:01.168Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f4/f750b29225fe77139f7ae5de89d4949f5a99f934c65a1f1c0b248f26f747/watchfiles-1.1.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:130e4876309e8686a5e37dba7d5e9bc77e6ed908266996ca26572437a5271e18", size = 404321, upload-time = "2025-10-14T15:05:02.063Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f9/f07a295cde762644aa4c4bb0f88921d2d141af45e735b965fb2e87858328/watchfiles-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f3bde70f157f84ece3765b42b4a52c6ac1a50334903c6eaf765362f6ccca88a", size = 391783, upload-time = "2025-10-14T15:05:03.052Z" }, + { url = "https://files.pythonhosted.org/packages/bc/11/fc2502457e0bea39a5c958d86d2cb69e407a4d00b85735ca724bfa6e0d1a/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e0b1fe858430fc0251737ef3824c54027bedb8c37c38114488b8e131cf8219", size = 449279, upload-time = "2025-10-14T15:05:04.004Z" }, + { url = "https://files.pythonhosted.org/packages/e3/1f/d66bc15ea0b728df3ed96a539c777acfcad0eb78555ad9efcaa1274688f0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f27db948078f3823a6bb3b465180db8ebecf26dd5dae6f6180bd87383b6b4428", size = 459405, upload-time = "2025-10-14T15:05:04.942Z" }, + { url = "https://files.pythonhosted.org/packages/be/90/9f4a65c0aec3ccf032703e6db02d89a157462fbb2cf20dd415128251cac0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:059098c3a429f62fc98e8ec62b982230ef2c8df68c79e826e37b895bc359a9c0", size = 488976, upload-time = "2025-10-14T15:05:05.905Z" }, + { url = "https://files.pythonhosted.org/packages/37/57/ee347af605d867f712be7029bb94c8c071732a4b44792e3176fa3c612d39/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfb5862016acc9b869bb57284e6cb35fdf8e22fe59f7548858e2f971d045f150", size = 595506, upload-time = "2025-10-14T15:05:06.906Z" }, + { url = "https://files.pythonhosted.org/packages/a8/78/cc5ab0b86c122047f75e8fc471c67a04dee395daf847d3e59381996c8707/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:319b27255aacd9923b8a276bb14d21a5f7ff82564c744235fc5eae58d95422ae", size = 474936, upload-time = "2025-10-14T15:05:07.906Z" }, + { url = "https://files.pythonhosted.org/packages/62/da/def65b170a3815af7bd40a3e7010bf6ab53089ef1b75d05dd5385b87cf08/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c755367e51db90e75b19454b680903631d41f9e3607fbd941d296a020c2d752d", size = 456147, upload-time = "2025-10-14T15:05:09.138Z" }, + { url = "https://files.pythonhosted.org/packages/57/99/da6573ba71166e82d288d4df0839128004c67d2778d3b566c138695f5c0b/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c22c776292a23bfc7237a98f791b9ad3144b02116ff10d820829ce62dff46d0b", size = 630007, upload-time = "2025-10-14T15:05:10.117Z" }, + { url = "https://files.pythonhosted.org/packages/a8/51/7439c4dd39511368849eb1e53279cd3454b4a4dbace80bab88feeb83c6b5/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3a476189be23c3686bc2f4321dd501cb329c0a0469e77b7b534ee10129ae6374", size = 622280, upload-time = "2025-10-14T15:05:11.146Z" }, + { url = "https://files.pythonhosted.org/packages/95/9c/8ed97d4bba5db6fdcdb2b298d3898f2dd5c20f6b73aee04eabe56c59677e/watchfiles-1.1.1-cp313-cp313-win32.whl", hash = "sha256:bf0a91bfb5574a2f7fc223cf95eeea79abfefa404bf1ea5e339c0c1560ae99a0", size = 272056, upload-time = "2025-10-14T15:05:12.156Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/c14e28429f744a260d8ceae18bf58c1d5fa56b50d006a7a9f80e1882cb0d/watchfiles-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:52e06553899e11e8074503c8e716d574adeeb7e68913115c4b3653c53f9bae42", size = 288162, upload-time = "2025-10-14T15:05:13.208Z" }, + { url = "https://files.pythonhosted.org/packages/dc/61/fe0e56c40d5cd29523e398d31153218718c5786b5e636d9ae8ae79453d27/watchfiles-1.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:ac3cc5759570cd02662b15fbcd9d917f7ecd47efe0d6b40474eafd246f91ea18", size = 277909, upload-time = "2025-10-14T15:05:14.49Z" }, + { url = "https://files.pythonhosted.org/packages/79/42/e0a7d749626f1e28c7108a99fb9bf524b501bbbeb9b261ceecde644d5a07/watchfiles-1.1.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:563b116874a9a7ce6f96f87cd0b94f7faf92d08d0021e837796f0a14318ef8da", size = 403389, upload-time = "2025-10-14T15:05:15.777Z" }, + { url = "https://files.pythonhosted.org/packages/15/49/08732f90ce0fbbc13913f9f215c689cfc9ced345fb1bcd8829a50007cc8d/watchfiles-1.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3ad9fe1dae4ab4212d8c91e80b832425e24f421703b5a42ef2e4a1e215aff051", size = 389964, upload-time = "2025-10-14T15:05:16.85Z" }, + { url = "https://files.pythonhosted.org/packages/27/0d/7c315d4bd5f2538910491a0393c56bf70d333d51bc5b34bee8e68e8cea19/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce70f96a46b894b36eba678f153f052967a0d06d5b5a19b336ab0dbbd029f73e", size = 448114, upload-time = "2025-10-14T15:05:17.876Z" }, + { url = "https://files.pythonhosted.org/packages/c3/24/9e096de47a4d11bc4df41e9d1e61776393eac4cb6eb11b3e23315b78b2cc/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb467c999c2eff23a6417e58d75e5828716f42ed8289fe6b77a7e5a91036ca70", size = 460264, upload-time = "2025-10-14T15:05:18.962Z" }, + { url = "https://files.pythonhosted.org/packages/cc/0f/e8dea6375f1d3ba5fcb0b3583e2b493e77379834c74fd5a22d66d85d6540/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:836398932192dae4146c8f6f737d74baeac8b70ce14831a239bdb1ca882fc261", size = 487877, upload-time = "2025-10-14T15:05:20.094Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5b/df24cfc6424a12deb41503b64d42fbea6b8cb357ec62ca84a5a3476f654a/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:743185e7372b7bc7c389e1badcc606931a827112fbbd37f14c537320fca08620", size = 595176, upload-time = "2025-10-14T15:05:21.134Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b5/853b6757f7347de4e9b37e8cc3289283fb983cba1ab4d2d7144694871d9c/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afaeff7696e0ad9f02cbb8f56365ff4686ab205fcf9c4c5b6fdfaaa16549dd04", size = 473577, upload-time = "2025-10-14T15:05:22.306Z" }, + { url = "https://files.pythonhosted.org/packages/e1/f7/0a4467be0a56e80447c8529c9fce5b38eab4f513cb3d9bf82e7392a5696b/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7eb7da0eb23aa2ba036d4f616d46906013a68caf61b7fdbe42fc8b25132e77", size = 455425, upload-time = "2025-10-14T15:05:23.348Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e0/82583485ea00137ddf69bc84a2db88bd92ab4a6e3c405e5fb878ead8d0e7/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:831a62658609f0e5c64178211c942ace999517f5770fe9436be4c2faeba0c0ef", size = 628826, upload-time = "2025-10-14T15:05:24.398Z" }, + { url = "https://files.pythonhosted.org/packages/28/9a/a785356fccf9fae84c0cc90570f11702ae9571036fb25932f1242c82191c/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf", size = 622208, upload-time = "2025-10-14T15:05:25.45Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f4/0872229324ef69b2c3edec35e84bd57a1289e7d3fe74588048ed8947a323/watchfiles-1.1.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:d1715143123baeeaeadec0528bb7441103979a1d5f6fd0e1f915383fea7ea6d5", size = 404315, upload-time = "2025-10-14T15:05:26.501Z" }, + { url = "https://files.pythonhosted.org/packages/7b/22/16d5331eaed1cb107b873f6ae1b69e9ced582fcf0c59a50cd84f403b1c32/watchfiles-1.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:39574d6370c4579d7f5d0ad940ce5b20db0e4117444e39b6d8f99db5676c52fd", size = 390869, upload-time = "2025-10-14T15:05:27.649Z" }, + { url = "https://files.pythonhosted.org/packages/b2/7e/5643bfff5acb6539b18483128fdc0ef2cccc94a5b8fbda130c823e8ed636/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7365b92c2e69ee952902e8f70f3ba6360d0d596d9299d55d7d386df84b6941fb", size = 449919, upload-time = "2025-10-14T15:05:28.701Z" }, + { url = "https://files.pythonhosted.org/packages/51/2e/c410993ba5025a9f9357c376f48976ef0e1b1aefb73b97a5ae01a5972755/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bfff9740c69c0e4ed32416f013f3c45e2ae42ccedd1167ef2d805c000b6c71a5", size = 460845, upload-time = "2025-10-14T15:05:30.064Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a4/2df3b404469122e8680f0fcd06079317e48db58a2da2950fb45020947734/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b27cf2eb1dda37b2089e3907d8ea92922b673c0c427886d4edc6b94d8dfe5db3", size = 489027, upload-time = "2025-10-14T15:05:31.064Z" }, + { url = "https://files.pythonhosted.org/packages/ea/84/4587ba5b1f267167ee715b7f66e6382cca6938e0a4b870adad93e44747e6/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:526e86aced14a65a5b0ec50827c745597c782ff46b571dbfe46192ab9e0b3c33", size = 595615, upload-time = "2025-10-14T15:05:32.074Z" }, + { url = "https://files.pythonhosted.org/packages/6a/0f/c6988c91d06e93cd0bb3d4a808bcf32375ca1904609835c3031799e3ecae/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04e78dd0b6352db95507fd8cb46f39d185cf8c74e4cf1e4fbad1d3df96faf510", size = 474836, upload-time = "2025-10-14T15:05:33.209Z" }, + { url = "https://files.pythonhosted.org/packages/b4/36/ded8aebea91919485b7bbabbd14f5f359326cb5ec218cd67074d1e426d74/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c85794a4cfa094714fb9c08d4a218375b2b95b8ed1666e8677c349906246c05", size = 455099, upload-time = "2025-10-14T15:05:34.189Z" }, + { url = "https://files.pythonhosted.org/packages/98/e0/8c9bdba88af756a2fce230dd365fab2baf927ba42cd47521ee7498fd5211/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:74d5012b7630714b66be7b7b7a78855ef7ad58e8650c73afc4c076a1f480a8d6", size = 630626, upload-time = "2025-10-14T15:05:35.216Z" }, + { url = "https://files.pythonhosted.org/packages/2a/84/a95db05354bf2d19e438520d92a8ca475e578c647f78f53197f5a2f17aaf/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:8fbe85cb3201c7d380d3d0b90e63d520f15d6afe217165d7f98c9c649654db81", size = 622519, upload-time = "2025-10-14T15:05:36.259Z" }, + { url = "https://files.pythonhosted.org/packages/1d/ce/d8acdc8de545de995c339be67711e474c77d643555a9bb74a9334252bd55/watchfiles-1.1.1-cp314-cp314-win32.whl", hash = "sha256:3fa0b59c92278b5a7800d3ee7733da9d096d4aabcfabb9a928918bd276ef9b9b", size = 272078, upload-time = "2025-10-14T15:05:37.63Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c9/a74487f72d0451524be827e8edec251da0cc1fcf111646a511ae752e1a3d/watchfiles-1.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:c2047d0b6cea13b3316bdbafbfa0c4228ae593d995030fda39089d36e64fc03a", size = 287664, upload-time = "2025-10-14T15:05:38.95Z" }, + { url = "https://files.pythonhosted.org/packages/df/b8/8ac000702cdd496cdce998c6f4ee0ca1f15977bba51bdf07d872ebdfc34c/watchfiles-1.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:842178b126593addc05acf6fce960d28bc5fae7afbaa2c6c1b3a7b9460e5be02", size = 277154, upload-time = "2025-10-14T15:05:39.954Z" }, + { url = "https://files.pythonhosted.org/packages/47/a8/e3af2184707c29f0f14b1963c0aace6529f9d1b8582d5b99f31bbf42f59e/watchfiles-1.1.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:88863fbbc1a7312972f1c511f202eb30866370ebb8493aef2812b9ff28156a21", size = 403820, upload-time = "2025-10-14T15:05:40.932Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ec/e47e307c2f4bd75f9f9e8afbe3876679b18e1bcec449beca132a1c5ffb2d/watchfiles-1.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:55c7475190662e202c08c6c0f4d9e345a29367438cf8e8037f3155e10a88d5a5", size = 390510, upload-time = "2025-10-14T15:05:41.945Z" }, + { url = "https://files.pythonhosted.org/packages/d5/a0/ad235642118090f66e7b2f18fd5c42082418404a79205cdfca50b6309c13/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f53fa183d53a1d7a8852277c92b967ae99c2d4dcee2bfacff8868e6e30b15f7", size = 448408, upload-time = "2025-10-14T15:05:43.385Z" }, + { url = "https://files.pythonhosted.org/packages/df/85/97fa10fd5ff3332ae17e7e40e20784e419e28521549780869f1413742e9d/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6aae418a8b323732fa89721d86f39ec8f092fc2af67f4217a2b07fd3e93c6101", size = 458968, upload-time = "2025-10-14T15:05:44.404Z" }, + { url = "https://files.pythonhosted.org/packages/47/c2/9059c2e8966ea5ce678166617a7f75ecba6164375f3b288e50a40dc6d489/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f096076119da54a6080e8920cbdaac3dbee667eb91dcc5e5b78840b87415bd44", size = 488096, upload-time = "2025-10-14T15:05:45.398Z" }, + { url = "https://files.pythonhosted.org/packages/94/44/d90a9ec8ac309bc26db808a13e7bfc0e4e78b6fc051078a554e132e80160/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00485f441d183717038ed2e887a7c868154f216877653121068107b227a2f64c", size = 596040, upload-time = "2025-10-14T15:05:46.502Z" }, + { url = "https://files.pythonhosted.org/packages/95/68/4e3479b20ca305cfc561db3ed207a8a1c745ee32bf24f2026a129d0ddb6e/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a55f3e9e493158d7bfdb60a1165035f1cf7d320914e7b7ea83fe22c6023b58fc", size = 473847, upload-time = "2025-10-14T15:05:47.484Z" }, + { url = "https://files.pythonhosted.org/packages/4f/55/2af26693fd15165c4ff7857e38330e1b61ab8c37d15dc79118cdba115b7a/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c91ed27800188c2ae96d16e3149f199d62f86c7af5f5f4d2c61a3ed8cd3666c", size = 455072, upload-time = "2025-10-14T15:05:48.928Z" }, + { url = "https://files.pythonhosted.org/packages/66/1d/d0d200b10c9311ec25d2273f8aad8c3ef7cc7ea11808022501811208a750/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:311ff15a0bae3714ffb603e6ba6dbfba4065ab60865d15a6ec544133bdb21099", size = 629104, upload-time = "2025-10-14T15:05:49.908Z" }, + { url = "https://files.pythonhosted.org/packages/e3/bd/fa9bb053192491b3867ba07d2343d9f2252e00811567d30ae8d0f78136fe/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01", size = 622112, upload-time = "2025-10-14T15:05:50.941Z" }, + { url = "https://files.pythonhosted.org/packages/a4/68/a7303a15cc797ab04d58f1fea7f67c50bd7f80090dfd7e750e7576e07582/watchfiles-1.1.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c882d69f6903ef6092bedfb7be973d9319940d56b8427ab9187d1ecd73438a70", size = 409220, upload-time = "2025-10-14T15:05:51.917Z" }, + { url = "https://files.pythonhosted.org/packages/99/b8/d1857ce9ac76034c053fa7ef0e0ef92d8bd031e842ea6f5171725d31e88f/watchfiles-1.1.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d6ff426a7cb54f310d51bfe83fe9f2bbe40d540c741dc974ebc30e6aa238f52e", size = 396712, upload-time = "2025-10-14T15:05:53.437Z" }, + { url = "https://files.pythonhosted.org/packages/41/7a/da7ada566f48beaa6a30b13335b49d1f6febaf3a5ddbd1d92163a1002cf4/watchfiles-1.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79ff6c6eadf2e3fc0d7786331362e6ef1e51125892c75f1004bd6b52155fb956", size = 451462, upload-time = "2025-10-14T15:05:54.742Z" }, + { url = "https://files.pythonhosted.org/packages/e2/b2/7cb9e0d5445a8d45c4cccd68a590d9e3a453289366b96ff37d1075aaebef/watchfiles-1.1.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c1f5210f1b8fc91ead1283c6fd89f70e76fb07283ec738056cf34d51e9c1d62c", size = 460811, upload-time = "2025-10-14T15:05:55.743Z" }, + { url = "https://files.pythonhosted.org/packages/04/9d/b07d4491dde6db6ea6c680fdec452f4be363d65c82004faf2d853f59b76f/watchfiles-1.1.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b9c4702f29ca48e023ffd9b7ff6b822acdf47cb1ff44cb490a3f1d5ec8987e9c", size = 490576, upload-time = "2025-10-14T15:05:56.983Z" }, + { url = "https://files.pythonhosted.org/packages/56/03/e64dcab0a1806157db272a61b7891b062f441a30580a581ae72114259472/watchfiles-1.1.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:acb08650863767cbc58bca4813b92df4d6c648459dcaa3d4155681962b2aa2d3", size = 597726, upload-time = "2025-10-14T15:05:57.986Z" }, + { url = "https://files.pythonhosted.org/packages/5c/8e/a827cf4a8d5f2903a19a934dcf512082eb07675253e154d4cd9367978a58/watchfiles-1.1.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08af70fd77eee58549cd69c25055dc344f918d992ff626068242259f98d598a2", size = 474900, upload-time = "2025-10-14T15:05:59.378Z" }, + { url = "https://files.pythonhosted.org/packages/dc/a6/94fed0b346b85b22303a12eee5f431006fae6af70d841cac2f4403245533/watchfiles-1.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c3631058c37e4a0ec440bf583bc53cdbd13e5661bb6f465bc1d88ee9a0a4d02", size = 457521, upload-time = "2025-10-14T15:06:00.419Z" }, + { url = "https://files.pythonhosted.org/packages/c4/64/bc3331150e8f3c778d48a4615d4b72b3d2d87868635e6c54bbd924946189/watchfiles-1.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:cf57a27fb986c6243d2ee78392c503826056ffe0287e8794503b10fb51b881be", size = 632191, upload-time = "2025-10-14T15:06:01.621Z" }, + { url = "https://files.pythonhosted.org/packages/e4/84/f39e19549c2f3ec97225dcb2ceb9a7bb3c5004ed227aad1f321bf0ff2051/watchfiles-1.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d7e7067c98040d646982daa1f37a33d3544138ea155536c2e0e63e07ff8a7e0f", size = 623923, upload-time = "2025-10-14T15:06:02.671Z" }, + { url = "https://files.pythonhosted.org/packages/0e/24/0759ae15d9a0c9c5fe946bd4cf45ab9e7bad7cfede2c06dc10f59171b29f/watchfiles-1.1.1-cp39-cp39-win32.whl", hash = "sha256:6c9c9262f454d1c4d8aaa7050121eb4f3aea197360553699520767daebf2180b", size = 274010, upload-time = "2025-10-14T15:06:03.779Z" }, + { url = "https://files.pythonhosted.org/packages/7e/3b/eb26cddd4dfa081e2bf6918be3b2fc05ee3b55c1d21331d5562ee0c6aaad/watchfiles-1.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:74472234c8370669850e1c312490f6026d132ca2d396abfad8830b4f1c096957", size = 289090, upload-time = "2025-10-14T15:06:04.821Z" }, + { url = "https://files.pythonhosted.org/packages/ba/4c/a888c91e2e326872fa4705095d64acd8aa2fb9c1f7b9bd0588f33850516c/watchfiles-1.1.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:17ef139237dfced9da49fb7f2232c86ca9421f666d78c264c7ffca6601d154c3", size = 409611, upload-time = "2025-10-14T15:06:05.809Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c7/5420d1943c8e3ce1a21c0a9330bcf7edafb6aa65d26b21dbb3267c9e8112/watchfiles-1.1.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:672b8adf25b1a0d35c96b5888b7b18699d27d4194bac8beeae75be4b7a3fc9b2", size = 396889, upload-time = "2025-10-14T15:06:07.035Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e5/0072cef3804ce8d3aaddbfe7788aadff6b3d3f98a286fdbee9fd74ca59a7/watchfiles-1.1.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77a13aea58bc2b90173bc69f2a90de8e282648939a00a602e1dc4ee23e26b66d", size = 451616, upload-time = "2025-10-14T15:06:08.072Z" }, + { url = "https://files.pythonhosted.org/packages/83/4e/b87b71cbdfad81ad7e83358b3e447fedd281b880a03d64a760fe0a11fc2e/watchfiles-1.1.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b495de0bb386df6a12b18335a0285dda90260f51bdb505503c02bcd1ce27a8b", size = 458413, upload-time = "2025-10-14T15:06:09.209Z" }, + { url = "https://files.pythonhosted.org/packages/d3/8e/e500f8b0b77be4ff753ac94dc06b33d8f0d839377fee1b78e8c8d8f031bf/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:db476ab59b6765134de1d4fe96a1a9c96ddf091683599be0f26147ea1b2e4b88", size = 408250, upload-time = "2025-10-14T15:06:10.264Z" }, + { url = "https://files.pythonhosted.org/packages/bd/95/615e72cd27b85b61eec764a5ca51bd94d40b5adea5ff47567d9ebc4d275a/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:89eef07eee5e9d1fda06e38822ad167a044153457e6fd997f8a858ab7564a336", size = 396117, upload-time = "2025-10-14T15:06:11.28Z" }, + { url = "https://files.pythonhosted.org/packages/c9/81/e7fe958ce8a7fb5c73cc9fb07f5aeaf755e6aa72498c57d760af760c91f8/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce19e06cbda693e9e7686358af9cd6f5d61312ab8b00488bc36f5aabbaf77e24", size = 450493, upload-time = "2025-10-14T15:06:12.321Z" }, + { url = "https://files.pythonhosted.org/packages/6e/d4/ed38dd3b1767193de971e694aa544356e63353c33a85d948166b5ff58b9e/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e6f39af2eab0118338902798b5aa6664f46ff66bc0280de76fca67a7f262a49", size = 457546, upload-time = "2025-10-14T15:06:13.372Z" }, + { url = "https://files.pythonhosted.org/packages/00/db/38a2c52fdbbfe2fc7ffaaaaaebc927d52b9f4d5139bba3186c19a7463001/watchfiles-1.1.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:cdab464fee731e0884c35ae3588514a9bcf718d0e2c82169c1c4a85cc19c3c7f", size = 409210, upload-time = "2025-10-14T15:06:14.492Z" }, + { url = "https://files.pythonhosted.org/packages/d1/43/d7e8b71f6c21ff813ee8da1006f89b6c7fff047fb4c8b16ceb5e840599c5/watchfiles-1.1.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:3dbd8cbadd46984f802f6d479b7e3afa86c42d13e8f0f322d669d79722c8ec34", size = 397286, upload-time = "2025-10-14T15:06:16.177Z" }, + { url = "https://files.pythonhosted.org/packages/1f/5d/884074a5269317e75bd0b915644b702b89de73e61a8a7446e2b225f45b1f/watchfiles-1.1.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5524298e3827105b61951a29c3512deb9578586abf3a7c5da4a8069df247cccc", size = 451768, upload-time = "2025-10-14T15:06:18.266Z" }, + { url = "https://files.pythonhosted.org/packages/17/71/7ffcaa9b5e8961a25026058058c62ec8f604d2a6e8e1e94bee8a09e1593f/watchfiles-1.1.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b943d3668d61cfa528eb949577479d3b077fd25fb83c641235437bc0b5bc60e", size = 458561, upload-time = "2025-10-14T15:06:19.323Z" }, +] + [[package]] name = "wcwidth" version = "0.5.3" @@ -4306,6 +6414,25 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22", size = 23591, upload-time = "2025-08-12T05:53:20.674Z" }, ] +[[package]] +name = "xai-sdk" +version = "1.8.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp", marker = "python_full_version >= '3.10'" }, + { name = "googleapis-common-protos", marker = "python_full_version >= '3.10'" }, + { name = "grpcio", marker = "python_full_version >= '3.10'" }, + { name = "opentelemetry-sdk", marker = "python_full_version >= '3.10'" }, + { name = "packaging", version = "25.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "protobuf", marker = "python_full_version >= '3.10'" }, + { name = "pydantic", marker = "python_full_version >= '3.10'" }, + { name = "requests", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d3/41/e39d9207c6f4ba0fd98c1f42747c57edd7785389e1b7464afb2edf844501/xai_sdk-1.8.1.tar.gz", hash = "sha256:3f3ff2a98888b3bb2b6d8184c82a56d475d501711e78e5e748073d5a67be0804", size = 391417, upload-time = "2026-03-11T03:04:24.404Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/76/4eba837410a4969c70f961a2c5d6a90761167f61a775525772f64b3f7eb0/xai_sdk-1.8.1-py3-none-any.whl", hash = "sha256:9a503a5716f9402a8639da5b5c806cfbef7cda7809c8c8bd090e26c2a5e32dad", size = 242353, upload-time = "2026-03-11T03:04:22.758Z" }, +] + [[package]] name = "yarl" version = "1.22.0" From 5b49e257df437d901de2a190c4710d0f0f105be0 Mon Sep 17 00:00:00 2001 From: jniestroy Date: Tue, 17 Mar 2026 11:06:36 -0400 Subject: [PATCH 07/27] workign --- build.sh | 14 +++++++ compose.yaml | 23 +++++----- mds/src/fairscape_mds/crud/interpretation.py | 42 +++++++++++++++++-- .../fairscape_mds/routers/interpretation.py | 2 +- 4 files changed, 66 insertions(+), 15 deletions(-) create mode 100644 build.sh diff --git a/build.sh b/build.sh new file mode 100644 index 0000000..7c45cf5 --- /dev/null +++ b/build.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +VERSION=v1 + +# Generate date in YYYY-MM-DD format +DATE=$(date '+%Y-%m-%d') + +# Build the Docker image +sudo docker build \ + --no-cache \ + -f Dockerfile \ + -t ghcr.io/fairscape/mds_python:RELEASE.${DATE}.${VERSION} . + +sudo docker push ghcr.io/fairscape/mds_python:RELEASE.${DATE}.${VERSION} diff --git a/compose.yaml b/compose.yaml index 2a5e32e..a718c94 100644 --- a/compose.yaml +++ b/compose.yaml @@ -14,6 +14,7 @@ services: env_file: "./deploy/docker_compose.env" environment: - GEMINI_API_KEY=${GEMINI_API_KEY} + - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY} - GITHUB_TOKEN=${GITHUB_TOKEN} command: - "uv" @@ -31,17 +32,17 @@ services: depends_on: - mongo - minio - fairscape-frontend: - image: ghcr.io/fairscape/fairscapefrontendlocal:RELEASE.2026-02-24 - ports: - - "5173:80" - environment: - VITE_FAIRSCAPE_API_URL: "http://localhost:8080/api" - SEMANTIC_SEARCH_ENABLED: "false" - depends_on: - - fairscape-api - - mongo - - minio + # fairscape-frontend: + # image: ghcr.io/fairscape/fairscapefrontendlocal:RELEASE.2026-02-24 + # ports: + # - "5173:80" + # environment: + # VITE_FAIRSCAPE_API_URL: "http://localhost:8080/api" + # SEMANTIC_SEARCH_ENABLED: "false" + # depends_on: + # - fairscape-api + # - mongo + # - minio redis: image: redis:7.2.4 diff --git a/mds/src/fairscape_mds/crud/interpretation.py b/mds/src/fairscape_mds/crud/interpretation.py index d6aec7f..c843577 100644 --- a/mds/src/fairscape_mds/crud/interpretation.py +++ b/mds/src/fairscape_mds/crud/interpretation.py @@ -59,7 +59,7 @@ def run_async(coro): # --------------------------------------------------------------------------- MAX_SOFTWARE_BYTES = 50_000 # 50KB per software entity -CODE_EXTENSIONS = {".py", ".r", ".R", ".sh", ".pl", ".java", ".scala", ".jl", ".m", ".cpp", ".go", ".rs", ".ipynb"} +CODE_EXTENSIONS = {".py", ".r", ".R", ".sh", ".pl", ".java", ".scala", ".jl", ".m", ".cpp", ".go", ".rs", ".ipynb", ".md"} GITHUB_REPO_PATTERN = re.compile(r"https?://github\.com/([^/]+)/([^/]+?)(?:\.git)?(?:/.*)?$") GITHUB_FILE_PATTERN = re.compile( r"https?://github\.com/([^/]+)/([^/]+)/blob/([^/]+)/(.*)" @@ -785,8 +785,16 @@ def build_and_store( ann_dict = ann.model_dump(by_alias=True, exclude_none=True, mode="json") graph_dict[ann.guid] = ann_dict - # Add evi:annotatedBy reverse link on the computation node - comp_id = ann.annotates.guid if hasattr(ann.annotates, 'guid') else str(ann.annotates) + # Extract comp_id robustly: handle IdentifierValue, dict, or string + annotates = ann.annotates + if hasattr(annotates, 'guid'): + comp_id = annotates.guid + elif isinstance(annotates, dict): + comp_id = annotates.get("@id", "") + else: + comp_id = str(annotates) + + # Add evi:annotatedBy reverse link on the computation node in the AEG graph if comp_id and comp_id in graph_dict: existing = graph_dict[comp_id].get("evi:annotatedBy", []) if isinstance(existing, dict): @@ -795,6 +803,12 @@ def build_and_store( existing = [] existing.append({"@id": ann.guid}) graph_dict[comp_id]["evi:annotatedBy"] = existing + else: + logger.warning( + "Could not back-link annotation %s -> computation %s " + "(not found in graph_dict, type=%s)", + ann.guid, comp_id, type(annotates).__name__, + ) # Build step annotation refs step_ann_refs = [{"@id": ann.guid} for ann in step_annotations] @@ -853,6 +867,22 @@ def build_and_store( {"$set": {"metadata.hasAnnotatedEvidenceGraph": {"@id": aeg_id}}} ) + # Update each computation's MongoDB document with evi:annotatedBy + for ann in step_annotations: + annotates = ann.annotates + if hasattr(annotates, 'guid'): + comp_id = annotates.guid + elif isinstance(annotates, dict): + comp_id = annotates.get("@id", "") + else: + comp_id = str(annotates) + + if comp_id: + self.config.identifierCollection.update_one( + {"@id": comp_id}, + {"$addToSet": {"metadata.evi:annotatedBy": {"@id": ann.guid}}} + ) + self._update_task(task_guid, {"annotated_evidence_graph_id": aeg_id}) logger.info(f"Stored AnnotatedEvidenceGraph {aeg_id}") return aeg_id @@ -863,6 +893,7 @@ def build_and_store( def interpret_rocrate(self, task_guid: str, user_token: str = "") -> str: """Main pipeline orchestrator. Returns the AnnotatedEvidenceGraph @id.""" + import os # Load task config task_doc = self.config.asyncCollection.find_one({"guid": task_guid}) if not task_doc: @@ -872,6 +903,11 @@ def interpret_rocrate(self, task_guid: str, user_token: str = "") -> str: llm_model = task_doc.get("llm_model", "google-gla:gemini-2.5-flash-lite") temperature = task_doc.get("llm_temperature", 0.2) + # # Diagnostic: confirm which API key and model are in use + # raw_key = os.environ.get("ANTHROPIC_API_KEY", "") + # masked_key = f"{raw_key[:8]}...{raw_key[-4:]}" if len(raw_key) > 12 else ("(not set)" if not raw_key else "(too short)") + # logger.info(f"[interpret_rocrate] task={task_guid} model={llm_model!r} ANTHROPIC_API_KEY={masked_key}") + self._update_task(task_guid, { "status": "PROCESSING", "time_started": datetime.datetime.utcnow(), diff --git a/mds/src/fairscape_mds/routers/interpretation.py b/mds/src/fairscape_mds/routers/interpretation.py index ce40709..2015c43 100644 --- a/mds/src/fairscape_mds/routers/interpretation.py +++ b/mds/src/fairscape_mds/routers/interpretation.py @@ -48,7 +48,7 @@ def trigger_interpretation( token: Annotated[str, Depends(OAuthScheme)], NAAN: str, postfix: str, - llm_model: str = Query(default="google-gla:gemini-2.5-flash-lite", description="PydanticAI model string"), + llm_model: str = Query(default="anthropic:claude-haiku-4-5-20251001", description="PydanticAI model string"), temperature: float = Query(default=0.2, ge=0.0, le=2.0, description="LLM temperature"), persona: str = Query(default="datasci", description="Interpretation persona"), force: bool = Query(default=True, description="Force re-interpretation if one already exists"), From 2fc69b3185b17dd0eb16cabb32b7167e1ffcffb5 Mon Sep 17 00:00:00 2001 From: jniestroy Date: Tue, 17 Mar 2026 12:11:05 -0400 Subject: [PATCH 08/27] latest --- mds/src/fairscape_mds/crud/interpretation.py | 57 +++++++++++++++---- .../models/annotated_computation.py | 49 ++++++++++++++-- .../models/annotated_evidence_graph.py | 12 +++- 3 files changed, 102 insertions(+), 16 deletions(-) diff --git a/mds/src/fairscape_mds/crud/interpretation.py b/mds/src/fairscape_mds/crud/interpretation.py index c843577..58b7f1a 100644 --- a/mds/src/fairscape_mds/crud/interpretation.py +++ b/mds/src/fairscape_mds/crud/interpretation.py @@ -20,8 +20,11 @@ from fairscape_mds.models.annotated_computation import ( AnnotatedComputation, CodeAnalysis, DatasetSummary, LLMComputationAnnotation, LLMCodeAnalysis, LLMDatasetSummary, + Concern, LLMConcern, ConcernLevel, normalize_concern, +) +from fairscape_mds.models.annotated_evidence_graph import ( + AnnotatedEvidenceGraph, GraphConcern, ) -from fairscape_mds.models.annotated_evidence_graph import AnnotatedEvidenceGraph from fairscape_mds.crud.fairscape_request import FairscapeRequest from fairscape_mds.crud.fairscape_response import FairscapeResponse @@ -93,16 +96,23 @@ def run_async(coro): - Selection bias: Consider whether filtering steps introduce bias - Reproducibility: Random seeds, version pinning, parameter documentation +### Concern Severity Levels +Every concern MUST be assigned exactly one of these three severity levels: +- CRITICAL: Fundamental flaws that invalidate results or conclusions (e.g., data leakage, evaluating on training data, target variable in features) +- MODERATE: Significant issues that weaken reliability but do not fully invalidate results (e.g., missing stratification, poor metric choice for imbalanced data, no cross-validation) +- MINOR: Best-practice improvements, style, or documentation gaps (e.g., missing docstrings, hardcoded magic numbers, no version pinning) + +Use ONLY these three levels. Do not invent additional levels like WARNING, INFO, or GOOD. + ### Output Requirements Return a structured annotation with: - stepSummary: A clear description of what this computation step does and why -- codeAnalysis: For each software entity, provide a summary, key functions, and concerns +- codeAnalysis: For each software entity, provide a summary, key functions, and concerns (each concern as {level, description}) - inputSummaries: For each input dataset, describe its role - outputSummaries: For each output dataset, describe what it contains -- concerns: List any methodological, statistical, or reproducibility concerns +- concerns: List any methodological, statistical, or reproducibility concerns (each as {level, description}) Be precise and evidence-based. Reference specific function names and parameter values. -Distinguish critical issues from minor improvements. Acknowledge when methods are well-chosen.""" @@ -117,7 +127,16 @@ def run_async(coro): 1. executiveSummary: 3-5 sentences covering what the pipeline does, its analytical approach, and the most important observation 2. narrativeSummary: A forward-chronological story of the entire pipeline, starting from origin data and ending at final outputs 3. keyFindings: Bulleted list of important observations, prioritized by severity -4. concerns: Bulleted list of methodological, statistical, or reproducibility concerns across the whole pipeline +4. concerns: List of methodological, statistical, or reproducibility concerns across the whole pipeline. Each concern must be a structured object with: + - level: One of CRITICAL, MODERATE, or MINOR (no other levels allowed) + - description: The concern text + +Concern severity levels: +- CRITICAL: Fundamental flaws that invalidate results or conclusions +- MODERATE: Significant issues that weaken reliability but do not fully invalidate results +- MINOR: Best-practice improvements, style, or documentation gaps + +Use ONLY these three levels. Do not invent additional levels. Write as if explaining to a colleague. Be precise and evidence-based.""" @@ -134,7 +153,7 @@ class GraphSynthesisResult(BaseModel): executiveSummary: str narrativeSummary: str keyFindings: List[str] = [] - concerns: List[str] = [] + concerns: List[LLMConcern] = [] # --------------------------------------------------------------------------- @@ -540,7 +559,7 @@ def _llm_to_annotated( name=ca.name, summary=ca.summary, keyFunctions=ca.keyFunctions, - concerns=ca.concerns, + concerns=[normalize_concern(c) for c in (ca.concerns or [])], ) for ca in (llm_result.codeAnalysis or []) ] @@ -575,7 +594,7 @@ def _llm_to_annotated( "evi:codeAnalysis": [ca.model_dump(by_alias=True) for ca in code_analyses], "evi:inputSummaries": [ds.model_dump(by_alias=True) for ds in input_summaries], "evi:outputSummaries": [ds.model_dump(by_alias=True) for ds in output_summaries], - "evi:concerns": llm_result.concerns or [], + "evi:concerns": [normalize_concern(c).model_dump() for c in (llm_result.concerns or [])], "evi:llmModel": llm_model, "evi:llmTemperature": temperature, "dateCreated": now, @@ -723,7 +742,7 @@ def synthesize_graph( parts.append(f"### Step {i}: {ann.annotates}") parts.append(f"**Summary:** {ann.stepSummary}") if ann.concerns: - parts.append(f"**Concerns:** {'; '.join(ann.concerns)}") + parts.append(f"**Concerns:** {'; '.join(f'[{c.level.value}] {c.description}' for c in ann.concerns)}") if ann.codeAnalysis: for ca in ann.codeAnalysis: parts.append(f"**Code ({ca.name or ca.software}):** {ca.summary}") @@ -813,6 +832,24 @@ def build_and_store( # Build step annotation refs step_ann_refs = [{"@id": ann.guid} for ann in step_annotations] + # Compile graph-level concerns from step annotations with source links + compiled_concerns = [] + for ann in step_annotations: + for concern in (ann.concerns or []): + compiled_concerns.append(GraphConcern( + level=concern.level, + description=concern.description, + sourceAnnotation={"@id": ann.guid}, + )) + # Add synthesis-level concerns (not tied to a single step) + for llm_concern in (synthesis.concerns or []): + normalized = normalize_concern(llm_concern) + compiled_concerns.append(GraphConcern( + level=normalized.level, + description=normalized.description, + sourceAnnotation={"@id": rocrate_id}, + )) + # Extract NAAN from rocrate_id for the new identifier ark_match = re.match(r"ark:(\d+)/(.*)", rocrate_id) if ark_match: @@ -835,7 +872,7 @@ def build_and_store( "evi:executiveSummary": synthesis.executiveSummary, "evi:narrativeSummary": synthesis.narrativeSummary, "evi:keyFindings": synthesis.keyFindings, - "evi:concerns": synthesis.concerns, + "evi:concerns": [c.model_dump(by_alias=True, mode="json") for c in compiled_concerns], "evi:stepAnnotations": step_ann_refs, "evi:llmModel": llm_model, "evi:llmTemperature": temperature, diff --git a/mds/src/fairscape_mds/models/annotated_computation.py b/mds/src/fairscape_mds/models/annotated_computation.py index 406201f..cc38ded 100644 --- a/mds/src/fairscape_mds/models/annotated_computation.py +++ b/mds/src/fairscape_mds/models/annotated_computation.py @@ -6,6 +6,8 @@ present in the published package. """ +from enum import Enum + from pydantic import BaseModel, Field, ConfigDict, model_validator from typing import Optional, List, Union, Dict, Any @@ -16,6 +18,45 @@ ANNOTATED_COMPUTATION_TYPE = "AnnotatedComputation" +# --------------------------------------------------------------------------- +# Concern severity levels +# --------------------------------------------------------------------------- + +class ConcernLevel(str, Enum): + """Exactly three severity levels for annotation concerns.""" + CRITICAL = "CRITICAL" + MODERATE = "MODERATE" + MINOR = "MINOR" + + +class Concern(BaseModel): + """A structured concern with a severity level.""" + level: ConcernLevel + description: str + + +class LLMConcern(BaseModel): + """What the LLM returns for a concern (level as str for flexibility).""" + level: str = Field(description="One of: CRITICAL, MODERATE, MINOR") + description: str + + +def normalize_concern(llm_concern: LLMConcern) -> Concern: + """Convert an LLMConcern to a validated Concern, normalizing the level.""" + raw = llm_concern.level.strip().upper() + try: + level = ConcernLevel(raw) + except ValueError: + # Fallback: map common alternatives + if "CRIT" in raw: + level = ConcernLevel.CRITICAL + elif "MOD" in raw or "WARN" in raw: + level = ConcernLevel.MODERATE + else: + level = ConcernLevel.MINOR + return Concern(level=level, description=llm_concern.description) + + class CodeAnalysis(BaseModel): """Analysis of a software entity used in the computation.""" model_config = ConfigDict(extra="allow", populate_by_name=True) @@ -24,7 +65,7 @@ class CodeAnalysis(BaseModel): name: Optional[str] = Field(default=None) summary: str keyFunctions: Optional[List[str]] = Field(default=None) - concerns: Optional[List[str]] = Field(default=None) + concerns: Optional[List[Concern]] = Field(default=None) class DatasetSummary(BaseModel): @@ -47,7 +88,7 @@ class LLMCodeAnalysis(BaseModel): name: Optional[str] = Field(default=None) summary: str keyFunctions: Optional[List[str]] = Field(default=None) - concerns: Optional[List[str]] = Field(default=None) + concerns: Optional[List[LLMConcern]] = Field(default=None) class LLMDatasetSummary(BaseModel): @@ -64,7 +105,7 @@ class LLMComputationAnnotation(BaseModel): codeAnalysis: Optional[List[LLMCodeAnalysis]] = Field(default=[]) inputSummaries: Optional[List[LLMDatasetSummary]] = Field(default=[]) outputSummaries: Optional[List[LLMDatasetSummary]] = Field(default=[]) - concerns: Optional[List[str]] = Field(default=[]) + concerns: Optional[List[LLMConcern]] = Field(default=[]) class AnnotatedComputation(DigitalObject): @@ -92,7 +133,7 @@ class AnnotatedComputation(DigitalObject): codeAnalysis: Optional[List[CodeAnalysis]] = Field(default=[], alias="evi:codeAnalysis") inputSummaries: Optional[List[DatasetSummary]] = Field(default=[], alias="evi:inputSummaries") outputSummaries: Optional[List[DatasetSummary]] = Field(default=[], alias="evi:outputSummaries") - concerns: Optional[List[str]] = Field(default=[], alias="evi:concerns") + concerns: Optional[List[Concern]] = Field(default=[], alias="evi:concerns") # Provenance of the annotation itself llmModel: str = Field(alias="evi:llmModel") diff --git a/mds/src/fairscape_mds/models/annotated_evidence_graph.py b/mds/src/fairscape_mds/models/annotated_evidence_graph.py index 046decb..cafe01d 100644 --- a/mds/src/fairscape_mds/models/annotated_evidence_graph.py +++ b/mds/src/fairscape_mds/models/annotated_evidence_graph.py @@ -6,15 +6,23 @@ present in the published package. """ -from pydantic import Field, model_validator +from pydantic import BaseModel, Field, model_validator from typing import Optional, List, Union, Dict, Any from fairscape_models.fairscape_base import IdentifierValue from fairscape_models.digital_object import DigitalObject +from fairscape_mds.models.annotated_computation import ConcernLevel ANNOTATED_EVIDENCE_GRAPH_TYPE = "AnnotatedEvidenceGraph" +class GraphConcern(BaseModel): + """A graph-level concern linked to its source annotation.""" + level: ConcernLevel + description: str + sourceAnnotation: IdentifierValue + + class AnnotatedEvidenceGraph(DigitalObject): """Full annotated condensed evidence graph -- the graph-level LLM output. @@ -43,7 +51,7 @@ class AnnotatedEvidenceGraph(DigitalObject): executiveSummary: str = Field(..., alias="evi:executiveSummary") narrativeSummary: str = Field(..., alias="evi:narrativeSummary") keyFindings: Optional[List[str]] = Field(default=[], alias="evi:keyFindings") - concerns: Optional[List[str]] = Field(default=[], alias="evi:concerns") + concerns: Optional[List[GraphConcern]] = Field(default=[], alias="evi:concerns") # Quick index of all AnnotatedComputation @ids in the graph stepAnnotations: Optional[List[IdentifierValue]] = Field(default=[], alias="evi:stepAnnotations") From f7029856fac576176d4d49f497198d235a2339c6 Mon Sep 17 00:00:00 2001 From: jniestroy Date: Tue, 17 Mar 2026 13:01:29 -0400 Subject: [PATCH 09/27] prompts --- mds/src/fairscape_mds/crud/interpretation.py | 93 +++++++++++++++++--- 1 file changed, 81 insertions(+), 12 deletions(-) diff --git a/mds/src/fairscape_mds/crud/interpretation.py b/mds/src/fairscape_mds/crud/interpretation.py index 58b7f1a..bd8debd 100644 --- a/mds/src/fairscape_mds/crud/interpretation.py +++ b/mds/src/fairscape_mds/crud/interpretation.py @@ -96,11 +96,67 @@ def run_async(coro): - Selection bias: Consider whether filtering steps introduce bias - Reproducibility: Random seeds, version pinning, parameter documentation -### Concern Severity Levels -Every concern MUST be assigned exactly one of these three severity levels: -- CRITICAL: Fundamental flaws that invalidate results or conclusions (e.g., data leakage, evaluating on training data, target variable in features) -- MODERATE: Significant issues that weaken reliability but do not fully invalidate results (e.g., missing stratification, poor metric choice for imbalanced data, no cross-validation) -- MINOR: Best-practice improvements, style, or documentation gaps (e.g., missing docstrings, hardcoded magic numbers, no version pinning) +### Concern Severity Levels — READ CAREFULLY +Every concern MUST be assigned exactly one of these three severity levels. Apply them strictly using the decision procedure below. + +**CRITICAL — Conclusions cannot be trusted** +The code produces results that are demonstrably wrong, or the methodology is fundamentally unsound such that the conclusions it supports are not warranted. You must be able to point to what the code actually does (or claims) that makes the output unreliable. + +Ask: "If a reader trusts this output at face value, will they be misled?" + +Applies when: +- The code does X but reports it as Y (e.g., evaluates on training data, labels it "test accuracy") +- A quantity is presented as measured or calibrated but is actually fabricated or an acknowledged guess +- Information from the target/outcome leaks into the features or model inputs +- A metric is used in a way that does not measure what it claims (e.g., treating UMAP distance as a statistical test of significance) + +Does NOT apply when: +- A risk exists but the code handles it correctly +- A methodology is claimed but not shown — that is unverifiable, not wrong +- Something *could* go wrong if a downstream user misuses the output + +**MODERATE — Methodology has a real, demonstrable weakness** +A reviewer would flag this as a gap that weakens confidence in the results, but the results are not necessarily wrong. The issue is concrete and present in the pipeline — not hypothetical. + +Ask: "Does this weaken the strength of evidence, even if the conclusions might still be correct?" + +Applies when: +- Stochastic steps lack random seeds, so results are not reproducible across runs +- Only a subset of available data is used without justification (e.g., one replicate of three) +- Evaluation metrics are inappropriate for the data characteristics (e.g., accuracy on imbalanced classes) +- Key parameters are hardcoded without sensitivity analysis and directly affect the results (e.g., similarity thresholds that determine network density) +- No validation or calibration of a method whose accuracy is not self-evident + +Does NOT apply when: +- The code does not re-verify something already done correctly upstream +- A parameter is hardcoded but has a reasonable default and low impact +- A claimed process is not shown in code but there is no evidence it was done wrong + +**MINOR — Recommendations and best-practice gaps** +Suggestions that would improve rigour, documentation, or robustness, but whose absence does not weaken the actual results or conclusions drawn from this pipeline. + +Ask: "Would fixing this change the results or conclusions?" If no, it is MINOR. + +Applies when: +- Missing version pinning, dependency documentation, or environment specifications +- No input validation or schema checks +- Hardcoded parameters with low impact on results +- Missing confidence intervals or uncertainty quantification (when results are otherwise sound) +- Documentation gaps, typos, missing docstrings +- Model artifacts not serialised + +### Decision procedure +1. Can you point to specific code/output where the result is wrong or misleading? → CRITICAL +2. No, but can you identify a concrete methodological gap that weakens the evidence? → MODERATE +3. No, but you have a recommendation that would improve the work? → MINOR +4. None of the above? → Do not raise a concern. + +### Key principles +- There is no minimum number of concerns at any level. Many well-written computation steps will have zero CRITICAL and zero MODERATE concerns. That is a valid and expected outcome — report it as such. +- Do not inflate severity to fill quotas. A concern list with 2 MINOR items is better analysis than one with fabricated CRITICAL findings. +- Grade based on what the code *actually does*, not hypothetical scenarios where someone might misuse its outputs. +- If the code handles something correctly, do not raise a concern about how a future user *could* get it wrong. +- When in doubt between two levels, choose the lower one. Use ONLY these three levels. Do not invent additional levels like WARNING, INFO, or GOOD. @@ -110,7 +166,7 @@ def run_async(coro): - codeAnalysis: For each software entity, provide a summary, key functions, and concerns (each concern as {level, description}) - inputSummaries: For each input dataset, describe its role - outputSummaries: For each output dataset, describe what it contains -- concerns: List any methodological, statistical, or reproducibility concerns (each as {level, description}) +- concerns: List any methodological, statistical, or reproducibility concerns (each as {level, description}). This list may be empty or contain only MINOR items if the step is methodologically sound. Be precise and evidence-based. Reference specific function names and parameter values. Acknowledge when methods are well-chosen.""" @@ -127,16 +183,29 @@ def run_async(coro): 1. executiveSummary: 3-5 sentences covering what the pipeline does, its analytical approach, and the most important observation 2. narrativeSummary: A forward-chronological story of the entire pipeline, starting from origin data and ending at final outputs 3. keyFindings: Bulleted list of important observations, prioritized by severity -4. concerns: List of methodological, statistical, or reproducibility concerns across the whole pipeline. Each concern must be a structured object with: +4. concerns: Cross-cutting methodological, statistical, or reproducibility concerns that span the pipeline. Each concern must be a structured object with: - level: One of CRITICAL, MODERATE, or MINOR (no other levels allowed) - description: The concern text -Concern severity levels: -- CRITICAL: Fundamental flaws that invalidate results or conclusions -- MODERATE: Significant issues that weaken reliability but do not fully invalidate results -- MINOR: Best-practice improvements, style, or documentation gaps +### Concern Severity — Apply the same rubric as step-level analysis -Use ONLY these three levels. Do not invent additional levels. +**CRITICAL — Conclusions cannot be trusted.** The pipeline produces results that are demonstrably wrong or misleading. You must trace the flaw through specific computation steps. Ask: "If a reader trusts the final output at face value, will they be misled?" + +**MODERATE — Methodology has a real, demonstrable weakness.** A reviewer would flag this as weakening confidence, but results are not necessarily wrong. Ask: "Does this weaken the strength of evidence?" + +**MINOR — Recommendations and best-practice gaps.** Would improve the work, but fixing it would not change the results or conclusions. + +### Decision procedure +1. Can you trace a specific flaw through the pipeline that makes an output wrong or misleading? → CRITICAL +2. No, but can you identify a concrete methodological gap that weakens the evidence? → MODERATE +3. No, but you have a recommendation? → MINOR + +### Key principles +- There is no minimum number of concerns at any level. A pipeline with zero CRITICAL and zero MODERATE concerns is a valid and expected outcome for well-constructed work. Report that clearly rather than manufacturing issues. +- Do NOT escalate step-level concerns. If a step annotation flagged something as MINOR, do not promote it to MODERATE or CRITICAL unless a cross-step interaction demonstrably makes it worse. +- Do NOT re-list every step-level concern. Only surface concerns that matter at the pipeline level — either because they span multiple steps, compound across steps, or are the most important findings. +- Grade based on what the code actually does, not hypothetical misuse scenarios. +- When in doubt between two levels, choose the lower one. Write as if explaining to a colleague. Be precise and evidence-based.""" From 79ea572de85f975a97d465e5c839e8990ecce575 Mon Sep 17 00:00:00 2001 From: jniestroy Date: Wed, 18 Mar 2026 10:28:01 -0400 Subject: [PATCH 10/27] include summary stats --- mds/src/fairscape_mds/crud/identifier.py | 6 +- mds/src/fairscape_mds/crud/interpretation.py | 227 +++++++++++++++++- mds/src/fairscape_mds/crud/statistics.py | 74 +++++- .../models/annotated_computation.py | 2 + mds/src/fairscape_mds/models/statistics.py | 8 +- 5 files changed, 302 insertions(+), 15 deletions(-) diff --git a/mds/src/fairscape_mds/crud/identifier.py b/mds/src/fairscape_mds/crud/identifier.py index 1cdaede..6430e33 100644 --- a/mds/src/fairscape_mds/crud/identifier.py +++ b/mds/src/fairscape_mds/crud/identifier.py @@ -16,7 +16,8 @@ ) from fairscape_mds.crud.statistics import ( generateSummaryStatistics, - generateSplitStatistics + generateSplitStatistics, + collectHistogramBins ) from fairscape_models import IdentifierValue from fairscape_models.model_card import ModelCard @@ -159,7 +160,8 @@ def generateStatistics( splitStats = None if splits: splitDicts = [s.model_dump() if hasattr(s, 'model_dump') else s for s in splits] - splitStats = generateSplitStatistics(dataframe, splitDicts) + totalBinEdges = collectHistogramBins(summaryStatistics) + splitStats = generateSplitStatistics(dataframe, splitDicts, totalBinEdges=totalBinEdges) # update identifier updateFields = {"descriptiveStatistics": summaryStatistics} diff --git a/mds/src/fairscape_mds/crud/interpretation.py b/mds/src/fairscape_mds/crud/interpretation.py index bd8debd..f00733b 100644 --- a/mds/src/fairscape_mds/crud/interpretation.py +++ b/mds/src/fairscape_mds/crud/interpretation.py @@ -62,6 +62,8 @@ def run_async(coro): # --------------------------------------------------------------------------- MAX_SOFTWARE_BYTES = 50_000 # 50KB per software entity +MAX_STATS_COLUMNS = 25 # max columns of stats to include per dataset in prompt +MAX_SPLITS = 10 # max splits to include per dataset in prompt CODE_EXTENSIONS = {".py", ".r", ".R", ".sh", ".pl", ".java", ".scala", ".jl", ".m", ".cpp", ".go", ".rs", ".ipynb", ".md"} GITHUB_REPO_PATTERN = re.compile(r"https?://github\.com/([^/]+)/([^/]+?)(?:\.git)?(?:/.*)?$") GITHUB_FILE_PATTERN = re.compile( @@ -77,6 +79,8 @@ def run_async(coro): You will receive: - The computation's metadata (name, description, command) - Input dataset metadata (names, descriptions, formats) +- Dataset data profiles (when available): per-column summary statistics including distributions, missing data rates, and histograms +- Dataset split statistics (when available): per-column statistics for each data split (train/test/validation) - Software source code used by the computation - Output dataset metadata (names, descriptions, formats) @@ -96,6 +100,14 @@ def run_async(coro): - Selection bias: Consider whether filtering steps introduce bias - Reproducibility: Random seeds, version pinning, parameter documentation +### Data Quality Assessment (when data profiles are provided) +- Missing data: Flag columns with >10% missing. Consider if missingness is random or systematic and whether it could bias results +- Distribution shape: Use histograms and quartiles to identify skew, multimodality, or outliers (compare mean vs median). Note if distributions look unexpected for the domain +- Split balance: Compare row counts and distributions across train/test/validation splits. Flag significant imbalance or distribution drift that could indicate data leakage +- Scale differences: Note features on very different scales (relevant for distance-based or gradient methods) +- Categorical dominance: Flag categoricals where one value >90% frequency — near-constant features add noise +- Low variance: Flag numerical columns where std is near zero relative to the mean + ### Concern Severity Levels — READ CAREFULLY Every concern MUST be assigned exactly one of these three severity levels. Apply them strictly using the decision procedure below. @@ -164,8 +176,8 @@ def run_async(coro): Return a structured annotation with: - stepSummary: A clear description of what this computation step does and why - codeAnalysis: For each software entity, provide a summary, key functions, and concerns (each concern as {level, description}) -- inputSummaries: For each input dataset, describe its role -- outputSummaries: For each output dataset, describe what it contains +- inputSummaries: For each input dataset, describe its role and include a dataQuality field with observations from the data profile (missing data, distribution issues, etc.) if statistics were provided +- outputSummaries: For each output dataset, describe what it contains and include a dataQuality field with observations if statistics were provided - concerns: List any methodological, statistical, or reproducibility concerns (each as {level, description}). This list may be empty or contain only MINOR items if the step is methodologically sound. Be precise and evidence-based. Reference specific function names and parameter values. @@ -278,6 +290,132 @@ def _build_index(graph: list) -> dict: return index +# --------------------------------------------------------------------------- +# Dataset statistics formatting for prompts +# --------------------------------------------------------------------------- + +_HIST_BARS = " ▁▂▃▄▅▆▇█" + + +def _mini_histogram(counts: list) -> str: + """Render a list of histogram counts as a sparkline string.""" + if not counts: + return "" + mx = max(counts) if max(counts) > 0 else 1 + return "".join(_HIST_BARS[min(int(c / mx * 8) + (1 if c > 0 else 0), 8)] for c in counts) + + +def _format_column_stats(col_name: str, col_data: dict) -> str: + """Format one column's stats as a markdown table row.""" + stats = col_data.get("statistics", {}) + + # Determine type by presence of 'mean' (numerical) vs 'unique' (categorical) + if "mean" in stats and stats["mean"] is not None: + # Numerical + missing_pct = stats.get("missing_percentage", "") + if missing_pct != "" and missing_pct is not None: + missing_pct = f"{missing_pct}%" + hist = _mini_histogram(stats.get("histogram_counts", [])) + return ( + f"| {col_name} | num | {stats.get('count', '')} | {missing_pct} " + f"| {stats.get('mean', '')} | {stats.get('std', '')} " + f"| {stats.get('min', '')} | {stats.get('first_quartile', '')} " + f"| {stats.get('second_quartile', '')} | {stats.get('third_quartile', '')} " + f"| {stats.get('max', '')} | {hist} |" + ) + else: + # Categorical + missing_pct = stats.get("missing_percentage", "") + if missing_pct != "" and missing_pct is not None: + missing_pct = f"{missing_pct}%" + return ( + f"| {col_name} | cat | {stats.get('count', '')} | {missing_pct} " + f"| top: {stats.get('top', '')} | uniq: {stats.get('unique', '')} " + f"| | | | | | freq: {stats.get('freq', '')} |" + ) + + +def _format_dataset_stats(ds_name: str, ds_stats: dict) -> str: + """Format descriptiveStatistics and splitStatistics for one dataset into + prompt-ready markdown. Returns empty string if no stats available.""" + desc_stats = ds_stats.get("descriptiveStatistics", {}) + split_stats = ds_stats.get("splitStatistics", {}) + + if not desc_stats and not split_stats: + return "" + + parts = [] + + # --- Overall descriptive statistics --- + if desc_stats: + columns = list(desc_stats.items()) + total_cols = len(columns) + + # Estimate row count from first column's count + first_stats = columns[0][1].get("statistics", {}) if columns else {} + row_count = first_stats.get("count", "?") + + # Total missing across all columns + total_missing = 0 + for _, col_data in columns: + mc = col_data.get("statistics", {}).get("missing_count") + if mc is not None: + total_missing += mc + + truncated = total_cols > MAX_STATS_COLUMNS + display_cols = columns[:MAX_STATS_COLUMNS] + + parts.append(f"#### Data Profile: {ds_name} ({total_cols} columns, ~{row_count} rows, {total_missing} missing values)") + parts.append("| Column | Type | Count | Missing% | Mean/Top | Std/Unique | Min | Q1 | Median | Q3 | Max | Hist |") + parts.append("|--------|------|-------|----------|----------|------------|-----|----|----|----|----|------|") + for col_name, col_data in display_cols: + parts.append(_format_column_stats(col_name, col_data)) + + if truncated: + parts.append(f"| ... | | | | | | | | | | | *{total_cols - MAX_STATS_COLUMNS} more columns omitted* |") + parts.append("") + + # --- Split statistics --- + if split_stats: + split_items = list(split_stats.items()) + total_splits = len(split_items) + truncated_splits = total_splits > MAX_SPLITS + display_splits = split_items[:MAX_SPLITS] + + for split_name, split_data in display_splits: + split_desc = split_data.get("description", "") + split_col_stats = split_data.get("statistics", {}) + if not split_col_stats: + continue + + # Row count from first column + first_split_col = list(split_col_stats.values())[0] if split_col_stats else {} + split_row_count = first_split_col.get("statistics", {}).get("count", "?") + + label = f'#### Split: "{split_name}"' + if split_desc: + label += f" ({split_desc})" + label += f" -- {split_row_count} rows" + parts.append(label) + + split_columns = list(split_col_stats.items()) + display_split_cols = split_columns[:MAX_STATS_COLUMNS] + + parts.append("| Column | Type | Count | Missing% | Mean/Top | Std/Unique | Min | Q1 | Median | Q3 | Max | Hist |") + parts.append("|--------|------|-------|----------|----------|------------|-----|----|----|----|----|------|") + for col_name, col_data in display_split_cols: + parts.append(_format_column_stats(col_name, col_data)) + + if len(split_columns) > MAX_STATS_COLUMNS: + parts.append(f"| ... | | | | | | | | | | | *{len(split_columns) - MAX_STATS_COLUMNS} more columns omitted* |") + parts.append("") + + if truncated_splits: + parts.append(f"*... {total_splits - MAX_SPLITS} more splits omitted*\n") + + return "\n".join(parts) + + # --------------------------------------------------------------------------- # GitHub source code fetching # --------------------------------------------------------------------------- @@ -549,11 +687,54 @@ def prefetch_all_software(self, task_guid: str, computations: list, index: dict, return software_cache + # ------------------------------------------------------------------ + # Step 3b: Pre-fetch dataset statistics + # ------------------------------------------------------------------ + + def prefetch_dataset_statistics( + self, task_guid: str, computations: list, index: dict + ) -> Dict[str, dict]: + """Fetch descriptiveStatistics and splitStatistics from MongoDB for all + datasets referenced by computations. Returns {dataset_id: {...}}.""" + # Collect all dataset IDs + dataset_ids = set() + for comp in computations: + for ds_id in _resolve_refs(comp.get("usedDataset")): + dataset_ids.add(ds_id) + for ds_id in _resolve_refs(comp.get("generated")): + dataset_ids.add(ds_id) + + if not dataset_ids: + return {} + + # Batch query MongoDB with projection + cursor = self.config.identifierCollection.find( + {"@id": {"$in": list(dataset_ids)}}, + {"@id": 1, "descriptiveStatistics": 1, "splitStatistics": 1}, + ) + + stats_cache: Dict[str, dict] = {} + for doc in cursor: + ds_id = doc.get("@id") + desc_stats = doc.get("descriptiveStatistics") + split_stats = doc.get("splitStatistics") + if desc_stats or split_stats: + stats_cache[ds_id] = { + "descriptiveStatistics": desc_stats or {}, + "splitStatistics": split_stats or {}, + } + + logger.info( + f"Pre-fetched dataset statistics: {len(stats_cache)} of " + f"{len(dataset_ids)} datasets have stats" + ) + return stats_cache + # ------------------------------------------------------------------ # Step 4: Annotate single computation # ------------------------------------------------------------------ - def _build_computation_prompt(self, computation: dict, software_cache: dict, index: dict) -> str: + def _build_computation_prompt(self, computation: dict, software_cache: dict, index: dict, stats_cache: Optional[Dict[str, dict]] = None) -> str: """Build the prompt for a single computation annotation.""" parts = [] @@ -567,17 +748,27 @@ def _build_computation_prompt(self, computation: dict, software_cache: dict, ind parts.append(f"**Date Created:** {computation.get('dateCreated', 'unknown')}") parts.append("") + if stats_cache is None: + stats_cache = {} + # Input datasets input_refs = _resolve_refs(computation.get("usedDataset")) if input_refs: parts.append("## Input Datasets") for ds_id in input_refs: ds_node = index.get(ds_id, {}) - parts.append(f"- **{ds_node.get('name', ds_id)}** ({ds_node.get('format', 'unknown format')})") + ds_name = ds_node.get('name', ds_id) + parts.append(f"- **{ds_name}** ({ds_node.get('format', 'unknown format')})") parts.append(f" ID: {ds_id}") parts.append(f" Description: {ds_node.get('description', 'No description')}") if ds_node.get("keywords"): parts.append(f" Keywords: {', '.join(ds_node['keywords']) if isinstance(ds_node['keywords'], list) else ds_node['keywords']}") + # Include dataset statistics if available + if ds_id in stats_cache: + formatted = _format_dataset_stats(ds_name, stats_cache[ds_id]) + if formatted: + parts.append("") + parts.append(formatted) parts.append("") # Software and source code @@ -603,12 +794,23 @@ def _build_computation_prompt(self, computation: dict, software_cache: dict, ind parts.append("## Output Datasets") for ds_id in output_refs: ds_node = index.get(ds_id, {}) - parts.append(f"- **{ds_node.get('name', ds_id)}** ({ds_node.get('format', 'unknown format')})") + ds_name = ds_node.get('name', ds_id) + parts.append(f"- **{ds_name}** ({ds_node.get('format', 'unknown format')})") parts.append(f" ID: {ds_id}") parts.append(f" Description: {ds_node.get('description', 'No description')}") + # Include dataset statistics if available + if ds_id in stats_cache: + formatted = _format_dataset_stats(ds_name, stats_cache[ds_id]) + if formatted: + parts.append("") + parts.append(formatted) parts.append("") - return "\n".join(parts) + prompt = "\n".join(parts) + comp_id = computation.get("@id", "unknown") + est_tokens = len(prompt) // 4 + logger.info(f"Prompt for {comp_id}: ~{est_tokens} tokens estimated ({len(prompt)} chars)") + return prompt def _llm_to_annotated( self, @@ -640,6 +842,7 @@ def _llm_to_annotated( name=ds.name, role=ds.role, description=ds.description, + dataQuality=ds.dataQuality, ) for ds in (llm_result.inputSummaries or []) ] @@ -649,6 +852,7 @@ def _llm_to_annotated( name=ds.name, role=ds.role, description=ds.description, + dataQuality=ds.dataQuality, ) for ds in (llm_result.outputSummaries or []) ] @@ -677,9 +881,10 @@ async def _annotate_single_computation( index: dict, llm_model: str, temperature: float, + stats_cache: Optional[Dict[str, dict]] = None, ) -> AnnotatedComputation: """Annotate a single computation using PydanticAI.""" - prompt = self._build_computation_prompt(computation, software_cache, index) + prompt = self._build_computation_prompt(computation, software_cache, index, stats_cache=stats_cache) comp_id = computation.get("@id", f"ark:59853/computation-{uuid.uuid4()}") agent = Agent( @@ -715,6 +920,7 @@ async def _annotate_computations_async( llm_model: str, temperature: float, max_workers: int = 4, + stats_cache: Optional[Dict[str, dict]] = None, ) -> List[AnnotatedComputation]: """Annotate all computations concurrently via asyncio.gather.""" self._update_task(task_guid, { @@ -730,6 +936,7 @@ async def _annotate_one(comp): try: annotated = await self._annotate_single_computation( task_guid, comp, software_cache, index, llm_model, temperature, + stats_cache=stats_cache, ) self.config.asyncCollection.update_one( {"guid": task_guid, "computation_details.computation_id": comp_id}, @@ -773,10 +980,12 @@ def annotate_computations_parallel( llm_model: str, temperature: float, max_workers: int = 4, + stats_cache: Optional[Dict[str, dict]] = None, ) -> List[AnnotatedComputation]: """Annotate computations concurrently using async I/O.""" return run_async(self._annotate_computations_async( task_guid, computations, software_cache, index, llm_model, temperature, max_workers, + stats_cache=stats_cache, )) # ------------------------------------------------------------------ @@ -1034,9 +1243,13 @@ def interpret_rocrate(self, task_guid: str, user_token: str = "") -> str: effective_token = user_token or task_doc.get("user_token", "") software_cache = self.prefetch_all_software(task_guid, computations, index, user_token=effective_token) + # Step 3b: Pre-fetch dataset statistics + stats_cache = self.prefetch_dataset_statistics(task_guid, computations, index) + # Step 4: Annotate computations in parallel step_annotations = self.annotate_computations_parallel( task_guid, computations, software_cache, index, llm_model, temperature, + stats_cache=stats_cache, ) # Step 5: Graph-level synthesis diff --git a/mds/src/fairscape_mds/crud/statistics.py b/mds/src/fairscape_mds/crud/statistics.py index 09b4dbe..1d3189f 100644 --- a/mds/src/fairscape_mds/crud/statistics.py +++ b/mds/src/fairscape_mds/crud/statistics.py @@ -14,7 +14,7 @@ pandasql = None -def generateNumericalStatistics(series) -> DescriptiveStatistics: +def generateNumericalStatistics(series, bin_edges=None) -> DescriptiveStatistics: descriptiveStats = series.describe() descriptiveStats = descriptiveStats.replace({numpy.nan: "NaN"}) @@ -23,6 +23,21 @@ def generateNumericalStatistics(series) -> DescriptiveStatistics: descriptiveStatsDict = descriptiveStats.to_dict() + # missing data + missing_count = int(series.isna().sum()) + total = len(series) + missing_percentage = round((missing_count / total) * 100, 2) if total > 0 else 0.0 + + # histogram on non-null values + clean = series.dropna() + histogram_bins = None + histogram_counts = None + if len(clean) > 0: + bins_arg = bin_edges if bin_edges is not None else 10 + counts, computed_edges = numpy.histogram(clean, bins=bins_arg) + histogram_counts = counts.tolist() + histogram_bins = computed_edges.tolist() + numericStats = NumericalStatistics.model_validate( { 'count': descriptiveStatsDict['count'], @@ -32,8 +47,12 @@ def generateNumericalStatistics(series) -> DescriptiveStatistics: 'first_quartile': descriptiveStatsDict['25%'], 'second_quartile': descriptiveStatsDict['50%'], 'third_quartile': descriptiveStatsDict['75%'], - 'max': descriptiveStatsDict['max'] - } + 'max': descriptiveStatsDict['max'], + 'missing_count': missing_count, + 'missing_percentage': missing_percentage, + 'histogram_bins': histogram_bins, + 'histogram_counts': histogram_counts, + } ) return DescriptiveStatistics.model_validate({ @@ -48,6 +67,12 @@ def generateCategoricalStatistics(series) -> DescriptiveStatistics: categoricalDict = describeSeries.to_dict() categoricalDict['top'] = categoricalDict.get('top') + # missing data + missing_count = int(series.isna().sum()) + total = len(series) + categoricalDict['missing_count'] = missing_count + categoricalDict['missing_percentage'] = round((missing_count / total) * 100, 2) if total > 0 else 0.0 + categoricalStats = CategoricalStatistics.model_validate(categoricalDict) return DescriptiveStatistics.model_validate({ @@ -78,6 +103,40 @@ def generateSummaryStatistics(dataframe)-> Dict[str, DescriptiveStatistics]: return statistics +def collectHistogramBins(statistics: Dict) -> Dict[str, list]: + """Extract histogram bin edges from total statistics for reuse in splits.""" + bin_edges = {} + for col_name, col_data in statistics.items(): + bins = col_data.get('statistics', {}).get('histogram_bins') + if bins is not None: + bin_edges[col_name] = bins + return bin_edges + + +def generateSummaryStatisticsWithBins( + dataframe, totalBinEdges: Dict[str, list] +) -> Dict[str, DescriptiveStatistics]: + """Like generateSummaryStatistics but uses pre-computed bin edges for histograms.""" + if dataframe.shape[1] > descriptiveStatisticsMaxCols: + return {} + + statistics = {} + numColumns = dataframe.shape[1] + + for i in range(numColumns): + series = dataframe.iloc[:, i] + + if pandas.api.types.is_numeric_dtype(series): + edges = totalBinEdges.get(series.name) + summaryStats = generateNumericalStatistics(series, bin_edges=edges) + else: + summaryStats = generateCategoricalStatistics(series) + + statistics[summaryStats.columnName] = summaryStats.model_dump(mode='json', by_alias=False) + + return statistics + + def applyQuery(dataframe: pandas.DataFrame, query: str, queryType: str) -> Optional[pandas.DataFrame]: queryType = queryType.upper() if queryType else None @@ -97,7 +156,8 @@ def applyQuery(dataframe: pandas.DataFrame, query: str, queryType: str) -> Optio def generateSplitStatistics( dataframe: pandas.DataFrame, - splits: List + splits: List, + totalBinEdges: Optional[Dict[str, list]] = None ) -> Dict[str, Dict]: splitStats = {} @@ -114,11 +174,15 @@ def generateSplitStatistics( try: subset = applyQuery(dataframe, query, queryType) if subset is not None and not subset.empty: + if totalBinEdges: + stats = generateSummaryStatisticsWithBins(subset, totalBinEdges) + else: + stats = generateSummaryStatistics(subset) splitStats[name] = { "query": query, "queryType": queryType, "description": description, - "statistics": generateSummaryStatistics(subset) + "statistics": stats } except Exception: # Skip splits that fail to query diff --git a/mds/src/fairscape_mds/models/annotated_computation.py b/mds/src/fairscape_mds/models/annotated_computation.py index cc38ded..6f0bfc4 100644 --- a/mds/src/fairscape_mds/models/annotated_computation.py +++ b/mds/src/fairscape_mds/models/annotated_computation.py @@ -76,6 +76,7 @@ class DatasetSummary(BaseModel): name: Optional[str] = Field(default=None) role: Optional[str] = Field(default=None) description: Optional[str] = Field(default=None) + dataQuality: Optional[str] = Field(default=None) # --------------------------------------------------------------------------- @@ -97,6 +98,7 @@ class LLMDatasetSummary(BaseModel): name: Optional[str] = Field(default=None) role: Optional[str] = Field(default=None) description: Optional[str] = Field(default=None) + dataQuality: Optional[str] = Field(default=None, description="Data quality observations based on summary statistics") class LLMComputationAnnotation(BaseModel): diff --git a/mds/src/fairscape_mds/models/statistics.py b/mds/src/fairscape_mds/models/statistics.py index 1d3bbae..c1ca78b 100644 --- a/mds/src/fairscape_mds/models/statistics.py +++ b/mds/src/fairscape_mds/models/statistics.py @@ -1,5 +1,5 @@ from pydantic import BaseModel, Field -from typing import Union, Optional, Annotated +from typing import List, Union, Optional, Annotated # TODO context for STATO values StatoContext = { @@ -22,6 +22,10 @@ class NumericalStatistics(BaseModel): second_quartile: Optional[Union[float,str]] = Field(default=None) third_quartile: Optional[Union[float,str]] = Field(default=None) max: Optional[Union[float,str]] = Field(default=None) + missing_count: Optional[int] = Field(default=None) + missing_percentage: Optional[float] = Field(default=None) + histogram_bins: Optional[List[float]] = Field(default=None) + histogram_counts: Optional[List[int]] = Field(default=None) def serializeStato(self): """ """ @@ -32,6 +36,8 @@ class CategoricalStatistics(BaseModel): unique: Optional[Union[int, str]] = Field(default=None) top: Optional[Union[str,bool]] = Field(default=None) freq: Optional[Union[int, str]] = Field(default=None) + missing_count: Optional[int] = Field(default=None) + missing_percentage: Optional[float] = Field(default=None) def serializeStato(self): """ """ From cc26927a4bc80fd0b9d70a4f29756cf08991c109 Mon Sep 17 00:00:00 2001 From: jniestroy Date: Wed, 18 Mar 2026 10:37:17 -0400 Subject: [PATCH 11/27] splits fix --- mds/src/fairscape_mds/crud/statistics.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/mds/src/fairscape_mds/crud/statistics.py b/mds/src/fairscape_mds/crud/statistics.py index 1d3189f..61e99bd 100644 --- a/mds/src/fairscape_mds/crud/statistics.py +++ b/mds/src/fairscape_mds/crud/statistics.py @@ -1,3 +1,4 @@ +import re import pandas import numpy from typing import Dict, List, Optional @@ -143,8 +144,12 @@ def applyQuery(dataframe: pandas.DataFrame, query: str, queryType: str) -> Optio if queryType == "SQL": if pandasql is None: raise ImportError("pandasql is required for SQL split queries. Install it with: pip install pandasql") - dataset = dataframe # noqa: F841 — referenced by SQL query - return pandasql.sqldf(query, locals()) + # Expose the dataframe under whatever table name the query uses + table_names = re.findall(r'\bFROM\s+(\w+)', query, re.IGNORECASE) + env = {"dataset": dataframe} + for name in table_names: + env[name] = dataframe + return pandasql.sqldf(query, env) elif queryType == "PANDAS": # Extract the condition from queries like "split == 'train'" From f708e565eb77ccee0a93b07255768fcf2e53a031 Mon Sep 17 00:00:00 2001 From: jniestroy Date: Mon, 23 Mar 2026 12:25:59 -0400 Subject: [PATCH 12/27] assumptions --- mds/src/fairscape_mds/core/config.py | 3 +- mds/src/fairscape_mds/crud/evidence_graph.py | 3 +- mds/src/fairscape_mds/crud/interpretation.py | 573 +++++++++++------- .../models/annotated_computation.py | 102 +++- .../models/annotated_evidence_graph.py | 28 +- .../fairscape_mds/routers/evidence_graph.py | 7 +- .../tests/crud/test_interpretation.py | 8 +- mds/src/fairscape_mds/worker.py | 4 +- 8 files changed, 455 insertions(+), 273 deletions(-) diff --git a/mds/src/fairscape_mds/core/config.py b/mds/src/fairscape_mds/core/config.py index 97c4e90..37983cf 100644 --- a/mds/src/fairscape_mds/core/config.py +++ b/mds/src/fairscape_mds/core/config.py @@ -159,8 +159,7 @@ def _add_header(request, **kwargs): celeryApp.conf.update( - task_concurrency=4, # Use 4 threads for concurrency - worker_prefetch_multiplier=4 # Prefetch one task at a time + worker_prefetch_multiplier=1 # Process one task at a time ) diff --git a/mds/src/fairscape_mds/crud/evidence_graph.py b/mds/src/fairscape_mds/crud/evidence_graph.py index fdf3489..e6915ef 100644 --- a/mds/src/fairscape_mds/crud/evidence_graph.py +++ b/mds/src/fairscape_mds/crud/evidence_graph.py @@ -54,7 +54,6 @@ def create_evidence_graph( ) insert_data = stored_identifier.model_dump(by_alias=True, mode="json") - logger.warning(f"insert_data after model_dump: {insert_data}") result = self.config.identifierCollection.insert_one(insert_data) if result.inserted_id: return FairscapeResponse(success=True, statusCode=201, model=stored_identifier) @@ -126,7 +125,7 @@ def build_evidence_graph_for_node( ) if existing_graph_data: try: - print("Returning existing graph") + logger.debug("Returning existing graph for %s", existing_graph_id) stored_identifier = StoredIdentifier.model_validate(existing_graph_data) return FairscapeResponse( success=True, diff --git a/mds/src/fairscape_mds/crud/interpretation.py b/mds/src/fairscape_mds/crud/interpretation.py index f00733b..77e4e5e 100644 --- a/mds/src/fairscape_mds/crud/interpretation.py +++ b/mds/src/fairscape_mds/crud/interpretation.py @@ -10,8 +10,10 @@ import asyncio import datetime import re +import time import uuid import logging +from collections import deque from typing import Any, Dict, List, Optional, Tuple import httpx @@ -20,10 +22,10 @@ from fairscape_mds.models.annotated_computation import ( AnnotatedComputation, CodeAnalysis, DatasetSummary, LLMComputationAnnotation, LLMCodeAnalysis, LLMDatasetSummary, - Concern, LLMConcern, ConcernLevel, normalize_concern, + Assumption, LLMAssumption, AssumptionImpact, normalize_assumption, ) from fairscape_mds.models.annotated_evidence_graph import ( - AnnotatedEvidenceGraph, GraphConcern, + AnnotatedEvidenceGraph, GraphAssumption, AudiencePerspective, ) from fairscape_mds.crud.fairscape_request import FairscapeRequest @@ -57,6 +59,58 @@ def run_async(coro): asyncio.set_event_loop(_worker_loop) return _worker_loop.run_until_complete(coro) +# --------------------------------------------------------------------------- +# Async Rate Limiter +# --------------------------------------------------------------------------- + +class AsyncRateLimiter: + """Sliding-window rate limiter for async contexts. + + Allows at most `max_requests` within any rolling `window_seconds` period. + Callers await `acquire()` before making an API call. + """ + + def __init__(self, max_requests: int = 2, window_seconds: float = 10.0): + self._max = max_requests + self._window = window_seconds + self._lock = asyncio.Lock() + self._timestamps: deque = deque() + + async def acquire(self): + async with self._lock: + now = time.monotonic() + # Evict timestamps outside the window + while self._timestamps and (now - self._timestamps[0]) >= self._window: + self._timestamps.popleft() + # If at capacity, sleep until the oldest timestamp exits the window + if len(self._timestamps) >= self._max: + sleep_for = self._window - (now - self._timestamps[0]) + if sleep_for > 0: + logger.info(f"Rate limiter: sleeping {sleep_for:.1f}s") + await asyncio.sleep(sleep_for) + self._timestamps.popleft() + self._timestamps.append(time.monotonic()) + + +MAX_API_RETRIES = 5 +API_RETRY_BASE_DELAY = 10.0 # seconds; doubles each retry + + +async def _run_agent_with_retry(agent, prompt, retries=MAX_API_RETRIES, base_delay=API_RETRY_BASE_DELAY): + """Run agent.run() with exponential backoff on overloaded/rate-limit errors.""" + for attempt in range(retries + 1): + try: + return await agent.run(prompt) + except Exception as e: + err_str = str(e) + is_retryable = "529" in err_str or "overloaded" in err_str.lower() or "rate" in err_str.lower() + if not is_retryable or attempt == retries: + raise + delay = base_delay * (2 ** attempt) + logger.warning(f"API overloaded (attempt {attempt + 1}/{retries + 1}), retrying in {delay:.0f}s: {err_str[:120]}") + await asyncio.sleep(delay) + + # --------------------------------------------------------------------------- # Constants # --------------------------------------------------------------------------- @@ -64,6 +118,7 @@ def run_async(coro): MAX_SOFTWARE_BYTES = 50_000 # 50KB per software entity MAX_STATS_COLUMNS = 25 # max columns of stats to include per dataset in prompt MAX_SPLITS = 10 # max splits to include per dataset in prompt +MAX_PROMPT_DATASETS = 3 # max input/output datasets to include per computation prompt CODE_EXTENSIONS = {".py", ".r", ".R", ".sh", ".pl", ".java", ".scala", ".jl", ".m", ".cpp", ".go", ".rs", ".ipynb", ".md"} GITHUB_REPO_PATTERN = re.compile(r"https?://github\.com/([^/]+)/([^/]+?)(?:\.git)?(?:/.*)?$") GITHUB_FILE_PATTERN = re.compile( @@ -74,152 +129,133 @@ def run_async(coro): # System Prompt -- Data Science Persona # --------------------------------------------------------------------------- -DATASCI_SYSTEM_PROMPT = """You are a senior data scientist and methodologist analyzing a single computation step from a scientific provenance graph (RO-Crate). - -You will receive: -- The computation's metadata (name, description, command) -- Input dataset metadata (names, descriptions, formats) -- Dataset data profiles (when available): per-column summary statistics including distributions, missing data rates, and histograms -- Dataset split statistics (when available): per-column statistics for each data split (train/test/validation) -- Software source code used by the computation -- Output dataset metadata (names, descriptions, formats) - -Your task is to produce a structured annotation of this computation step. - -## Analysis Guidelines - -### Code Analysis (Deep) -- Libraries and frameworks: Note specific libraries and whether they are standard choices -- Data transformations: Trace transformations applied. Flag hidden assumptions -- Statistical methods: Evaluate appropriateness for the data type and research question -- Hardcoded values: Flag magic numbers and parameters that should be configurable -- Error handling: Note whether edge cases are handled - -### Methodology Assessment -- Data leakage: Check for test/validation information leaking into training -- Selection bias: Consider whether filtering steps introduce bias -- Reproducibility: Random seeds, version pinning, parameter documentation - -### Data Quality Assessment (when data profiles are provided) -- Missing data: Flag columns with >10% missing. Consider if missingness is random or systematic and whether it could bias results -- Distribution shape: Use histograms and quartiles to identify skew, multimodality, or outliers (compare mean vs median). Note if distributions look unexpected for the domain -- Split balance: Compare row counts and distributions across train/test/validation splits. Flag significant imbalance or distribution drift that could indicate data leakage -- Scale differences: Note features on very different scales (relevant for distance-based or gradient methods) -- Categorical dominance: Flag categoricals where one value >90% frequency — near-constant features add noise -- Low variance: Flag numerical columns where std is near zero relative to the mean +DATASCI_SYSTEM_PROMPT = """You are an analyst making explicit the assumptions that support the claims produced by a computation step in a scientific provenance graph (RO-Crate). -### Concern Severity Levels — READ CAREFULLY -Every concern MUST be assigned exactly one of these three severity levels. Apply them strictly using the decision procedure below. - -**CRITICAL — Conclusions cannot be trusted** -The code produces results that are demonstrably wrong, or the methodology is fundamentally unsound such that the conclusions it supports are not warranted. You must be able to point to what the code actually does (or claims) that makes the output unreliable. - -Ask: "If a reader trusts this output at face value, will they be misled?" - -Applies when: -- The code does X but reports it as Y (e.g., evaluates on training data, labels it "test accuracy") -- A quantity is presented as measured or calibrated but is actually fabricated or an acknowledged guess -- Information from the target/outcome leaks into the features or model inputs -- A metric is used in a way that does not measure what it claims (e.g., treating UMAP distance as a statistical test of significance) +You will receive the computation's metadata, input/output datasets with data profiles (when available), and software source code. -Does NOT apply when: -- A risk exists but the code handles it correctly -- A methodology is claimed but not shown — that is unverifiable, not wrong -- Something *could* go wrong if a downstream user misuses the output +Your task: produce a structured annotation that surfaces the assumptions this step relies on — what the analysis is built on, and what would change the interpretation if it turned out to be wrong. -**MODERATE — Methodology has a real, demonstrable weakness** -A reviewer would flag this as a gap that weakens confidence in the results, but the results are not necessarily wrong. The issue is concrete and present in the pipeline — not hypothetical. +## What to Look For -Ask: "Does this weaken the strength of evidence, even if the conclusions might still be correct?" +### Data Assumptions +What properties of the input data does this step rely on? Consider: completeness, distribution shape, independence of observations, representativeness of the sample, encoding and formatting, absence of systematic missingness. -Applies when: -- Stochastic steps lack random seeds, so results are not reproducible across runs -- Only a subset of available data is used without justification (e.g., one replicate of three) -- Evaluation metrics are inappropriate for the data characteristics (e.g., accuracy on imbalanced classes) -- Key parameters are hardcoded without sensitivity analysis and directly affect the results (e.g., similarity thresholds that determine network density) -- No validation or calibration of a method whose accuracy is not self-evident +### Methodological Assumptions +What analytical choices were made, and what do they assume about the problem? Consider: model family appropriateness, distance/similarity metrics, evaluation strategy, splitting procedure, threshold values, handling of confounders. -Does NOT apply when: -- The code does not re-verify something already done correctly upstream -- A parameter is hardcoded but has a reasonable default and low impact -- A claimed process is not shown in code but there is no evidence it was done wrong +### Software/Parameter Assumptions +What do the code's defaults, hardcoded values, and library choices assume? Consider: parameter sensitivity, random seed presence, version-dependent behavior, implicit ordering. -**MINOR — Recommendations and best-practice gaps** -Suggestions that would improve rigour, documentation, or robustness, but whose absence does not weaken the actual results or conclusions drawn from this pipeline. +## Impact Classification -Ask: "Would fixing this change the results or conclusions?" If no, it is MINOR. +Every assumption MUST be assigned exactly one impact level: -Applies when: -- Missing version pinning, dependency documentation, or environment specifications -- No input validation or schema checks -- Hardcoded parameters with low impact on results -- Missing confidence intervals or uncertainty quantification (when results are otherwise sound) -- Documentation gaps, typos, missing docstrings -- Model artifacts not serialised +**CRITICAL** — The entire result rests on this. If this assumption is wrong, the main conclusions do not hold. +Ask: "If this assumption fails, do the conclusions still stand?" If no → CRITICAL. +Examples: training/test independence, outcome variable validity, core statistical model appropriateness. -### Decision procedure -1. Can you point to specific code/output where the result is wrong or misleading? → CRITICAL -2. No, but can you identify a concrete methodological gap that weakens the evidence? → MODERATE -3. No, but you have a recommendation that would improve the work? → MINOR -4. None of the above? → Do not raise a concern. +**MAJOR** — Critical for a subset of results. If wrong, specific results break or change, but other portions of the analysis may still hold. +Ask: "If this assumption fails, do specific results or secondary claims break?" If yes → MAJOR. +Examples: default hyperparameters being adequate, a particular threshold choice, evaluation metric appropriateness for the data. -### Key principles -- There is no minimum number of concerns at any level. Many well-written computation steps will have zero CRITICAL and zero MODERATE concerns. That is a valid and expected outcome — report it as such. -- Do not inflate severity to fill quotas. A concern list with 2 MINOR items is better analysis than one with fabricated CRITICAL findings. -- Grade based on what the code *actually does*, not hypothetical scenarios where someone might misuse its outputs. -- If the code handles something correctly, do not raise a concern about how a future user *could* get it wrong. -- When in doubt between two levels, choose the lower one. +**MINOR** — Present but unlikely to change the main conclusions. Worth recording for reuse or extension. +Ask: "Would correcting this change the results?" If no → MINOR. +Examples: version pinning, input validation, documentation completeness. -Use ONLY these three levels. Do not invent additional levels like WARNING, INFO, or GOOD. +## Key Principles +- State what the assumption IS, not just that something could go wrong. "Assumes input features are independently distributed" is better than "features might be correlated." +- Be specific: name the parameter, the data property, the function. +- Many well-written steps will have only MINOR assumptions. That is valid. +- Do not manufacture assumptions to fill quotas. +- Weave critical assumptions into the stepSummary itself — they are part of the story of what this step does. +- If code is demonstrably wrong (produces misleading results), flag that as a CRITICAL assumption violation. -### Output Requirements -Return a structured annotation with: -- stepSummary: A clear description of what this computation step does and why -- codeAnalysis: For each software entity, provide a summary, key functions, and concerns (each concern as {level, description}) -- inputSummaries: For each input dataset, describe its role and include a dataQuality field with observations from the data profile (missing data, distribution issues, etc.) if statistics were provided -- outputSummaries: For each output dataset, describe what it contains and include a dataQuality field with observations if statistics were provided -- concerns: List any methodological, statistical, or reproducibility concerns (each as {level, description}). This list may be empty or contain only MINOR items if the step is methodologically sound. +## Output +- stepSummary: What this step does, why, and what critical assumptions it rests on. +- codeAnalysis: Per software entity — summary, key functions, and assumptions (each with the structured format below). +- inputSummaries: Per input dataset — role, description, dataQuality observations from data profile. +- outputSummaries: Per output dataset — what it contains, dataQuality observations. +- assumptions: Step-level assumptions. May be empty if the step makes no notable assumptions. -Be precise and evidence-based. Reference specific function names and parameter values. -Acknowledge when methods are well-chosen.""" +Each assumption MUST be a structured object: +{ + impact: "CRITICAL" | "MAJOR" | "MINOR", + name: "Short label (3-8 words) — e.g. 'Train/test split independence'", + description: "Briefly describe what is being assumed", + downstreamImpacts: "What changes if this assumption is wrong — potential downstream effects", + evidence: { artifact: {"@id": "<@id of the data file or software entity>"}, location: "" } +} +Be precise and evidence-based. Reference specific function names and parameter values.""" -SYNTHESIS_SYSTEM_PROMPT = """You are a senior data scientist synthesizing annotations from all computation steps in a scientific provenance graph (RO-Crate) into a graph-level summary. -You will receive: -- The RO-Crate name and description -- Step-by-step annotations for each computation in the pipeline -- The pipeline's final outputs - -Your task is to produce: -1. executiveSummary: 3-5 sentences covering what the pipeline does, its analytical approach, and the most important observation -2. narrativeSummary: A forward-chronological story of the entire pipeline, starting from origin data and ending at final outputs -3. keyFindings: Bulleted list of important observations, prioritized by severity -4. concerns: Cross-cutting methodological, statistical, or reproducibility concerns that span the pipeline. Each concern must be a structured object with: - - level: One of CRITICAL, MODERATE, or MINOR (no other levels allowed) - - description: The concern text +DATASCI_SYNTHESIS_PROMPT = """You are a senior data scientist synthesizing step annotations from a scientific analysis pipeline (RO-Crate) into a coherent picture of what supports the pipeline's claims. -### Concern Severity — Apply the same rubric as step-level analysis +You will receive the RO-Crate overview and step-by-step annotations including assumptions. -**CRITICAL — Conclusions cannot be trusted.** The pipeline produces results that are demonstrably wrong or misleading. You must trace the flaw through specific computation steps. Ask: "If a reader trusts the final output at face value, will they be misled?" +Produce: +1. executiveSummary: 3-5 sentences covering what the pipeline does, its approach, and the most important critical assumptions it rests on. Weave the load-bearing assumptions into this summary. +2. narrativeSummary: A forward-chronological story of the pipeline, explicitly noting where key assumptions enter and what claims they support. The reader should finish this knowing what the results depend on. +3. keyFindings: Bulleted list of important observations about what the pipeline discovered. +4. assumptions: Cross-cutting assumptions that span the pipeline, each as a structured object: + {impact, name, description, downstreamImpacts} + - impact: "CRITICAL" (if wrong, pipeline conclusions don't hold), "MAJOR" (if wrong, specific results break but others may hold), or "MINOR" (worth noting but won't change conclusions) + - name: Short label (3-8 words) + - description: What is being assumed + - downstreamImpacts: What changes if this assumption is wrong -**MODERATE — Methodology has a real, demonstrable weakness.** A reviewer would flag this as weakening confidence, but results are not necessarily wrong. Ask: "Does this weaken the strength of evidence?" +Do NOT re-list every step-level assumption. Surface those that matter at the pipeline level — because they span steps, compound across steps, or are the most consequential for trusting the results. A pipeline with no CRITICAL assumptions at the graph level is valid if step-level ones don't compound.""" -**MINOR — Recommendations and best-practice gaps.** Would improve the work, but fixing it would not change the results or conclusions. -### Decision procedure -1. Can you trace a specific flaw through the pipeline that makes an output wrong or misleading? → CRITICAL -2. No, but can you identify a concrete methodological gap that weakens the evidence? → MODERATE -3. No, but you have a recommendation? → MINOR +BIOSTAT_SYNTHESIS_PROMPT = """You are a biostatistician reviewing a scientific analysis pipeline (RO-Crate). Synthesize the step annotations into a perspective focused on statistical rigor and the assumptions that underpin the quantitative claims. -### Key principles -- There is no minimum number of concerns at any level. A pipeline with zero CRITICAL and zero MODERATE concerns is a valid and expected outcome for well-constructed work. Report that clearly rather than manufacturing issues. -- Do NOT escalate step-level concerns. If a step annotation flagged something as MINOR, do not promote it to MODERATE or CRITICAL unless a cross-step interaction demonstrably makes it worse. -- Do NOT re-list every step-level concern. Only surface concerns that matter at the pipeline level — either because they span multiple steps, compound across steps, or are the most important findings. -- Grade based on what the code actually does, not hypothetical misuse scenarios. -- When in doubt between two levels, choose the lower one. +You will receive the RO-Crate overview and step-by-step annotations including assumptions. -Write as if explaining to a colleague. Be precise and evidence-based.""" +Produce: +1. executiveSummary: 3-5 sentences on the pipeline's statistical approach and the critical statistical assumptions it rests on. +2. narrativeSummary: Forward-chronological story emphasizing where statistical assumptions enter — distributional assumptions, independence assumptions, sample size considerations, multiple testing implications, model specification choices. The reader should understand the statistical scaffolding supporting the claims. +3. keyFindings: Bulleted observations focused on statistical methodology — what was done well and what gaps exist. +4. assumptions: Cross-cutting statistical assumptions, each as a structured object: + {impact, name, description, downstreamImpacts} + - impact: "CRITICAL" (core statistical assumptions, e.g. distributional assumptions, independence of observations), "MAJOR" (statistical choices that shape specific results, e.g. correction methods, model selection), or "MINOR" (minor statistical notes for reproducibility) + - name: Short label (3-8 words) + - description: What is being assumed + - downstreamImpacts: What changes if this assumption is wrong + +Focus on what a statistician reviewing this work would want to verify. Do NOT re-list every step-level assumption.""" + + +CLINICIAN_SYNTHESIS_PROMPT = """You are a clinician reviewing a scientific analysis pipeline (RO-Crate). Synthesize the step annotations into a perspective focused on clinical applicability and what assumptions must hold for these results to inform patient care. + +You will receive the RO-Crate overview and step-by-step annotations including assumptions. + +Produce: +1. executiveSummary: 3-5 sentences on what clinical question this pipeline addresses and what critical assumptions must hold for the results to be clinically actionable. +2. narrativeSummary: Forward-chronological story emphasizing clinical relevance — what patient population is assumed, what outcome measures are used and whether they map to clinical endpoints, what generalizability assumptions are made. The reader should understand what would need to be true for these results to apply in practice. +3. keyFindings: Bulleted observations focused on clinical applicability — effect sizes, clinical vs statistical significance, population representativeness. +4. assumptions: Cross-cutting clinical assumptions, each as a structured object: + {impact, name, description, downstreamImpacts} + - impact: "CRITICAL" (assumptions about patient population, outcome validity, clinical relevance that conclusions rest on), "MAJOR" (assumptions affecting how broadly or confidently specific results can be applied), or "MINOR" (notes for clinical context unlikely to change interpretation) + - name: Short label (3-8 words) + - description: What is being assumed + - downstreamImpacts: What changes if this assumption is wrong + +Focus on what a clinician deciding whether to act on these results would want to know. Do NOT re-list every step-level assumption.""" + + +# Audience configuration for synthesis +AUDIENCE_CONFIGS = [ + { + "key": "biostat", + "label": "Biostatistician", + "prompt": BIOSTAT_SYNTHESIS_PROMPT, + }, + { + "key": "clinician", + "label": "Clinician", + "prompt": CLINICIAN_SYNTHESIS_PROMPT, + }, +] # --------------------------------------------------------------------------- @@ -234,7 +270,7 @@ class GraphSynthesisResult(BaseModel): executiveSummary: str narrativeSummary: str keyFindings: List[str] = [] - concerns: List[LLMConcern] = [] + assumptions: List[LLMAssumption] = [] # --------------------------------------------------------------------------- @@ -362,18 +398,20 @@ def _format_dataset_stats(ds_name: str, ds_stats: dict) -> str: if mc is not None: total_missing += mc - truncated = total_cols > MAX_STATS_COLUMNS - display_cols = columns[:MAX_STATS_COLUMNS] - - parts.append(f"#### Data Profile: {ds_name} ({total_cols} columns, ~{row_count} rows, {total_missing} missing values)") - parts.append("| Column | Type | Count | Missing% | Mean/Top | Std/Unique | Min | Q1 | Median | Q3 | Max | Hist |") - parts.append("|--------|------|-------|----------|----------|------------|-----|----|----|----|----|------|") - for col_name, col_data in display_cols: - parts.append(_format_column_stats(col_name, col_data)) + # Skip detailed stats for wide datasets (>10 columns) to keep prompts small + if total_cols > 10: + parts.append(f"#### Data Profile: {ds_name} ({total_cols} columns, ~{row_count} rows, {total_missing} missing values)") + parts.append("*(Column-level statistics omitted for wide dataset)*") + parts.append("") + else: + display_cols = columns - if truncated: - parts.append(f"| ... | | | | | | | | | | | *{total_cols - MAX_STATS_COLUMNS} more columns omitted* |") - parts.append("") + parts.append(f"#### Data Profile: {ds_name} ({total_cols} columns, ~{row_count} rows, {total_missing} missing values)") + parts.append("| Column | Type | Count | Missing% | Mean/Top | Std/Unique | Min | Q1 | Median | Q3 | Max | Hist |") + parts.append("|--------|------|-------|----------|----------|------------|-----|----|----|----|----|------|") + for col_name, col_data in display_cols: + parts.append(_format_column_stats(col_name, col_data)) + parts.append("") # --- Split statistics --- if split_stats: @@ -399,16 +437,17 @@ def _format_dataset_stats(ds_name: str, ds_stats: dict) -> str: parts.append(label) split_columns = list(split_col_stats.items()) - display_split_cols = split_columns[:MAX_STATS_COLUMNS] - parts.append("| Column | Type | Count | Missing% | Mean/Top | Std/Unique | Min | Q1 | Median | Q3 | Max | Hist |") - parts.append("|--------|------|-------|----------|----------|------------|-----|----|----|----|----|------|") - for col_name, col_data in display_split_cols: - parts.append(_format_column_stats(col_name, col_data)) - - if len(split_columns) > MAX_STATS_COLUMNS: - parts.append(f"| ... | | | | | | | | | | | *{len(split_columns) - MAX_STATS_COLUMNS} more columns omitted* |") - parts.append("") + # Skip detailed split stats for wide datasets + if len(split_columns) > 10: + parts.append(f"*({len(split_columns)} columns, stats omitted for wide dataset)*") + parts.append("") + else: + parts.append("| Column | Type | Count | Missing% | Mean/Top | Std/Unique | Min | Q1 | Median | Q3 | Max | Hist |") + parts.append("|--------|------|-------|----------|----------|------------|-----|----|----|----|----|------|") + for col_name, col_data in split_columns: + parts.append(_format_column_stats(col_name, col_data)) + parts.append("") if truncated_splits: parts.append(f"*... {total_splits - MAX_SPLITS} more splits omitted*\n") @@ -754,8 +793,10 @@ def _build_computation_prompt(self, computation: dict, software_cache: dict, ind # Input datasets input_refs = _resolve_refs(computation.get("usedDataset")) if input_refs: + truncated_inputs = len(input_refs) > MAX_PROMPT_DATASETS + display_inputs = input_refs[:MAX_PROMPT_DATASETS] parts.append("## Input Datasets") - for ds_id in input_refs: + for ds_id in display_inputs: ds_node = index.get(ds_id, {}) ds_name = ds_node.get('name', ds_id) parts.append(f"- **{ds_name}** ({ds_node.get('format', 'unknown format')})") @@ -769,6 +810,8 @@ def _build_computation_prompt(self, computation: dict, software_cache: dict, ind if formatted: parts.append("") parts.append(formatted) + if truncated_inputs: + parts.append(f"*({len(input_refs) - MAX_PROMPT_DATASETS} more input datasets omitted)*") parts.append("") # Software and source code @@ -791,8 +834,10 @@ def _build_computation_prompt(self, computation: dict, software_cache: dict, ind # Output datasets output_refs = _resolve_refs(computation.get("generated")) if output_refs: + truncated_outputs = len(output_refs) > MAX_PROMPT_DATASETS + display_outputs = output_refs[:MAX_PROMPT_DATASETS] parts.append("## Output Datasets") - for ds_id in output_refs: + for ds_id in display_outputs: ds_node = index.get(ds_id, {}) ds_name = ds_node.get('name', ds_id) parts.append(f"- **{ds_name}** ({ds_node.get('format', 'unknown format')})") @@ -804,6 +849,8 @@ def _build_computation_prompt(self, computation: dict, software_cache: dict, ind if formatted: parts.append("") parts.append(formatted) + if truncated_outputs: + parts.append(f"*({len(output_refs) - MAX_PROMPT_DATASETS} more output datasets omitted)*") parts.append("") prompt = "\n".join(parts) @@ -830,7 +877,7 @@ def _llm_to_annotated( name=ca.name, summary=ca.summary, keyFunctions=ca.keyFunctions, - concerns=[normalize_concern(c) for c in (ca.concerns or [])], + assumptions=[normalize_assumption(c) for c in (ca.assumptions or [])], ) for ca in (llm_result.codeAnalysis or []) ] @@ -867,7 +914,7 @@ def _llm_to_annotated( "evi:codeAnalysis": [ca.model_dump(by_alias=True) for ca in code_analyses], "evi:inputSummaries": [ds.model_dump(by_alias=True) for ds in input_summaries], "evi:outputSummaries": [ds.model_dump(by_alias=True) for ds in output_summaries], - "evi:concerns": [normalize_concern(c).model_dump() for c in (llm_result.concerns or [])], + "evi:assumptions": [normalize_assumption(a).model_dump() for a in (llm_result.assumptions or [])], "evi:llmModel": llm_model, "evi:llmTemperature": temperature, "dateCreated": now, @@ -894,7 +941,7 @@ async def _annotate_single_computation( retries=3, ) - result = await agent.run(prompt) + result = await _run_agent_with_retry(agent, prompt) llm_output: LLMComputationAnnotation = result.output # Persist raw LLM output for debugging @@ -919,8 +966,9 @@ async def _annotate_computations_async( index: dict, llm_model: str, temperature: float, - max_workers: int = 4, + max_workers: int = 2, stats_cache: Optional[Dict[str, dict]] = None, + rate_limiter: Optional["AsyncRateLimiter"] = None, ) -> List[AnnotatedComputation]: """Annotate all computations concurrently via asyncio.gather.""" self._update_task(task_guid, { @@ -928,36 +976,35 @@ async def _annotate_computations_async( "status": "PROMPTING", }) - sem = asyncio.Semaphore(max_workers) - async def _annotate_one(comp): comp_id = comp.get("@id", "unknown") - async with sem: - try: - annotated = await self._annotate_single_computation( - task_guid, comp, software_cache, index, llm_model, temperature, - stats_cache=stats_cache, - ) - self.config.asyncCollection.update_one( - {"guid": task_guid, "computation_details.computation_id": comp_id}, - {"$set": {"computation_details.$.status": "done"}} - ) - return ("ok", comp_id, annotated) - except Exception as e: - logger.error(f"Failed to annotate computation {comp_id}: {e}") - self.config.asyncCollection.update_one( - {"guid": task_guid, "computation_details.computation_id": comp_id}, - {"$set": { - "computation_details.$.status": "error", - "computation_details.$.error": str(e), - }} - ) - return ("error", comp_id, str(e)) - finally: - self.config.asyncCollection.update_one( - {"guid": task_guid}, - {"$inc": {"completed_computations": 1}} - ) + if rate_limiter: + await rate_limiter.acquire() + try: + annotated = await self._annotate_single_computation( + task_guid, comp, software_cache, index, llm_model, temperature, + stats_cache=stats_cache, + ) + self.config.asyncCollection.update_one( + {"guid": task_guid, "computation_details.computation_id": comp_id}, + {"$set": {"computation_details.$.status": "done"}} + ) + return ("ok", comp_id, annotated) + except Exception as e: + logger.error(f"Failed to annotate computation {comp_id}: {e}") + self.config.asyncCollection.update_one( + {"guid": task_guid, "computation_details.computation_id": comp_id}, + {"$set": { + "computation_details.$.status": "error", + "computation_details.$.error": str(e), + }} + ) + return ("error", comp_id, str(e)) + finally: + self.config.asyncCollection.update_one( + {"guid": task_guid}, + {"$inc": {"completed_computations": 1}} + ) outcomes = await asyncio.gather(*[_annotate_one(c) for c in computations]) @@ -979,34 +1026,26 @@ def annotate_computations_parallel( index: dict, llm_model: str, temperature: float, - max_workers: int = 4, + max_workers: int = 2, stats_cache: Optional[Dict[str, dict]] = None, + rate_limiter: Optional["AsyncRateLimiter"] = None, ) -> List[AnnotatedComputation]: """Annotate computations concurrently using async I/O.""" return run_async(self._annotate_computations_async( task_guid, computations, software_cache, index, llm_model, temperature, max_workers, - stats_cache=stats_cache, + stats_cache=stats_cache, rate_limiter=rate_limiter, )) # ------------------------------------------------------------------ # Step 5: Graph-level synthesis # ------------------------------------------------------------------ - def synthesize_graph( + def _build_synthesis_prompt( self, - task_guid: str, root_node: dict, step_annotations: List[AnnotatedComputation], - llm_model: str, - temperature: float, - ) -> GraphSynthesisResult: - """Synthesize graph-level summary from all step annotations.""" - self._update_task(task_guid, { - "current_step": "SYNTHESIZING", - "status": "SYNTHESIZING", - }) - - # Build synthesis prompt + ) -> str: + """Build the shared synthesis prompt from step annotations.""" parts = [] parts.append("## RO-Crate Overview") parts.append(f"**Name:** {root_node.get('name', 'Unknown')}") @@ -1019,35 +1058,75 @@ def synthesize_graph( for i, ann in enumerate(step_annotations, 1): parts.append(f"### Step {i}: {ann.annotates}") parts.append(f"**Summary:** {ann.stepSummary}") - if ann.concerns: - parts.append(f"**Concerns:** {'; '.join(f'[{c.level.value}] {c.description}' for c in ann.concerns)}") + if ann.assumptions: + parts.append(f"**Assumptions:** {'; '.join(f'[{a.impact.value}] {a.description}' for a in ann.assumptions)}") if ann.codeAnalysis: for ca in ann.codeAnalysis: parts.append(f"**Code ({ca.name or ca.software}):** {ca.summary}") parts.append("") - prompt = "\n".join(parts) + return "\n".join(parts) - async def _run_synthesis(): - agent = Agent( - llm_model, - output_type=GraphSynthesisResult, - system_prompt=SYNTHESIS_SYSTEM_PROMPT, - retries=2, - ) - result = await agent.run(prompt) - return result.output + def synthesize_graph( + self, + task_guid: str, + root_node: dict, + step_annotations: List[AnnotatedComputation], + llm_model: str, + temperature: float, + rate_limiter: Optional["AsyncRateLimiter"] = None, + ) -> Tuple[GraphSynthesisResult, List[dict]]: + """Synthesize graph-level summary from all step annotations. - synthesis = run_async(_run_synthesis()) + Returns (datasci_synthesis, audience_perspectives) where + audience_perspectives is a list of AudiencePerspective dicts. + """ + self._update_task(task_guid, { + "current_step": "SYNTHESIZING", + "status": "SYNTHESIZING", + }) - # Persist raw LLM output for debugging - self._save_llm_result( - task_guid, - "synthesis", - synthesis.model_dump(mode="json"), - ) + prompt = self._build_synthesis_prompt(root_node, step_annotations) + + async def _run_all_syntheses(): + # Run data scientist + audience syntheses in parallel + async def _run_one(system_prompt: str, label: str) -> GraphSynthesisResult: + if rate_limiter: + await rate_limiter.acquire() + agent = Agent( + llm_model, + output_type=GraphSynthesisResult, + system_prompt=system_prompt, + retries=2, + ) + result = await _run_agent_with_retry(agent, prompt) + self._save_llm_result(task_guid, label, result.output.model_dump(mode="json")) + return result.output - return synthesis + tasks = [_run_one(DATASCI_SYNTHESIS_PROMPT, "synthesis:datasci")] + # for aud in AUDIENCE_CONFIGS: + # tasks.append(_run_one(aud["prompt"], f"synthesis:{aud['key']}")) + + return await asyncio.gather(*tasks) + + results = run_async(_run_all_syntheses()) + + datasci_synthesis = results[0] + audience_perspectives = [] + for i, aud in enumerate(AUDIENCE_CONFIGS): + if i + 1 >= len(results): + break + aud_result = results[i + 1] + audience_perspectives.append({ + "targetAudience": aud["key"], + "audienceLabel": aud["label"], + "executiveSummary": aud_result.executiveSummary, + "narrativeSummary": aud_result.narrativeSummary, + "keyFindings": aud_result.keyFindings, + "assumptions_raw": aud_result.assumptions, # LLMAssumption list, normalized later + }) + + return datasci_synthesis, audience_perspectives # ------------------------------------------------------------------ # Step 6: Build and store AnnotatedEvidenceGraph @@ -1061,6 +1140,7 @@ def build_and_store( graph: list, step_annotations: List[AnnotatedComputation], synthesis: GraphSynthesisResult, + audience_perspectives: List[dict], llm_model: str, temperature: float, ) -> str: @@ -1110,24 +1190,53 @@ def build_and_store( # Build step annotation refs step_ann_refs = [{"@id": ann.guid} for ann in step_annotations] - # Compile graph-level concerns from step annotations with source links - compiled_concerns = [] + # Compile graph-level assumptions from step annotations with source links + compiled_assumptions = [] for ann in step_annotations: - for concern in (ann.concerns or []): - compiled_concerns.append(GraphConcern( - level=concern.level, - description=concern.description, + for assumption in (ann.assumptions or []): + compiled_assumptions.append(GraphAssumption( + impact=assumption.impact, + name=assumption.name, + description=assumption.description, + downstreamImpacts=assumption.downstreamImpacts, + evidence=assumption.evidence, sourceAnnotation={"@id": ann.guid}, )) - # Add synthesis-level concerns (not tied to a single step) - for llm_concern in (synthesis.concerns or []): - normalized = normalize_concern(llm_concern) - compiled_concerns.append(GraphConcern( - level=normalized.level, + # Add synthesis-level assumptions (not tied to a single step) + for llm_assumption in (synthesis.assumptions or []): + normalized = normalize_assumption(llm_assumption) + compiled_assumptions.append(GraphAssumption( + impact=normalized.impact, + name=normalized.name, description=normalized.description, + downstreamImpacts=normalized.downstreamImpacts, + evidence=normalized.evidence, sourceAnnotation={"@id": rocrate_id}, )) + # Build audience perspectives with normalized assumptions + audiences = [] + for aud_data in audience_perspectives: + aud_assumptions = [] + for llm_a in (aud_data.get("assumptions_raw") or []): + normalized = normalize_assumption(llm_a) + aud_assumptions.append(GraphAssumption( + impact=normalized.impact, + name=normalized.name, + description=normalized.description, + downstreamImpacts=normalized.downstreamImpacts, + evidence=normalized.evidence, + sourceAnnotation={"@id": rocrate_id}, + )) + audiences.append(AudiencePerspective( + targetAudience=aud_data["targetAudience"], + audienceLabel=aud_data["audienceLabel"], + executiveSummary=aud_data["executiveSummary"], + narrativeSummary=aud_data["narrativeSummary"], + keyFindings=aud_data.get("keyFindings", []), + assumptions=aud_assumptions, + )) + # Extract NAAN from rocrate_id for the new identifier ark_match = re.match(r"ark:(\d+)/(.*)", rocrate_id) if ark_match: @@ -1150,7 +1259,8 @@ def build_and_store( "evi:executiveSummary": synthesis.executiveSummary, "evi:narrativeSummary": synthesis.narrativeSummary, "evi:keyFindings": synthesis.keyFindings, - "evi:concerns": [c.model_dump(by_alias=True, mode="json") for c in compiled_concerns], + "evi:assumptions": [a.model_dump(by_alias=True, mode="json") for a in compiled_assumptions], + "evi:audiences": [a.model_dump(by_alias=True, mode="json") for a in audiences], "evi:stepAnnotations": step_ann_refs, "evi:llmModel": llm_model, "evi:llmTemperature": temperature, @@ -1217,6 +1327,7 @@ def interpret_rocrate(self, task_guid: str, user_token: str = "") -> str: rocrate_id = task_doc["rocrate_id"] llm_model = task_doc.get("llm_model", "google-gla:gemini-2.5-flash-lite") temperature = task_doc.get("llm_temperature", 0.2) + rate_limiter = AsyncRateLimiter(max_requests=2, window_seconds=10.0) # # Diagnostic: confirm which API key and model are in use # raw_key = os.environ.get("ANTHROPIC_API_KEY", "") @@ -1246,21 +1357,23 @@ def interpret_rocrate(self, task_guid: str, user_token: str = "") -> str: # Step 3b: Pre-fetch dataset statistics stats_cache = self.prefetch_dataset_statistics(task_guid, computations, index) - # Step 4: Annotate computations in parallel + # Step 4: Annotate computations (paced by rate limiter) step_annotations = self.annotate_computations_parallel( task_guid, computations, software_cache, index, llm_model, temperature, - stats_cache=stats_cache, + stats_cache=stats_cache, rate_limiter=rate_limiter, ) - # Step 5: Graph-level synthesis - synthesis = self.synthesize_graph( + # Step 5: Graph-level synthesis (paced by rate limiter) + synthesis, audience_perspectives = self.synthesize_graph( task_guid, root_node, step_annotations, llm_model, temperature, + rate_limiter=rate_limiter, ) # Step 6: Build and store aeg_id = self.build_and_store( task_guid, rocrate_id, condensed_id, graph, - step_annotations, synthesis, llm_model, temperature, + step_annotations, synthesis, audience_perspectives, + llm_model, temperature, ) # Success diff --git a/mds/src/fairscape_mds/models/annotated_computation.py b/mds/src/fairscape_mds/models/annotated_computation.py index 6f0bfc4..e386dc2 100644 --- a/mds/src/fairscape_mds/models/annotated_computation.py +++ b/mds/src/fairscape_mds/models/annotated_computation.py @@ -19,42 +19,94 @@ # --------------------------------------------------------------------------- -# Concern severity levels +# Assumption impact levels # --------------------------------------------------------------------------- -class ConcernLevel(str, Enum): - """Exactly three severity levels for annotation concerns.""" +class AssumptionImpact(str, Enum): + """How much this assumption matters for trusting/reusing results.""" CRITICAL = "CRITICAL" - MODERATE = "MODERATE" + MAJOR = "MAJOR" MINOR = "MINOR" -class Concern(BaseModel): - """A structured concern with a severity level.""" - level: ConcernLevel +class EvidencePointer(BaseModel): + """Points to the specific artifact and location supporting an assumption.""" + model_config = ConfigDict(extra="allow") + + artifact: IdentifierValue = Field(description='{"@id": "ark:..."} pointing to data file or software') + location: Optional[str] = Field(default=None, description="Specific location, e.g. line number, function name, column name") + + @model_validator(mode='before') + @classmethod + def normalize_artifact(cls, values): + """Accept various LLM artifact formats and normalize to IdentifierValue.""" + if not isinstance(values, dict): + return values + artifact = values.get("artifact") + if artifact is None: + return values + if isinstance(artifact, str): + values["artifact"] = {"@id": artifact} + elif isinstance(artifact, dict) and "@id" not in artifact: + # LLM may return {guid: "ark:..."} or {id: "ark:..."} etc + ark_id = ( + artifact.get("guid") + or artifact.get("id") + or artifact.get("@id") + or next(iter(artifact.values()), None) + ) + if ark_id: + values["artifact"] = {"@id": str(ark_id)} + return values + + +class Assumption(BaseModel): + """A structured assumption with an impact level.""" + impact: AssumptionImpact + name: str = Field(default="", description="Short label for the assumption (3-8 words)") description: str + downstreamImpacts: Optional[str] = Field(default=None, description="What changes if this assumption is wrong") + evidence: Optional[EvidencePointer] = Field(default=None, description="Pointer to supporting artifact") -class LLMConcern(BaseModel): - """What the LLM returns for a concern (level as str for flexibility).""" - level: str = Field(description="One of: CRITICAL, MODERATE, MINOR") +class LLMAssumption(BaseModel): + """What the LLM returns for an assumption (impact as str for flexibility).""" + impact: str = Field(description="One of: CRITICAL, MAJOR, MINOR") + name: str = Field(default="", description="Short label for this assumption (3-8 words)") description: str + downstreamImpacts: Optional[str] = Field(default=None, description="What changes downstream if this assumption is wrong") + evidence: Optional[dict] = Field(default=None, description='Pointer: {artifact: {"@id": "ark:..."}, location: "..."}') -def normalize_concern(llm_concern: LLMConcern) -> Concern: - """Convert an LLMConcern to a validated Concern, normalizing the level.""" - raw = llm_concern.level.strip().upper() +def normalize_assumption(llm_assumption: LLMAssumption) -> Assumption: + """Convert an LLMAssumption to a validated Assumption, normalizing the impact.""" + raw = llm_assumption.impact.strip().upper() try: - level = ConcernLevel(raw) + impact = AssumptionImpact(raw) except ValueError: - # Fallback: map common alternatives - if "CRIT" in raw: - level = ConcernLevel.CRITICAL - elif "MOD" in raw or "WARN" in raw: - level = ConcernLevel.MODERATE + # Fallback: map common alternatives, old concern levels, and legacy names + if "CRIT" in raw or "FOUND" in raw: + impact = AssumptionImpact.CRITICAL + elif "MAJ" in raw or "CONSEQ" in raw or "SIG" in raw or "MOD" in raw or "WARN" in raw: + impact = AssumptionImpact.MAJOR else: - level = ConcernLevel.MINOR - return Concern(level=level, description=llm_concern.description) + impact = AssumptionImpact.MINOR + + evidence = None + if llm_assumption.evidence and isinstance(llm_assumption.evidence, dict): + try: + evidence = EvidencePointer.model_validate(llm_assumption.evidence) + except Exception: + # If evidence can't be parsed, skip it rather than failing + pass + + return Assumption( + impact=impact, + name=getattr(llm_assumption, "name", "") or "", + description=llm_assumption.description, + downstreamImpacts=getattr(llm_assumption, "downstreamImpacts", None), + evidence=evidence, + ) class CodeAnalysis(BaseModel): @@ -65,7 +117,7 @@ class CodeAnalysis(BaseModel): name: Optional[str] = Field(default=None) summary: str keyFunctions: Optional[List[str]] = Field(default=None) - concerns: Optional[List[Concern]] = Field(default=None) + assumptions: Optional[List[Assumption]] = Field(default=None) class DatasetSummary(BaseModel): @@ -89,7 +141,7 @@ class LLMCodeAnalysis(BaseModel): name: Optional[str] = Field(default=None) summary: str keyFunctions: Optional[List[str]] = Field(default=None) - concerns: Optional[List[LLMConcern]] = Field(default=None) + assumptions: Optional[List[LLMAssumption]] = Field(default=None) class LLMDatasetSummary(BaseModel): @@ -107,7 +159,7 @@ class LLMComputationAnnotation(BaseModel): codeAnalysis: Optional[List[LLMCodeAnalysis]] = Field(default=[]) inputSummaries: Optional[List[LLMDatasetSummary]] = Field(default=[]) outputSummaries: Optional[List[LLMDatasetSummary]] = Field(default=[]) - concerns: Optional[List[LLMConcern]] = Field(default=[]) + assumptions: Optional[List[LLMAssumption]] = Field(default=[]) class AnnotatedComputation(DigitalObject): @@ -135,7 +187,7 @@ class AnnotatedComputation(DigitalObject): codeAnalysis: Optional[List[CodeAnalysis]] = Field(default=[], alias="evi:codeAnalysis") inputSummaries: Optional[List[DatasetSummary]] = Field(default=[], alias="evi:inputSummaries") outputSummaries: Optional[List[DatasetSummary]] = Field(default=[], alias="evi:outputSummaries") - concerns: Optional[List[Concern]] = Field(default=[], alias="evi:concerns") + assumptions: Optional[List[Assumption]] = Field(default=[], alias="evi:assumptions") # Provenance of the annotation itself llmModel: str = Field(alias="evi:llmModel") diff --git a/mds/src/fairscape_mds/models/annotated_evidence_graph.py b/mds/src/fairscape_mds/models/annotated_evidence_graph.py index cafe01d..2fbfa5b 100644 --- a/mds/src/fairscape_mds/models/annotated_evidence_graph.py +++ b/mds/src/fairscape_mds/models/annotated_evidence_graph.py @@ -11,18 +11,31 @@ from fairscape_models.fairscape_base import IdentifierValue from fairscape_models.digital_object import DigitalObject -from fairscape_mds.models.annotated_computation import ConcernLevel +from fairscape_mds.models.annotated_computation import AssumptionImpact, EvidencePointer ANNOTATED_EVIDENCE_GRAPH_TYPE = "AnnotatedEvidenceGraph" -class GraphConcern(BaseModel): - """A graph-level concern linked to its source annotation.""" - level: ConcernLevel +class GraphAssumption(BaseModel): + """A graph-level assumption linked to its source annotation.""" + impact: AssumptionImpact + name: str = Field(default="", description="Short label for the assumption") description: str + downstreamImpacts: Optional[str] = Field(default=None, description="What changes if this assumption is wrong") + evidence: Optional[EvidencePointer] = Field(default=None, description="Pointer to supporting artifact") sourceAnnotation: IdentifierValue +class AudiencePerspective(BaseModel): + """Audience-specific synthesis of the annotated evidence graph.""" + targetAudience: str + audienceLabel: str + executiveSummary: str + narrativeSummary: str + keyFindings: Optional[List[str]] = Field(default=[]) + assumptions: Optional[List[GraphAssumption]] = Field(default=[]) + + class AnnotatedEvidenceGraph(DigitalObject): """Full annotated condensed evidence graph -- the graph-level LLM output. @@ -47,11 +60,14 @@ class AnnotatedEvidenceGraph(DigitalObject): # Flat entity lookup -- all entities keyed by ARK @id graph: Dict[str, Any] = Field(..., alias="@graph") - # Graph-level LLM outputs + # Graph-level LLM outputs (data scientist perspective — the default) executiveSummary: str = Field(..., alias="evi:executiveSummary") narrativeSummary: str = Field(..., alias="evi:narrativeSummary") keyFindings: Optional[List[str]] = Field(default=[], alias="evi:keyFindings") - concerns: Optional[List[GraphConcern]] = Field(default=[], alias="evi:concerns") + assumptions: Optional[List[GraphAssumption]] = Field(default=[], alias="evi:assumptions") + + # Audience-specific perspectives (biostatistician, clinician, etc.) + audiences: Optional[List[AudiencePerspective]] = Field(default=[], alias="evi:audiences") # Quick index of all AnnotatedComputation @ids in the graph stepAnnotations: Optional[List[IdentifierValue]] = Field(default=[], alias="evi:stepAnnotations") diff --git a/mds/src/fairscape_mds/routers/evidence_graph.py b/mds/src/fairscape_mds/routers/evidence_graph.py index 868e034..9a3a231 100644 --- a/mds/src/fairscape_mds/routers/evidence_graph.py +++ b/mds/src/fairscape_mds/routers/evidence_graph.py @@ -1,8 +1,11 @@ from fastapi import APIRouter, Depends, HTTPException, Path, status from fastapi.responses import JSONResponse from typing import Annotated, List, Dict +import logging import uuid +logger = logging.getLogger(__name__) + from fairscape_mds.models.user import UserWriteModel from fairscape_mds.models.evidence_graph import EvidenceGraph, EvidenceGraphCreate, EvidenceGraphBuildRequest from fairscape_mds.models.identifier import StoredIdentifier @@ -118,7 +121,7 @@ def initiate_build_evidence_graph_for_node_route( task_request_model = EvidenceGraphBuildRequest.model_validate(task_request_data) appConfig.asyncCollection.insert_one(task_request_model.model_dump(by_alias=True)) except Exception as e: - print(f"Failed to create task record for evidence graph build: {e}") + logger.error("Failed to create task record for evidence graph build: %s", e) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to initiate evidence graph build task." @@ -150,5 +153,5 @@ def get_build_evidence_graph_status_route( task_model = EvidenceGraphBuildRequest.model_validate(task_status_doc) return task_model except Exception as e: - print(f"Data validation error for task {task_id}: {e}") + logger.error("Data validation error for task %s: %s", task_id, e) raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Error retrieving task status.") \ No newline at end of file diff --git a/mds/src/fairscape_mds/tests/crud/test_interpretation.py b/mds/src/fairscape_mds/tests/crud/test_interpretation.py index a46fceb..a926080 100644 --- a/mds/src/fairscape_mds/tests/crud/test_interpretation.py +++ b/mds/src/fairscape_mds/tests/crud/test_interpretation.py @@ -308,7 +308,7 @@ def test_create_annotated_computation(self): "name": "preprocess.py", "summary": "Reads CSV, drops NaN, normalizes columns", "keyFunctions": ["clean_data", "normalize"], - "concerns": ["No logging of dropped rows"], + "assumptions": ["No logging of dropped rows"], } ], "evi:inputSummaries": [ @@ -326,7 +326,7 @@ def test_create_annotated_computation(self): "role": "Cleaned output", } ], - "evi:concerns": ["No random seed set"], + "evi:assumptions": ["No random seed set"], "evi:llmModel": "gemini-2.5-flash", "evi:llmTemperature": 0.2, "dateCreated": "2025-01-15T12:00:00", @@ -354,7 +354,7 @@ def test_create_annotated_evidence_graph(self): "evi:executiveSummary": "This pipeline preprocesses and analyzes experimental data.", "evi:narrativeSummary": "The pipeline starts with raw data and produces analysis results.", "evi:keyFindings": ["Data is properly normalized", "No random seeds used"], - "evi:concerns": ["Reproducibility could be improved"], + "evi:assumptions": ["Reproducibility could be improved"], "evi:stepAnnotations": [ {"@id": "ark:59853/annotated-computation-1"}, {"@id": "ark:59853/annotated-computation-2"}, @@ -378,7 +378,7 @@ def test_create_synthesis_result(self): executiveSummary="The pipeline processes data.", narrativeSummary="Starting from raw data, the pipeline...", keyFindings=["Finding 1", "Finding 2"], - concerns=["Concern 1"], + assumptions=["Assumption 1"], ) assert result.executiveSummary == "The pipeline processes data." assert len(result.keyFindings) == 2 diff --git a/mds/src/fairscape_mds/worker.py b/mds/src/fairscape_mds/worker.py index 4640ec3..ff2c5ca 100644 --- a/mds/src/fairscape_mds/worker.py +++ b/mds/src/fairscape_mds/worker.py @@ -353,7 +353,7 @@ def process_llm_assist_task(self, task_guid: str): ) return {"status": "FAILURE", "error": error_msg} -@celeryApp.task(name='fairscape_mds.worker.interpret_rocrate_task', bind=True) +@celeryApp.task(name='fairscape_mds.worker.interpret_rocrate_task', bind=True, rate_limit='6/m') def interpret_rocrate_task(self, task_guid: str, rocrate_id: str, llm_model: str = "google-gla:gemini-2.5-flash", temperature: float = 0.2, user_token: str = ""): print(f"Starting Interpretation Task: {task_guid} for {rocrate_id}") @@ -382,5 +382,5 @@ def interpret_rocrate_task(self, task_guid: str, rocrate_id: str, llm_model: str if __name__ == '__main__': - args = ['worker', '--loglevel=INFO'] + args = ['worker', '--loglevel=INFO', '--concurrency=1'] celeryApp.worker_main(argv=args) \ No newline at end of file From 81185ebe78c3e603697cbfc8c4807e925b5314c8 Mon Sep 17 00:00:00 2001 From: jniestroy Date: Fri, 27 Mar 2026 12:25:50 -0400 Subject: [PATCH 13/27] try something --- mds/src/fairscape_mds/crud/interpretation.py | 39 ++++++++++++++++++- .../models/annotated_evidence_graph.py | 13 +++++++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/mds/src/fairscape_mds/crud/interpretation.py b/mds/src/fairscape_mds/crud/interpretation.py index 77e4e5e..97aadb1 100644 --- a/mds/src/fairscape_mds/crud/interpretation.py +++ b/mds/src/fairscape_mds/crud/interpretation.py @@ -25,7 +25,7 @@ Assumption, LLMAssumption, AssumptionImpact, normalize_assumption, ) from fairscape_mds.models.annotated_evidence_graph import ( - AnnotatedEvidenceGraph, GraphAssumption, AudiencePerspective, + AnnotatedEvidenceGraph, GraphAssumption, AudiencePerspective, DataOverview, ) from fairscape_mds.crud.fairscape_request import FairscapeRequest @@ -1248,6 +1248,42 @@ def build_and_store( now = datetime.datetime.utcnow().isoformat() + # Build the overview from RO-Crate metadata + root_entity = graph_dict.get(rocrate_id, {}) + root_name = root_entity.get("name", "") + root_desc = root_entity.get("description", "") + if root_name and root_desc: + overview_description = f"{root_name}. {root_desc}" + else: + overview_description = root_name or root_desc or "No description available" + + # Collect unique data formats from all entities with a format field + data_formats = sorted({ + entity.get("format") + for entity in graph_dict.values() + if isinstance(entity, dict) and entity.get("format") + }) + + # Collect keywords from the root entity + root_keywords = root_entity.get("keywords", []) + if isinstance(root_keywords, str): + root_keywords = [root_keywords] + + # Pick top 1-2 assumptions (CRITICAL first, then MAJOR) + top_assumptions = [a for a in compiled_assumptions if a.impact == "CRITICAL"][:2] + if len(top_assumptions) < 2: + major = [a for a in compiled_assumptions if a.impact == "MAJOR"] + top_assumptions.extend(major[:2 - len(top_assumptions)]) + + overview = DataOverview( + dataDescription=overview_description, + dataFormats=data_formats, + keywords=root_keywords, + license=root_entity.get("license"), + conditionsOfAccess=root_entity.get("conditionsOfAccess"), + topAssumptions=top_assumptions, + ) + # Build the AnnotatedEvidenceGraph aeg_data = { "@id": aeg_id, @@ -1260,6 +1296,7 @@ def build_and_store( "evi:narrativeSummary": synthesis.narrativeSummary, "evi:keyFindings": synthesis.keyFindings, "evi:assumptions": [a.model_dump(by_alias=True, mode="json") for a in compiled_assumptions], + "evi:overview": overview.model_dump(by_alias=True, mode="json"), "evi:audiences": [a.model_dump(by_alias=True, mode="json") for a in audiences], "evi:stepAnnotations": step_ann_refs, "evi:llmModel": llm_model, diff --git a/mds/src/fairscape_mds/models/annotated_evidence_graph.py b/mds/src/fairscape_mds/models/annotated_evidence_graph.py index 2fbfa5b..231284a 100644 --- a/mds/src/fairscape_mds/models/annotated_evidence_graph.py +++ b/mds/src/fairscape_mds/models/annotated_evidence_graph.py @@ -26,6 +26,16 @@ class GraphAssumption(BaseModel): sourceAnnotation: IdentifierValue +class DataOverview(BaseModel): + """Brief top-level overview for quick orientation.""" + dataDescription: str = Field(description="1-2 sentences: what this data is") + dataFormats: List[str] = Field(default=[], description="File formats found in datasets") + keywords: List[str] = Field(default=[], description="Keywords from RO-Crate") + license: Optional[str] = Field(default=None, description="License URL or name") + conditionsOfAccess: Optional[str] = Field(default=None) + topAssumptions: List[GraphAssumption] = Field(default=[], description="1-2 most critical assumptions") + + class AudiencePerspective(BaseModel): """Audience-specific synthesis of the annotated evidence graph.""" targetAudience: str @@ -66,6 +76,9 @@ class AnnotatedEvidenceGraph(DigitalObject): keyFindings: Optional[List[str]] = Field(default=[], alias="evi:keyFindings") assumptions: Optional[List[GraphAssumption]] = Field(default=[], alias="evi:assumptions") + # Brief top-level overview (data type, license, top assumptions) + overview: Optional[DataOverview] = Field(default=None, alias="evi:overview") + # Audience-specific perspectives (biostatistician, clinician, etc.) audiences: Optional[List[AudiencePerspective]] = Field(default=[], alias="evi:audiences") From 64accd7fdb99582ba502f6eb166afbccd68709b4 Mon Sep 17 00:00:00 2001 From: jniestroy Date: Wed, 1 Apr 2026 08:39:59 -0400 Subject: [PATCH 14/27] latest with errors --- mds/src/fairscape_mds/crud/evidence_graph.py | 1 - mds/src/fairscape_mds/crud/interpretation.py | 240 +++++++++++++++++- .../models/annotated_computation.py | 57 +++++ .../models/annotated_evidence_graph.py | 4 + .../fairscape_mds/models/evidence_graph.py | 12 +- mds/src/fairscape_mds/models/identifier.py | 5 + 6 files changed, 302 insertions(+), 17 deletions(-) diff --git a/mds/src/fairscape_mds/crud/evidence_graph.py b/mds/src/fairscape_mds/crud/evidence_graph.py index e6915ef..d2c48b0 100644 --- a/mds/src/fairscape_mds/crud/evidence_graph.py +++ b/mds/src/fairscape_mds/crud/evidence_graph.py @@ -178,7 +178,6 @@ def build_evidence_graph_for_node( try: insert_data = stored_identifier.model_dump(by_alias=True, mode="json") - logger.warning(f"build_evidence_graph insert_data: {insert_data}") self.config.identifierCollection.insert_one(insert_data) except pymongo.errors.DuplicateKeyError: return FairscapeResponse( diff --git a/mds/src/fairscape_mds/crud/interpretation.py b/mds/src/fairscape_mds/crud/interpretation.py index 97aadb1..d12f3cf 100644 --- a/mds/src/fairscape_mds/crud/interpretation.py +++ b/mds/src/fairscape_mds/crud/interpretation.py @@ -23,6 +23,7 @@ AnnotatedComputation, CodeAnalysis, DatasetSummary, LLMComputationAnnotation, LLMCodeAnalysis, LLMDatasetSummary, Assumption, LLMAssumption, AssumptionImpact, normalize_assumption, + normalize_error, ) from fairscape_mds.models.annotated_evidence_graph import ( AnnotatedEvidenceGraph, GraphAssumption, AudiencePerspective, DataOverview, @@ -168,7 +169,60 @@ async def _run_agent_with_retry(agent, prompt, retries=MAX_API_RETRIES, base_del - Many well-written steps will have only MINOR assumptions. That is valid. - Do not manufacture assumptions to fill quotas. - Weave critical assumptions into the stepSummary itself — they are part of the story of what this step does. -- If code is demonstrably wrong (produces misleading results), flag that as a CRITICAL assumption violation. + +## Errors — CHECK FIRST + +BEFORE writing assumptions, check the code and metadata for actual mistakes. Errors are things that are **demonstrably wrong** — a competent scientist reading the code would say "this is a bug" not "this is a risk." + +**The distinction is critical:** An assumption is something that *could* be wrong depending on context. An error is something that *is* wrong based on what the code actually does. + +Examples of ERRORS (put in the `errors` field, NOT in assumptions): +- A function uses the wrong variable and produces incorrect output +- Logic is inverted — a condition checks the opposite of what it should +- Data leakage of any kind visible in the code — fitting on full data before splitting, label information leaking into features, test data influencing training preprocessing. Data leakage is ALWAYS an error, never an assumption. +- Metadata contradicts what the code actually does +- Off-by-one errors, unreachable code paths that should execute + +Examples of ASSUMPTIONS (put in the `assumptions` field, NOT in errors): +- "Assumes features are independent" — might or might not be true +- "Assumes N epochs is sufficient" — a judgment call +- "Assumes class imbalance won't affect results" — risky but not a bug + +**Self-check:** If you find yourself writing an assumption whose description says the code "does X but should do Y" or "uses the wrong variable" — that is an error, not an assumption. Move it to the `errors` field. + +Each error MUST be a structured object: +{ + description: "What is wrong and why it is wrong", + severity: "CRITICAL" | "MAJOR", + evidence: { artifact: {"@id": "<@id>"}, location: "" }, + affectedOutputs: "Which downstream results are impacted" +} + +Many computaions will have NO errors — that is expected. But when an error exists, it MUST go in the `errors` field, not be buried as an assumption. + +## Review Recommendation + +Each assumption must include a `reviewRecommended` boolean: + +- `reviewRecommended: false` ("Routine assumption") — Things a scientist would generally accept without checking: the RO-Crate manifest is correct, proprietary software does what its documentation says, standard libraries (numpy, pandas, scikit-learn) work correctly, file formats are as declared, the person who ran it used the right data. + +- `reviewRecommended: true` ("Review suggested") — Things a scientist would actually want to validate before trusting the results: custom data processing logic, parameter selections that affect results, threshold choices, model selection rationale, assumptions about data distributions, handling of edge cases in custom code. + +When in doubt, mark as `reviewRecommended: false`. The goal is to surface the small number of assumptions that genuinely warrant a scientist's attention, not to flag everything. + +## Computation Status + +You MUST set `computationStatus` to one of: + +- "clear" — No errors found, and assumptions are routine (standard library trust, manifest correctness, etc.) or well-documented. A scientist can trust this step without detailed review. + +- "review_recommended" — No errors, but one or more assumptions warrant a scientist's attention (e.g., custom logic, parameter choices, data processing decisions). NOTE: A computation with CRITICAL-impact assumptions can still be "clear" if those assumptions are routine (e.g., "assumes proprietary software works as documented"). + +- "error_detected" — One or more actual errors were found. This should be rare. + +**Consistency rule:** If the `errors` list is non-empty, `computationStatus` MUST be "error_detected". If any assumption has `reviewRecommended: true`, `computationStatus` should be at least "review_recommended" (unless errors exist, in which case use "error_detected"). + +Use your judgment. The status reflects whether a scientist should spend time reviewing this step, not just a count of assumption severity levels. ## Output - stepSummary: What this step does, why, and what critical assumptions it rests on. @@ -176,6 +230,8 @@ async def _run_agent_with_retry(agent, prompt, retries=MAX_API_RETRIES, base_del - inputSummaries: Per input dataset — role, description, dataQuality observations from data profile. - outputSummaries: Per output dataset — what it contains, dataQuality observations. - assumptions: Step-level assumptions. May be empty if the step makes no notable assumptions. +- errors: Obvious mistakes found. Each as a structured object (see Errors section above). +- computationStatus: "clear" | "review_recommended" | "error_detected" (see Computation Status section above). Each assumption MUST be a structured object: { @@ -183,26 +239,35 @@ async def _run_agent_with_retry(agent, prompt, retries=MAX_API_RETRIES, base_del name: "Short label (3-8 words) — e.g. 'Train/test split independence'", description: "Briefly describe what is being assumed", downstreamImpacts: "What changes if this assumption is wrong — potential downstream effects", - evidence: { artifact: {"@id": "<@id of the data file or software entity>"}, location: "" } + evidence: { artifact: {"@id": "<@id of the data file or software entity>"}, location: "" }, + reviewRecommended: true | false, + recommendedValidation: "A concrete step a scientist could take to test this assumption — e.g. 'Run Shapiro-Wilk test on residuals' or 'Check feature correlation matrix for multicollinearity'. Include when reviewRecommended is true. Omit for routine assumptions." } +## Recommended Validation + +For assumptions where reviewRecommended is true, include a recommendedValidation string: a specific, actionable step a scientist could perform to check whether this assumption holds. Be concrete — name the test, the plot, or the comparison. For MINOR or routine assumptions, omit this field. + Be precise and evidence-based. Reference specific function names and parameter values.""" DATASCI_SYNTHESIS_PROMPT = """You are a senior data scientist synthesizing step annotations from a scientific analysis pipeline (RO-Crate) into a coherent picture of what supports the pipeline's claims. -You will receive the RO-Crate overview and step-by-step annotations including assumptions. +You will receive the RO-Crate overview, step-by-step annotations including assumptions, and a Pipeline DAG Structure section showing the topological order of steps. Produce: -1. executiveSummary: 3-5 sentences covering what the pipeline does, its approach, and the most important critical assumptions it rests on. Weave the load-bearing assumptions into this summary. -2. narrativeSummary: A forward-chronological story of the pipeline, explicitly noting where key assumptions enter and what claims they support. The reader should finish this knowing what the results depend on. -3. keyFindings: Bulleted list of important observations about what the pipeline discovered. -4. assumptions: Cross-cutting assumptions that span the pipeline, each as a structured object: - {impact, name, description, downstreamImpacts} +1. pipelineDescription: 1-2 sentences about the primary output of the pipeline. Weave in the key findings — what the pipeline discovered and its most important results. This is NOT a repeat of the RO-Crate metadata description. Focus on what the pipeline produces and what was found. Keep it brief. +2. pipelineSteps: An ordered list of pipeline steps. Follow the DAG structure provided in the prompt. Each entry is one sentence describing what the step does. Use the numbering from the DAG structure section (1a, 1b for parallel branches at the same level, then 2, 3, etc. following DAG order). Start from raw inputs and work to the final output. +3. executiveSummary: 3-5 sentences covering what the pipeline does, its approach, and the most important critical assumptions it rests on. Weave the load-bearing assumptions into this summary. +4. narrativeSummary: A forward-chronological story of the pipeline, explicitly noting where key assumptions enter and what claims they support. The reader should finish this knowing what the results depend on. +5. keyFindings: Bulleted list of important observations about what the pipeline discovered. +6. assumptions: Cross-cutting assumptions that span the pipeline, each as a structured object: + {impact, name, description, downstreamImpacts, recommendedValidation} - impact: "CRITICAL" (if wrong, pipeline conclusions don't hold), "MAJOR" (if wrong, specific results break but others may hold), or "MINOR" (worth noting but won't change conclusions) - name: Short label (3-8 words) - description: What is being assumed - downstreamImpacts: What changes if this assumption is wrong + - recommendedValidation: A concrete step a scientist could take to test this assumption. Be specific — name the test, plot, or comparison. Omit for MINOR assumptions. Do NOT re-list every step-level assumption. Surface those that matter at the pipeline level — because they span steps, compound across steps, or are the most consequential for trusting the results. A pipeline with no CRITICAL assumptions at the graph level is valid if step-level ones don't compound.""" @@ -267,6 +332,8 @@ async def _run_agent_with_retry(agent, prompt, retries=MAX_API_RETRIES, base_del class GraphSynthesisResult(BaseModel): """Result model for the graph-level synthesis LLM call.""" + pipelineDescription: Optional[str] = None + pipelineSteps: List[str] = [] executiveSummary: str narrativeSummary: str keyFindings: List[str] = [] @@ -722,7 +789,10 @@ def prefetch_all_software(self, task_guid: str, computations: list, index: dict, # Fall back to GitHub/external URL fetching code = prefetch_software_code(content_url) software_cache[sw_id] = code - logger.info(f"Pre-fetched software {sw_id}: {len(code)} chars") + if not code: + logger.warning(f"Pre-fetched software {sw_id}: returned empty string for {content_url!r}") + else: + logger.info(f"Pre-fetched software {sw_id}: {len(code)} chars") return software_cache @@ -826,8 +896,16 @@ def _build_computation_prompt(self, computation: dict, software_cache: dict, ind parts.append(f"**Content URL:** {sw_node.get('contentUrl', 'N/A')}") code = software_cache.get(sw_id, "") if code and not code.startswith("["): + preview = f"{repr(code[:10])}…{repr(code[-10:])}" if len(code) > 20 else repr(code) + logger.info( + f"Prompt build: inserted software {sw_id} ({len(code)} chars) preview={preview}" + ) parts.append(f"\n**Source Code:**\n```\n{code}\n```") else: + logger.warning( + f"Prompt build: NO code for software {sw_id} " + f"(cache_hit={sw_id in software_cache!r}, value={repr((code or '')[:60])})" + ) parts.append(f"\n**Source Code:** {code}") parts.append("") @@ -915,6 +993,8 @@ def _llm_to_annotated( "evi:inputSummaries": [ds.model_dump(by_alias=True) for ds in input_summaries], "evi:outputSummaries": [ds.model_dump(by_alias=True) for ds in output_summaries], "evi:assumptions": [normalize_assumption(a).model_dump() for a in (llm_result.assumptions or [])], + "evi:errors": [normalize_error(e).model_dump() for e in (llm_result.errors or [])], + "evi:computationStatus": llm_result.computationStatus or "clear", "evi:llmModel": llm_model, "evi:llmTemperature": temperature, "dateCreated": now, @@ -1040,10 +1120,102 @@ def annotate_computations_parallel( # Step 5: Graph-level synthesis # ------------------------------------------------------------------ + @staticmethod + def _compute_dag_order( + step_annotations: List[AnnotatedComputation], + graph_dict: dict, + ) -> List[tuple]: + """Compute topological order of step annotations based on the DAG. + + Returns a list of (label, annotation) tuples where label is e.g. + "1", "1a", "1b", "2", etc. + """ + # Map computation @id -> annotation + ann_by_comp: Dict[str, AnnotatedComputation] = {} + for ann in step_annotations: + annotates = ann.annotates + if hasattr(annotates, 'guid'): + comp_id = annotates.guid + elif isinstance(annotates, dict): + comp_id = annotates.get("@id", "") + else: + comp_id = str(annotates) + ann_by_comp[comp_id] = ann + + # Build adjacency: comp_id -> set of comp_ids it depends on + deps: Dict[str, set] = {cid: set() for cid in ann_by_comp} + # Map dataset @id -> comp_id that generated it + generated_by: Dict[str, str] = {} + for node in graph_dict.values(): + if not isinstance(node, dict): + continue + gen = node.get("generatedBy") + if gen: + gen_ids = _resolve_refs(gen) + node_id = node.get("@id", "") + for gid in gen_ids: + if gid in ann_by_comp: + generated_by[node_id] = gid + + for comp_id in ann_by_comp: + comp_node = graph_dict.get(comp_id, {}) + input_refs = _resolve_refs(comp_node.get("usedDataset")) + for ds_id in input_refs: + producer = generated_by.get(ds_id) + if producer and producer != comp_id and producer in ann_by_comp: + deps[comp_id].add(producer) + + # Kahn's algorithm for topological sort with level tracking + in_degree = {cid: len(d) for cid, d in deps.items()} + # Reverse adjacency for decrementing + rdeps: Dict[str, set] = {cid: set() for cid in ann_by_comp} + for cid, dep_set in deps.items(): + for d in dep_set: + rdeps[d].add(cid) + + queue = [cid for cid, deg in in_degree.items() if deg == 0] + levels: List[List[str]] = [] + visited = set() + + while queue: + levels.append(sorted(queue)) # sort for determinism + visited.update(queue) + next_queue = [] + for cid in queue: + for child in rdeps.get(cid, set()): + if child in visited: + continue + in_degree[child] -= 1 + if in_degree[child] == 0: + next_queue.append(child) + queue = next_queue + + # Handle any remaining nodes (cycles) — add them at the end + remaining = [cid for cid in ann_by_comp if cid not in visited] + if remaining: + levels.append(sorted(remaining)) + + # Assign labels + result = [] + for level_num, level_cids in enumerate(levels, 1): + if len(level_cids) == 1: + result.append((str(level_num), ann_by_comp[level_cids[0]])) + else: + for branch_idx, cid in enumerate(level_cids): + letter = chr(ord('a') + branch_idx) if branch_idx < 26 else str(branch_idx) + result.append((f"{level_num}{letter}", ann_by_comp[cid])) + + # Fallback: if somehow empty, return original order + if not result: + return [(str(i), ann) for i, ann in enumerate(step_annotations, 1)] + + return result + def _build_synthesis_prompt( self, root_node: dict, step_annotations: List[AnnotatedComputation], + graph_dict: Optional[dict] = None, ) -> str: """Build the shared synthesis prompt from step annotations.""" parts = [] @@ -1054,10 +1226,44 @@ def _build_synthesis_prompt( parts.append(f"**Keywords:** {root_node.get('keywords', '')}") parts.append("") + # Compute DAG order if graph_dict is available + if graph_dict: + ordered = self._compute_dag_order(step_annotations, graph_dict) + else: + ordered = [(str(i), ann) for i, ann in enumerate(step_annotations, 1)] + + # DAG structure section for the LLM + parts.append("## Pipeline DAG Structure") + for label, ann in ordered: + annotates = ann.annotates + if hasattr(annotates, 'guid'): + comp_id = annotates.guid + elif isinstance(annotates, dict): + comp_id = annotates.get("@id", "") + else: + comp_id = str(annotates) + comp_node = graph_dict.get(comp_id, {}) if graph_dict else {} + comp_name = comp_node.get("name", comp_id) + inputs = _resolve_refs(comp_node.get("usedDataset")) + outputs = _resolve_refs(comp_node.get("hasOutputs")) or _resolve_refs(comp_node.get("generated")) + input_names = ", ".join( + graph_dict.get(i, {}).get("name", i) if graph_dict else i + for i in inputs + ) or "raw inputs" + output_names = ", ".join( + graph_dict.get(o, {}).get("name", o) if graph_dict else o + for o in outputs + ) or "outputs" + parts.append(f"Step {label}: {comp_name} (inputs: {input_names}; outputs: {output_names})") + parts.append("") + parts.append("## Step Annotations") - for i, ann in enumerate(step_annotations, 1): - parts.append(f"### Step {i}: {ann.annotates}") + for label, ann in ordered: + parts.append(f"### Step {label}: {ann.annotates}") + parts.append(f"**Status:** {getattr(ann, 'computationStatus', 'clear')}") parts.append(f"**Summary:** {ann.stepSummary}") + if ann.errors: + parts.append(f"**Errors:** {'; '.join(f'[{e.severity}] {e.description}' for e in ann.errors)}") if ann.assumptions: parts.append(f"**Assumptions:** {'; '.join(f'[{a.impact.value}] {a.description}' for a in ann.assumptions)}") if ann.codeAnalysis: @@ -1075,6 +1281,7 @@ def synthesize_graph( llm_model: str, temperature: float, rate_limiter: Optional["AsyncRateLimiter"] = None, + index: Optional[dict] = None, ) -> Tuple[GraphSynthesisResult, List[dict]]: """Synthesize graph-level summary from all step annotations. @@ -1086,7 +1293,7 @@ def synthesize_graph( "status": "SYNTHESIZING", }) - prompt = self._build_synthesis_prompt(root_node, step_annotations) + prompt = self._build_synthesis_prompt(root_node, step_annotations, graph_dict=index) async def _run_all_syntheses(): # Run data scientist + audience syntheses in parallel @@ -1200,6 +1407,8 @@ def build_and_store( description=assumption.description, downstreamImpacts=assumption.downstreamImpacts, evidence=assumption.evidence, + reviewRecommended=getattr(assumption, "reviewRecommended", False), + recommendedValidation=getattr(assumption, "recommendedValidation", None), sourceAnnotation={"@id": ann.guid}, )) # Add synthesis-level assumptions (not tied to a single step) @@ -1211,6 +1420,8 @@ def build_and_store( description=normalized.description, downstreamImpacts=normalized.downstreamImpacts, evidence=normalized.evidence, + reviewRecommended=getattr(normalized, "reviewRecommended", False), + recommendedValidation=getattr(normalized, "recommendedValidation", None), sourceAnnotation={"@id": rocrate_id}, )) @@ -1226,6 +1437,8 @@ def build_and_store( description=normalized.description, downstreamImpacts=normalized.downstreamImpacts, evidence=normalized.evidence, + reviewRecommended=getattr(normalized, "reviewRecommended", False), + recommendedValidation=getattr(normalized, "recommendedValidation", None), sourceAnnotation={"@id": rocrate_id}, )) audiences.append(AudiencePerspective( @@ -1282,6 +1495,8 @@ def build_and_store( license=root_entity.get("license"), conditionsOfAccess=root_entity.get("conditionsOfAccess"), topAssumptions=top_assumptions, + pipelineDescription=synthesis.pipelineDescription, + pipelineSteps=synthesis.pipelineSteps or None, ) # Build the AnnotatedEvidenceGraph @@ -1404,6 +1619,7 @@ def interpret_rocrate(self, task_guid: str, user_token: str = "") -> str: synthesis, audience_perspectives = self.synthesize_graph( task_guid, root_node, step_annotations, llm_model, temperature, rate_limiter=rate_limiter, + index=index, ) # Step 6: Build and store diff --git a/mds/src/fairscape_mds/models/annotated_computation.py b/mds/src/fairscape_mds/models/annotated_computation.py index e386dc2..fbe78a7 100644 --- a/mds/src/fairscape_mds/models/annotated_computation.py +++ b/mds/src/fairscape_mds/models/annotated_computation.py @@ -29,6 +29,13 @@ class AssumptionImpact(str, Enum): MINOR = "MINOR" +class ComputationReviewStatus(str, Enum): + """LLM-decided status indicating whether a scientist should review this computation.""" + CLEAR = "clear" + REVIEW_RECOMMENDED = "review_recommended" + ERROR_DETECTED = "error_detected" + + class EvidencePointer(BaseModel): """Points to the specific artifact and location supporting an assumption.""" model_config = ConfigDict(extra="allow") @@ -67,6 +74,8 @@ class Assumption(BaseModel): description: str downstreamImpacts: Optional[str] = Field(default=None, description="What changes if this assumption is wrong") evidence: Optional[EvidencePointer] = Field(default=None, description="Pointer to supporting artifact") + reviewRecommended: bool = Field(default=False, description="True if a scientist should validate this assumption; False for routine assumptions") + recommendedValidation: Optional[str] = Field(default=None, description="Concrete step a scientist could take to test this assumption") class LLMAssumption(BaseModel): @@ -76,6 +85,8 @@ class LLMAssumption(BaseModel): description: str downstreamImpacts: Optional[str] = Field(default=None, description="What changes downstream if this assumption is wrong") evidence: Optional[dict] = Field(default=None, description='Pointer: {artifact: {"@id": "ark:..."}, location: "..."}') + reviewRecommended: bool = Field(default=False, description="True if a scientist should validate this; False for routine assumptions like trusting standard libraries") + recommendedValidation: Optional[str] = Field(default=None, description="Concrete step a scientist could take to test this assumption") def normalize_assumption(llm_assumption: LLMAssumption) -> Assumption: @@ -106,6 +117,48 @@ def normalize_assumption(llm_assumption: LLMAssumption) -> Assumption: description=llm_assumption.description, downstreamImpacts=getattr(llm_assumption, "downstreamImpacts", None), evidence=evidence, + reviewRecommended=getattr(llm_assumption, "reviewRecommended", False), + recommendedValidation=getattr(llm_assumption, "recommendedValidation", None), + ) + + +# --------------------------------------------------------------------------- +# Error models — for flagging obvious mistakes (not risky assumptions) +# --------------------------------------------------------------------------- + +class LLMError(BaseModel): + """What the LLM returns when it finds an obvious coding/methodology error.""" + description: str = Field(description="What is wrong and why it is wrong") + severity: str = Field(default="MAJOR", description='One of: "CRITICAL", "MAJOR"') + evidence: Optional[dict] = Field(default=None, description='Pointer: {artifact: {"@id": "ark:..."}, location: "..."}') + affectedOutputs: Optional[str] = Field(default=None, description="Which downstream results are impacted") + + +class ComputationError(BaseModel): + """Validated error found in a computation step.""" + description: str + severity: str = Field(default="MAJOR") + evidence: Optional[EvidencePointer] = Field(default=None) + affectedOutputs: Optional[str] = Field(default=None) + + +def normalize_error(llm_error: LLMError) -> ComputationError: + """Convert an LLMError to a validated ComputationError.""" + raw_severity = llm_error.severity.strip().upper() + severity = "CRITICAL" if "CRIT" in raw_severity else "MAJOR" + + evidence = None + if llm_error.evidence and isinstance(llm_error.evidence, dict): + try: + evidence = EvidencePointer.model_validate(llm_error.evidence) + except Exception: + pass + + return ComputationError( + description=llm_error.description, + severity=severity, + evidence=evidence, + affectedOutputs=getattr(llm_error, "affectedOutputs", None), ) @@ -160,6 +213,8 @@ class LLMComputationAnnotation(BaseModel): inputSummaries: Optional[List[LLMDatasetSummary]] = Field(default=[]) outputSummaries: Optional[List[LLMDatasetSummary]] = Field(default=[]) assumptions: Optional[List[LLMAssumption]] = Field(default=[]) + errors: Optional[List[LLMError]] = Field(default=[], description="Obvious mistakes found in code or methodology (usually empty)") + computationStatus: str = Field(default="clear", description='One of: "clear", "review_recommended", "error_detected"') class AnnotatedComputation(DigitalObject): @@ -188,6 +243,8 @@ class AnnotatedComputation(DigitalObject): inputSummaries: Optional[List[DatasetSummary]] = Field(default=[], alias="evi:inputSummaries") outputSummaries: Optional[List[DatasetSummary]] = Field(default=[], alias="evi:outputSummaries") assumptions: Optional[List[Assumption]] = Field(default=[], alias="evi:assumptions") + errors: Optional[List[ComputationError]] = Field(default=[], alias="evi:errors") + computationStatus: Optional[str] = Field(default="clear", alias="evi:computationStatus") # Provenance of the annotation itself llmModel: str = Field(alias="evi:llmModel") diff --git a/mds/src/fairscape_mds/models/annotated_evidence_graph.py b/mds/src/fairscape_mds/models/annotated_evidence_graph.py index 231284a..d317568 100644 --- a/mds/src/fairscape_mds/models/annotated_evidence_graph.py +++ b/mds/src/fairscape_mds/models/annotated_evidence_graph.py @@ -23,6 +23,8 @@ class GraphAssumption(BaseModel): description: str downstreamImpacts: Optional[str] = Field(default=None, description="What changes if this assumption is wrong") evidence: Optional[EvidencePointer] = Field(default=None, description="Pointer to supporting artifact") + reviewRecommended: bool = Field(default=False, description="True if a scientist should validate this assumption") + recommendedValidation: Optional[str] = Field(default=None, description="Concrete step a scientist could take to test this assumption") sourceAnnotation: IdentifierValue @@ -34,6 +36,8 @@ class DataOverview(BaseModel): license: Optional[str] = Field(default=None, description="License URL or name") conditionsOfAccess: Optional[str] = Field(default=None) topAssumptions: List[GraphAssumption] = Field(default=[], description="1-2 most critical assumptions") + pipelineDescription: Optional[str] = Field(default=None, description="LLM-generated 1-2 sentence description of what the pipeline produces and its key findings") + pipelineSteps: Optional[List[str]] = Field(default=None, description="Ordered list of pipeline steps following DAG order") class AudiencePerspective(BaseModel): diff --git a/mds/src/fairscape_mds/models/evidence_graph.py b/mds/src/fairscape_mds/models/evidence_graph.py index c76dd60..2f4f51d 100644 --- a/mds/src/fairscape_mds/models/evidence_graph.py +++ b/mds/src/fairscape_mds/models/evidence_graph.py @@ -50,7 +50,7 @@ def _is_rocrate(self, node_type_field: Any) -> bool: def _get_rocrate_outputs(self, node: Dict) -> List[Dict]: output_fields = ["https://w3id.org/EVI#outputs", "EVI:outputs", "outputs"] - + for field in output_fields: if field in node: outputs = node[field] @@ -72,6 +72,7 @@ def _extract_referenced_ids(self, node: Dict) -> Set[str]: elif "Software" in node_type_field: current_node_type_str = "Software" elif "MLModel" in node_type_field: current_node_type_str = "MLModel" elif "Experiment" in node_type_field: current_node_type_str = "Experiment" + elif "Activity" in node_type_field: current_node_type_str = "Activity" elif node_type_field: current_node_type_str = node_type_field[-1] elif isinstance(node_type_field, str): current_node_type_str = node_type_field @@ -80,7 +81,7 @@ def _extract_referenced_ids(self, node: Dict) -> Set[str]: "Sample" in current_node_type_str or \ "Instrument" in current_node_type_str or \ "Software" in current_node_type_str or \ - "MLModel" in current_node_type_str: + "MLModel" in current_node_type_str: generated_by_info = node.get("generatedBy") if generated_by_info: if isinstance(generated_by_info, list) and generated_by_info: @@ -94,7 +95,8 @@ def _extract_referenced_ids(self, node: Dict) -> Set[str]: elif "Computation" in current_node_type_str or \ "Experiment" in current_node_type_str or \ - "Annotation" in current_node_type_str: + "Annotation" in current_node_type_str or \ + "Activity" in current_node_type_str: for field_name in ["usedDataset", "usedSoftware", "usedSample", "usedInstrument", "usedMLModel"]: field_info = node.get(field_name) if field_info: @@ -187,6 +189,7 @@ def _build_node_from_cache( elif "Software" in node_type_field: current_node_type_str = "Software" elif "MLModel" in node_type_field: current_node_type_str = "MLModel" elif "Experiment" in node_type_field: current_node_type_str = "Experiment" + elif "Activity" in node_type_field: current_node_type_str = "Activity" elif node_type_field: current_node_type_str = node_type_field[-1] elif isinstance(node_type_field, str): current_node_type_str = node_type_field @@ -213,7 +216,8 @@ def _build_node_from_cache( elif "Computation" in current_node_type_str or \ "Experiment" in current_node_type_str or \ - "Annotation" in current_node_type_str: + "Annotation" in current_node_type_str or \ + "Activity" in current_node_type_str: used_dataset_info = node.get("usedDataset") if used_dataset_info: dataset_refs = self._process_used_dataset(used_dataset_info, node_cache) diff --git a/mds/src/fairscape_mds/models/identifier.py b/mds/src/fairscape_mds/models/identifier.py index 86e4aea..d01262a 100644 --- a/mds/src/fairscape_mds/models/identifier.py +++ b/mds/src/fairscape_mds/models/identifier.py @@ -19,6 +19,7 @@ from fairscape_models.instrument import Instrument from fairscape_models.medical_condition import MedicalCondition from fairscape_models.annotation import Annotation +from fairscape_models.activity import Activity from fairscape_models.conversion.models.AIReady import AIReadyScore from fairscape_models.model_card import ModelCard from fairscape_models.fairscape_base import IdentifierValue @@ -52,6 +53,7 @@ class MetadataTypeEnum(Enum): MEDICAL_CONDITION = "https://schema.org/MedicalCondition" CREATIVE_WORK = "https://schema.org/CreativeWork" ML_MODEL = ["prov:Entity","https://w3id.org/EVI#MLModel"] + ACTIVITY = ["prov:Activity"] ANNOTATION = "https://w3id.org/EVI#Annotation" EVIDENCE_GRAPH = "evi:EvidenceGraph" ANNOTATED_EVIDENCE_GRAPH = ["prov:Entity", "https://w3id.org/EVI#EvidenceGraph", "https://w3id.org/EVI#AnnotatedEvidenceGraph"] @@ -73,6 +75,7 @@ class MetadataTypeEnum(Enum): Instrument, MedicalCondition, Annotation, + Activity, EvidenceGraph, AIReadyScore, ModelCard, @@ -160,6 +163,8 @@ def determineMetadataType(inputType)->MetadataTypeEnum: return MetadataTypeEnum.ML_MODEL elif 'Annotation' in inputType: return MetadataTypeEnum.ANNOTATION + elif 'Activity' in inputType: + return MetadataTypeEnum.ACTIVITY else: raise Exception(f"Type not found for value {inputType}") From 6ec8eb6b3f52a07ba1f4c6f55c7c15a72cd5c368 Mon Sep 17 00:00:00 2001 From: jniestroy Date: Mon, 20 Apr 2026 08:46:59 -0400 Subject: [PATCH 15/27] add AEG upload endpoint and condensation CRUD --- mds/src/fairscape_mds/crud/condensation.py | 119 ++++++++++++++++ mds/src/fairscape_mds/crud/evidence_graph.py | 6 +- .../fairscape_mds/models/evidence_graph.py | 35 ++++- .../fairscape_mds/routers/evidence_graph.py | 12 +- .../fairscape_mds/routers/interpretation.py | 83 +++++++++++- mds/src/fairscape_mds/worker.py | 6 +- pyproject.toml | 2 +- uv.lock | 128 ++++++++++-------- 8 files changed, 326 insertions(+), 65 deletions(-) diff --git a/mds/src/fairscape_mds/crud/condensation.py b/mds/src/fairscape_mds/crud/condensation.py index a413c52..980bb95 100644 --- a/mds/src/fairscape_mds/crud/condensation.py +++ b/mds/src/fairscape_mds/crud/condensation.py @@ -329,6 +329,125 @@ def traverse_and_condense( return keep_ids, collapsed_ids, group_nodes, comp_updates +# --------------------------------------------------------------------------- +# Evidence-graph condensation (operates on node_cache in-place) +# --------------------------------------------------------------------------- + +def condense_evidence_graph_cache( + node_cache: dict[str, dict], + threshold: int = 5, + max_member_ids: int = 0, +) -> dict: + """ + Condense an evidence graph's node_cache in-place by collapsing sibling + datasets at each Computation that share identical provenance signatures. + + Mutates node_cache: adds DatasetGroup nodes, removes collapsed nodes, + updates Computation usedDataset references. + + Returns a stats dict. + """ + original_count = len(node_cache) + sig_cache: dict[str, tuple] = {} + all_group_nodes: list[dict] = [] + total_collapsed: set[str] = set() + + # Snapshot computation IDs (we'll mutate node_cache during iteration) + comp_ids = [ + nid for nid, node in node_cache.items() + if is_computation(node) + ] + + for comp_id in comp_ids: + node = node_cache.get(comp_id) + if node is None: + continue + + input_ids = get_id_list(node, "usedDataset") + # Only consider inputs that are actual datasets present in the cache + input_dataset_ids = [ + ds_id for ds_id in input_ids + if ds_id in node_cache and is_dataset(node_cache[ds_id]) + ] + + if len(input_dataset_ids) <= threshold: + continue + + sig_to_ids: dict[tuple, list[str]] = defaultdict(list) + for ds_id in input_dataset_ids: + sig = compute_provenance_signature(ds_id, node_cache, sig_cache) + sig_to_ids[sig].append(ds_id) + + for sig, member_ids in sig_to_ids.items(): + if len(member_ids) <= threshold: + continue + + representative_id = sorted(member_ids)[0] + non_rep_ids = [mid for mid in member_ids if mid != representative_id] + + # Collect exclusive backward chains for non-representatives + collapsed_here: set[str] = set() + for mid in non_rep_ids: + _collect_exclusive_backward(mid, representative_id, node_cache, collapsed_here) + + # Build group node + group = { + "consuming_comp_id": comp_id, + "signature": sig, + "member_ids": member_ids, + "representative_id": representative_id, + } + group_node = create_dataset_group_node(group, node_cache, max_member_ids) + all_group_nodes.append(group_node) + + # Update computation's usedDataset: replace members with group ref + member_set = set(member_ids) + current_used = node.get("usedDataset", []) + if isinstance(current_used, dict): + current_used = [current_used] + kept = [ref for ref in current_used + if not (isinstance(ref, dict) and ref.get("@id") in member_set)] + kept.append(make_id_ref(group_node["@id"])) + node["usedDataset"] = kept + + # Track all collapsed IDs + total_collapsed.update(collapsed_here) + + # Remove collapsed nodes from cache + for cid in total_collapsed: + node_cache.pop(cid, None) + + # Add group nodes to cache + for gn in all_group_nodes: + node_cache[gn["@id"]] = gn + + condensed_count = len(node_cache) + + if not all_group_nodes: + return { + "condensed": False, + "originalEntityCount": original_count, + "condensedEntityCount": original_count, + "datasetGroupCount": 0, + } + + return { + "condensed": True, + "originalEntityCount": original_count, + "condensedEntityCount": condensed_count, + "datasetGroupCount": len(all_group_nodes), + "entitiesRemoved": len(total_collapsed), + "groups": [ + { + "memberCount": gn["evi:memberCount"], + "format": gn.get("format", "unknown"), + "groupId": gn["@id"], + } + for gn in all_group_nodes + ], + } + + def _collect_exclusive_backward( dataset_id: str, representative_id: str, diff --git a/mds/src/fairscape_mds/crud/evidence_graph.py b/mds/src/fairscape_mds/crud/evidence_graph.py index d2c48b0..9af5500 100644 --- a/mds/src/fairscape_mds/crud/evidence_graph.py +++ b/mds/src/fairscape_mds/crud/evidence_graph.py @@ -101,7 +101,9 @@ def build_evidence_graph_for_node( self, requesting_user: UserWriteModel, naan: str, - postfix: str + postfix: str, + condense: bool = True, + condense_threshold: int = 5, ) -> FairscapeResponse: node_id = f"ark:{naan}/{postfix}" evidence_graph_id = f"ark:{naan}/evidence-graph-{postfix}" @@ -150,7 +152,7 @@ def build_evidence_graph_for_node( try: evidence_graph = EvidenceGraph.model_validate(evidence_graph_data_to_validate) - evidence_graph.build_graph(node_id, self.config.identifierCollection) + evidence_graph.build_graph(node_id, self.config.identifierCollection, condense=condense, condense_threshold=condense_threshold) except Exception as e: return FairscapeResponse( success=False, diff --git a/mds/src/fairscape_mds/models/evidence_graph.py b/mds/src/fairscape_mds/models/evidence_graph.py index 2f4f51d..1da0aaa 100644 --- a/mds/src/fairscape_mds/models/evidence_graph.py +++ b/mds/src/fairscape_mds/models/evidence_graph.py @@ -24,6 +24,7 @@ class EvidenceGraph(BaseModel): name: str = Field(default="Evidence Graph") outputs: Optional[List[Dict[str, str]]] = Field(default=None) graph: Optional[Dict[str, Dict[str, Any]]] = Field(default=None, alias="@graph") + condensation_stats: Optional[Dict[str, Any]] = Field(default=None) class Config: extra = 'allow' @@ -283,9 +284,31 @@ def _build_node_from_cache( if mlmodel_refs: result_node["usedMLModel"] = mlmodel_refs + # Preserve DatasetGroup summary fields and recurse into the + # representative dataset so it actually appears in the graph. + # The other members are dropped by condensation by design. + if isinstance(node_type_field, list) and any("DatasetGroup" in str(t) for t in node_type_field): + for field in ("evi:memberCount", "evi:representativeDataset", + "evi:commonFormat", "evi:commonSoftware", "format", + "evi:memberIds"): + if field in node: + result_node[field] = node[field] + + rep_ref = node.get("evi:representativeDataset") + rep_id = None + if isinstance(rep_ref, dict): + rep_id = rep_ref.get("@id") + elif isinstance(rep_ref, str): + rep_id = rep_ref + if rep_id: + self._build_node_from_cache( + rep_id, node_cache, graph_dict, + start_rocrate_id, rocrate_outputs, + ) + graph_dict[node_id] = result_node - def build_graph(self, start_node_id: str, mongo_collection: pymongo.collection.Collection): + def build_graph(self, start_node_id: str, mongo_collection: pymongo.collection.Collection, condense: bool = True, condense_threshold: int = 5): graph_dict = {} output_nodes = [] @@ -348,7 +371,13 @@ def build_graph(self, start_node_id: str, mongo_collection: pymongo.collection.C next_level.update(referenced_ids) current_level = next_level - + + if condense: + from fairscape_mds.crud.condensation import condense_evidence_graph_cache + self.condensation_stats = condense_evidence_graph_cache( + node_cache, condense_threshold + ) + for output_node in output_nodes: output_id = output_node.get("@id") if output_id: @@ -382,6 +411,8 @@ class EvidenceGraphBuildRequest(BaseModel): owner_email: str naan: str postfix: str + condense: bool = Field(default=True) + condense_threshold: int = Field(default=5) status: str = Field(default="PENDING") time_created: datetime.datetime = Field(default_factory=datetime.datetime.utcnow) time_started: Optional[datetime.datetime] = None diff --git a/mds/src/fairscape_mds/routers/evidence_graph.py b/mds/src/fairscape_mds/routers/evidence_graph.py index 9a3a231..594e799 100644 --- a/mds/src/fairscape_mds/routers/evidence_graph.py +++ b/mds/src/fairscape_mds/routers/evidence_graph.py @@ -1,4 +1,4 @@ -from fastapi import APIRouter, Depends, HTTPException, Path, status +from fastapi import APIRouter, Depends, HTTPException, Path, Query, status from fastapi.responses import JSONResponse from typing import Annotated, List, Dict import logging @@ -106,6 +106,8 @@ def initiate_build_evidence_graph_for_node_route( NAAN: Annotated[str, Path(description="NAAN of the node to build graph for")], postfix: Annotated[str, Path(description="Postfix of the node to build graph for")], current_user: Annotated[UserWriteModel, Depends(getCurrentUser)], + condense: bool = Query(default=True, description="Apply condensation to reduce large graphs"), + condense_threshold: int = Query(default=5, ge=2, description="Min group size to trigger condensation"), ): task_guid = str(uuid.uuid4()) @@ -114,9 +116,11 @@ def initiate_build_evidence_graph_for_node_route( "owner_email": current_user.email, "naan": NAAN, "postfix": postfix, + "condense": condense, + "condense_threshold": condense_threshold, "status": "PENDING", } - + try: task_request_model = EvidenceGraphBuildRequest.model_validate(task_request_data) appConfig.asyncCollection.insert_one(task_request_model.model_dump(by_alias=True)) @@ -131,7 +135,9 @@ def initiate_build_evidence_graph_for_node_route( task_guid=task_guid, user_email=current_user.email, naan=NAAN, - postfix=postfix + postfix=postfix, + condense=condense, + condense_threshold=condense_threshold, ) return {"message": "EvidenceGraph build process initiated.", "task_id": task_guid, "status_endpoint": f"/evidencegraph/build/status/{task_guid}"} diff --git a/mds/src/fairscape_mds/routers/interpretation.py b/mds/src/fairscape_mds/routers/interpretation.py index 2015c43..991c102 100644 --- a/mds/src/fairscape_mds/routers/interpretation.py +++ b/mds/src/fairscape_mds/routers/interpretation.py @@ -1,13 +1,15 @@ """Router for AI-mediated interpretation of RO-Crates.""" from typing import Annotated -from fastapi import APIRouter, Depends, Query +from fastapi import APIRouter, Depends, Query, UploadFile, File from fastapi.responses import JSONResponse +import json import uuid import datetime -from fairscape_mds.models.user import UserWriteModel +from fairscape_mds.models.user import UserWriteModel, Permissions +from fairscape_mds.models.identifier import StoredIdentifier, PublicationStatusEnum from fairscape_mds.core.config import appConfig from fairscape_mds.deps import getCurrentUser, OAuthScheme from fairscape_mds.crud.fairscape_request import flexible_ark_query @@ -205,3 +207,80 @@ def get_interpretation_result(NAAN: str, postfix: str): metadata = aeg_doc.get("metadata", {}) return JSONResponse(status_code=200, content=metadata) + + +# --------------------------------------------------------------------------- +# POST /interpretation/upload -- upload a pre-computed AnnotatedEvidenceGraph +# --------------------------------------------------------------------------- + +@router.post( + "/upload", + summary="Upload a pre-computed AnnotatedEvidenceGraph StoredIdentifier", +) +def upload_annotated_evidence_graph( + currentUser: Annotated[UserWriteModel, Depends(getCurrentUser)], + file: UploadFile = File(..., description="StoredIdentifier JSON for an AnnotatedEvidenceGraph"), +): + """Register a previously generated AnnotatedEvidenceGraph without re-running interpretation. + + The uploaded JSON must be a complete StoredIdentifier whose metadata includes + `evi:annotates` pointing at an existing RO-Crate ARK. If a StoredIdentifier + with the same @id already exists it is replaced.""" + + try: + raw = file.file.read() + doc = json.loads(raw) + except json.JSONDecodeError as exc: + return JSONResponse(status_code=400, content={"error": f"Invalid JSON: {exc}"}) + + try: + stored = StoredIdentifier.model_validate(doc) + except Exception as exc: + return JSONResponse(status_code=400, content={"error": f"Not a valid StoredIdentifier: {exc}"}) + + metadata = stored.metadata.model_dump(by_alias=True, mode="json") if hasattr(stored.metadata, "model_dump") else doc.get("metadata", {}) + + annotates_ref = metadata.get("evi:annotates") + annotates_id = annotates_ref.get("@id") if isinstance(annotates_ref, dict) else annotates_ref + if not annotates_id: + return JSONResponse(status_code=400, content={"error": "metadata['evi:annotates'] missing or has no @id"}) + + parent = _flexible_find(annotates_id) + if not parent: + return JSONResponse( + status_code=404, + content={"error": f"Annotated entity {annotates_id} not found"}, + ) + + aeg_id = stored.guid + now = datetime.datetime.utcnow() + + # Force system ownership + DRAFT status, regardless of what the file carried. + permissions = Permissions(owner=currentUser.email, group="", acl=[]) + record = stored.model_dump(by_alias=True, mode="json") + record["permissions"] = permissions.model_dump() + record["publicationStatus"] = PublicationStatusEnum.DRAFT.value + record["dateModified"] = now.isoformat() + record.setdefault("dateCreated", now.isoformat()) + + replaced = appConfig.identifierCollection.find_one({"@id": aeg_id}, {"_id": False}) is not None + appConfig.identifierCollection.replace_one( + {"@id": aeg_id}, + record, + upsert=True, + ) + + appConfig.identifierCollection.update_one( + {"@id": annotates_id}, + {"$set": {"metadata.hasAnnotatedEvidenceGraph": {"@id": aeg_id}}}, + ) + + return JSONResponse( + status_code=200, + content={ + "message": "AnnotatedEvidenceGraph replaced" if replaced else "AnnotatedEvidenceGraph stored", + "annotated_evidence_graph_id": aeg_id, + "annotates": annotates_id, + "replaced": replaced, + }, + ) diff --git a/mds/src/fairscape_mds/worker.py b/mds/src/fairscape_mds/worker.py index ff2c5ca..3a48bbc 100644 --- a/mds/src/fairscape_mds/worker.py +++ b/mds/src/fairscape_mds/worker.py @@ -94,7 +94,7 @@ def processROCrate(transactionGUID: str): #Are the guids supposed to be @id? @celeryApp.task(name='fairscape_mds.worker.build_evidence_graph_task', bind=True) -def build_evidence_graph_task(self, task_guid: str, user_email: str, naan: str, postfix: str): +def build_evidence_graph_task(self, task_guid: str, user_email: str, naan: str, postfix: str, condense: bool = True, condense_threshold: int = 5): print(f"Starting Evidence Graph Build Job: Task GUID {task_guid} for ark:{naan}/{postfix}") try: @@ -122,7 +122,9 @@ def build_evidence_graph_task(self, task_guid: str, user_email: str, naan: str, response = evidenceGraphRequests.build_evidence_graph_for_node( requesting_user=requesting_user, naan=naan, - postfix=postfix + postfix=postfix, + condense=condense, + condense_threshold=condense_threshold, ) if response.success: diff --git a/pyproject.toml b/pyproject.toml index d5bace8..ade360b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,7 @@ classifiers = [ "Programming Language :: Python :: 3"] keywords = ["fairscape", "reproducibility", "fair"] dependencies = [ - "fairscape_models>=1.0.27", + "fairscape_models>=1.1.1", "fastapi>=0.109.0", "uvicorn>=0.27.0", "requests>=2.31.0", diff --git a/uv.lock b/uv.lock index 90ca869..099cd30 100644 --- a/uv.lock +++ b/uv.lock @@ -1111,7 +1111,7 @@ requires-dist = [ { name = "boto3", specifier = ">=1.34.26" }, { name = "celery", extras = ["redis"], specifier = ">=5.3.6" }, { name = "docker", specifier = ">=7.0.0" }, - { name = "fairscape-models", specifier = ">=1.0.27" }, + { name = "fairscape-models", specifier = ">=1.1.1" }, { name = "fastapi", specifier = ">=0.109.0" }, { name = "google-genai", specifier = ">=1.0.0" }, { name = "httpx", specifier = ">=0.28.1" }, @@ -1141,17 +1141,18 @@ requires-dist = [ [[package]] name = "fairscape-models" -version = "1.0.27" +version = "1.1.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mongomock" }, { name = "pydantic" }, { name = "pymongo" }, + { name = "pyyaml" }, { name = "typing" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/55/36/340f98118540a5b03f56c961dfc2ca5294f1e5b7489d98f119b5ba2e60ed/fairscape_models-1.0.27.tar.gz", hash = "sha256:865978aec4ae43bf63917fcfcd16a60478c580df8643cc401a4ed1bf86138710", size = 80138, upload-time = "2026-02-24T21:15:38.392Z" } +sdist = { url = "https://files.pythonhosted.org/packages/15/a3/7dab5b3a6b7926c5c5b1d16d2af8484e4c28c19cc12d33e3d090779765ef/fairscape_models-1.1.3.tar.gz", hash = "sha256:9d771a9d51b3d38bdb8d27f6352144f5b04591339788898a828efc7ad9d1eed3", size = 93985, upload-time = "2026-04-14T20:43:14.307Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cf/39/0cab93268285270bc4ac11b7bae629d9fe88d169ebeebeccbeebc1446ec9/fairscape_models-1.0.27-py3-none-any.whl", hash = "sha256:45905e57d6b934eb86ece25b3ce92c28771919564f9f66d89e15417132282e9c", size = 86190, upload-time = "2026-02-24T21:15:37.263Z" }, + { url = "https://files.pythonhosted.org/packages/69/25/42a7bba270ffb3f491c5c1ecd9c0f80169982e566c48c273eb073ee0e281/fairscape_models-1.1.3-py3-none-any.whl", hash = "sha256:1b7d4d725c9ac3b9e37fd51966d6bb9d513b72ca5369fa55a8bbd848c2ffc059", size = 97032, upload-time = "2026-04-14T20:43:12.94Z" }, ] [[package]] @@ -1711,6 +1712,7 @@ wheels = [ name = "griffelib" version = "2.0.0" source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ad/06/eccbd311c9e2b3ca45dbc063b93134c57a1ccc7607c5e545264ad092c4a9/griffelib-2.0.0.tar.gz", hash = "sha256:e504d637a089f5cab9b5daf18f7645970509bf4f53eda8d79ed71cce8bd97934", size = 166312, upload-time = "2026-03-23T21:06:55.954Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/4d/51/c936033e16d12b627ea334aaaaf42229c37620d0f15593456ab69ab48161/griffelib-2.0.0-py3-none-any.whl", hash = "sha256:01284878c966508b6d6f1dbff9b6fa607bc062d8261c5c7253cb285b06422a7f", size = 142004, upload-time = "2026-02-09T19:09:40.561Z" }, ] @@ -4878,55 +4880,75 @@ wheels = [ [[package]] name = "pyyaml" -version = "6.0.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199, upload-time = "2024-08-06T20:31:40.178Z" }, - { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758, upload-time = "2024-08-06T20:31:42.173Z" }, - { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463, upload-time = "2024-08-06T20:31:44.263Z" }, - { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280, upload-time = "2024-08-06T20:31:50.199Z" }, - { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239, upload-time = "2024-08-06T20:31:52.292Z" }, - { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802, upload-time = "2024-08-06T20:31:53.836Z" }, - { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527, upload-time = "2024-08-06T20:31:55.565Z" }, - { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052, upload-time = "2024-08-06T20:31:56.914Z" }, - { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774, upload-time = "2024-08-06T20:31:58.304Z" }, - { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612, upload-time = "2024-08-06T20:32:03.408Z" }, - { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040, upload-time = "2024-08-06T20:32:04.926Z" }, - { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829, upload-time = "2024-08-06T20:32:06.459Z" }, - { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167, upload-time = "2024-08-06T20:32:08.338Z" }, - { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952, upload-time = "2024-08-06T20:32:14.124Z" }, - { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301, upload-time = "2024-08-06T20:32:16.17Z" }, - { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638, upload-time = "2024-08-06T20:32:18.555Z" }, - { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850, upload-time = "2024-08-06T20:32:19.889Z" }, - { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980, upload-time = "2024-08-06T20:32:21.273Z" }, - { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873, upload-time = "2024-08-06T20:32:25.131Z" }, - { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302, upload-time = "2024-08-06T20:32:26.511Z" }, - { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154, upload-time = "2024-08-06T20:32:28.363Z" }, - { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223, upload-time = "2024-08-06T20:32:30.058Z" }, - { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542, upload-time = "2024-08-06T20:32:31.881Z" }, - { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164, upload-time = "2024-08-06T20:32:37.083Z" }, - { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611, upload-time = "2024-08-06T20:32:38.898Z" }, - { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591, upload-time = "2024-08-06T20:32:40.241Z" }, - { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338, upload-time = "2024-08-06T20:32:41.93Z" }, - { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" }, - { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" }, - { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" }, - { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload-time = "2024-08-06T20:32:51.188Z" }, - { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload-time = "2024-08-06T20:32:53.019Z" }, - { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload-time = "2024-08-06T20:32:54.708Z" }, - { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" }, - { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" }, - { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" }, - { url = "https://files.pythonhosted.org/packages/65/d8/b7a1db13636d7fb7d4ff431593c510c8b8fca920ade06ca8ef20015493c5/PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d", size = 184777, upload-time = "2024-08-06T20:33:25.896Z" }, - { url = "https://files.pythonhosted.org/packages/0a/02/6ec546cd45143fdf9840b2c6be8d875116a64076218b61d68e12548e5839/PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f", size = 172318, upload-time = "2024-08-06T20:33:27.212Z" }, - { url = "https://files.pythonhosted.org/packages/0e/9a/8cc68be846c972bda34f6c2a93abb644fb2476f4dcc924d52175786932c9/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290", size = 720891, upload-time = "2024-08-06T20:33:28.974Z" }, - { url = "https://files.pythonhosted.org/packages/e9/6c/6e1b7f40181bc4805e2e07f4abc10a88ce4648e7e95ff1abe4ae4014a9b2/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12", size = 722614, upload-time = "2024-08-06T20:33:34.157Z" }, - { url = "https://files.pythonhosted.org/packages/3d/32/e7bd8535d22ea2874cef6a81021ba019474ace0d13a4819c2a4bce79bd6a/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19", size = 737360, upload-time = "2024-08-06T20:33:35.84Z" }, - { url = "https://files.pythonhosted.org/packages/d7/12/7322c1e30b9be969670b672573d45479edef72c9a0deac3bb2868f5d7469/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e", size = 699006, upload-time = "2024-08-06T20:33:37.501Z" }, - { url = "https://files.pythonhosted.org/packages/82/72/04fcad41ca56491995076630c3ec1e834be241664c0c09a64c9a2589b507/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725", size = 723577, upload-time = "2024-08-06T20:33:39.389Z" }, - { url = "https://files.pythonhosted.org/packages/ed/5e/46168b1f2757f1fcd442bc3029cd8767d88a98c9c05770d8b420948743bb/PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631", size = 144593, upload-time = "2024-08-06T20:33:46.63Z" }, - { url = "https://files.pythonhosted.org/packages/19/87/5124b1c1f2412bb95c59ec481eaf936cd32f0fe2a7b16b97b81c4c017a6a/PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8", size = 162312, upload-time = "2024-08-06T20:33:49.073Z" }, +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, + { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, + { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, + { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, + { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, + { url = "https://files.pythonhosted.org/packages/9f/62/67fc8e68a75f738c9200422bf65693fb79a4cd0dc5b23310e5202e978090/pyyaml-6.0.3-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da", size = 184450, upload-time = "2025-09-25T21:33:00.618Z" }, + { url = "https://files.pythonhosted.org/packages/ae/92/861f152ce87c452b11b9d0977952259aa7df792d71c1053365cc7b09cc08/pyyaml-6.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917", size = 174319, upload-time = "2025-09-25T21:33:02.086Z" }, + { url = "https://files.pythonhosted.org/packages/d0/cd/f0cfc8c74f8a030017a2b9c771b7f47e5dd702c3e28e5b2071374bda2948/pyyaml-6.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9", size = 737631, upload-time = "2025-09-25T21:33:03.25Z" }, + { url = "https://files.pythonhosted.org/packages/ef/b2/18f2bd28cd2055a79a46c9b0895c0b3d987ce40ee471cecf58a1a0199805/pyyaml-6.0.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5", size = 836795, upload-time = "2025-09-25T21:33:05.014Z" }, + { url = "https://files.pythonhosted.org/packages/73/b9/793686b2d54b531203c160ef12bec60228a0109c79bae6c1277961026770/pyyaml-6.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a", size = 750767, upload-time = "2025-09-25T21:33:06.398Z" }, + { url = "https://files.pythonhosted.org/packages/a9/86/a137b39a611def2ed78b0e66ce2fe13ee701a07c07aebe55c340ed2a050e/pyyaml-6.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926", size = 727982, upload-time = "2025-09-25T21:33:08.708Z" }, + { url = "https://files.pythonhosted.org/packages/dd/62/71c27c94f457cf4418ef8ccc71735324c549f7e3ea9d34aba50874563561/pyyaml-6.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7", size = 755677, upload-time = "2025-09-25T21:33:09.876Z" }, + { url = "https://files.pythonhosted.org/packages/29/3d/6f5e0d58bd924fb0d06c3a6bad00effbdae2de5adb5cda5648006ffbd8d3/pyyaml-6.0.3-cp39-cp39-win32.whl", hash = "sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0", size = 142592, upload-time = "2025-09-25T21:33:10.983Z" }, + { url = "https://files.pythonhosted.org/packages/f0/0c/25113e0b5e103d7f1490c0e947e303fe4a696c10b501dea7a9f49d4e876c/pyyaml-6.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007", size = 158777, upload-time = "2025-09-25T21:33:15.55Z" }, ] [[package]] From c07cf8a89a57431a9c359d9d7960afc48da3e188 Mon Sep 17 00:00:00 2001 From: jniestroy Date: Mon, 20 Apr 2026 12:18:56 -0400 Subject: [PATCH 16/27] Phase 0: migrate pure helpers/prompts/models to fairscape_interpret. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Structural refactor only — no behavior change. Storage-agnostic pieces of the AI-interpretation pipeline move into a sibling package (../fairscape_interpret) so the forthcoming fairscape-cli can share the same code without duplicating prompts, scoring, and condensation heuristics that iterate rapidly. - pyproject.toml: add fairscape_interpret as editable [tool.uv.sources] - crud/interpretation.py: 1650 -> 1066 lines; imports helpers/prompts from fairscape_interpret instead of defining them inline - crud/condensation.py: 1020 -> 317 lines; keeps only Mongo-specific glue (BFS batch loader + condensed-crate persistence); condensation algorithm itself lives in fairscape_interpret.pipeline.condense - crud/fairscape_request.py: re-exports flexible_ark_query from the shared package; FairscapeRequest class unchanged - models/annotated_computation.py: 298 -> 44-line re-export shim - models/annotated_evidence_graph.py: 119 -> 22-line re-export shim Ports + orchestrators + Mongo adapters land in Phase 1. See ../fairscape_interpret/MIGRATION.md for full plan and tracking. Co-Authored-By: Claude Opus 4.7 --- mds/src/fairscape_mds/crud/condensation.py | 757 +----------------- .../fairscape_mds/crud/fairscape_request.py | 17 +- mds/src/fairscape_mds/crud/interpretation.py | 678 ++-------------- .../models/annotated_computation.py | 298 +------ .../models/annotated_evidence_graph.py | 119 +-- pyproject.toml | 4 + 6 files changed, 140 insertions(+), 1733 deletions(-) diff --git a/mds/src/fairscape_mds/crud/condensation.py b/mds/src/fairscape_mds/crud/condensation.py index 980bb95..ee511af 100644 --- a/mds/src/fairscape_mds/crud/condensation.py +++ b/mds/src/fairscape_mds/crud/condensation.py @@ -5,12 +5,15 @@ via BFS expansion) and then condenses it by collapsing repetitive provenance chains into DatasetGroup summary nodes. -Ported from fairscape-interpreter/condense_crate.py with a new MongoDB-backed -graph collection layer replacing the local-file reader. +All graph-condensation helpers (signature computation, traversal, DatasetGroup +construction, `condense_graph`, etc.) now live in +`fairscape_interpret.pipeline.condense` so the CLI can reuse them. This file +keeps only the MongoDB glue: StoredIdentifier-flattening, BFS across +identifierCollection, and the FairscapeCondensationRequest class. """ import datetime -from collections import defaultdict, deque +from collections import deque from typing import Any from fairscape_mds.crud.fairscape_request import FairscapeRequest @@ -22,727 +25,32 @@ ) from fairscape_mds.models.user import Permissions - -# --------------------------------------------------------------------------- -# Type detection helpers (operate on raw JSON dicts) -# --------------------------------------------------------------------------- - -EVI_TYPES = { - "Dataset", "Software", "MLModel", "Computation", "Annotation", - "Experiment", "ROCrate", "CreativeWork", "Schema", -} - - -def _extract_short_types(node: dict) -> list[str]: - """Extract all short EVI type names from a node's @type field.""" - raw = node.get("@type", []) - if isinstance(raw, str): - raw = [raw] - shorts = [] - for t in raw: - short = t.split("#")[-1] if "#" in t else t.split(":")[-1] if ":" in t else t - if short in EVI_TYPES: - shorts.append(short) - return shorts - - -def get_evi_type(node: dict) -> str | None: - """Extract the primary EVI type from a node's @type field.""" - shorts = _extract_short_types(node) - if not shorts: - return None - for preferred in ("ROCrate", "Computation", "Software", "MLModel", - "Experiment", "Annotation", "Schema"): - if preferred in shorts: - return preferred - return shorts[0] - - -def is_dataset(node: dict) -> bool: - types = _extract_short_types(node) - return "Dataset" in types and "ROCrate" not in types - - -def is_computation(node: dict) -> bool: - return get_evi_type(node) == "Computation" - - -def is_software(node: dict) -> bool: - return get_evi_type(node) == "Software" - - -def is_rocrate_root(node: dict) -> bool: - return "ROCrate" in _extract_short_types(node) - - -# --------------------------------------------------------------------------- -# Reference helpers -# --------------------------------------------------------------------------- - -def get_id_list(node: dict, *fields) -> list[str]: - """Extract a list of @id strings from one or more reference fields.""" - ids = [] - for field in fields: - val = node.get(field, []) - if val is None: - continue - if isinstance(val, dict): - val = [val] - if isinstance(val, list): - for item in val: - if isinstance(item, dict) and "@id" in item: - ids.append(item["@id"]) - elif isinstance(item, str): - ids.append(item) - return ids - - -def get_generatedby_ids(dataset: dict) -> list[str]: - """Get the @ids of computations that generated this dataset.""" - return get_id_list(dataset, "generatedBy", "prov:wasGeneratedBy") - - -def make_id_ref(entity_id: str) -> dict: - """Create a {"@id": ...} reference.""" - return {"@id": entity_id} - - -# --------------------------------------------------------------------------- -# Provenance Signature -# --------------------------------------------------------------------------- - -def compute_provenance_signature( - dataset_id: str, - index: dict[str, dict], - cache: dict[str, tuple], -) -> tuple: - """ - Compute a hashable signature for a dataset's provenance structure. - - Two datasets with the same signature went through identical software - pipelines, regardless of which specific data/computation instances - were involved. - """ - if dataset_id in cache: - return cache[dataset_id] - - dataset = index.get(dataset_id) - if dataset is None: - sig = ("unknown", (), None) - cache[dataset_id] = sig - return sig - - fmt = dataset.get("format", "unknown") - schema_ids = tuple(sorted(get_id_list(dataset, "evi:Schema"))) - - gen_comp_ids = get_generatedby_ids(dataset) - - if not gen_comp_ids: - sig = (fmt, schema_ids, None) - else: - comp_sigs = [] - for comp_id in sorted(gen_comp_ids): - comp = index.get(comp_id) - if comp is None: - comp_sigs.append(((), ())) - continue - - sw_ids = tuple(sorted(get_id_list(comp, "usedSoftware"))) - - input_dataset_ids = get_id_list(comp, "usedDataset") - input_sigs = tuple(sorted( - compute_provenance_signature(ds_id, index, cache) - for ds_id in input_dataset_ids - )) - - comp_sigs.append((sw_ids, input_sigs)) - - sig = (fmt, schema_ids, tuple(sorted(comp_sigs))) - - cache[dataset_id] = sig - return sig - - -# --------------------------------------------------------------------------- -# Output-first backward traversal with inline condensation -# --------------------------------------------------------------------------- - -def traverse_and_condense( - index: dict[str, dict], - threshold: int, - max_member_ids: int = 0, -) -> tuple[set[str], set[str], list[dict], dict[str, list[tuple[list[str], str]]]]: - """ - Traverse backward from the primary crate's outputs, keeping everything - reachable and collapsing large groups of sibling datasets that share the - same provenance signature. - - Returns: - keep_ids, remove_ids, group_nodes, comp_updates - """ - # Find root crate node - root_id = None - for nid, node in index.items(): - if is_rocrate_root(node): - root_id = nid - break - - if root_id is None: - return set(index.keys()), set(), [], {} - - root = index[root_id] - - output_ids = get_id_list(root, "EVI:outputs") - part_ids = get_id_list(root, "hasPart", "EVI:outputs") - - keep_ids: set[str] = {root_id, "ro-crate-metadata.json"} - collapsed_ids: set[str] = set() - group_nodes: list[dict] = [] - comp_updates: dict[str, list[tuple[list[str], str]]] = defaultdict(list) - sig_cache: dict[str, tuple] = {} - - # Pre-pass: condense repetitive top-level outputs - output_dataset_ids = [ - oid for oid in output_ids - if oid in index and is_dataset(index[oid]) - ] - if len(output_dataset_ids) > threshold: - sig_to_ids: dict[tuple, list[str]] = defaultdict(list) - for ds_id in output_dataset_ids: - sig = compute_provenance_signature(ds_id, index, sig_cache) - sig_to_ids[sig].append(ds_id) - - for sig, member_ids in sig_to_ids.items(): - if len(member_ids) > threshold: - representative_id = sorted(member_ids)[0] - non_rep_ids = [mid for mid in member_ids - if mid != representative_id] - collapsed_ids.update(non_rep_ids) - - for mid in non_rep_ids: - _collect_exclusive_backward( - mid, representative_id, index, collapsed_ids, - ) - - group = { - "consuming_comp_id": root_id, - "signature": sig, - "member_ids": member_ids, - "representative_id": representative_id, - } - group_node = create_dataset_group_node(group, index, max_member_ids) - group_nodes.append(group_node) - comp_updates[root_id].append( - (member_ids, group_node["@id"]) - ) - - # Backward traversal - visited: set[str] = set() - stack = list(part_ids) - - while stack: - current_id = stack.pop() - if current_id in visited or current_id in collapsed_ids: - continue - visited.add(current_id) - keep_ids.add(current_id) - - node = index.get(current_id) - if node is None: - continue - - evi_type = get_evi_type(node) - - if is_dataset(node): - for comp_id in get_generatedby_ids(node): - stack.append(comp_id) - for schema_id in get_id_list(node, "evi:Schema"): - stack.append(schema_id) - - elif evi_type == "Computation": - for sw_id in get_id_list(node, "usedSoftware"): - keep_ids.add(sw_id) - stack.append(sw_id) - - for ml_id in get_id_list(node, "usedMLModel"): - keep_ids.add(ml_id) - stack.append(ml_id) - - input_ids = get_id_list(node, "usedDataset") - - if len(input_ids) > threshold: - sig_to_ids = defaultdict(list) - for ds_id in input_ids: - sig = compute_provenance_signature(ds_id, index, sig_cache) - sig_to_ids[sig].append(ds_id) - - for sig, member_ids in sig_to_ids.items(): - if len(member_ids) > threshold: - representative_id = sorted(member_ids)[0] - non_rep_ids = [mid for mid in member_ids - if mid != representative_id] - collapsed_ids.update(non_rep_ids) - - for mid in non_rep_ids: - _collect_exclusive_backward( - mid, representative_id, index, collapsed_ids, - ) - - group = { - "consuming_comp_id": current_id, - "signature": sig, - "member_ids": member_ids, - "representative_id": representative_id, - } - group_node = create_dataset_group_node(group, index, max_member_ids) - group_nodes.append(group_node) - comp_updates[current_id].append( - (member_ids, group_node["@id"]) - ) - - stack.append(representative_id) - else: - for ds_id in member_ids: - stack.append(ds_id) - else: - for ds_id in input_ids: - stack.append(ds_id) - - for out_id in get_id_list(node, "generated"): - stack.append(out_id) - - elif evi_type == "Experiment": - for ref_id in get_id_list(node, "usedSample", "usedInstrument", - "usedTreatment", "usedStain"): - stack.append(ref_id) - - elif is_software(node): - pass - - # Keep all software nodes - for nid, n in index.items(): - if is_software(n): - keep_ids.add(nid) - - keep_ids -= collapsed_ids - - return keep_ids, collapsed_ids, group_nodes, comp_updates - - -# --------------------------------------------------------------------------- -# Evidence-graph condensation (operates on node_cache in-place) -# --------------------------------------------------------------------------- - -def condense_evidence_graph_cache( - node_cache: dict[str, dict], - threshold: int = 5, - max_member_ids: int = 0, -) -> dict: - """ - Condense an evidence graph's node_cache in-place by collapsing sibling - datasets at each Computation that share identical provenance signatures. - - Mutates node_cache: adds DatasetGroup nodes, removes collapsed nodes, - updates Computation usedDataset references. - - Returns a stats dict. - """ - original_count = len(node_cache) - sig_cache: dict[str, tuple] = {} - all_group_nodes: list[dict] = [] - total_collapsed: set[str] = set() - - # Snapshot computation IDs (we'll mutate node_cache during iteration) - comp_ids = [ - nid for nid, node in node_cache.items() - if is_computation(node) - ] - - for comp_id in comp_ids: - node = node_cache.get(comp_id) - if node is None: - continue - - input_ids = get_id_list(node, "usedDataset") - # Only consider inputs that are actual datasets present in the cache - input_dataset_ids = [ - ds_id for ds_id in input_ids - if ds_id in node_cache and is_dataset(node_cache[ds_id]) - ] - - if len(input_dataset_ids) <= threshold: - continue - - sig_to_ids: dict[tuple, list[str]] = defaultdict(list) - for ds_id in input_dataset_ids: - sig = compute_provenance_signature(ds_id, node_cache, sig_cache) - sig_to_ids[sig].append(ds_id) - - for sig, member_ids in sig_to_ids.items(): - if len(member_ids) <= threshold: - continue - - representative_id = sorted(member_ids)[0] - non_rep_ids = [mid for mid in member_ids if mid != representative_id] - - # Collect exclusive backward chains for non-representatives - collapsed_here: set[str] = set() - for mid in non_rep_ids: - _collect_exclusive_backward(mid, representative_id, node_cache, collapsed_here) - - # Build group node - group = { - "consuming_comp_id": comp_id, - "signature": sig, - "member_ids": member_ids, - "representative_id": representative_id, - } - group_node = create_dataset_group_node(group, node_cache, max_member_ids) - all_group_nodes.append(group_node) - - # Update computation's usedDataset: replace members with group ref - member_set = set(member_ids) - current_used = node.get("usedDataset", []) - if isinstance(current_used, dict): - current_used = [current_used] - kept = [ref for ref in current_used - if not (isinstance(ref, dict) and ref.get("@id") in member_set)] - kept.append(make_id_ref(group_node["@id"])) - node["usedDataset"] = kept - - # Track all collapsed IDs - total_collapsed.update(collapsed_here) - - # Remove collapsed nodes from cache - for cid in total_collapsed: - node_cache.pop(cid, None) - - # Add group nodes to cache - for gn in all_group_nodes: - node_cache[gn["@id"]] = gn - - condensed_count = len(node_cache) - - if not all_group_nodes: - return { - "condensed": False, - "originalEntityCount": original_count, - "condensedEntityCount": original_count, - "datasetGroupCount": 0, - } - - return { - "condensed": True, - "originalEntityCount": original_count, - "condensedEntityCount": condensed_count, - "datasetGroupCount": len(all_group_nodes), - "entitiesRemoved": len(total_collapsed), - "groups": [ - { - "memberCount": gn["evi:memberCount"], - "format": gn.get("format", "unknown"), - "groupId": gn["@id"], - } - for gn in all_group_nodes - ], - } - - -def _collect_exclusive_backward( - dataset_id: str, - representative_id: str, - index: dict[str, dict], - collapsed: set[str], -) -> None: - """ - Collect backward chain entities of a non-representative dataset that are - NOT shared with the representative's chain. Add them to collapsed set. - """ - rep_chain = _collect_backward_chain(representative_id, index) - stack = [dataset_id] - visited = set() - - while stack: - cid = stack.pop() - if cid in visited: - continue - visited.add(cid) - - if cid in rep_chain: - continue - - node = index.get(cid) - if node is None: - continue - - if is_software(node): - continue - - collapsed.add(cid) - - if is_dataset(node): - for comp_id in get_generatedby_ids(node): - stack.append(comp_id) - - if is_computation(node): - for ref_id in get_id_list(node, "usedDataset"): - stack.append(ref_id) - - -def _collect_backward_chain(dataset_id: str, index: dict[str, dict]) -> set[str]: - """Collect all entity @ids in the backward provenance chain.""" - visited = set() - stack = [dataset_id] - while stack: - cid = stack.pop() - if cid in visited: - continue - visited.add(cid) - node = index.get(cid) - if node is None: - continue - if is_dataset(node): - for comp_id in get_generatedby_ids(node): - stack.append(comp_id) - if is_computation(node): - for ref_id in get_id_list(node, "usedDataset", "usedSoftware", - "usedMLModel"): - stack.append(ref_id) - return visited - - -# --------------------------------------------------------------------------- -# Build condensed graph -# --------------------------------------------------------------------------- - -def create_dataset_group_node( - group: dict, - index: dict[str, dict], - max_member_ids: int = 0, -) -> dict: - """Create a DatasetGroup summary node for a group of similar datasets.""" - representative = index[group["representative_id"]] - member_ids = group["member_ids"] - count = len(member_ids) - sig = group["signature"] - - common_sw_ids = [] - if sig[2]: - for comp_sig in sig[2]: - sw_ids, _ = comp_sig - common_sw_ids.extend(sw_ids) - common_sw_ids = sorted(set(common_sw_ids)) - - fmt = sig[0] - - consuming_node = index.get(group["consuming_comp_id"], {}) - if is_rocrate_root(consuming_node): - crate_name = consuming_node.get("name", "unknown").lower().replace(" ", "-") - group_id = f"ark:group/{crate_name}-{fmt.replace('/', '_').lstrip('.')}-outputs" - else: - comp_name = consuming_node.get("name", "unknown").lower().replace(" ", "-") - group_id = f"ark:group/{comp_name}-{fmt.replace('/', '_').lstrip('.')}-inputs" - - sw_names = [] - for sw_id in common_sw_ids: - sw = index.get(sw_id, {}) - sw_names.append(sw.get("name", sw_id)) - - description = f"{count} {fmt} files with identical provenance structure." - if sw_names: - description += f" All processed by {', '.join(sw_names)}." - - schema_ids = list(sig[1]) if sig[1] else [] - - node = { - "@id": group_id, - "@type": ["prov:Entity", "https://w3id.org/EVI#DatasetGroup"], - "name": f"{representative.get('name', fmt + ' files')} (and {count - 1} similar)", - "description": description, - "format": fmt, - "evi:memberCount": count, - "evi:representativeDataset": make_id_ref(group["representative_id"]), - "evi:commonFormat": fmt, - "evi:commonSoftware": [make_id_ref(sw_id) for sw_id in common_sw_ids], - "evi:provenanceSignature": str(sig), - "evi:memberIds": _truncate_member_ids(sorted(member_ids), max_member_ids), - } - - if schema_ids: - node["evi:commonSchema"] = [make_id_ref(sid) for sid in schema_ids] - - return node - - -def _truncate_member_ids(ids: list[str], max_ids: int) -> list[str]: - """Truncate member ID list if max_ids > 0, appending a summary entry.""" - if max_ids <= 0 or len(ids) <= max_ids: - return ids - excluded = len(ids) - max_ids - return ids[:max_ids] + [f"... and {excluded} more (total: {len(ids)})"] - - -def condense_graph( - graph: list[dict], - threshold: int, - max_member_ids: int = 0, -) -> tuple[list[dict], dict]: - """ - Condense an RO-Crate @graph by collapsing repetitive provenance. - - Returns (condensed_graph, stats). - """ - index: dict[str, dict] = {} - for node in graph: - node_id = node.get("@id") - if node_id: - index[node_id] = node - - original_count = len(graph) - - keep_ids, collapsed_ids, group_nodes, comp_updates = \ - traverse_and_condense(index, threshold, max_member_ids) - - if not group_nodes: - stats = { - "condensed": False, - "originalEntityCount": original_count, - "condensedEntityCount": original_count, - "datasetGroupCount": 0, - "note": "No repetitive provenance found above threshold.", - } - return graph, stats - - new_graph = [] - for node in graph: - node_id = node.get("@id") - - if node_id not in keep_ids: - continue - - if node_id in comp_updates: - node = dict(node) - for member_ids, group_id in comp_updates[node_id]: - member_set = set(member_ids) - - if "usedDataset" in node and node["usedDataset"]: - kept = [ref for ref in node["usedDataset"] - if ref.get("@id") not in member_set] - kept.append(make_id_ref(group_id)) - node["usedDataset"] = kept - - if "prov:used" in node and node["prov:used"]: - kept = [ref for ref in node["prov:used"] - if ref.get("@id") not in member_set] - kept.append(make_id_ref(group_id)) - node["prov:used"] = kept - - if is_rocrate_root(node): - node = dict(node) - condensed_count = len(keep_ids) + len(group_nodes) - node["evi:condensed"] = True - node["evi:condensationThreshold"] = threshold - node["evi:condensationDate"] = str(datetime.date.today()) - node["evi:originalEntityCount"] = original_count - node["evi:condensedEntityCount"] = condensed_count - node["evi:datasetGroupCount"] = len(group_nodes) - - total_collapsed = len(collapsed_ids) - node["evi:condensationNote"] = ( - f"Condensed from {original_count} entities to " - f"{condensed_count}. " - f"{len(group_nodes)} dataset group(s) created by collapsing " - f"{total_collapsed} datasets with identical provenance signatures " - f"(same software chain). Full member lists preserved in evi:memberIds." - ) - - if "hasPart" in node and node["hasPart"]: - kept = [ref for ref in node["hasPart"] - if ref.get("@id") not in collapsed_ids] - for gn in group_nodes: - kept.append(make_id_ref(gn["@id"])) - node["hasPart"] = kept - - if node_id in comp_updates: - for member_ids, group_id in comp_updates[node_id]: - member_set = set(member_ids) - if "EVI:outputs" in node and node["EVI:outputs"]: - kept = [ref for ref in node["EVI:outputs"] - if ref.get("@id") not in member_set] - kept.append(make_id_ref(group_id)) - node["EVI:outputs"] = kept - - new_graph.append(node) - - new_graph.extend(group_nodes) - - # Clean up dangling references - final_ids = {n.get("@id") for n in new_graph if "@id" in n} - ref_fields = ("usedDataset", "usedSoftware", "usedMLModel", "generated", - "hasPart", "prov:used", "generatedBy", "prov:wasGeneratedBy", - "derivedFrom", "prov:wasDerivedFrom", "usedByComputation", - "isPartOf", "EVI:outputs") - - for i, node in enumerate(new_graph): - modified = False - node_copy = None - for field in ref_fields: - val = node.get(field) - if val is None: - continue - if isinstance(val, dict) and "@id" in val: - if val["@id"] not in final_ids: - if not modified: - node_copy = dict(node) - modified = True - node_copy[field] = [] - elif isinstance(val, list): - cleaned = [ref for ref in val - if not (isinstance(ref, dict) and "@id" in ref - and ref["@id"] not in final_ids)] - if len(cleaned) != len(val): - if not modified: - node_copy = dict(node) - modified = True - node_copy[field] = cleaned - if modified: - new_graph[i] = node_copy - - condensed_count = len(new_graph) - groups_info = [] - for gn in group_nodes: - groups_info.append({ - "memberCount": gn["evi:memberCount"], - "format": gn["format"], - "groupId": gn["@id"], - }) - - stats = { - "condensed": True, - "originalEntityCount": original_count, - "condensedEntityCount": condensed_count, - "datasetGroupCount": len(group_nodes), - "entitiesRemoved": len(collapsed_ids), - "groups": groups_info, - } - - return new_graph, stats +# Pure condensation helpers live in the shared package. +from fairscape_interpret.pipeline.condense import ( + ARK_REF_FIELDS, + compute_provenance_signature, + condense_evidence_graph_cache, + condense_graph, + create_dataset_group_node, + traverse_and_condense, +) +from fairscape_interpret.pipeline.graph_utils import ( + EVI_TYPES, + get_evi_type, + get_generatedby_ids, + get_id_list, + is_computation, + is_dataset, + is_rocrate_root, + is_software, + make_id_ref, +) # --------------------------------------------------------------------------- # MongoDB graph collection (replaces local file merge_additional_crates) # --------------------------------------------------------------------------- -# Fields that contain ARK references to other entities -_ARK_REF_FIELDS = ( - "hasPart", "EVI:outputs", "outputs", - "generatedBy", "prov:wasGeneratedBy", - "usedDataset", "usedSoftware", "usedMLModel", - "derivedFrom", "prov:wasDerivedFrom", - "usedSample", "usedInstrument", "usedTreatment", "usedStain", - "generated", "prov:used", -) - # Batch size for MongoDB $in queries _BATCH_SIZE = 500 @@ -757,10 +65,8 @@ def _flatten_metadata(doc: dict) -> dict: {"@id": ..., "@type": ..., "name": ..., "generatedBy": [...]}. """ flattened = {} - # Start with the metadata sub-dict (the actual RO-Crate fields) if "metadata" in doc and isinstance(doc["metadata"], dict): flattened.update(doc["metadata"]) - # Ensure @id and @type are present (from top-level StoredIdentifier) if "@id" in doc: flattened["@id"] = doc["@id"] if "@type" in doc and "@type" not in flattened: @@ -771,7 +77,7 @@ def _flatten_metadata(doc: dict) -> dict: def _extract_all_ark_refs(entity: dict) -> list[str]: """Extract all ARK identifier references from an entity's provenance fields.""" refs = [] - for field in _ARK_REF_FIELDS: + for field in ARK_REF_FIELDS: val = entity.get(field) if val is None: continue @@ -901,11 +207,6 @@ def condense_rocrate( # @graph[1] = ROCrateMetadataElem (the root crate node — already in condensed_graph) # @graph[2..] = rest of condensed entities - # The condensed_graph already contains the root ROCrateMetadataElem - # (with evi:condensed=True set by condense_graph). We need to: - # 1. Find it and update its @id to the condensed_id - # 2. Prepend the ROCrateMetadataFileElem - # Find the root crate element in the condensed graph root_idx = None for idx, node in enumerate(condensed_graph): @@ -918,8 +219,6 @@ def condense_rocrate( root_node = dict(condensed_graph[root_idx]) root_node["evi:sourceROCrate"] = {"@id": rocrate_id} root_node["evi:condensationStats"] = stats - # Keep original @id so provenance refs stay valid, - # but add the condensed_id as an alternate condensed_graph[root_idx] = root_node # Build the ro-crate-metadata.json file descriptor element @@ -931,14 +230,12 @@ def condense_rocrate( } # Assemble the @graph: file descriptor first, then rest - # Remove the root from its current position and place it second ordered_graph = [file_elem] if root_idx is not None: ordered_graph.append(condensed_graph[root_idx]) for idx, node in enumerate(condensed_graph): if idx == root_idx: continue - # Skip any existing file descriptor from the source graph if node.get("@id") == "ro-crate-metadata.json": continue ordered_graph.append(node) diff --git a/mds/src/fairscape_mds/crud/fairscape_request.py b/mds/src/fairscape_mds/crud/fairscape_request.py index 3fab410..abebc4f 100644 --- a/mds/src/fairscape_mds/crud/fairscape_request.py +++ b/mds/src/fairscape_mds/crud/fairscape_request.py @@ -1,20 +1,7 @@ -import re from fairscape_mds.core.config import FairscapeConfig +from fairscape_interpret.pipeline.graph_utils import flexible_ark_query - -def flexible_ark_query(guid: str): - """Build a MongoDB query that matches an ARK with or without dashes - and with or without a slash after 'ark:'. Returns None if guid - doesn't look like an ARK. Matches ARK SPEC""" - ark_match = re.match(r'^ark:/?([\d]+)/(.*)', guid) - if not ark_match: - return None - naan = ark_match.group(1) - postfix = ark_match.group(2) - stripped = postfix.replace('-', '') - fuzzy_postfix = '-?'.join(re.escape(c) for c in stripped) - pattern = f'^ark:{naan}/{fuzzy_postfix}$' - return {"@id": {"$regex": pattern}} +__all__ = ["FairscapeRequest", "flexible_ark_query"] class FairscapeRequest(): diff --git a/mds/src/fairscape_mds/crud/interpretation.py b/mds/src/fairscape_mds/crud/interpretation.py index d12f3cf..6f2a3b9 100644 --- a/mds/src/fairscape_mds/crud/interpretation.py +++ b/mds/src/fairscape_mds/crud/interpretation.py @@ -42,285 +42,56 @@ logger = logging.getLogger(__name__) # --------------------------------------------------------------------------- -# Global Event Loop for Worker +# Shared helpers -- all pure functions, prompts, and runtime utilities now +# live in the fairscape_interpret package so the CLI can reuse them. # --------------------------------------------------------------------------- -_worker_loop = None - - -def run_async(coro): - """Run an async coroutine using a single, persistent event loop per worker process. - - This prevents 'RuntimeError: bound to a different event loop' caused by - PydanticAI's globally cached HTTP client. - """ - global _worker_loop - if _worker_loop is None or _worker_loop.is_closed(): - _worker_loop = asyncio.new_event_loop() - asyncio.set_event_loop(_worker_loop) - return _worker_loop.run_until_complete(coro) - -# --------------------------------------------------------------------------- -# Async Rate Limiter -# --------------------------------------------------------------------------- - -class AsyncRateLimiter: - """Sliding-window rate limiter for async contexts. - - Allows at most `max_requests` within any rolling `window_seconds` period. - Callers await `acquire()` before making an API call. - """ - - def __init__(self, max_requests: int = 2, window_seconds: float = 10.0): - self._max = max_requests - self._window = window_seconds - self._lock = asyncio.Lock() - self._timestamps: deque = deque() - - async def acquire(self): - async with self._lock: - now = time.monotonic() - # Evict timestamps outside the window - while self._timestamps and (now - self._timestamps[0]) >= self._window: - self._timestamps.popleft() - # If at capacity, sleep until the oldest timestamp exits the window - if len(self._timestamps) >= self._max: - sleep_for = self._window - (now - self._timestamps[0]) - if sleep_for > 0: - logger.info(f"Rate limiter: sleeping {sleep_for:.1f}s") - await asyncio.sleep(sleep_for) - self._timestamps.popleft() - self._timestamps.append(time.monotonic()) - - -MAX_API_RETRIES = 5 -API_RETRY_BASE_DELAY = 10.0 # seconds; doubles each retry - - -async def _run_agent_with_retry(agent, prompt, retries=MAX_API_RETRIES, base_delay=API_RETRY_BASE_DELAY): - """Run agent.run() with exponential backoff on overloaded/rate-limit errors.""" - for attempt in range(retries + 1): - try: - return await agent.run(prompt) - except Exception as e: - err_str = str(e) - is_retryable = "529" in err_str or "overloaded" in err_str.lower() or "rate" in err_str.lower() - if not is_retryable or attempt == retries: - raise - delay = base_delay * (2 ** attempt) - logger.warning(f"API overloaded (attempt {attempt + 1}/{retries + 1}), retrying in {delay:.0f}s: {err_str[:120]}") - await asyncio.sleep(delay) - - -# --------------------------------------------------------------------------- -# Constants -# --------------------------------------------------------------------------- +from fairscape_interpret.runtime import ( + AsyncRateLimiter, + API_RETRY_BASE_DELAY, + MAX_API_RETRIES, + run_agent_with_retry as _run_agent_with_retry, + run_async, +) +from fairscape_interpret.pipeline.graph_utils import ( + _build_index, + _compute_dag_order, + _is_computation, + _is_rocrate_root, + _resolve_refs, +) +from fairscape_interpret.pipeline.stats import ( + MAX_SPLITS, + _format_column_stats, + _format_dataset_stats, + _mini_histogram, +) +from fairscape_interpret.pipeline.github import ( + CODE_EXTENSIONS, + GITHUB_FILE_PATTERN, + GITHUB_REPO_PATTERN, + MAX_SOFTWARE_BYTES, + _fetch_github_file, + _fetch_github_repo_code, + prefetch_software_code, +) +from fairscape_interpret.prompts import ( + AUDIENCE_CONFIGS, + BIOSTAT_SYNTHESIS_PROMPT, + CLINICIAN_SYNTHESIS_PROMPT, + DATASCI_SYNTHESIS_PROMPT, + DATASCI_SYSTEM_PROMPT, +) -MAX_SOFTWARE_BYTES = 50_000 # 50KB per software entity +# Additional interpretation-only constants MAX_STATS_COLUMNS = 25 # max columns of stats to include per dataset in prompt -MAX_SPLITS = 10 # max splits to include per dataset in prompt MAX_PROMPT_DATASETS = 3 # max input/output datasets to include per computation prompt -CODE_EXTENSIONS = {".py", ".r", ".R", ".sh", ".pl", ".java", ".scala", ".jl", ".m", ".cpp", ".go", ".rs", ".ipynb", ".md"} -GITHUB_REPO_PATTERN = re.compile(r"https?://github\.com/([^/]+)/([^/]+?)(?:\.git)?(?:/.*)?$") -GITHUB_FILE_PATTERN = re.compile( - r"https?://github\.com/([^/]+)/([^/]+)/blob/([^/]+)/(.*)" -) - -# --------------------------------------------------------------------------- -# System Prompt -- Data Science Persona -# --------------------------------------------------------------------------- - -DATASCI_SYSTEM_PROMPT = """You are an analyst making explicit the assumptions that support the claims produced by a computation step in a scientific provenance graph (RO-Crate). - -You will receive the computation's metadata, input/output datasets with data profiles (when available), and software source code. - -Your task: produce a structured annotation that surfaces the assumptions this step relies on — what the analysis is built on, and what would change the interpretation if it turned out to be wrong. - -## What to Look For - -### Data Assumptions -What properties of the input data does this step rely on? Consider: completeness, distribution shape, independence of observations, representativeness of the sample, encoding and formatting, absence of systematic missingness. - -### Methodological Assumptions -What analytical choices were made, and what do they assume about the problem? Consider: model family appropriateness, distance/similarity metrics, evaluation strategy, splitting procedure, threshold values, handling of confounders. - -### Software/Parameter Assumptions -What do the code's defaults, hardcoded values, and library choices assume? Consider: parameter sensitivity, random seed presence, version-dependent behavior, implicit ordering. - -## Impact Classification - -Every assumption MUST be assigned exactly one impact level: - -**CRITICAL** — The entire result rests on this. If this assumption is wrong, the main conclusions do not hold. -Ask: "If this assumption fails, do the conclusions still stand?" If no → CRITICAL. -Examples: training/test independence, outcome variable validity, core statistical model appropriateness. - -**MAJOR** — Critical for a subset of results. If wrong, specific results break or change, but other portions of the analysis may still hold. -Ask: "If this assumption fails, do specific results or secondary claims break?" If yes → MAJOR. -Examples: default hyperparameters being adequate, a particular threshold choice, evaluation metric appropriateness for the data. - -**MINOR** — Present but unlikely to change the main conclusions. Worth recording for reuse or extension. -Ask: "Would correcting this change the results?" If no → MINOR. -Examples: version pinning, input validation, documentation completeness. - -## Key Principles -- State what the assumption IS, not just that something could go wrong. "Assumes input features are independently distributed" is better than "features might be correlated." -- Be specific: name the parameter, the data property, the function. -- Many well-written steps will have only MINOR assumptions. That is valid. -- Do not manufacture assumptions to fill quotas. -- Weave critical assumptions into the stepSummary itself — they are part of the story of what this step does. - -## Errors — CHECK FIRST - -BEFORE writing assumptions, check the code and metadata for actual mistakes. Errors are things that are **demonstrably wrong** — a competent scientist reading the code would say "this is a bug" not "this is a risk." - -**The distinction is critical:** An assumption is something that *could* be wrong depending on context. An error is something that *is* wrong based on what the code actually does. - -Examples of ERRORS (put in the `errors` field, NOT in assumptions): -- A function uses the wrong variable and produces incorrect output -- Logic is inverted — a condition checks the opposite of what it should -- Data leakage of any kind visible in the code — fitting on full data before splitting, label information leaking into features, test data influencing training preprocessing. Data leakage is ALWAYS an error, never an assumption. -- Metadata contradicts what the code actually does -- Off-by-one errors, unreachable code paths that should execute - -Examples of ASSUMPTIONS (put in the `assumptions` field, NOT in errors): -- "Assumes features are independent" — might or might not be true -- "Assumes N epochs is sufficient" — a judgment call -- "Assumes class imbalance won't affect results" — risky but not a bug - -**Self-check:** If you find yourself writing an assumption whose description says the code "does X but should do Y" or "uses the wrong variable" — that is an error, not an assumption. Move it to the `errors` field. - -Each error MUST be a structured object: -{ - description: "What is wrong and why it is wrong", - severity: "CRITICAL" | "MAJOR", - evidence: { artifact: {"@id": "<@id>"}, location: "" }, - affectedOutputs: "Which downstream results are impacted" -} - -Many computaions will have NO errors — that is expected. But when an error exists, it MUST go in the `errors` field, not be buried as an assumption. - -## Review Recommendation - -Each assumption must include a `reviewRecommended` boolean: - -- `reviewRecommended: false` ("Routine assumption") — Things a scientist would generally accept without checking: the RO-Crate manifest is correct, proprietary software does what its documentation says, standard libraries (numpy, pandas, scikit-learn) work correctly, file formats are as declared, the person who ran it used the right data. - -- `reviewRecommended: true` ("Review suggested") — Things a scientist would actually want to validate before trusting the results: custom data processing logic, parameter selections that affect results, threshold choices, model selection rationale, assumptions about data distributions, handling of edge cases in custom code. - -When in doubt, mark as `reviewRecommended: false`. The goal is to surface the small number of assumptions that genuinely warrant a scientist's attention, not to flag everything. - -## Computation Status - -You MUST set `computationStatus` to one of: - -- "clear" — No errors found, and assumptions are routine (standard library trust, manifest correctness, etc.) or well-documented. A scientist can trust this step without detailed review. - -- "review_recommended" — No errors, but one or more assumptions warrant a scientist's attention (e.g., custom logic, parameter choices, data processing decisions). NOTE: A computation with CRITICAL-impact assumptions can still be "clear" if those assumptions are routine (e.g., "assumes proprietary software works as documented"). - -- "error_detected" — One or more actual errors were found. This should be rare. - -**Consistency rule:** If the `errors` list is non-empty, `computationStatus` MUST be "error_detected". If any assumption has `reviewRecommended: true`, `computationStatus` should be at least "review_recommended" (unless errors exist, in which case use "error_detected"). - -Use your judgment. The status reflects whether a scientist should spend time reviewing this step, not just a count of assumption severity levels. - -## Output -- stepSummary: What this step does, why, and what critical assumptions it rests on. -- codeAnalysis: Per software entity — summary, key functions, and assumptions (each with the structured format below). -- inputSummaries: Per input dataset — role, description, dataQuality observations from data profile. -- outputSummaries: Per output dataset — what it contains, dataQuality observations. -- assumptions: Step-level assumptions. May be empty if the step makes no notable assumptions. -- errors: Obvious mistakes found. Each as a structured object (see Errors section above). -- computationStatus: "clear" | "review_recommended" | "error_detected" (see Computation Status section above). - -Each assumption MUST be a structured object: -{ - impact: "CRITICAL" | "MAJOR" | "MINOR", - name: "Short label (3-8 words) — e.g. 'Train/test split independence'", - description: "Briefly describe what is being assumed", - downstreamImpacts: "What changes if this assumption is wrong — potential downstream effects", - evidence: { artifact: {"@id": "<@id of the data file or software entity>"}, location: "" }, - reviewRecommended: true | false, - recommendedValidation: "A concrete step a scientist could take to test this assumption — e.g. 'Run Shapiro-Wilk test on residuals' or 'Check feature correlation matrix for multicollinearity'. Include when reviewRecommended is true. Omit for routine assumptions." -} - -## Recommended Validation - -For assumptions where reviewRecommended is true, include a recommendedValidation string: a specific, actionable step a scientist could perform to check whether this assumption holds. Be concrete — name the test, the plot, or the comparison. For MINOR or routine assumptions, omit this field. - -Be precise and evidence-based. Reference specific function names and parameter values.""" - - -DATASCI_SYNTHESIS_PROMPT = """You are a senior data scientist synthesizing step annotations from a scientific analysis pipeline (RO-Crate) into a coherent picture of what supports the pipeline's claims. - -You will receive the RO-Crate overview, step-by-step annotations including assumptions, and a Pipeline DAG Structure section showing the topological order of steps. - -Produce: -1. pipelineDescription: 1-2 sentences about the primary output of the pipeline. Weave in the key findings — what the pipeline discovered and its most important results. This is NOT a repeat of the RO-Crate metadata description. Focus on what the pipeline produces and what was found. Keep it brief. -2. pipelineSteps: An ordered list of pipeline steps. Follow the DAG structure provided in the prompt. Each entry is one sentence describing what the step does. Use the numbering from the DAG structure section (1a, 1b for parallel branches at the same level, then 2, 3, etc. following DAG order). Start from raw inputs and work to the final output. -3. executiveSummary: 3-5 sentences covering what the pipeline does, its approach, and the most important critical assumptions it rests on. Weave the load-bearing assumptions into this summary. -4. narrativeSummary: A forward-chronological story of the pipeline, explicitly noting where key assumptions enter and what claims they support. The reader should finish this knowing what the results depend on. -5. keyFindings: Bulleted list of important observations about what the pipeline discovered. -6. assumptions: Cross-cutting assumptions that span the pipeline, each as a structured object: - {impact, name, description, downstreamImpacts, recommendedValidation} - - impact: "CRITICAL" (if wrong, pipeline conclusions don't hold), "MAJOR" (if wrong, specific results break but others may hold), or "MINOR" (worth noting but won't change conclusions) - - name: Short label (3-8 words) - - description: What is being assumed - - downstreamImpacts: What changes if this assumption is wrong - - recommendedValidation: A concrete step a scientist could take to test this assumption. Be specific — name the test, plot, or comparison. Omit for MINOR assumptions. - -Do NOT re-list every step-level assumption. Surface those that matter at the pipeline level — because they span steps, compound across steps, or are the most consequential for trusting the results. A pipeline with no CRITICAL assumptions at the graph level is valid if step-level ones don't compound.""" -BIOSTAT_SYNTHESIS_PROMPT = """You are a biostatistician reviewing a scientific analysis pipeline (RO-Crate). Synthesize the step annotations into a perspective focused on statistical rigor and the assumptions that underpin the quantitative claims. +# Constants, prompts, and AUDIENCE_CONFIGS are now imported from +# fairscape_interpret (see top of file). -You will receive the RO-Crate overview and step-by-step annotations including assumptions. - -Produce: -1. executiveSummary: 3-5 sentences on the pipeline's statistical approach and the critical statistical assumptions it rests on. -2. narrativeSummary: Forward-chronological story emphasizing where statistical assumptions enter — distributional assumptions, independence assumptions, sample size considerations, multiple testing implications, model specification choices. The reader should understand the statistical scaffolding supporting the claims. -3. keyFindings: Bulleted observations focused on statistical methodology — what was done well and what gaps exist. -4. assumptions: Cross-cutting statistical assumptions, each as a structured object: - {impact, name, description, downstreamImpacts} - - impact: "CRITICAL" (core statistical assumptions, e.g. distributional assumptions, independence of observations), "MAJOR" (statistical choices that shape specific results, e.g. correction methods, model selection), or "MINOR" (minor statistical notes for reproducibility) - - name: Short label (3-8 words) - - description: What is being assumed - - downstreamImpacts: What changes if this assumption is wrong - -Focus on what a statistician reviewing this work would want to verify. Do NOT re-list every step-level assumption.""" - - -CLINICIAN_SYNTHESIS_PROMPT = """You are a clinician reviewing a scientific analysis pipeline (RO-Crate). Synthesize the step annotations into a perspective focused on clinical applicability and what assumptions must hold for these results to inform patient care. - -You will receive the RO-Crate overview and step-by-step annotations including assumptions. - -Produce: -1. executiveSummary: 3-5 sentences on what clinical question this pipeline addresses and what critical assumptions must hold for the results to be clinically actionable. -2. narrativeSummary: Forward-chronological story emphasizing clinical relevance — what patient population is assumed, what outcome measures are used and whether they map to clinical endpoints, what generalizability assumptions are made. The reader should understand what would need to be true for these results to apply in practice. -3. keyFindings: Bulleted observations focused on clinical applicability — effect sizes, clinical vs statistical significance, population representativeness. -4. assumptions: Cross-cutting clinical assumptions, each as a structured object: - {impact, name, description, downstreamImpacts} - - impact: "CRITICAL" (assumptions about patient population, outcome validity, clinical relevance that conclusions rest on), "MAJOR" (assumptions affecting how broadly or confidently specific results can be applied), or "MINOR" (notes for clinical context unlikely to change interpretation) - - name: Short label (3-8 words) - - description: What is being assumed - - downstreamImpacts: What changes if this assumption is wrong - -Focus on what a clinician deciding whether to act on these results would want to know. Do NOT re-list every step-level assumption.""" - - -# Audience configuration for synthesis -AUDIENCE_CONFIGS = [ - { - "key": "biostat", - "label": "Biostatistician", - "prompt": BIOSTAT_SYNTHESIS_PROMPT, - }, - { - "key": "clinician", - "label": "Clinician", - "prompt": CLINICIAN_SYNTHESIS_PROMPT, - }, -] +# (Prompts and AUDIENCE_CONFIGS are imported from fairscape_interpret.prompts at the top of this file.) # --------------------------------------------------------------------------- @@ -340,275 +111,8 @@ class GraphSynthesisResult(BaseModel): assumptions: List[LLMAssumption] = [] -# --------------------------------------------------------------------------- -# Helper functions -# --------------------------------------------------------------------------- - -def _extract_short_types(node: dict) -> list: - """Extract short EVI type names from a node's @type field.""" - raw = node.get("@type", []) - if isinstance(raw, str): - raw = [raw] - shorts = [] - for t in raw: - short = t.split("#")[-1] if "#" in t else t.split(":")[-1] if ":" in t else t - shorts.append(short) - return shorts - - -def _is_computation(node: dict) -> bool: - return "Computation" in _extract_short_types(node) - - -def _is_rocrate_root(node: dict) -> bool: - return "ROCrate" in _extract_short_types(node) - - -def _resolve_refs(field) -> list: - """Normalize a reference field to a list of @id strings.""" - if field is None: - return [] - if isinstance(field, str): - return [field] - if isinstance(field, dict): - return [field.get("@id", "")] if "@id" in field else [] - if isinstance(field, list): - result = [] - for item in field: - if isinstance(item, str): - result.append(item) - elif isinstance(item, dict) and "@id" in item: - result.append(item["@id"]) - return result - return [] - - -def _build_index(graph: list) -> dict: - """Build {`@id` -> node} index from @graph array.""" - index = {} - for node in graph: - node_id = node.get("@id") - if node_id: - index[node_id] = node - return index - - -# --------------------------------------------------------------------------- -# Dataset statistics formatting for prompts -# --------------------------------------------------------------------------- - -_HIST_BARS = " ▁▂▃▄▅▆▇█" - - -def _mini_histogram(counts: list) -> str: - """Render a list of histogram counts as a sparkline string.""" - if not counts: - return "" - mx = max(counts) if max(counts) > 0 else 1 - return "".join(_HIST_BARS[min(int(c / mx * 8) + (1 if c > 0 else 0), 8)] for c in counts) - - -def _format_column_stats(col_name: str, col_data: dict) -> str: - """Format one column's stats as a markdown table row.""" - stats = col_data.get("statistics", {}) - - # Determine type by presence of 'mean' (numerical) vs 'unique' (categorical) - if "mean" in stats and stats["mean"] is not None: - # Numerical - missing_pct = stats.get("missing_percentage", "") - if missing_pct != "" and missing_pct is not None: - missing_pct = f"{missing_pct}%" - hist = _mini_histogram(stats.get("histogram_counts", [])) - return ( - f"| {col_name} | num | {stats.get('count', '')} | {missing_pct} " - f"| {stats.get('mean', '')} | {stats.get('std', '')} " - f"| {stats.get('min', '')} | {stats.get('first_quartile', '')} " - f"| {stats.get('second_quartile', '')} | {stats.get('third_quartile', '')} " - f"| {stats.get('max', '')} | {hist} |" - ) - else: - # Categorical - missing_pct = stats.get("missing_percentage", "") - if missing_pct != "" and missing_pct is not None: - missing_pct = f"{missing_pct}%" - return ( - f"| {col_name} | cat | {stats.get('count', '')} | {missing_pct} " - f"| top: {stats.get('top', '')} | uniq: {stats.get('unique', '')} " - f"| | | | | | freq: {stats.get('freq', '')} |" - ) - - -def _format_dataset_stats(ds_name: str, ds_stats: dict) -> str: - """Format descriptiveStatistics and splitStatistics for one dataset into - prompt-ready markdown. Returns empty string if no stats available.""" - desc_stats = ds_stats.get("descriptiveStatistics", {}) - split_stats = ds_stats.get("splitStatistics", {}) - - if not desc_stats and not split_stats: - return "" - - parts = [] - - # --- Overall descriptive statistics --- - if desc_stats: - columns = list(desc_stats.items()) - total_cols = len(columns) - - # Estimate row count from first column's count - first_stats = columns[0][1].get("statistics", {}) if columns else {} - row_count = first_stats.get("count", "?") - - # Total missing across all columns - total_missing = 0 - for _, col_data in columns: - mc = col_data.get("statistics", {}).get("missing_count") - if mc is not None: - total_missing += mc - - # Skip detailed stats for wide datasets (>10 columns) to keep prompts small - if total_cols > 10: - parts.append(f"#### Data Profile: {ds_name} ({total_cols} columns, ~{row_count} rows, {total_missing} missing values)") - parts.append("*(Column-level statistics omitted for wide dataset)*") - parts.append("") - else: - display_cols = columns - - parts.append(f"#### Data Profile: {ds_name} ({total_cols} columns, ~{row_count} rows, {total_missing} missing values)") - parts.append("| Column | Type | Count | Missing% | Mean/Top | Std/Unique | Min | Q1 | Median | Q3 | Max | Hist |") - parts.append("|--------|------|-------|----------|----------|------------|-----|----|----|----|----|------|") - for col_name, col_data in display_cols: - parts.append(_format_column_stats(col_name, col_data)) - parts.append("") - - # --- Split statistics --- - if split_stats: - split_items = list(split_stats.items()) - total_splits = len(split_items) - truncated_splits = total_splits > MAX_SPLITS - display_splits = split_items[:MAX_SPLITS] - - for split_name, split_data in display_splits: - split_desc = split_data.get("description", "") - split_col_stats = split_data.get("statistics", {}) - if not split_col_stats: - continue - - # Row count from first column - first_split_col = list(split_col_stats.values())[0] if split_col_stats else {} - split_row_count = first_split_col.get("statistics", {}).get("count", "?") - - label = f'#### Split: "{split_name}"' - if split_desc: - label += f" ({split_desc})" - label += f" -- {split_row_count} rows" - parts.append(label) - - split_columns = list(split_col_stats.items()) - - # Skip detailed split stats for wide datasets - if len(split_columns) > 10: - parts.append(f"*({len(split_columns)} columns, stats omitted for wide dataset)*") - parts.append("") - else: - parts.append("| Column | Type | Count | Missing% | Mean/Top | Std/Unique | Min | Q1 | Median | Q3 | Max | Hist |") - parts.append("|--------|------|-------|----------|----------|------------|-----|----|----|----|----|------|") - for col_name, col_data in split_columns: - parts.append(_format_column_stats(col_name, col_data)) - parts.append("") - - if truncated_splits: - parts.append(f"*... {total_splits - MAX_SPLITS} more splits omitted*\n") - - return "\n".join(parts) - - -# --------------------------------------------------------------------------- -# GitHub source code fetching -# --------------------------------------------------------------------------- - -def _fetch_github_file(owner: str, repo: str, branch: str, path: str) -> str: - """Fetch a single file from GitHub via raw URL.""" - raw_url = f"https://raw.githubusercontent.com/{owner}/{repo}/{branch}/{path}" - try: - resp = httpx.get(raw_url, timeout=15, follow_redirects=True) - resp.raise_for_status() - return resp.text - except Exception as e: - logger.warning(f"Failed to fetch {raw_url}: {e}") - return "" - - -def _fetch_github_repo_code(owner: str, repo: str, max_bytes: int = MAX_SOFTWARE_BYTES) -> str: - """Fetch code files from a GitHub repo up to max_bytes total.""" - try: - # Get default branch - api_url = f"https://api.github.com/repos/{owner}/{repo}" - resp = httpx.get(api_url, timeout=15, follow_redirects=True) - resp.raise_for_status() - default_branch = resp.json().get("default_branch", "main") - - # Get tree - tree_url = f"https://api.github.com/repos/{owner}/{repo}/git/trees/{default_branch}?recursive=1" - resp = httpx.get(tree_url, timeout=15, follow_redirects=True) - resp.raise_for_status() - tree = resp.json().get("tree", []) - - # Filter for code files - code_files = [] - for item in tree: - if item.get("type") != "blob": - continue - path = item.get("path", "") - ext = "." + path.rsplit(".", 1)[-1] if "." in path else "" - if ext.lower() in {e.lower() for e in CODE_EXTENSIONS}: - code_files.append(path) - - # Sort: prefer top-level files, then by name - code_files.sort(key=lambda p: (p.count("/"), p)) - - # Fetch files up to limit - collected = [] - total_bytes = 0 - for path in code_files: - if total_bytes >= max_bytes: - collected.append(f"\n--- [Truncated: reached {max_bytes} byte limit] ---\n") - break - content = _fetch_github_file(owner, repo, default_branch, path) - if content: - header = f"\n{'='*60}\n# FILE: {path}\n{'='*60}\n" - collected.append(header + content) - total_bytes += len(content.encode("utf-8")) - - return "\n".join(collected) - - except Exception as e: - logger.warning(f"Failed to fetch repo {owner}/{repo}: {e}") - return f"[Could not fetch repository code: {e}]" - - -def prefetch_software_code(content_url: str) -> str: - """Fetch source code from a contentUrl, handling GitHub URLs.""" - if not content_url: - return "[No contentUrl available]" - - # Check for GitHub file URL: github.com/owner/repo/blob/branch/path - file_match = GITHUB_FILE_PATTERN.match(content_url) - if file_match: - owner, repo, branch, path = file_match.groups() - code = _fetch_github_file(owner, repo, branch, path) - return code if code else f"[Could not fetch file from {content_url}]" - - # Check for GitHub repo URL: github.com/owner/repo - repo_match = GITHUB_REPO_PATTERN.match(content_url) - if repo_match: - owner, repo = repo_match.groups() - return _fetch_github_repo_code(owner, repo) - - # Non-GitHub HTTP URL - if content_url.startswith("http"): - return f"[External URL, not fetched: {content_url}]" - - return f"[Local/relative path: {content_url}]" +# (Graph helpers, stats formatting, and GitHub fetchers are imported from +# fairscape_interpret at the top of this file.) # --------------------------------------------------------------------------- @@ -1120,96 +624,8 @@ def annotate_computations_parallel( # Step 5: Graph-level synthesis # ------------------------------------------------------------------ - @staticmethod - def _compute_dag_order( - step_annotations: List[AnnotatedComputation], - graph_dict: dict, - ) -> List[tuple]: - """Compute topological order of step annotations based on the DAG. - - Returns a list of (label, annotation) tuples where label is e.g. - "1", "1a", "1b", "2", etc. - """ - # Map computation @id -> annotation - ann_by_comp: Dict[str, AnnotatedComputation] = {} - for ann in step_annotations: - annotates = ann.annotates - if hasattr(annotates, 'guid'): - comp_id = annotates.guid - elif isinstance(annotates, dict): - comp_id = annotates.get("@id", "") - else: - comp_id = str(annotates) - ann_by_comp[comp_id] = ann - - # Build adjacency: comp_id -> set of comp_ids it depends on - deps: Dict[str, set] = {cid: set() for cid in ann_by_comp} - # Map dataset @id -> comp_id that generated it - generated_by: Dict[str, str] = {} - for node in graph_dict.values(): - if not isinstance(node, dict): - continue - gen = node.get("generatedBy") - if gen: - gen_ids = _resolve_refs(gen) - node_id = node.get("@id", "") - for gid in gen_ids: - if gid in ann_by_comp: - generated_by[node_id] = gid - - for comp_id in ann_by_comp: - comp_node = graph_dict.get(comp_id, {}) - input_refs = _resolve_refs(comp_node.get("usedDataset")) - for ds_id in input_refs: - producer = generated_by.get(ds_id) - if producer and producer != comp_id and producer in ann_by_comp: - deps[comp_id].add(producer) - - # Kahn's algorithm for topological sort with level tracking - in_degree = {cid: len(d) for cid, d in deps.items()} - # Reverse adjacency for decrementing - rdeps: Dict[str, set] = {cid: set() for cid in ann_by_comp} - for cid, dep_set in deps.items(): - for d in dep_set: - rdeps[d].add(cid) - - queue = [cid for cid, deg in in_degree.items() if deg == 0] - levels: List[List[str]] = [] - visited = set() - - while queue: - levels.append(sorted(queue)) # sort for determinism - visited.update(queue) - next_queue = [] - for cid in queue: - for child in rdeps.get(cid, set()): - if child in visited: - continue - in_degree[child] -= 1 - if in_degree[child] == 0: - next_queue.append(child) - queue = next_queue - - # Handle any remaining nodes (cycles) — add them at the end - remaining = [cid for cid in ann_by_comp if cid not in visited] - if remaining: - levels.append(sorted(remaining)) - - # Assign labels - result = [] - for level_num, level_cids in enumerate(levels, 1): - if len(level_cids) == 1: - result.append((str(level_num), ann_by_comp[level_cids[0]])) - else: - for branch_idx, cid in enumerate(level_cids): - letter = chr(ord('a') + branch_idx) if branch_idx < 26 else str(branch_idx) - result.append((f"{level_num}{letter}", ann_by_comp[cid])) - - # Fallback: if somehow empty, return original order - if not result: - return [(str(i), ann) for i, ann in enumerate(step_annotations, 1)] - - return result + # _compute_dag_order is imported from fairscape_interpret.pipeline.graph_utils + # (used as a module-level function, not a method). def _build_synthesis_prompt( self, @@ -1228,7 +644,7 @@ def _build_synthesis_prompt( # Compute DAG order if graph_dict is available if graph_dict: - ordered = self._compute_dag_order(step_annotations, graph_dict) + ordered = _compute_dag_order(step_annotations, graph_dict) else: ordered = [(str(i), ann) for i, ann in enumerate(step_annotations, 1)] diff --git a/mds/src/fairscape_mds/models/annotated_computation.py b/mds/src/fairscape_mds/models/annotated_computation.py index fbe78a7..f4f49e2 100644 --- a/mds/src/fairscape_mds/models/annotated_computation.py +++ b/mds/src/fairscape_mds/models/annotated_computation.py @@ -1,260 +1,44 @@ -"""AnnotatedComputation model -- local copy for Docker builds that use -an older fairscape_models release without this module. +"""Compatibility shim -- re-exports from the shared fairscape_interpret package. -Copied from fairscape_models/fairscape_models/annotated_computation.py -with imports adjusted to use fairscape_models base classes that ARE -present in the published package. +The model definitions now live in fairscape_interpret.models.annotated_computation. +This module is kept so existing `from fairscape_mds.models.annotated_computation +import X` paths keep working; new code should import from fairscape_interpret directly. """ -from enum import Enum - -from pydantic import BaseModel, Field, ConfigDict, model_validator -from typing import Optional, List, Union, Dict, Any - -# These base classes exist in the published fairscape_models package -from fairscape_models.fairscape_base import IdentifierValue -from fairscape_models.digital_object import DigitalObject - -ANNOTATED_COMPUTATION_TYPE = "AnnotatedComputation" - - -# --------------------------------------------------------------------------- -# Assumption impact levels -# --------------------------------------------------------------------------- - -class AssumptionImpact(str, Enum): - """How much this assumption matters for trusting/reusing results.""" - CRITICAL = "CRITICAL" - MAJOR = "MAJOR" - MINOR = "MINOR" - - -class ComputationReviewStatus(str, Enum): - """LLM-decided status indicating whether a scientist should review this computation.""" - CLEAR = "clear" - REVIEW_RECOMMENDED = "review_recommended" - ERROR_DETECTED = "error_detected" - - -class EvidencePointer(BaseModel): - """Points to the specific artifact and location supporting an assumption.""" - model_config = ConfigDict(extra="allow") - - artifact: IdentifierValue = Field(description='{"@id": "ark:..."} pointing to data file or software') - location: Optional[str] = Field(default=None, description="Specific location, e.g. line number, function name, column name") - - @model_validator(mode='before') - @classmethod - def normalize_artifact(cls, values): - """Accept various LLM artifact formats and normalize to IdentifierValue.""" - if not isinstance(values, dict): - return values - artifact = values.get("artifact") - if artifact is None: - return values - if isinstance(artifact, str): - values["artifact"] = {"@id": artifact} - elif isinstance(artifact, dict) and "@id" not in artifact: - # LLM may return {guid: "ark:..."} or {id: "ark:..."} etc - ark_id = ( - artifact.get("guid") - or artifact.get("id") - or artifact.get("@id") - or next(iter(artifact.values()), None) - ) - if ark_id: - values["artifact"] = {"@id": str(ark_id)} - return values - - -class Assumption(BaseModel): - """A structured assumption with an impact level.""" - impact: AssumptionImpact - name: str = Field(default="", description="Short label for the assumption (3-8 words)") - description: str - downstreamImpacts: Optional[str] = Field(default=None, description="What changes if this assumption is wrong") - evidence: Optional[EvidencePointer] = Field(default=None, description="Pointer to supporting artifact") - reviewRecommended: bool = Field(default=False, description="True if a scientist should validate this assumption; False for routine assumptions") - recommendedValidation: Optional[str] = Field(default=None, description="Concrete step a scientist could take to test this assumption") - - -class LLMAssumption(BaseModel): - """What the LLM returns for an assumption (impact as str for flexibility).""" - impact: str = Field(description="One of: CRITICAL, MAJOR, MINOR") - name: str = Field(default="", description="Short label for this assumption (3-8 words)") - description: str - downstreamImpacts: Optional[str] = Field(default=None, description="What changes downstream if this assumption is wrong") - evidence: Optional[dict] = Field(default=None, description='Pointer: {artifact: {"@id": "ark:..."}, location: "..."}') - reviewRecommended: bool = Field(default=False, description="True if a scientist should validate this; False for routine assumptions like trusting standard libraries") - recommendedValidation: Optional[str] = Field(default=None, description="Concrete step a scientist could take to test this assumption") - - -def normalize_assumption(llm_assumption: LLMAssumption) -> Assumption: - """Convert an LLMAssumption to a validated Assumption, normalizing the impact.""" - raw = llm_assumption.impact.strip().upper() - try: - impact = AssumptionImpact(raw) - except ValueError: - # Fallback: map common alternatives, old concern levels, and legacy names - if "CRIT" in raw or "FOUND" in raw: - impact = AssumptionImpact.CRITICAL - elif "MAJ" in raw or "CONSEQ" in raw or "SIG" in raw or "MOD" in raw or "WARN" in raw: - impact = AssumptionImpact.MAJOR - else: - impact = AssumptionImpact.MINOR - - evidence = None - if llm_assumption.evidence and isinstance(llm_assumption.evidence, dict): - try: - evidence = EvidencePointer.model_validate(llm_assumption.evidence) - except Exception: - # If evidence can't be parsed, skip it rather than failing - pass - - return Assumption( - impact=impact, - name=getattr(llm_assumption, "name", "") or "", - description=llm_assumption.description, - downstreamImpacts=getattr(llm_assumption, "downstreamImpacts", None), - evidence=evidence, - reviewRecommended=getattr(llm_assumption, "reviewRecommended", False), - recommendedValidation=getattr(llm_assumption, "recommendedValidation", None), - ) - - -# --------------------------------------------------------------------------- -# Error models — for flagging obvious mistakes (not risky assumptions) -# --------------------------------------------------------------------------- - -class LLMError(BaseModel): - """What the LLM returns when it finds an obvious coding/methodology error.""" - description: str = Field(description="What is wrong and why it is wrong") - severity: str = Field(default="MAJOR", description='One of: "CRITICAL", "MAJOR"') - evidence: Optional[dict] = Field(default=None, description='Pointer: {artifact: {"@id": "ark:..."}, location: "..."}') - affectedOutputs: Optional[str] = Field(default=None, description="Which downstream results are impacted") - - -class ComputationError(BaseModel): - """Validated error found in a computation step.""" - description: str - severity: str = Field(default="MAJOR") - evidence: Optional[EvidencePointer] = Field(default=None) - affectedOutputs: Optional[str] = Field(default=None) - - -def normalize_error(llm_error: LLMError) -> ComputationError: - """Convert an LLMError to a validated ComputationError.""" - raw_severity = llm_error.severity.strip().upper() - severity = "CRITICAL" if "CRIT" in raw_severity else "MAJOR" - - evidence = None - if llm_error.evidence and isinstance(llm_error.evidence, dict): - try: - evidence = EvidencePointer.model_validate(llm_error.evidence) - except Exception: - pass - - return ComputationError( - description=llm_error.description, - severity=severity, - evidence=evidence, - affectedOutputs=getattr(llm_error, "affectedOutputs", None), - ) - - -class CodeAnalysis(BaseModel): - """Analysis of a software entity used in the computation.""" - model_config = ConfigDict(extra="allow", populate_by_name=True) - - software: IdentifierValue - name: Optional[str] = Field(default=None) - summary: str - keyFunctions: Optional[List[str]] = Field(default=None) - assumptions: Optional[List[Assumption]] = Field(default=None) - - -class DatasetSummary(BaseModel): - """Summary of a dataset's role in the computation.""" - model_config = ConfigDict(extra="allow", populate_by_name=True) - - dataset: IdentifierValue - name: Optional[str] = Field(default=None) - role: Optional[str] = Field(default=None) - description: Optional[str] = Field(default=None) - dataQuality: Optional[str] = Field(default=None) - - -# --------------------------------------------------------------------------- -# Lightweight LLM output models (no infrastructure fields the LLM can't know) -# --------------------------------------------------------------------------- - -class LLMCodeAnalysis(BaseModel): - """What the LLM returns for a software entity analysis.""" - software_id: str = Field(description="The @id of the software entity being analyzed") - name: Optional[str] = Field(default=None) - summary: str - keyFunctions: Optional[List[str]] = Field(default=None) - assumptions: Optional[List[LLMAssumption]] = Field(default=None) - - -class LLMDatasetSummary(BaseModel): - """What the LLM returns for a dataset summary.""" - dataset_id: str = Field(description="The @id of the dataset") - name: Optional[str] = Field(default=None) - role: Optional[str] = Field(default=None) - description: Optional[str] = Field(default=None) - dataQuality: Optional[str] = Field(default=None, description="Data quality observations based on summary statistics") - - -class LLMComputationAnnotation(BaseModel): - """Lightweight model for LLM output — only the fields the LLM should fill.""" - stepSummary: str - codeAnalysis: Optional[List[LLMCodeAnalysis]] = Field(default=[]) - inputSummaries: Optional[List[LLMDatasetSummary]] = Field(default=[]) - outputSummaries: Optional[List[LLMDatasetSummary]] = Field(default=[]) - assumptions: Optional[List[LLMAssumption]] = Field(default=[]) - errors: Optional[List[LLMError]] = Field(default=[], description="Obvious mistakes found in code or methodology (usually empty)") - computationStatus: str = Field(default="clear", description='One of: "clear", "review_recommended", "error_detected"') - - -class AnnotatedComputation(DigitalObject): - """LLM-generated annotation of a single evi:Computation step. - - A DigitalObject (Document) that annotates an evi:Computation. - The original Computation stays in the graph in its original form; - this annotation points to it via evi:annotates. - """ - metadataType: Optional[Union[List[str], str]] = Field( - default=[ - 'prov:Entity', - "https://w3id.org/EVI#Annotation", - "https://w3id.org/EVI#AnnotatedComputation", - ], - alias="@type", - ) - additionalType: Optional[str] = Field(default=ANNOTATED_COMPUTATION_TYPE) - - # Points to the original Computation this annotates - annotates: IdentifierValue = Field(..., alias="evi:annotates") - - # LLM-generated content - stepSummary: str = Field(..., alias="evi:stepSummary") - codeAnalysis: Optional[List[CodeAnalysis]] = Field(default=[], alias="evi:codeAnalysis") - inputSummaries: Optional[List[DatasetSummary]] = Field(default=[], alias="evi:inputSummaries") - outputSummaries: Optional[List[DatasetSummary]] = Field(default=[], alias="evi:outputSummaries") - assumptions: Optional[List[Assumption]] = Field(default=[], alias="evi:assumptions") - errors: Optional[List[ComputationError]] = Field(default=[], alias="evi:errors") - computationStatus: Optional[str] = Field(default="clear", alias="evi:computationStatus") - - # Provenance of the annotation itself - llmModel: str = Field(alias="evi:llmModel") - llmTemperature: Optional[float] = Field(default=None, alias="evi:llmTemperature") - dateCreated: str - interpreterVersion: Optional[str] = Field(default=None, alias="evi:interpreterVersion") - - @model_validator(mode='after') - def populate_prov_fields(self): - """Auto-populate PROV-O fields.""" - self.wasDerivedFrom = [self.annotates] - self.wasAttributedTo = [self.llmModel] - return self +from fairscape_interpret.models.annotated_computation import ( + ANNOTATED_COMPUTATION_TYPE, + AnnotatedComputation, + Assumption, + AssumptionImpact, + CodeAnalysis, + ComputationError, + ComputationReviewStatus, + DatasetSummary, + EvidencePointer, + LLMAssumption, + LLMCodeAnalysis, + LLMComputationAnnotation, + LLMDatasetSummary, + LLMError, + normalize_assumption, + normalize_error, +) + +__all__ = [ + "ANNOTATED_COMPUTATION_TYPE", + "AnnotatedComputation", + "Assumption", + "AssumptionImpact", + "CodeAnalysis", + "ComputationError", + "ComputationReviewStatus", + "DatasetSummary", + "EvidencePointer", + "LLMAssumption", + "LLMCodeAnalysis", + "LLMComputationAnnotation", + "LLMDatasetSummary", + "LLMError", + "normalize_assumption", + "normalize_error", +] diff --git a/mds/src/fairscape_mds/models/annotated_evidence_graph.py b/mds/src/fairscape_mds/models/annotated_evidence_graph.py index d317568..94827bc 100644 --- a/mds/src/fairscape_mds/models/annotated_evidence_graph.py +++ b/mds/src/fairscape_mds/models/annotated_evidence_graph.py @@ -1,103 +1,22 @@ -"""AnnotatedEvidenceGraph model -- local copy for Docker builds that use -an older fairscape_models release without this module. +"""Compatibility shim -- re-exports from the shared fairscape_interpret package. -Copied from fairscape_models/fairscape_models/annotated_evidence_graph.py -with imports adjusted to use fairscape_models base classes that ARE -present in the published package. +The model definitions now live in fairscape_interpret.models.annotated_evidence_graph. +This module is kept so existing `from fairscape_mds.models.annotated_evidence_graph +import X` paths keep working; new code should import from fairscape_interpret directly. """ -from pydantic import BaseModel, Field, model_validator -from typing import Optional, List, Union, Dict, Any - -from fairscape_models.fairscape_base import IdentifierValue -from fairscape_models.digital_object import DigitalObject -from fairscape_mds.models.annotated_computation import AssumptionImpact, EvidencePointer - -ANNOTATED_EVIDENCE_GRAPH_TYPE = "AnnotatedEvidenceGraph" - - -class GraphAssumption(BaseModel): - """A graph-level assumption linked to its source annotation.""" - impact: AssumptionImpact - name: str = Field(default="", description="Short label for the assumption") - description: str - downstreamImpacts: Optional[str] = Field(default=None, description="What changes if this assumption is wrong") - evidence: Optional[EvidencePointer] = Field(default=None, description="Pointer to supporting artifact") - reviewRecommended: bool = Field(default=False, description="True if a scientist should validate this assumption") - recommendedValidation: Optional[str] = Field(default=None, description="Concrete step a scientist could take to test this assumption") - sourceAnnotation: IdentifierValue - - -class DataOverview(BaseModel): - """Brief top-level overview for quick orientation.""" - dataDescription: str = Field(description="1-2 sentences: what this data is") - dataFormats: List[str] = Field(default=[], description="File formats found in datasets") - keywords: List[str] = Field(default=[], description="Keywords from RO-Crate") - license: Optional[str] = Field(default=None, description="License URL or name") - conditionsOfAccess: Optional[str] = Field(default=None) - topAssumptions: List[GraphAssumption] = Field(default=[], description="1-2 most critical assumptions") - pipelineDescription: Optional[str] = Field(default=None, description="LLM-generated 1-2 sentence description of what the pipeline produces and its key findings") - pipelineSteps: Optional[List[str]] = Field(default=None, description="Ordered list of pipeline steps following DAG order") - - -class AudiencePerspective(BaseModel): - """Audience-specific synthesis of the annotated evidence graph.""" - targetAudience: str - audienceLabel: str - executiveSummary: str - narrativeSummary: str - keyFindings: Optional[List[str]] = Field(default=[]) - assumptions: Optional[List[GraphAssumption]] = Field(default=[]) - - -class AnnotatedEvidenceGraph(DigitalObject): - """Full annotated condensed evidence graph -- the graph-level LLM output. - - Contains all original crate entities plus AnnotatedComputation nodes - in a flat dict keyed by @id. Computation nodes are replaced by their - annotated supersets. DAG is reconstructable from cross-references - (generatedBy, usedDataset, evi:annotates, etc.). - """ - metadataType: Optional[Union[List[str], str]] = Field( - default=[ - 'prov:Entity', - "https://w3id.org/EVI#EvidenceGraph", - "https://w3id.org/EVI#AnnotatedEvidenceGraph", - ], - alias="@type", - ) - additionalType: Optional[str] = Field(default=ANNOTATED_EVIDENCE_GRAPH_TYPE) - - # Reference to the original evidence graph or RO-Crate root - annotates: IdentifierValue = Field(..., alias="evi:annotates") - - # Flat entity lookup -- all entities keyed by ARK @id - graph: Dict[str, Any] = Field(..., alias="@graph") - - # Graph-level LLM outputs (data scientist perspective — the default) - executiveSummary: str = Field(..., alias="evi:executiveSummary") - narrativeSummary: str = Field(..., alias="evi:narrativeSummary") - keyFindings: Optional[List[str]] = Field(default=[], alias="evi:keyFindings") - assumptions: Optional[List[GraphAssumption]] = Field(default=[], alias="evi:assumptions") - - # Brief top-level overview (data type, license, top assumptions) - overview: Optional[DataOverview] = Field(default=None, alias="evi:overview") - - # Audience-specific perspectives (biostatistician, clinician, etc.) - audiences: Optional[List[AudiencePerspective]] = Field(default=[], alias="evi:audiences") - - # Quick index of all AnnotatedComputation @ids in the graph - stepAnnotations: Optional[List[IdentifierValue]] = Field(default=[], alias="evi:stepAnnotations") - - # Provenance of the graph-level analysis - llmModel: str = Field(alias="evi:llmModel") - llmTemperature: Optional[float] = Field(default=None, alias="evi:llmTemperature") - dateCreated: str - interpreterVersion: Optional[str] = Field(default=None, alias="evi:interpreterVersion") - - @model_validator(mode='after') - def populate_prov_fields(self): - """Auto-populate PROV-O fields.""" - self.wasDerivedFrom = [self.annotates] - self.wasAttributedTo = [self.llmModel] - return self +from fairscape_interpret.models.annotated_evidence_graph import ( + ANNOTATED_EVIDENCE_GRAPH_TYPE, + AnnotatedEvidenceGraph, + AudiencePerspective, + DataOverview, + GraphAssumption, +) + +__all__ = [ + "ANNOTATED_EVIDENCE_GRAPH_TYPE", + "AnnotatedEvidenceGraph", + "AudiencePerspective", + "DataOverview", + "GraphAssumption", +] diff --git a/pyproject.toml b/pyproject.toml index ade360b..165b656 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,7 +45,11 @@ dependencies = [ "PyYaml>=6.0.2", "PyGithub", "pydantic-ai[google]>=0.1.0", + "fairscape_interpret", ] + +[tool.uv.sources] +fairscape_interpret = { path = "../fairscape_interpret", editable = true } requires-python = ">=3.9" [project.urls] From e7882a036029f01d21c90e1e35c901e186319742 Mon Sep 17 00:00:00 2001 From: jniestroy Date: Mon, 20 Apr 2026 14:56:46 -0400 Subject: [PATCH 17/27] Phase 1 #10: Mongo adapters for fairscape_interpret ports. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds crud/interpret_adapters.py with four Mongo-backed adapters satisfying the shared package's port protocols: MongoGraphSource -> GraphSource MongoResultSink -> ResultSink MongoTaskTracker -> TaskTracker ServerSoftwareFetcher -> SoftwareFetcher Behavior is faithful to the pre-refactor interpretation.py + condensation.py: StoredIdentifier wrapping, hasCondensedROCrate / hasAnnotatedEvidenceGraph back-pointers, evi:annotatedBy reverse links, and the /software/download/ endpoint fetch with baseUrl->internalUrl rewrite + GitHub fallback. No call sites wired yet — Phase 1 #11 will rewire the CRUD classes to use these adapters. MongoGraphSource composes a FairscapeCondensationRequest instead of inheriting from it, to keep the adapter's public surface limited to port methods only. See fairscape_interpret/MIGRATION.md decisions log for the full rationale. --- .../fairscape_mds/crud/interpret_adapters.py | 277 ++++++++++++++++++ 1 file changed, 277 insertions(+) create mode 100644 mds/src/fairscape_mds/crud/interpret_adapters.py diff --git a/mds/src/fairscape_mds/crud/interpret_adapters.py b/mds/src/fairscape_mds/crud/interpret_adapters.py new file mode 100644 index 0000000..3a394f4 --- /dev/null +++ b/mds/src/fairscape_mds/crud/interpret_adapters.py @@ -0,0 +1,277 @@ +"""MongoDB-backed adapters for the fairscape_interpret ports. + +Four adapters bridge `fairscape_interpret`'s port protocols to the +Mongo-and-FastAPI world of mds_python: + + - `MongoGraphSource` -> `GraphSource` + - `MongoResultSink` -> `ResultSink` + - `MongoTaskTracker` -> `TaskTracker` + - `ServerSoftwareFetcher` -> `SoftwareFetcher` + +The shared package has no knowledge of MongoDB, StoredIdentifier, +Permissions, Celery, or the `/software/download/` endpoint -- all of +that lives here. See `fairscape_interpret/ports.py` for the port +contracts this file satisfies. +""" + +from __future__ import annotations + +import datetime +import logging +from typing import Iterable, List + +import httpx + +from fairscape_mds.core.config import FairscapeConfig +from fairscape_mds.crud.condensation import FairscapeCondensationRequest +from fairscape_mds.crud.fairscape_request import FairscapeRequest +from fairscape_mds.models.identifier import ( + MetadataTypeEnum, + PublicationStatusEnum, + StoredIdentifier, +) +from fairscape_mds.models.user import Permissions + +from fairscape_interpret.models.annotated_computation import AnnotatedComputation +from fairscape_interpret.models.annotated_evidence_graph import AnnotatedEvidenceGraph +from fairscape_interpret.pipeline.github import ( + MAX_SOFTWARE_BYTES, + prefetch_software_code, +) + +logger = logging.getLogger(__name__) + + +def _flatten_metadata(doc: dict) -> dict: + """Unwrap a StoredIdentifier doc to the bare RO-Crate-node shape the + shared pipeline expects (mirror of condensation.py's helper).""" + flattened: dict = {} + if "metadata" in doc and isinstance(doc["metadata"], dict): + flattened.update(doc["metadata"]) + if "@id" in doc: + flattened["@id"] = doc["@id"] + if "@type" in doc and "@type" not in flattened: + flattened["@type"] = doc["@type"] + return flattened + + +def _extract_annotates_id(ann: AnnotatedComputation) -> str: + """Pull the @id the annotation targets, handling IdentifierValue / + dict / bare-string field shapes.""" + annotates = ann.annotates + if hasattr(annotates, "guid"): + return annotates.guid + if isinstance(annotates, dict): + return annotates.get("@id", "") + return str(annotates) + + +class MongoGraphSource(FairscapeRequest): + """GraphSource adapter backed by `identifierCollection`.""" + + def __init__(self, config: FairscapeConfig): + super().__init__(config) + self._condensation = FairscapeCondensationRequest(config) + + def find_entity(self, ark_id: str) -> dict | None: + doc = self.flexibleFind(ark_id) + if not doc: + return None + return _flatten_metadata(doc) + + def find_dataset_stats(self, ark_ids: Iterable[str]) -> dict[str, dict]: + ids = list(ark_ids) + if not ids: + return {} + cursor = self.config.identifierCollection.find( + {"@id": {"$in": ids}}, + {"@id": 1, "descriptiveStatistics": 1, "splitStatistics": 1}, + ) + stats_cache: dict[str, dict] = {} + for doc in cursor: + ds_id = doc.get("@id") + desc_stats = doc.get("descriptiveStatistics") + split_stats = doc.get("splitStatistics") + if ds_id and (desc_stats or split_stats): + stats_cache[ds_id] = { + "descriptiveStatistics": desc_stats or {}, + "splitStatistics": split_stats or {}, + } + logger.info( + f"Pre-fetched dataset statistics: {len(stats_cache)} of {len(ids)} " + f"datasets have stats" + ) + return stats_cache + + def build_full_graph(self, rocrate_id: str) -> list[dict]: + return self._condensation.build_full_graph_for_rocrate(rocrate_id) + + +class MongoResultSink: + """ResultSink adapter: writes StoredIdentifier docs + back-pointers.""" + + def __init__( + self, + config: FairscapeConfig, + *, + owner_email: str = "system@fairscape.org", + ): + self.config = config + self.owner_email = owner_email + + def persist_condensed( + self, + condensed_id: str, + condensed_metadata: dict, + source_rocrate_id: str, + stats: dict, + ) -> str: + now = datetime.datetime.utcnow() + stored_doc = { + "@id": condensed_id, + "@type": MetadataTypeEnum.ROCRATE.value, + "metadata": condensed_metadata, + "publicationStatus": PublicationStatusEnum.PUBLISHED.value, + "permissions": {"owner": self.owner_email, "group": None}, + "distribution": None, + "descriptiveStatistics": {}, + "contentSummary": None, + "dateCreated": now, + "dateModified": now, + } + self.config.identifierCollection.insert_one(stored_doc) + self.config.identifierCollection.update_one( + {"@id": source_rocrate_id}, + {"$set": {"metadata.hasCondensedROCrate": {"@id": condensed_id}}}, + ) + return condensed_id + + def persist_aeg( + self, + aeg: AnnotatedEvidenceGraph, + rocrate_id: str, + step_annotations: List[AnnotatedComputation], + ) -> str: + aeg_id = aeg.guid + permissions = Permissions(owner=self.owner_email, group="", acl=[]) + now = datetime.datetime.utcnow() + + stored = StoredIdentifier.model_validate({ + "@id": aeg_id, + "@type": MetadataTypeEnum.ANNOTATED_EVIDENCE_GRAPH.value, + "metadata": aeg.model_dump(by_alias=True, mode="json"), + "permissions": permissions.model_dump(), + "publicationStatus": PublicationStatusEnum.DRAFT, + "dateCreated": now, + "dateModified": now, + "distribution": None, + }) + self.config.identifierCollection.insert_one( + stored.model_dump(by_alias=True, mode="json") + ) + self.config.identifierCollection.update_one( + {"@id": rocrate_id}, + {"$set": {"metadata.hasAnnotatedEvidenceGraph": {"@id": aeg_id}}}, + ) + for ann in step_annotations: + comp_id = _extract_annotates_id(ann) + if comp_id: + self.config.identifierCollection.update_one( + {"@id": comp_id}, + {"$addToSet": {"metadata.evi:annotatedBy": {"@id": ann.guid}}}, + ) + logger.info(f"Stored AnnotatedEvidenceGraph {aeg_id}") + return aeg_id + + +class MongoTaskTracker: + """TaskTracker adapter: writes to `asyncCollection` for the Celery + task document identified by `task_guid`.""" + + def __init__(self, config: FairscapeConfig, task_guid: str): + self.config = config + self.task_guid = task_guid + + def update(self, updates: dict) -> None: + self.config.asyncCollection.update_one( + {"guid": self.task_guid}, + {"$set": updates}, + ) + + def update_computation_status(self, comp_id: str, updates: dict) -> None: + set_payload = {f"computation_details.$.{k}": v for k, v in updates.items()} + self.config.asyncCollection.update_one( + {"guid": self.task_guid, "computation_details.computation_id": comp_id}, + {"$set": set_payload}, + ) + + def increment_completed(self) -> None: + self.config.asyncCollection.update_one( + {"guid": self.task_guid}, + {"$inc": {"completed_computations": 1}}, + ) + + def push_llm_result(self, label: str, raw_output: dict) -> None: + self.config.asyncCollection.update_one( + {"guid": self.task_guid}, + {"$push": {"llm_results": { + "label": label, + "timestamp": datetime.datetime.utcnow().isoformat(), + "output": raw_output, + }}}, + ) + + +class ServerSoftwareFetcher: + """SoftwareFetcher adapter for the server. + + Uses the `/software/download/` endpoint with the caller's bearer + token when the `contentUrl` is Fairscape-hosted (applying the + configured `baseUrl -> internalUrl` rewrite), and falls back to the + shared `prefetch_software_code` helper for GitHub / external URLs. + Never raises -- returns an empty or placeholder string on failure. + """ + + def __init__(self, config: FairscapeConfig, user_token: str = ""): + self.config = config + self.user_token = user_token + + def fetch(self, software_node: dict) -> str: + content_url = software_node.get("contentUrl", "") or "" + sw_id = software_node.get("@id", "?") + + if "/software/download/" in content_url and self.user_token: + try: + internal_url = content_url + if self.config.internalUrl and self.config.baseUrl: + internal_url = content_url.replace( + self.config.baseUrl, self.config.internalUrl + ) + resp = httpx.get( + internal_url, + headers={"Authorization": f"Bearer {self.user_token}"}, + timeout=30.0, + ) + resp.raise_for_status() + code = resp.text + if len(code.encode("utf-8")) > MAX_SOFTWARE_BYTES: + code = code[:MAX_SOFTWARE_BYTES] + "\n[...truncated...]" + logger.info( + f"Pre-fetched software {sw_id} from download endpoint: " + f"{len(code)} chars" + ) + return code + except Exception as e: + logger.warning( + f"Failed to fetch software {sw_id} from download endpoint: {e}" + ) + + code = prefetch_software_code(content_url) + if not code: + logger.warning( + f"Pre-fetched software {sw_id}: returned empty string for " + f"{content_url!r}" + ) + else: + logger.info(f"Pre-fetched software {sw_id}: {len(code)} chars") + return code From dbc99227c200b2fd9d04330b8a9a212d5b385624 Mon Sep 17 00:00:00 2001 From: jniestroy Date: Mon, 20 Apr 2026 15:34:25 -0400 Subject: [PATCH 18/27] Phase 1 #11: shrink interpretation + condensation CRUDs to thin wrappers. interpret_rocrate (1066 -> 88 lines): load task config, build four Mongo adapters + Condenser + Interpreter, dispatch to run_sync. Module re-exports (_build_index, _is_computation, _is_rocrate_root, _resolve_refs, prefetch_software_code, GraphSynthesisResult, httpx) keep the existing test file importable. condense_rocrate (317 -> 251 lines total): pre-check 409, hand off to Condenser.condense, map ValueError to 404 and other exceptions to 500. build_full_graph_for_rocrate and delete_condensed_rocrate unchanged. Lazy-imports the Mongo adapter module inside the method to break the condensation <-> interpret_adapters cycle. MongoResultSink: track last_stats in persist_condensed so the thin wrapper can return {condensed_id, stats} to the Celery worker without widening the ResultSink port. --- mds/src/fairscape_mds/crud/condensation.py | 128 +- .../fairscape_mds/crud/interpret_adapters.py | 2 + mds/src/fairscape_mds/crud/interpretation.py | 1102 +---------------- 3 files changed, 95 insertions(+), 1137 deletions(-) diff --git a/mds/src/fairscape_mds/crud/condensation.py b/mds/src/fairscape_mds/crud/condensation.py index ee511af..bc1a195 100644 --- a/mds/src/fairscape_mds/crud/condensation.py +++ b/mds/src/fairscape_mds/crud/condensation.py @@ -163,13 +163,25 @@ def condense_rocrate( max_member_ids: int = 0, owner_email: str = "system@fairscape.org", ) -> FairscapeResponse: - """Build the full graph, condense it, and store the result. + """Build the full graph, condense it, store the result. - Returns a FairscapeResponse with the condensed ROCrate's StoredIdentifier. + Thin wrapper over `fairscape_interpret.Condenser.condense`. The + Mongo adapters and the shared orchestrator do all the real work; + this method only maps their exceptions onto the HTTP-shaped + `FairscapeResponse` the router and Celery worker expect. """ + # Lazy import: `interpret_adapters` imports this class, so a + # top-level import here would close the cycle at module load. + from fairscape_mds.crud.interpret_adapters import ( + MongoGraphSource, + MongoResultSink, + ) + from fairscape_interpret.condenser import Condenser + condensed_id = f"{rocrate_id}-condensed" - # Check if already exists + # Pre-check preserves the exact 409 error text (no hyphen in + # "ROCrate") that downstream clients may depend on. existing = self.config.identifierCollection.find_one({"@id": condensed_id}) if existing: return FairscapeResponse( @@ -178,111 +190,33 @@ def condense_rocrate( error={"message": f"Condensed ROCrate {condensed_id} already exists"} ) - # Collect the full graph from MongoDB - graph = self.build_full_graph_for_rocrate(rocrate_id) - if not graph: - return FairscapeResponse( - success=False, - statusCode=404, - error={"message": f"No metadata found for RO-Crate {rocrate_id}"} - ) - - # Run condensation - condensed_graph, stats = condense_graph(graph, threshold, max_member_ids) - - # Find context from the original ROCrate root doc - rocrate_doc = self.config.identifierCollection.find_one( - {"@id": rocrate_id}, {"_id": 0} + source = MongoGraphSource(self.config) + sink = MongoResultSink(self.config, owner_email=owner_email) + condenser = Condenser( + source, sink, threshold=threshold, max_member_ids=max_member_ids, ) - rocrate_name = "Unknown RO-Crate" - if rocrate_doc: - meta = rocrate_doc.get("metadata", {}) - if isinstance(meta, dict): - rocrate_name = meta.get("name", rocrate_doc.get("name", rocrate_id)) - else: - rocrate_name = rocrate_doc.get("name", rocrate_id) - - # Build the condensed ROCrate as a proper RO-Crate structure: - # @graph[0] = ROCrateMetadataFileElem ("about" this crate) - # @graph[1] = ROCrateMetadataElem (the root crate node — already in condensed_graph) - # @graph[2..] = rest of condensed entities - - # Find the root crate element in the condensed graph - root_idx = None - for idx, node in enumerate(condensed_graph): - if is_rocrate_root(node): - root_idx = idx - break - - if root_idx is not None: - # Update the root element with condensed-specific metadata - root_node = dict(condensed_graph[root_idx]) - root_node["evi:sourceROCrate"] = {"@id": rocrate_id} - root_node["evi:condensationStats"] = stats - condensed_graph[root_idx] = root_node - - # Build the ro-crate-metadata.json file descriptor element - file_elem = { - "@id": "ro-crate-metadata.json", - "@type": "CreativeWork", - "conformsTo": {"@id": "https://w3id.org/ro/crate/1.2-DRAFT"}, - "about": {"@id": rocrate_id}, - } - - # Assemble the @graph: file descriptor first, then rest - ordered_graph = [file_elem] - if root_idx is not None: - ordered_graph.append(condensed_graph[root_idx]) - for idx, node in enumerate(condensed_graph): - if idx == root_idx: - continue - if node.get("@id") == "ro-crate-metadata.json": - continue - ordered_graph.append(node) - - condensed_metadata = { - "@context": {"@vocab": "https://schema.org/"}, - "@graph": ordered_graph, - } - - now = datetime.datetime.utcnow() - stored_doc = { - "@id": condensed_id, - "@type": MetadataTypeEnum.ROCRATE.value, - "metadata": condensed_metadata, - "publicationStatus": PublicationStatusEnum.PUBLISHED.value, - "permissions": { - "owner": owner_email, - "group": None, - }, - "distribution": None, - "descriptiveStatistics": {}, - "contentSummary": None, - "dateCreated": now, - "dateModified": now, - } try: - self.config.identifierCollection.insert_one(stored_doc) - - # Update original ROCrate with pointer - self.config.identifierCollection.update_one( - {"@id": rocrate_id}, - {"$set": {"metadata.hasCondensedROCrate": {"@id": condensed_id}}} - ) - + persisted_id = condenser.condense(rocrate_id) + except ValueError as e: return FairscapeResponse( - success=True, - statusCode=201, - model={"condensed_id": condensed_id, "stats": stats} + success=False, + statusCode=404, + error={"message": str(e)}, ) except Exception as e: return FairscapeResponse( success=False, statusCode=500, - error={"message": f"Error storing condensed ROCrate: {str(e)}"} + error={"message": f"Error storing condensed ROCrate: {str(e)}"}, ) + return FairscapeResponse( + success=True, + statusCode=201, + model={"condensed_id": persisted_id, "stats": sink.last_stats}, + ) + def delete_condensed_rocrate(self, rocrate_id: str) -> FairscapeResponse: """Delete an existing condensed ROCrate and clear the pointer.""" condensed_id = f"{rocrate_id}-condensed" diff --git a/mds/src/fairscape_mds/crud/interpret_adapters.py b/mds/src/fairscape_mds/crud/interpret_adapters.py index 3a394f4..33b69ea 100644 --- a/mds/src/fairscape_mds/crud/interpret_adapters.py +++ b/mds/src/fairscape_mds/crud/interpret_adapters.py @@ -118,6 +118,7 @@ def __init__( ): self.config = config self.owner_email = owner_email + self.last_stats: dict = {} def persist_condensed( self, @@ -126,6 +127,7 @@ def persist_condensed( source_rocrate_id: str, stats: dict, ) -> str: + self.last_stats = stats or {} now = datetime.datetime.utcnow() stored_doc = { "@id": condensed_id, diff --git a/mds/src/fairscape_mds/crud/interpretation.py b/mds/src/fairscape_mds/crud/interpretation.py index 6f2a3b9..298e342 100644 --- a/mds/src/fairscape_mds/crud/interpretation.py +++ b/mds/src/fairscape_mds/crud/interpretation.py @@ -1,993 +1,67 @@ +"""interpretation.py -- Server-side thin wrapper for the AI-mediated +interpretation pipeline. + +The pipeline itself lives in `fairscape_interpret`. This module only +loads the per-task configuration from `asyncCollection`, assembles the +four Mongo-backed adapters from `crud/interpret_adapters.py`, and hands +them to the shared `Interpreter`. Keeping +`interpret_rocrate(task_guid, user_token)` at the same signature lets +`worker.py`'s Celery task continue to call it unchanged. + +Module-level re-exports below preserve the import surface that other +callers (notably the existing test suite under +`tests/crud/test_interpretation.py`) expect. The helpers themselves now +live in `fairscape_interpret`. """ -interpretation.py -- AI-Mediated Interpretation Pipeline -Takes an RO-Crate, ensures it is condensed, finds all Computation nodes, -pre-fetches software source code from GitHub, prompts an LLM per computation -(in parallel via ThreadPoolExecutor), synthesizes a graph-level summary, -and stores the resulting AnnotatedEvidenceGraph. -""" - -import asyncio -import datetime -import re -import time -import uuid import logging -from collections import deque -from typing import Any, Dict, List, Optional, Tuple - -import httpx -from pydantic_ai import Agent - -from fairscape_mds.models.annotated_computation import ( - AnnotatedComputation, CodeAnalysis, DatasetSummary, - LLMComputationAnnotation, LLMCodeAnalysis, LLMDatasetSummary, - Assumption, LLMAssumption, AssumptionImpact, normalize_assumption, - normalize_error, -) -from fairscape_mds.models.annotated_evidence_graph import ( - AnnotatedEvidenceGraph, GraphAssumption, AudiencePerspective, DataOverview, -) -from fairscape_mds.crud.fairscape_request import FairscapeRequest -from fairscape_mds.crud.fairscape_response import FairscapeResponse -from fairscape_mds.crud.condensation import FairscapeCondensationRequest -from fairscape_mds.models.identifier import ( - MetadataTypeEnum, - StoredIdentifier, - PublicationStatusEnum, -) -from fairscape_mds.models.user import Permissions - -logger = logging.getLogger(__name__) +import httpx # noqa: F401 -- kept so `patch("...interpretation.httpx.get")` still resolves -# --------------------------------------------------------------------------- -# Shared helpers -- all pure functions, prompts, and runtime utilities now -# live in the fairscape_interpret package so the CLI can reuse them. -# --------------------------------------------------------------------------- - -from fairscape_interpret.runtime import ( - AsyncRateLimiter, - API_RETRY_BASE_DELAY, - MAX_API_RETRIES, - run_agent_with_retry as _run_agent_with_retry, - run_async, -) +from fairscape_interpret.condenser import Condenser +from fairscape_interpret.interpreter import Interpreter, InterpretConfig +from fairscape_interpret.pipeline.github import prefetch_software_code from fairscape_interpret.pipeline.graph_utils import ( _build_index, - _compute_dag_order, _is_computation, _is_rocrate_root, _resolve_refs, ) -from fairscape_interpret.pipeline.stats import ( - MAX_SPLITS, - _format_column_stats, - _format_dataset_stats, - _mini_histogram, -) -from fairscape_interpret.pipeline.github import ( - CODE_EXTENSIONS, - GITHUB_FILE_PATTERN, - GITHUB_REPO_PATTERN, - MAX_SOFTWARE_BYTES, - _fetch_github_file, - _fetch_github_repo_code, - prefetch_software_code, -) -from fairscape_interpret.prompts import ( - AUDIENCE_CONFIGS, - BIOSTAT_SYNTHESIS_PROMPT, - CLINICIAN_SYNTHESIS_PROMPT, - DATASCI_SYNTHESIS_PROMPT, - DATASCI_SYSTEM_PROMPT, -) - -# Additional interpretation-only constants -MAX_STATS_COLUMNS = 25 # max columns of stats to include per dataset in prompt -MAX_PROMPT_DATASETS = 3 # max input/output datasets to include per computation prompt - - -# Constants, prompts, and AUDIENCE_CONFIGS are now imported from -# fairscape_interpret (see top of file). - -# (Prompts and AUDIENCE_CONFIGS are imported from fairscape_interpret.prompts at the top of this file.) - - -# --------------------------------------------------------------------------- -# Graph-level synthesis result model (just the fields we need from LLM) -# --------------------------------------------------------------------------- - -from pydantic import BaseModel, Field as PydanticField - - -class GraphSynthesisResult(BaseModel): - """Result model for the graph-level synthesis LLM call.""" - pipelineDescription: Optional[str] = None - pipelineSteps: List[str] = [] - executiveSummary: str - narrativeSummary: str - keyFindings: List[str] = [] - assumptions: List[LLMAssumption] = [] +from fairscape_interpret.pipeline.synthesize import GraphSynthesisResult +from fairscape_mds.crud.fairscape_request import FairscapeRequest +from fairscape_mds.crud.interpret_adapters import ( + MongoGraphSource, + MongoResultSink, + MongoTaskTracker, + ServerSoftwareFetcher, +) -# (Graph helpers, stats formatting, and GitHub fetchers are imported from -# fairscape_interpret at the top of this file.) +__all__ = [ + "FairscapeInterpretationRequest", + "GraphSynthesisResult", + "_build_index", + "_is_computation", + "_is_rocrate_root", + "_resolve_refs", + "prefetch_software_code", +] +logger = logging.getLogger(__name__) -# --------------------------------------------------------------------------- -# CRUD Class -# --------------------------------------------------------------------------- class FairscapeInterpretationRequest(FairscapeRequest): - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self._condensation = FairscapeCondensationRequest(self.config) - - def _update_task(self, task_guid: str, updates: dict): - """Update the async task document in MongoDB.""" - self.config.asyncCollection.update_one( - {"guid": task_guid}, - {"$set": updates} - ) - - def _save_llm_result(self, task_guid: str, label: str, raw_output: dict): - """Append a raw LLM result to the task document for debugging.""" - self.config.asyncCollection.update_one( - {"guid": task_guid}, - {"$push": {"llm_results": { - "label": label, - "timestamp": datetime.datetime.utcnow().isoformat(), - "output": raw_output, - }}} - ) - - # ------------------------------------------------------------------ - # Step 1: Ensure condensed - # ------------------------------------------------------------------ - - def ensure_condensed(self, task_guid: str, rocrate_id: str) -> Tuple[list, str, dict]: - """Ensure the RO-Crate is condensed. Returns (graph_list, condensed_id, root_node). - - Checks: - 1. metadata.hasCondensedROCrate pointer -> fetch that doc - 2. evi:condensed: true on the crate itself -> it IS condensed - 3. Otherwise, trigger condensation synchronously - """ - self._update_task(task_guid, { - "current_step": "CONDENSING", - "status": "CONDENSING", - }) - - # Fetch the RO-Crate from MongoDB - entity = self.flexibleFind(rocrate_id) - if not entity: - raise ValueError(f"RO-Crate {rocrate_id} not found") - - metadata = entity.get("metadata", {}) - - # Case 1: Has pointer to condensed version - condensed_ref = metadata.get("hasCondensedROCrate") - if condensed_ref: - condensed_id = condensed_ref.get("@id") if isinstance(condensed_ref, dict) else condensed_ref - condensed_doc = self.flexibleFind(condensed_id) - if condensed_doc: - condensed_metadata = condensed_doc.get("metadata", {}) - graph = condensed_metadata.get("@graph", []) - if isinstance(graph, dict): - graph = list(graph.values()) - index = _build_index(graph) - root = next((n for n in graph if _is_rocrate_root(n)), {}) - return graph, condensed_id, root - - # Case 2: The crate itself is condensed - # Check the root node in the graph for evi:condensed - graph = metadata.get("@graph", []) - if isinstance(graph, dict): - graph = list(graph.values()) - - for node in graph: - if _is_rocrate_root(node): - if node.get("evi:condensed") is True: - return graph, rocrate_id, node - - # Case 3: Need to condense - logger.info(f"Condensing RO-Crate {rocrate_id}") - response = self._condensation.condense_rocrate( - rocrate_id=rocrate_id, - threshold=5, - max_member_ids=0, - owner_email="system@fairscape.org", - ) - - if not response.success: - raise RuntimeError(f"Condensation failed: {response.error}") - - # Fetch the newly created condensed doc - condensed_id = f"{rocrate_id}-condensed" - condensed_doc = self.flexibleFind(condensed_id) - if not condensed_doc: - raise RuntimeError(f"Condensed RO-Crate {condensed_id} not found after condensation") - - condensed_metadata = condensed_doc.get("metadata", {}) - graph = condensed_metadata.get("@graph", []) - if isinstance(graph, dict): - graph = list(graph.values()) - root = next((n for n in graph if _is_rocrate_root(n)), {}) - - self._update_task(task_guid, {"condensed_rocrate_id": condensed_id}) - return graph, condensed_id, root - - # ------------------------------------------------------------------ - # Step 2: Find computations - # ------------------------------------------------------------------ - - def find_computations(self, task_guid: str, graph: list) -> Tuple[List[dict], dict]: - """Find all Computation nodes in the graph. Returns (computations, index).""" - self._update_task(task_guid, { - "current_step": "TRAVERSING", - "status": "TRAVERSING", - }) - - index = _build_index(graph) - computations = [node for node in graph if _is_computation(node)] - - # Initialize computation_details for progress tracking - comp_details = [ - {"computation_id": c.get("@id", ""), "name": c.get("name", ""), "status": "pending"} - for c in computations - ] - self._update_task(task_guid, { - "total_computations": len(computations), - "computation_details": comp_details, - }) - - logger.info(f"Found {len(computations)} computation(s) in graph") - return computations, index - - # ------------------------------------------------------------------ - # Step 3: Pre-fetch software - # ------------------------------------------------------------------ - - def prefetch_all_software(self, task_guid: str, computations: list, index: dict, user_token: str = "") -> Dict[str, str]: - """Pre-fetch source code for all software referenced by computations. - Returns {software_id: source_code_text}. - """ - self._update_task(task_guid, { - "current_step": "PREFETCHING", - "status": "PREFETCHING", - }) - - software_cache: Dict[str, str] = {} - for comp in computations: - software_refs = _resolve_refs(comp.get("usedSoftware")) - for sw_id in software_refs: - if sw_id in software_cache: - continue - sw_node = index.get(sw_id, {}) - content_url = sw_node.get("contentUrl", "") - - # Fairscape-hosted software: call download endpoint with user auth - if "/software/download/" in content_url and user_token: - try: - # Rewrite external URLs to internal service URL if configured - internal_url = content_url - if self.config.internalUrl and self.config.baseUrl: - internal_url = content_url.replace(self.config.baseUrl, self.config.internalUrl) - resp = httpx.get( - internal_url, - headers={"Authorization": f"Bearer {user_token}"}, - timeout=30.0 - ) - resp.raise_for_status() - code = resp.text - if len(code.encode("utf-8")) > MAX_SOFTWARE_BYTES: - code = code[:MAX_SOFTWARE_BYTES] + "\n[...truncated...]" - software_cache[sw_id] = code - logger.info(f"Pre-fetched software {sw_id} from download endpoint: {len(code)} chars") - continue - except Exception as e: - logger.warning(f"Failed to fetch software {sw_id} from download endpoint: {e}") - - # Fall back to GitHub/external URL fetching - code = prefetch_software_code(content_url) - software_cache[sw_id] = code - if not code: - logger.warning(f"Pre-fetched software {sw_id}: returned empty string for {content_url!r}") - else: - logger.info(f"Pre-fetched software {sw_id}: {len(code)} chars") - - return software_cache - - # ------------------------------------------------------------------ - # Step 3b: Pre-fetch dataset statistics - # ------------------------------------------------------------------ - - def prefetch_dataset_statistics( - self, task_guid: str, computations: list, index: dict - ) -> Dict[str, dict]: - """Fetch descriptiveStatistics and splitStatistics from MongoDB for all - datasets referenced by computations. Returns {dataset_id: {...}}.""" - # Collect all dataset IDs - dataset_ids = set() - for comp in computations: - for ds_id in _resolve_refs(comp.get("usedDataset")): - dataset_ids.add(ds_id) - for ds_id in _resolve_refs(comp.get("generated")): - dataset_ids.add(ds_id) - - if not dataset_ids: - return {} - - # Batch query MongoDB with projection - cursor = self.config.identifierCollection.find( - {"@id": {"$in": list(dataset_ids)}}, - {"@id": 1, "descriptiveStatistics": 1, "splitStatistics": 1}, - ) - - stats_cache: Dict[str, dict] = {} - for doc in cursor: - ds_id = doc.get("@id") - desc_stats = doc.get("descriptiveStatistics") - split_stats = doc.get("splitStatistics") - if desc_stats or split_stats: - stats_cache[ds_id] = { - "descriptiveStatistics": desc_stats or {}, - "splitStatistics": split_stats or {}, - } - - logger.info( - f"Pre-fetched dataset statistics: {len(stats_cache)} of " - f"{len(dataset_ids)} datasets have stats" - ) - return stats_cache - - # ------------------------------------------------------------------ - # Step 4: Annotate single computation - # ------------------------------------------------------------------ - - def _build_computation_prompt(self, computation: dict, software_cache: dict, index: dict, stats_cache: Optional[Dict[str, dict]] = None) -> str: - """Build the prompt for a single computation annotation.""" - parts = [] - - # Computation metadata - parts.append("## Computation") - parts.append(f"**ID:** {computation.get('@id', 'unknown')}") - parts.append(f"**Name:** {computation.get('name', 'unnamed')}") - parts.append(f"**Description:** {computation.get('description', 'No description')}") - parts.append(f"**Command:** {computation.get('command', 'N/A')}") - parts.append(f"**Run By:** {computation.get('runBy', 'unknown')}") - parts.append(f"**Date Created:** {computation.get('dateCreated', 'unknown')}") - parts.append("") - - if stats_cache is None: - stats_cache = {} - - # Input datasets - input_refs = _resolve_refs(computation.get("usedDataset")) - if input_refs: - truncated_inputs = len(input_refs) > MAX_PROMPT_DATASETS - display_inputs = input_refs[:MAX_PROMPT_DATASETS] - parts.append("## Input Datasets") - for ds_id in display_inputs: - ds_node = index.get(ds_id, {}) - ds_name = ds_node.get('name', ds_id) - parts.append(f"- **{ds_name}** ({ds_node.get('format', 'unknown format')})") - parts.append(f" ID: {ds_id}") - parts.append(f" Description: {ds_node.get('description', 'No description')}") - if ds_node.get("keywords"): - parts.append(f" Keywords: {', '.join(ds_node['keywords']) if isinstance(ds_node['keywords'], list) else ds_node['keywords']}") - # Include dataset statistics if available - if ds_id in stats_cache: - formatted = _format_dataset_stats(ds_name, stats_cache[ds_id]) - if formatted: - parts.append("") - parts.append(formatted) - if truncated_inputs: - parts.append(f"*({len(input_refs) - MAX_PROMPT_DATASETS} more input datasets omitted)*") - parts.append("") - - # Software and source code - software_refs = _resolve_refs(computation.get("usedSoftware")) - if software_refs: - parts.append("## Software") - for sw_id in software_refs: - sw_node = index.get(sw_id, {}) - parts.append(f"### {sw_node.get('name', sw_id)}") - parts.append(f"**ID:** {sw_id}") - parts.append(f"**Description:** {sw_node.get('description', 'No description')}") - parts.append(f"**Content URL:** {sw_node.get('contentUrl', 'N/A')}") - code = software_cache.get(sw_id, "") - if code and not code.startswith("["): - preview = f"{repr(code[:10])}…{repr(code[-10:])}" if len(code) > 20 else repr(code) - logger.info( - f"Prompt build: inserted software {sw_id} ({len(code)} chars) preview={preview}" - ) - parts.append(f"\n**Source Code:**\n```\n{code}\n```") - else: - logger.warning( - f"Prompt build: NO code for software {sw_id} " - f"(cache_hit={sw_id in software_cache!r}, value={repr((code or '')[:60])})" - ) - parts.append(f"\n**Source Code:** {code}") - parts.append("") - - # Output datasets - output_refs = _resolve_refs(computation.get("generated")) - if output_refs: - truncated_outputs = len(output_refs) > MAX_PROMPT_DATASETS - display_outputs = output_refs[:MAX_PROMPT_DATASETS] - parts.append("## Output Datasets") - for ds_id in display_outputs: - ds_node = index.get(ds_id, {}) - ds_name = ds_node.get('name', ds_id) - parts.append(f"- **{ds_name}** ({ds_node.get('format', 'unknown format')})") - parts.append(f" ID: {ds_id}") - parts.append(f" Description: {ds_node.get('description', 'No description')}") - # Include dataset statistics if available - if ds_id in stats_cache: - formatted = _format_dataset_stats(ds_name, stats_cache[ds_id]) - if formatted: - parts.append("") - parts.append(formatted) - if truncated_outputs: - parts.append(f"*({len(output_refs) - MAX_PROMPT_DATASETS} more output datasets omitted)*") - parts.append("") - - prompt = "\n".join(parts) - comp_id = computation.get("@id", "unknown") - est_tokens = len(prompt) // 4 - logger.info(f"Prompt for {comp_id}: ~{est_tokens} tokens estimated ({len(prompt)} chars)") - return prompt - - def _llm_to_annotated( - self, - llm_result: LLMComputationAnnotation, - comp_id: str, - llm_model: str, - temperature: float, - ) -> AnnotatedComputation: - """Convert lightweight LLM output into a full AnnotatedComputation.""" - annotation_id = f"{comp_id}-annotation" - now = datetime.datetime.utcnow().isoformat() - - # Convert LLM code analyses -> CodeAnalysis with IdentifierValue - code_analyses = [ - CodeAnalysis( - software={"@id": ca.software_id}, - name=ca.name, - summary=ca.summary, - keyFunctions=ca.keyFunctions, - assumptions=[normalize_assumption(c) for c in (ca.assumptions or [])], - ) - for ca in (llm_result.codeAnalysis or []) - ] - - # Convert LLM dataset summaries -> DatasetSummary with IdentifierValue - input_summaries = [ - DatasetSummary( - dataset={"@id": ds.dataset_id}, - name=ds.name, - role=ds.role, - description=ds.description, - dataQuality=ds.dataQuality, - ) - for ds in (llm_result.inputSummaries or []) - ] - output_summaries = [ - DatasetSummary( - dataset={"@id": ds.dataset_id}, - name=ds.name, - role=ds.role, - description=ds.description, - dataQuality=ds.dataQuality, - ) - for ds in (llm_result.outputSummaries or []) - ] - - return AnnotatedComputation.model_validate({ - "@id": annotation_id, - "name": f"Annotation of {comp_id}", - "author": llm_model, - "description": llm_result.stepSummary[:200] if len(llm_result.stepSummary) >= 10 else llm_result.stepSummary + " " * (10 - len(llm_result.stepSummary)), - "evi:annotates": {"@id": comp_id}, - "evi:stepSummary": llm_result.stepSummary, - "evi:codeAnalysis": [ca.model_dump(by_alias=True) for ca in code_analyses], - "evi:inputSummaries": [ds.model_dump(by_alias=True) for ds in input_summaries], - "evi:outputSummaries": [ds.model_dump(by_alias=True) for ds in output_summaries], - "evi:assumptions": [normalize_assumption(a).model_dump() for a in (llm_result.assumptions or [])], - "evi:errors": [normalize_error(e).model_dump() for e in (llm_result.errors or [])], - "evi:computationStatus": llm_result.computationStatus or "clear", - "evi:llmModel": llm_model, - "evi:llmTemperature": temperature, - "dateCreated": now, - }) - - async def _annotate_single_computation( - self, - task_guid: str, - computation: dict, - software_cache: dict, - index: dict, - llm_model: str, - temperature: float, - stats_cache: Optional[Dict[str, dict]] = None, - ) -> AnnotatedComputation: - """Annotate a single computation using PydanticAI.""" - prompt = self._build_computation_prompt(computation, software_cache, index, stats_cache=stats_cache) - comp_id = computation.get("@id", f"ark:59853/computation-{uuid.uuid4()}") - - agent = Agent( - llm_model, - output_type=LLMComputationAnnotation, - system_prompt=DATASCI_SYSTEM_PROMPT, - retries=3, - ) - - result = await _run_agent_with_retry(agent, prompt) - llm_output: LLMComputationAnnotation = result.output - - # Persist raw LLM output for debugging - self._save_llm_result( - task_guid, - f"computation:{comp_id}", - llm_output.model_dump(mode="json"), - ) - - # Convert lightweight LLM output -> full AnnotatedComputation - return self._llm_to_annotated(llm_output, comp_id, llm_model, temperature) - - # ------------------------------------------------------------------ - # Step 4b: Parallel computation processing - # ------------------------------------------------------------------ - - async def _annotate_computations_async( - self, - task_guid: str, - computations: list, - software_cache: dict, - index: dict, - llm_model: str, - temperature: float, - max_workers: int = 2, - stats_cache: Optional[Dict[str, dict]] = None, - rate_limiter: Optional["AsyncRateLimiter"] = None, - ) -> List[AnnotatedComputation]: - """Annotate all computations concurrently via asyncio.gather.""" - self._update_task(task_guid, { - "current_step": "PROMPTING", - "status": "PROMPTING", - }) - - async def _annotate_one(comp): - comp_id = comp.get("@id", "unknown") - if rate_limiter: - await rate_limiter.acquire() - try: - annotated = await self._annotate_single_computation( - task_guid, comp, software_cache, index, llm_model, temperature, - stats_cache=stats_cache, - ) - self.config.asyncCollection.update_one( - {"guid": task_guid, "computation_details.computation_id": comp_id}, - {"$set": {"computation_details.$.status": "done"}} - ) - return ("ok", comp_id, annotated) - except Exception as e: - logger.error(f"Failed to annotate computation {comp_id}: {e}") - self.config.asyncCollection.update_one( - {"guid": task_guid, "computation_details.computation_id": comp_id}, - {"$set": { - "computation_details.$.status": "error", - "computation_details.$.error": str(e), - }} - ) - return ("error", comp_id, str(e)) - finally: - self.config.asyncCollection.update_one( - {"guid": task_guid}, - {"$inc": {"completed_computations": 1}} - ) - - outcomes = await asyncio.gather(*[_annotate_one(c) for c in computations]) - - results = [o[2] for o in outcomes if o[0] == "ok"] - errors = [{"computation_id": o[1], "error": o[2]} for o in outcomes if o[0] == "error"] - - if errors and not results: - raise RuntimeError(f"All {len(errors)} computation annotations failed. First error: {errors[0]['error']}") - if errors: - logger.warning(f"{len(errors)} of {len(computations)} computation annotations failed") - - return results - - def annotate_computations_parallel( - self, - task_guid: str, - computations: list, - software_cache: dict, - index: dict, - llm_model: str, - temperature: float, - max_workers: int = 2, - stats_cache: Optional[Dict[str, dict]] = None, - rate_limiter: Optional["AsyncRateLimiter"] = None, - ) -> List[AnnotatedComputation]: - """Annotate computations concurrently using async I/O.""" - return run_async(self._annotate_computations_async( - task_guid, computations, software_cache, index, llm_model, temperature, max_workers, - stats_cache=stats_cache, rate_limiter=rate_limiter, - )) - - # ------------------------------------------------------------------ - # Step 5: Graph-level synthesis - # ------------------------------------------------------------------ - - # _compute_dag_order is imported from fairscape_interpret.pipeline.graph_utils - # (used as a module-level function, not a method). - - def _build_synthesis_prompt( - self, - root_node: dict, - step_annotations: List[AnnotatedComputation], - graph_dict: Optional[dict] = None, - ) -> str: - """Build the shared synthesis prompt from step annotations.""" - parts = [] - parts.append("## RO-Crate Overview") - parts.append(f"**Name:** {root_node.get('name', 'Unknown')}") - parts.append(f"**Description:** {root_node.get('description', 'No description')}") - parts.append(f"**Author:** {root_node.get('author', 'Unknown')}") - parts.append(f"**Keywords:** {root_node.get('keywords', '')}") - parts.append("") - - # Compute DAG order if graph_dict is available - if graph_dict: - ordered = _compute_dag_order(step_annotations, graph_dict) - else: - ordered = [(str(i), ann) for i, ann in enumerate(step_annotations, 1)] - - # DAG structure section for the LLM - parts.append("## Pipeline DAG Structure") - for label, ann in ordered: - annotates = ann.annotates - if hasattr(annotates, 'guid'): - comp_id = annotates.guid - elif isinstance(annotates, dict): - comp_id = annotates.get("@id", "") - else: - comp_id = str(annotates) - comp_node = graph_dict.get(comp_id, {}) if graph_dict else {} - comp_name = comp_node.get("name", comp_id) - inputs = _resolve_refs(comp_node.get("usedDataset")) - outputs = _resolve_refs(comp_node.get("hasOutputs")) or _resolve_refs(comp_node.get("generated")) - input_names = ", ".join( - graph_dict.get(i, {}).get("name", i) if graph_dict else i - for i in inputs - ) or "raw inputs" - output_names = ", ".join( - graph_dict.get(o, {}).get("name", o) if graph_dict else o - for o in outputs - ) or "outputs" - parts.append(f"Step {label}: {comp_name} (inputs: {input_names}; outputs: {output_names})") - parts.append("") - - parts.append("## Step Annotations") - for label, ann in ordered: - parts.append(f"### Step {label}: {ann.annotates}") - parts.append(f"**Status:** {getattr(ann, 'computationStatus', 'clear')}") - parts.append(f"**Summary:** {ann.stepSummary}") - if ann.errors: - parts.append(f"**Errors:** {'; '.join(f'[{e.severity}] {e.description}' for e in ann.errors)}") - if ann.assumptions: - parts.append(f"**Assumptions:** {'; '.join(f'[{a.impact.value}] {a.description}' for a in ann.assumptions)}") - if ann.codeAnalysis: - for ca in ann.codeAnalysis: - parts.append(f"**Code ({ca.name or ca.software}):** {ca.summary}") - parts.append("") - - return "\n".join(parts) - - def synthesize_graph( - self, - task_guid: str, - root_node: dict, - step_annotations: List[AnnotatedComputation], - llm_model: str, - temperature: float, - rate_limiter: Optional["AsyncRateLimiter"] = None, - index: Optional[dict] = None, - ) -> Tuple[GraphSynthesisResult, List[dict]]: - """Synthesize graph-level summary from all step annotations. - - Returns (datasci_synthesis, audience_perspectives) where - audience_perspectives is a list of AudiencePerspective dicts. - """ - self._update_task(task_guid, { - "current_step": "SYNTHESIZING", - "status": "SYNTHESIZING", - }) - - prompt = self._build_synthesis_prompt(root_node, step_annotations, graph_dict=index) - - async def _run_all_syntheses(): - # Run data scientist + audience syntheses in parallel - async def _run_one(system_prompt: str, label: str) -> GraphSynthesisResult: - if rate_limiter: - await rate_limiter.acquire() - agent = Agent( - llm_model, - output_type=GraphSynthesisResult, - system_prompt=system_prompt, - retries=2, - ) - result = await _run_agent_with_retry(agent, prompt) - self._save_llm_result(task_guid, label, result.output.model_dump(mode="json")) - return result.output - - tasks = [_run_one(DATASCI_SYNTHESIS_PROMPT, "synthesis:datasci")] - # for aud in AUDIENCE_CONFIGS: - # tasks.append(_run_one(aud["prompt"], f"synthesis:{aud['key']}")) - - return await asyncio.gather(*tasks) - - results = run_async(_run_all_syntheses()) - - datasci_synthesis = results[0] - audience_perspectives = [] - for i, aud in enumerate(AUDIENCE_CONFIGS): - if i + 1 >= len(results): - break - aud_result = results[i + 1] - audience_perspectives.append({ - "targetAudience": aud["key"], - "audienceLabel": aud["label"], - "executiveSummary": aud_result.executiveSummary, - "narrativeSummary": aud_result.narrativeSummary, - "keyFindings": aud_result.keyFindings, - "assumptions_raw": aud_result.assumptions, # LLMAssumption list, normalized later - }) - - return datasci_synthesis, audience_perspectives - - # ------------------------------------------------------------------ - # Step 6: Build and store AnnotatedEvidenceGraph - # ------------------------------------------------------------------ - - def build_and_store( - self, - task_guid: str, - rocrate_id: str, - condensed_id: str, - graph: list, - step_annotations: List[AnnotatedComputation], - synthesis: GraphSynthesisResult, - audience_perspectives: List[dict], - llm_model: str, - temperature: float, - ) -> str: - """Assemble, validate, and store the AnnotatedEvidenceGraph.""" - self._update_task(task_guid, { - "current_step": "STORING", - "status": "STORING", - }) - - # Build the @graph dict: original entities + annotated computations - graph_dict = {} - for node in graph: - node_id = node.get("@id") - if node_id: - graph_dict[node_id] = node - - # Add annotated computations to graph and back-link computations - for ann in step_annotations: - ann_dict = ann.model_dump(by_alias=True, exclude_none=True, mode="json") - graph_dict[ann.guid] = ann_dict - - # Extract comp_id robustly: handle IdentifierValue, dict, or string - annotates = ann.annotates - if hasattr(annotates, 'guid'): - comp_id = annotates.guid - elif isinstance(annotates, dict): - comp_id = annotates.get("@id", "") - else: - comp_id = str(annotates) - - # Add evi:annotatedBy reverse link on the computation node in the AEG graph - if comp_id and comp_id in graph_dict: - existing = graph_dict[comp_id].get("evi:annotatedBy", []) - if isinstance(existing, dict): - existing = [existing] - elif not isinstance(existing, list): - existing = [] - existing.append({"@id": ann.guid}) - graph_dict[comp_id]["evi:annotatedBy"] = existing - else: - logger.warning( - "Could not back-link annotation %s -> computation %s " - "(not found in graph_dict, type=%s)", - ann.guid, comp_id, type(annotates).__name__, - ) - - # Build step annotation refs - step_ann_refs = [{"@id": ann.guid} for ann in step_annotations] - - # Compile graph-level assumptions from step annotations with source links - compiled_assumptions = [] - for ann in step_annotations: - for assumption in (ann.assumptions or []): - compiled_assumptions.append(GraphAssumption( - impact=assumption.impact, - name=assumption.name, - description=assumption.description, - downstreamImpacts=assumption.downstreamImpacts, - evidence=assumption.evidence, - reviewRecommended=getattr(assumption, "reviewRecommended", False), - recommendedValidation=getattr(assumption, "recommendedValidation", None), - sourceAnnotation={"@id": ann.guid}, - )) - # Add synthesis-level assumptions (not tied to a single step) - for llm_assumption in (synthesis.assumptions or []): - normalized = normalize_assumption(llm_assumption) - compiled_assumptions.append(GraphAssumption( - impact=normalized.impact, - name=normalized.name, - description=normalized.description, - downstreamImpacts=normalized.downstreamImpacts, - evidence=normalized.evidence, - reviewRecommended=getattr(normalized, "reviewRecommended", False), - recommendedValidation=getattr(normalized, "recommendedValidation", None), - sourceAnnotation={"@id": rocrate_id}, - )) - - # Build audience perspectives with normalized assumptions - audiences = [] - for aud_data in audience_perspectives: - aud_assumptions = [] - for llm_a in (aud_data.get("assumptions_raw") or []): - normalized = normalize_assumption(llm_a) - aud_assumptions.append(GraphAssumption( - impact=normalized.impact, - name=normalized.name, - description=normalized.description, - downstreamImpacts=normalized.downstreamImpacts, - evidence=normalized.evidence, - reviewRecommended=getattr(normalized, "reviewRecommended", False), - recommendedValidation=getattr(normalized, "recommendedValidation", None), - sourceAnnotation={"@id": rocrate_id}, - )) - audiences.append(AudiencePerspective( - targetAudience=aud_data["targetAudience"], - audienceLabel=aud_data["audienceLabel"], - executiveSummary=aud_data["executiveSummary"], - narrativeSummary=aud_data["narrativeSummary"], - keyFindings=aud_data.get("keyFindings", []), - assumptions=aud_assumptions, - )) - - # Extract NAAN from rocrate_id for the new identifier - ark_match = re.match(r"ark:(\d+)/(.*)", rocrate_id) - if ark_match: - naan = ark_match.group(1) - postfix = ark_match.group(2) - aeg_id = f"ark:{naan}/annotated-eg-{postfix}" - else: - aeg_id = f"ark:59853/annotated-eg-{uuid.uuid4()}" - - now = datetime.datetime.utcnow().isoformat() - - # Build the overview from RO-Crate metadata - root_entity = graph_dict.get(rocrate_id, {}) - root_name = root_entity.get("name", "") - root_desc = root_entity.get("description", "") - if root_name and root_desc: - overview_description = f"{root_name}. {root_desc}" - else: - overview_description = root_name or root_desc or "No description available" - - # Collect unique data formats from all entities with a format field - data_formats = sorted({ - entity.get("format") - for entity in graph_dict.values() - if isinstance(entity, dict) and entity.get("format") - }) - - # Collect keywords from the root entity - root_keywords = root_entity.get("keywords", []) - if isinstance(root_keywords, str): - root_keywords = [root_keywords] - - # Pick top 1-2 assumptions (CRITICAL first, then MAJOR) - top_assumptions = [a for a in compiled_assumptions if a.impact == "CRITICAL"][:2] - if len(top_assumptions) < 2: - major = [a for a in compiled_assumptions if a.impact == "MAJOR"] - top_assumptions.extend(major[:2 - len(top_assumptions)]) - - overview = DataOverview( - dataDescription=overview_description, - dataFormats=data_formats, - keywords=root_keywords, - license=root_entity.get("license"), - conditionsOfAccess=root_entity.get("conditionsOfAccess"), - topAssumptions=top_assumptions, - pipelineDescription=synthesis.pipelineDescription, - pipelineSteps=synthesis.pipelineSteps or None, - ) - - # Build the AnnotatedEvidenceGraph - aeg_data = { - "@id": aeg_id, - "name": f"Annotated Evidence Graph: {graph_dict.get(rocrate_id, {}).get('name', rocrate_id)}", - "description": f"AI-mediated interpretation of RO-Crate {rocrate_id}", - "author": llm_model, - "evi:annotates": {"@id": rocrate_id}, - "@graph": graph_dict, - "evi:executiveSummary": synthesis.executiveSummary, - "evi:narrativeSummary": synthesis.narrativeSummary, - "evi:keyFindings": synthesis.keyFindings, - "evi:assumptions": [a.model_dump(by_alias=True, mode="json") for a in compiled_assumptions], - "evi:overview": overview.model_dump(by_alias=True, mode="json"), - "evi:audiences": [a.model_dump(by_alias=True, mode="json") for a in audiences], - "evi:stepAnnotations": step_ann_refs, - "evi:llmModel": llm_model, - "evi:llmTemperature": temperature, - "dateCreated": now, - } - - aeg = AnnotatedEvidenceGraph.model_validate(aeg_data) - - # Store as StoredIdentifier - permissions = Permissions(owner="system@fairscape.org", group="", acl=[]) - stored = StoredIdentifier.model_validate({ - "@id": aeg_id, - "@type": ["prov:Entity", "https://w3id.org/EVI#EvidenceGraph", "https://w3id.org/EVI#AnnotatedEvidenceGraph"], - "metadata": aeg.model_dump(by_alias=True, mode="json"), - "permissions": permissions.model_dump(), - "publicationStatus": PublicationStatusEnum.DRAFT, - "dateCreated": datetime.datetime.utcnow(), - "dateModified": datetime.datetime.utcnow(), - "distribution": None, - }) - - self.config.identifierCollection.insert_one( - stored.model_dump(by_alias=True, mode="json") - ) - - # Update original RO-Crate with pointer - self.config.identifierCollection.update_one( - {"@id": rocrate_id}, - {"$set": {"metadata.hasAnnotatedEvidenceGraph": {"@id": aeg_id}}} - ) - - # Update each computation's MongoDB document with evi:annotatedBy - for ann in step_annotations: - annotates = ann.annotates - if hasattr(annotates, 'guid'): - comp_id = annotates.guid - elif isinstance(annotates, dict): - comp_id = annotates.get("@id", "") - else: - comp_id = str(annotates) - - if comp_id: - self.config.identifierCollection.update_one( - {"@id": comp_id}, - {"$addToSet": {"metadata.evi:annotatedBy": {"@id": ann.guid}}} - ) - - self._update_task(task_guid, {"annotated_evidence_graph_id": aeg_id}) - logger.info(f"Stored AnnotatedEvidenceGraph {aeg_id}") - return aeg_id - - # ------------------------------------------------------------------ - # Main orchestrator - # ------------------------------------------------------------------ + """Thin wrapper: assemble Mongo-backed adapters and dispatch to the + shared `fairscape_interpret.Interpreter`.""" def interpret_rocrate(self, task_guid: str, user_token: str = "") -> str: - """Main pipeline orchestrator. Returns the AnnotatedEvidenceGraph @id.""" - import os - # Load task config + """Run the interpretation pipeline for the RO-Crate recorded in + the async task document. Returns the persisted + AnnotatedEvidenceGraph `@id`. + + Signature is load-bearing: `worker.py`'s Celery task calls this + positionally with a bearer token for the `/software/download/` + endpoint.""" task_doc = self.config.asyncCollection.find_one({"guid": task_guid}) if not task_doc: raise ValueError(f"Task {task_guid} not found") @@ -995,72 +69,20 @@ def interpret_rocrate(self, task_guid: str, user_token: str = "") -> str: rocrate_id = task_doc["rocrate_id"] llm_model = task_doc.get("llm_model", "google-gla:gemini-2.5-flash-lite") temperature = task_doc.get("llm_temperature", 0.2) - rate_limiter = AsyncRateLimiter(max_requests=2, window_seconds=10.0) - - # # Diagnostic: confirm which API key and model are in use - # raw_key = os.environ.get("ANTHROPIC_API_KEY", "") - # masked_key = f"{raw_key[:8]}...{raw_key[-4:]}" if len(raw_key) > 12 else ("(not set)" if not raw_key else "(too short)") - # logger.info(f"[interpret_rocrate] task={task_guid} model={llm_model!r} ANTHROPIC_API_KEY={masked_key}") - - self._update_task(task_guid, { - "status": "PROCESSING", - "time_started": datetime.datetime.utcnow(), - }) - - try: - # Step 1: Ensure condensed - graph, condensed_id, root_node = self.ensure_condensed(task_guid, rocrate_id) - - # Step 2: Find computations - computations, index = self.find_computations(task_guid, graph) - - if not computations: - raise ValueError(f"No Computation nodes found in RO-Crate {rocrate_id}") - - # Step 3: Pre-fetch software - # Use token from argument, or fall back to task doc - effective_token = user_token or task_doc.get("user_token", "") - software_cache = self.prefetch_all_software(task_guid, computations, index, user_token=effective_token) - - # Step 3b: Pre-fetch dataset statistics - stats_cache = self.prefetch_dataset_statistics(task_guid, computations, index) - - # Step 4: Annotate computations (paced by rate limiter) - step_annotations = self.annotate_computations_parallel( - task_guid, computations, software_cache, index, llm_model, temperature, - stats_cache=stats_cache, rate_limiter=rate_limiter, - ) - - # Step 5: Graph-level synthesis (paced by rate limiter) - synthesis, audience_perspectives = self.synthesize_graph( - task_guid, root_node, step_annotations, llm_model, temperature, - rate_limiter=rate_limiter, - index=index, - ) - - # Step 6: Build and store - aeg_id = self.build_and_store( - task_guid, rocrate_id, condensed_id, graph, - step_annotations, synthesis, audience_perspectives, - llm_model, temperature, - ) - - # Success - self._update_task(task_guid, { - "status": "SUCCESS", - "current_step": "COMPLETE", - "time_finished": datetime.datetime.utcnow(), - }) - - return aeg_id - - except Exception as e: - import traceback - logger.error(f"Interpretation failed for task {task_guid}: {e}") - traceback.print_exc() - self._update_task(task_guid, { - "status": "FAILURE", - "error": {"message": str(e), "error_type": type(e).__name__}, - "time_finished": datetime.datetime.utcnow(), - }) - raise + effective_token = user_token or task_doc.get("user_token", "") + + source = MongoGraphSource(self.config) + sink = MongoResultSink(self.config) + tracker = MongoTaskTracker(self.config, task_guid) + software = ServerSoftwareFetcher(self.config, user_token=effective_token) + condenser = Condenser(source, sink) + + interpreter = Interpreter( + graph=source, + sink=sink, + tracker=tracker, + software=software, + condenser=condenser, + config=InterpretConfig(llm_model=llm_model, temperature=temperature), + ) + return interpreter.run_sync(rocrate_id) From 664222753f4c8c814aeb95256fdd833c506d293c Mon Sep 17 00:00:00 2001 From: jniestroy Date: Tue, 21 Apr 2026 12:40:52 -0400 Subject: [PATCH 19/27] Phase 0: switch dependency fairscape_interpret -> fairscape_graph_tools. pyproject dep + uv.sources path and all import sites under crud/, models/, tests/crud/ updated to the renamed package. Behavior unchanged. See fairscape_graph_tools/EVIDENCE_GRAPH_MIGRATION.md for the coordinated rename across mds_python, fairscape-cli, and the shared package. --- mds/src/fairscape_mds/crud/condensation.py | 10 +- .../fairscape_mds/crud/fairscape_request.py | 2 +- .../fairscape_mds/crud/interpret_adapters.py | 12 +- mds/src/fairscape_mds/crud/interpretation.py | 16 +- .../models/annotated_computation.py | 8 +- .../models/annotated_evidence_graph.py | 8 +- .../tests/crud/test_interpretation.py | 512 ++++++++++++------ pyproject.toml | 4 +- 8 files changed, 391 insertions(+), 181 deletions(-) diff --git a/mds/src/fairscape_mds/crud/condensation.py b/mds/src/fairscape_mds/crud/condensation.py index bc1a195..ce254e0 100644 --- a/mds/src/fairscape_mds/crud/condensation.py +++ b/mds/src/fairscape_mds/crud/condensation.py @@ -7,7 +7,7 @@ All graph-condensation helpers (signature computation, traversal, DatasetGroup construction, `condense_graph`, etc.) now live in -`fairscape_interpret.pipeline.condense` so the CLI can reuse them. This file +`fairscape_graph_tools.pipeline.condense` so the CLI can reuse them. This file keeps only the MongoDB glue: StoredIdentifier-flattening, BFS across identifierCollection, and the FairscapeCondensationRequest class. """ @@ -26,7 +26,7 @@ from fairscape_mds.models.user import Permissions # Pure condensation helpers live in the shared package. -from fairscape_interpret.pipeline.condense import ( +from fairscape_graph_tools.pipeline.condense import ( ARK_REF_FIELDS, compute_provenance_signature, condense_evidence_graph_cache, @@ -34,7 +34,7 @@ create_dataset_group_node, traverse_and_condense, ) -from fairscape_interpret.pipeline.graph_utils import ( +from fairscape_graph_tools.pipeline.graph_utils import ( EVI_TYPES, get_evi_type, get_generatedby_ids, @@ -165,7 +165,7 @@ def condense_rocrate( ) -> FairscapeResponse: """Build the full graph, condense it, store the result. - Thin wrapper over `fairscape_interpret.Condenser.condense`. The + Thin wrapper over `fairscape_graph_tools.Condenser.condense`. The Mongo adapters and the shared orchestrator do all the real work; this method only maps their exceptions onto the HTTP-shaped `FairscapeResponse` the router and Celery worker expect. @@ -176,7 +176,7 @@ def condense_rocrate( MongoGraphSource, MongoResultSink, ) - from fairscape_interpret.condenser import Condenser + from fairscape_graph_tools.condenser import Condenser condensed_id = f"{rocrate_id}-condensed" diff --git a/mds/src/fairscape_mds/crud/fairscape_request.py b/mds/src/fairscape_mds/crud/fairscape_request.py index abebc4f..a415767 100644 --- a/mds/src/fairscape_mds/crud/fairscape_request.py +++ b/mds/src/fairscape_mds/crud/fairscape_request.py @@ -1,5 +1,5 @@ from fairscape_mds.core.config import FairscapeConfig -from fairscape_interpret.pipeline.graph_utils import flexible_ark_query +from fairscape_graph_tools.pipeline.graph_utils import flexible_ark_query __all__ = ["FairscapeRequest", "flexible_ark_query"] diff --git a/mds/src/fairscape_mds/crud/interpret_adapters.py b/mds/src/fairscape_mds/crud/interpret_adapters.py index 33b69ea..391473c 100644 --- a/mds/src/fairscape_mds/crud/interpret_adapters.py +++ b/mds/src/fairscape_mds/crud/interpret_adapters.py @@ -1,6 +1,6 @@ -"""MongoDB-backed adapters for the fairscape_interpret ports. +"""MongoDB-backed adapters for the fairscape_graph_tools ports. -Four adapters bridge `fairscape_interpret`'s port protocols to the +Four adapters bridge `fairscape_graph_tools`'s port protocols to the Mongo-and-FastAPI world of mds_python: - `MongoGraphSource` -> `GraphSource` @@ -10,7 +10,7 @@ The shared package has no knowledge of MongoDB, StoredIdentifier, Permissions, Celery, or the `/software/download/` endpoint -- all of -that lives here. See `fairscape_interpret/ports.py` for the port +that lives here. See `fairscape_graph_tools/ports.py` for the port contracts this file satisfies. """ @@ -32,9 +32,9 @@ ) from fairscape_mds.models.user import Permissions -from fairscape_interpret.models.annotated_computation import AnnotatedComputation -from fairscape_interpret.models.annotated_evidence_graph import AnnotatedEvidenceGraph -from fairscape_interpret.pipeline.github import ( +from fairscape_graph_tools.models.annotated_computation import AnnotatedComputation +from fairscape_graph_tools.models.annotated_evidence_graph import AnnotatedEvidenceGraph +from fairscape_graph_tools.pipeline.github import ( MAX_SOFTWARE_BYTES, prefetch_software_code, ) diff --git a/mds/src/fairscape_mds/crud/interpretation.py b/mds/src/fairscape_mds/crud/interpretation.py index 298e342..1314579 100644 --- a/mds/src/fairscape_mds/crud/interpretation.py +++ b/mds/src/fairscape_mds/crud/interpretation.py @@ -1,7 +1,7 @@ """interpretation.py -- Server-side thin wrapper for the AI-mediated interpretation pipeline. -The pipeline itself lives in `fairscape_interpret`. This module only +The pipeline itself lives in `fairscape_graph_tools`. This module only loads the per-task configuration from `asyncCollection`, assembles the four Mongo-backed adapters from `crud/interpret_adapters.py`, and hands them to the shared `Interpreter`. Keeping @@ -11,23 +11,23 @@ Module-level re-exports below preserve the import surface that other callers (notably the existing test suite under `tests/crud/test_interpretation.py`) expect. The helpers themselves now -live in `fairscape_interpret`. +live in `fairscape_graph_tools`. """ import logging import httpx # noqa: F401 -- kept so `patch("...interpretation.httpx.get")` still resolves -from fairscape_interpret.condenser import Condenser -from fairscape_interpret.interpreter import Interpreter, InterpretConfig -from fairscape_interpret.pipeline.github import prefetch_software_code -from fairscape_interpret.pipeline.graph_utils import ( +from fairscape_graph_tools.condenser import Condenser +from fairscape_graph_tools.interpreter import Interpreter, InterpretConfig +from fairscape_graph_tools.pipeline.github import prefetch_software_code +from fairscape_graph_tools.pipeline.graph_utils import ( _build_index, _is_computation, _is_rocrate_root, _resolve_refs, ) -from fairscape_interpret.pipeline.synthesize import GraphSynthesisResult +from fairscape_graph_tools.pipeline.synthesize import GraphSynthesisResult from fairscape_mds.crud.fairscape_request import FairscapeRequest from fairscape_mds.crud.interpret_adapters import ( @@ -52,7 +52,7 @@ class FairscapeInterpretationRequest(FairscapeRequest): """Thin wrapper: assemble Mongo-backed adapters and dispatch to the - shared `fairscape_interpret.Interpreter`.""" + shared `fairscape_graph_tools.Interpreter`.""" def interpret_rocrate(self, task_guid: str, user_token: str = "") -> str: """Run the interpretation pipeline for the RO-Crate recorded in diff --git a/mds/src/fairscape_mds/models/annotated_computation.py b/mds/src/fairscape_mds/models/annotated_computation.py index f4f49e2..4029e33 100644 --- a/mds/src/fairscape_mds/models/annotated_computation.py +++ b/mds/src/fairscape_mds/models/annotated_computation.py @@ -1,11 +1,11 @@ -"""Compatibility shim -- re-exports from the shared fairscape_interpret package. +"""Compatibility shim -- re-exports from the shared fairscape_graph_tools package. -The model definitions now live in fairscape_interpret.models.annotated_computation. +The model definitions now live in fairscape_graph_tools.models.annotated_computation. This module is kept so existing `from fairscape_mds.models.annotated_computation -import X` paths keep working; new code should import from fairscape_interpret directly. +import X` paths keep working; new code should import from fairscape_graph_tools directly. """ -from fairscape_interpret.models.annotated_computation import ( +from fairscape_graph_tools.models.annotated_computation import ( ANNOTATED_COMPUTATION_TYPE, AnnotatedComputation, Assumption, diff --git a/mds/src/fairscape_mds/models/annotated_evidence_graph.py b/mds/src/fairscape_mds/models/annotated_evidence_graph.py index 94827bc..226d53a 100644 --- a/mds/src/fairscape_mds/models/annotated_evidence_graph.py +++ b/mds/src/fairscape_mds/models/annotated_evidence_graph.py @@ -1,11 +1,11 @@ -"""Compatibility shim -- re-exports from the shared fairscape_interpret package. +"""Compatibility shim -- re-exports from the shared fairscape_graph_tools package. -The model definitions now live in fairscape_interpret.models.annotated_evidence_graph. +The model definitions now live in fairscape_graph_tools.models.annotated_evidence_graph. This module is kept so existing `from fairscape_mds.models.annotated_evidence_graph -import X` paths keep working; new code should import from fairscape_interpret directly. +import X` paths keep working; new code should import from fairscape_graph_tools directly. """ -from fairscape_interpret.models.annotated_evidence_graph import ( +from fairscape_graph_tools.models.annotated_evidence_graph import ( ANNOTATED_EVIDENCE_GRAPH_TYPE, AnnotatedEvidenceGraph, AudiencePerspective, diff --git a/mds/src/fairscape_mds/tests/crud/test_interpretation.py b/mds/src/fairscape_mds/tests/crud/test_interpretation.py index a926080..6af87da 100644 --- a/mds/src/fairscape_mds/tests/crud/test_interpretation.py +++ b/mds/src/fairscape_mds/tests/crud/test_interpretation.py @@ -1,25 +1,49 @@ -"""Tests for the AI-mediated interpretation pipeline. - -Uses a minimal condensed RO-Crate fixture, mongomock for DB isolation, -and PydanticAI TestModel to avoid real LLM calls. +"""Tests for the server-side AI-mediated interpretation wiring. + +After the Phase 1 #11 refactor, the pipeline proper lives in +``fairscape_graph_tools`` and is exercised by that package's own tests. +This file covers the mds_python glue: + +- pure helper re-exports (still importable from + ``fairscape_mds.crud.interpretation``), +- the Mongo adapters in ``crud/interpret_adapters.py``, +- the Condenser orchestrator against a port-shaped fake source, and +- an end-to-end run of the thin ``condense_rocrate`` wrapper against + a mongomock-backed config. + +LLM-driven ``interpret_rocrate`` is not covered here yet — that needs +a ``pydantic_ai.TestModel`` scaffold; noted as followup in +MIGRATION.md. """ import datetime + +import mongomock import pytest -from unittest.mock import patch, MagicMock +from unittest.mock import MagicMock from fairscape_mds.crud.interpretation import ( FairscapeInterpretationRequest, + GraphSynthesisResult, _build_index, _is_computation, _is_rocrate_root, _resolve_refs, prefetch_software_code, - GraphSynthesisResult, +) +from fairscape_mds.crud.condensation import FairscapeCondensationRequest +from fairscape_mds.crud.interpret_adapters import ( + MongoGraphSource, + MongoResultSink, + MongoTaskTracker, ) from fairscape_mds.models.annotated_computation import AnnotatedComputation from fairscape_mds.models.annotated_evidence_graph import AnnotatedEvidenceGraph +from fairscape_graph_tools.condenser import Condenser +from fairscape_graph_tools.interpreter import Interpreter, InterpretConfig +from fairscape_graph_tools.pipeline.annotate import build_computation_prompt + # --------------------------------------------------------------------------- # Minimal condensed RO-Crate fixture @@ -121,8 +145,47 @@ ] +def _mongomock_config() -> MagicMock: + """Build a minimal FairscapeConfig stand-in backed by mongomock. + + Only the attributes the adapters actually touch are provided: + `identifierCollection`, `asyncCollection`, and the + baseUrl/internalUrl rewrite pair used by `ServerSoftwareFetcher`. + """ + cfg = MagicMock() + client = mongomock.MongoClient() + db = client["fairscape_test"] + cfg.identifierCollection = db["identifier"] + cfg.asyncCollection = db["async"] + cfg.baseUrl = None + cfg.internalUrl = None + return cfg + + +def _insert_as_stored_identifiers(collection, graph: list[dict]) -> None: + """Wrap each RO-Crate node as a StoredIdentifier doc and insert. + + Mirrors how the real server stores crates; the adapters flatten + `metadata` back to the bare node shape on read. + """ + now = datetime.datetime.utcnow() + for node in graph: + if not node.get("@id"): + continue + doc = { + "@id": node["@id"], + "@type": node.get("@type", ""), + "metadata": node, + "permissions": {"owner": "test@fairscape.org", "group": None}, + "distribution": None, + "dateCreated": now, + "dateModified": now, + } + collection.insert_one(doc) + + # --------------------------------------------------------------------------- -# Helper tests +# Helper tests (re-exports from fairscape_graph_tools via interpretation.py) # --------------------------------------------------------------------------- @@ -164,7 +227,6 @@ class TestFindComputations: """Test computation finding without DB.""" def test_finds_all_computations(self): - index = _build_index(MINIMAL_CONDENSED_GRAPH) computations = [node for node in MINIMAL_CONDENSED_GRAPH if _is_computation(node)] assert len(computations) == 2 comp_ids = {c["@id"] for c in computations} @@ -172,22 +234,20 @@ def test_finds_all_computations(self): assert "ark:59853/computation-step2" in comp_ids -class TestPromptBuilding: - """Test prompt construction for computation annotation.""" +# --------------------------------------------------------------------------- +# Prompt construction — now a free function in fairscape_graph_tools +# --------------------------------------------------------------------------- - def setup_method(self): - """Create a mock config for FairscapeInterpretationRequest.""" - self.mock_config = MagicMock() - self.mock_config.identifierCollection = MagicMock() - self.mock_config.asyncCollection = MagicMock() - self.request = FairscapeInterpretationRequest(self.mock_config) +class TestPromptBuilding: def test_build_computation_prompt(self): index = _build_index(MINIMAL_CONDENSED_GRAPH) computation = index["ark:59853/computation-step1"] - software_cache = {"ark:59853/software-preprocess": "import pandas as pd\ndf = pd.read_csv('input.csv')"} + software_cache = { + "ark:59853/software-preprocess": "import pandas as pd\ndf = pd.read_csv('input.csv')" + } - prompt = self.request._build_computation_prompt(computation, software_cache, index) + prompt = build_computation_prompt(computation, software_cache, index) assert "Preprocessing Step" in prompt assert "python preprocess.py" in prompt @@ -198,85 +258,95 @@ def test_build_computation_prompt(self): def test_build_prompt_missing_software(self): index = _build_index(MINIMAL_CONDENSED_GRAPH) computation = index["ark:59853/computation-step1"] - software_cache = {} # empty + prompt = build_computation_prompt(computation, {}, index) + assert "Preprocessing Step" in prompt - prompt = self.request._build_computation_prompt(computation, software_cache, index) - # Should still work, just no source code section - assert "Preprocessing Step" in prompt +# --------------------------------------------------------------------------- +# Condenser orchestrator (replaces the old FairscapeInterpretationRequest +# .ensure_condensed Mongo-coupled tests) +# --------------------------------------------------------------------------- + + +class _FakeSource: + """Port-shaped stand-in for `GraphSource`.""" + + def __init__(self, entities: dict[str, dict]): + self._entities = entities + + def find_entity(self, ark_id): + return self._entities.get(ark_id) + def find_dataset_stats(self, ark_ids): + return {} -class TestEnsureCondensed: - """Test the ensure_condensed logic.""" + def build_full_graph(self, rocrate_id): + return list(self._entities.values()) - def setup_method(self): - self.mock_config = MagicMock() - self.mock_config.identifierCollection = MagicMock() - self.mock_config.asyncCollection = MagicMock() - self.request = FairscapeInterpretationRequest(self.mock_config) +class _NoopSink: + """Port-shaped stand-in for `ResultSink`; returns the id it was handed.""" + + def persist_condensed(self, condensed_id, condensed_metadata, source_rocrate_id, stats): + return condensed_id + + def persist_aeg(self, aeg, rocrate_id, step_annotations): + return getattr(aeg, "guid", "ark:test/aeg") + + +class TestCondenserEnsureCondensed: def test_already_condensed_crate(self): - """When the crate itself has evi:condensed: true.""" - self.mock_config.identifierCollection.find_one.return_value = { + """Root has `evi:condensed: true` → returned in place.""" + source_rocrate = { "@id": "ark:59853/rocrate-test", - "@type": ["Dataset", "https://w3id.org/EVI#ROCrate"], - "metadata": { - "@graph": MINIMAL_CONDENSED_GRAPH, - } + "@graph": MINIMAL_CONDENSED_GRAPH, } + source = _FakeSource({"ark:59853/rocrate-test": source_rocrate}) + condenser = Condenser(source, _NoopSink()) - graph, condensed_id, root = self.request.ensure_condensed("task-123", "ark:59853/rocrate-test") + graph, condensed_id, root = condenser.ensure_condensed("ark:59853/rocrate-test") assert len(graph) == len(MINIMAL_CONDENSED_GRAPH) assert condensed_id == "ark:59853/rocrate-test" assert root.get("evi:condensed") is True def test_has_condensed_pointer(self): - """When the crate has hasCondensedROCrate pointer.""" - # First call: get original crate - # Second call: get condensed crate - self.mock_config.identifierCollection.find_one.side_effect = [ - { - "@id": "ark:59853/rocrate-test", - "@type": ["Dataset", "https://w3id.org/EVI#ROCrate"], - "metadata": { - "hasCondensedROCrate": {"@id": "ark:59853/rocrate-test-condensed"}, - "@graph": [], - } - }, + """RO-Crate has `hasCondensedROCrate` → Condenser fetches that crate.""" + source_rocrate = { + "@id": "ark:59853/rocrate-test", + "hasCondensedROCrate": {"@id": "ark:59853/rocrate-test-condensed"}, + "@graph": [], + } + condensed = { + "@id": "ark:59853/rocrate-test-condensed", + "@graph": MINIMAL_CONDENSED_GRAPH, + } + source = _FakeSource( { - "@id": "ark:59853/rocrate-test-condensed", - "metadata": { - "@graph": MINIMAL_CONDENSED_GRAPH, - } - }, - ] + "ark:59853/rocrate-test": source_rocrate, + "ark:59853/rocrate-test-condensed": condensed, + } + ) + condenser = Condenser(source, _NoopSink()) - graph, condensed_id, root = self.request.ensure_condensed("task-123", "ark:59853/rocrate-test") + graph, condensed_id, _ = condenser.ensure_condensed("ark:59853/rocrate-test") assert len(graph) == len(MINIMAL_CONDENSED_GRAPH) + assert condensed_id == "ark:59853/rocrate-test-condensed" def test_not_found_raises(self): - """When the RO-Crate doesn't exist.""" - self.mock_config.identifierCollection.find_one.return_value = None - + condenser = Condenser(_FakeSource({}), _NoopSink()) with pytest.raises(ValueError, match="not found"): - self.request.ensure_condensed("task-123", "ark:59853/nonexistent") + condenser.ensure_condensed("ark:59853/nonexistent") -class TestPrefetchSoftware: - """Test software pre-fetching with mocked HTTP.""" +# --------------------------------------------------------------------------- +# Software prefetching (now in fairscape_graph_tools.pipeline.github) +# --------------------------------------------------------------------------- - @patch("fairscape_mds.crud.interpretation.httpx.get") - def test_github_file_url(self, mock_get): - mock_response = MagicMock() - mock_response.status_code = 200 - mock_response.text = "import pandas as pd\nprint('hello')" - mock_response.raise_for_status = MagicMock() - mock_get.return_value = mock_response - code = prefetch_software_code("https://github.com/owner/repo/blob/main/script.py") - assert "import pandas" in code +class TestPrefetchSoftware: + """Test software pre-fetching with mocked HTTP.""" def test_no_content_url(self): code = prefetch_software_code("") @@ -291,46 +361,63 @@ def test_local_path(self): assert "Local/relative" in code -class TestAnnotatedComputationModel: - """Test that AnnotatedComputation can be constructed correctly.""" +# --------------------------------------------------------------------------- +# Pydantic model shims (re-exports) +# --------------------------------------------------------------------------- + +class TestAnnotatedComputationModel: def test_create_annotated_computation(self): - ac = AnnotatedComputation.model_validate({ - "@id": "ark:59853/annotated-computation-test", - "name": "Test Annotation", - "description": "Test annotation description", - "author": "gemini-2.5-flash", - "evi:annotates": {"@id": "ark:59853/computation-step1"}, - "evi:stepSummary": "This step preprocesses raw data by cleaning and normalizing it.", - "evi:codeAnalysis": [ - { - "software": {"@id": "ark:59853/software-preprocess"}, - "name": "preprocess.py", - "summary": "Reads CSV, drops NaN, normalizes columns", - "keyFunctions": ["clean_data", "normalize"], - "assumptions": ["No logging of dropped rows"], - } - ], - "evi:inputSummaries": [ - { - "dataset": {"@id": "ark:59853/dataset-input-raw"}, - "name": "Raw Input Data", - "role": "Primary input", - "description": "Raw experimental measurements", - } - ], - "evi:outputSummaries": [ - { - "dataset": {"@id": "ark:59853/dataset-intermediate"}, - "name": "Preprocessed Data", - "role": "Cleaned output", - } - ], - "evi:assumptions": ["No random seed set"], - "evi:llmModel": "gemini-2.5-flash", - "evi:llmTemperature": 0.2, - "dateCreated": "2025-01-15T12:00:00", - }) + ac = AnnotatedComputation.model_validate( + { + "@id": "ark:59853/annotated-computation-test", + "name": "Test Annotation", + "description": "Test annotation description", + "author": "gemini-2.5-flash", + "evi:annotates": {"@id": "ark:59853/computation-step1"}, + "evi:stepSummary": "This step preprocesses raw data by cleaning and normalizing it.", + "evi:codeAnalysis": [ + { + "software": {"@id": "ark:59853/software-preprocess"}, + "name": "preprocess.py", + "summary": "Reads CSV, drops NaN, normalizes columns", + "keyFunctions": ["clean_data", "normalize"], + "assumptions": [ + { + "impact": "MINOR", + "name": "Silent drops", + "description": "No logging of dropped rows", + } + ], + } + ], + "evi:inputSummaries": [ + { + "dataset": {"@id": "ark:59853/dataset-input-raw"}, + "name": "Raw Input Data", + "role": "Primary input", + "description": "Raw experimental measurements", + } + ], + "evi:outputSummaries": [ + { + "dataset": {"@id": "ark:59853/dataset-intermediate"}, + "name": "Preprocessed Data", + "role": "Cleaned output", + } + ], + "evi:assumptions": [ + { + "impact": "MINOR", + "name": "No random seed", + "description": "No random seed was set for the normalization step", + } + ], + "evi:llmModel": "gemini-2.5-flash", + "evi:llmTemperature": 0.2, + "dateCreated": "2025-01-15T12:00:00", + } + ) assert ac.stepSummary == "This step preprocesses raw data by cleaning and normalizing it." assert len(ac.codeAnalysis) == 1 @@ -339,30 +426,36 @@ def test_create_annotated_computation(self): class TestAnnotatedEvidenceGraphModel: - """Test that AnnotatedEvidenceGraph can be constructed correctly.""" - def test_create_annotated_evidence_graph(self): graph_dict = {node["@id"]: node for node in MINIMAL_CONDENSED_GRAPH if "@id" in node} - aeg = AnnotatedEvidenceGraph.model_validate({ - "@id": "ark:59853/annotated-eg-test", - "name": "Test Annotated Evidence Graph", - "description": "Test AEG", - "author": "gemini-2.5-flash", - "evi:annotates": {"@id": "ark:59853/rocrate-test-pipeline"}, - "@graph": graph_dict, - "evi:executiveSummary": "This pipeline preprocesses and analyzes experimental data.", - "evi:narrativeSummary": "The pipeline starts with raw data and produces analysis results.", - "evi:keyFindings": ["Data is properly normalized", "No random seeds used"], - "evi:assumptions": ["Reproducibility could be improved"], - "evi:stepAnnotations": [ - {"@id": "ark:59853/annotated-computation-1"}, - {"@id": "ark:59853/annotated-computation-2"}, - ], - "evi:llmModel": "gemini-2.5-flash", - "evi:llmTemperature": 0.2, - "dateCreated": "2025-01-15T12:00:00", - }) + aeg = AnnotatedEvidenceGraph.model_validate( + { + "@id": "ark:59853/annotated-eg-test", + "name": "Test Annotated Evidence Graph", + "description": "Test AEG fixture", + "author": "gemini-2.5-flash", + "evi:annotates": {"@id": "ark:59853/rocrate-test-pipeline"}, + "@graph": graph_dict, + "evi:executiveSummary": "This pipeline preprocesses and analyzes experimental data.", + "evi:narrativeSummary": "The pipeline starts with raw data and produces analysis results.", + "evi:keyFindings": ["Data is properly normalized", "No random seeds used"], + "evi:assumptions": [ + { + "impact": "MINOR", + "name": "Reproducibility", + "description": "Reproducibility could be improved", + } + ], + "evi:stepAnnotations": [ + {"@id": "ark:59853/annotated-computation-1"}, + {"@id": "ark:59853/annotated-computation-2"}, + ], + "evi:llmModel": "gemini-2.5-flash", + "evi:llmTemperature": 0.2, + "dateCreated": "2025-01-15T12:00:00", + } + ) assert aeg.executiveSummary.startswith("This pipeline") assert len(aeg.keyFindings) == 2 @@ -371,42 +464,159 @@ def test_create_annotated_evidence_graph(self): class TestGraphSynthesisResult: - """Test the synthesis result model.""" - def test_create_synthesis_result(self): result = GraphSynthesisResult( executiveSummary="The pipeline processes data.", narrativeSummary="Starting from raw data, the pipeline...", keyFindings=["Finding 1", "Finding 2"], - assumptions=["Assumption 1"], + assumptions=[], ) assert result.executiveSummary == "The pipeline processes data." assert len(result.keyFindings) == 2 -class TestStatusTracking: - """Test that status updates are called correctly.""" +# --------------------------------------------------------------------------- +# Mongo adapter tests (replace the old FairscapeInterpretationRequest- +# scoped status-tracking tests) +# --------------------------------------------------------------------------- - def setup_method(self): - self.mock_config = MagicMock() - self.mock_config.identifierCollection = MagicMock() - self.mock_config.asyncCollection = MagicMock() - self.request = FairscapeInterpretationRequest(self.mock_config) - def test_update_task(self): - self.request._update_task("task-123", {"status": "PROCESSING", "current_step": "CONDENSING"}) - self.mock_config.asyncCollection.update_one.assert_called_once_with( +class TestMongoTaskTracker: + def test_update_sets_status_fields(self): + cfg = MagicMock() + tracker = MongoTaskTracker(cfg, "task-123") + tracker.update({"status": "PROCESSING", "current_step": "CONDENSING"}) + cfg.asyncCollection.update_one.assert_called_once_with( {"guid": "task-123"}, - {"$set": {"status": "PROCESSING", "current_step": "CONDENSING"}} + {"$set": {"status": "PROCESSING", "current_step": "CONDENSING"}}, ) - def test_find_computations_updates_status(self): - self.request.find_computations("task-123", MINIMAL_CONDENSED_GRAPH) + def test_update_computation_status_uses_positional_filter(self): + cfg = MagicMock() + tracker = MongoTaskTracker(cfg, "task-123") + tracker.update_computation_status("ark:59853/comp-1", {"status": "done"}) + cfg.asyncCollection.update_one.assert_called_once_with( + {"guid": "task-123", "computation_details.computation_id": "ark:59853/comp-1"}, + {"$set": {"computation_details.$.status": "done"}}, + ) + + def test_increment_completed(self): + cfg = MagicMock() + tracker = MongoTaskTracker(cfg, "task-123") + tracker.increment_completed() + cfg.asyncCollection.update_one.assert_called_once_with( + {"guid": "task-123"}, + {"$inc": {"completed_computations": 1}}, + ) + + +class TestInterpreterFindComputations: + """`Interpreter._find_computations` drives both the traversal and + the per-computation tracker state the old + `FairscapeInterpretationRequest.find_computations` was asserted + against.""" + + def test_finds_computations_and_pushes_to_tracker(self): + tracker = MagicMock() + interpreter = Interpreter( + graph=MagicMock(), + sink=MagicMock(), + tracker=tracker, + software=MagicMock(), + condenser=MagicMock(), + config=InterpretConfig(), + ) + + computations, index = interpreter._find_computations(MINIMAL_CONDENSED_GRAPH) + + assert len(computations) == 2 + assert "ark:59853/computation-step1" in index + + calls = tracker.update.call_args_list + assert len(calls) >= 2 # TRAVERSING status + total_computations payload + last_payload = calls[-1][0][0] + assert last_payload["total_computations"] == 2 + assert len(last_payload["computation_details"]) == 2 - # Should have been called to update TRAVERSING status and computation details - calls = self.mock_config.asyncCollection.update_one.call_args_list - assert len(calls) >= 2 # at least status update + computation details update - # Check that total_computations was set - last_call_args = calls[-1][0][1]["$set"] - assert last_call_args["total_computations"] == 2 +# --------------------------------------------------------------------------- +# End-to-end integration: FairscapeCondensationRequest.condense_rocrate +# via mongomock (Phase 1 structural acceptance for the condense path). +# --------------------------------------------------------------------------- + + +class TestCondenseRocrateIntegration: + """Exercises the full post-refactor condense path: + `FairscapeCondensationRequest.condense_rocrate` -> `MongoGraphSource` -> + `Condenser` -> `MongoResultSink` -> insert into `identifierCollection`. + """ + + def test_condense_creates_condensed_doc_and_pointer(self): + cfg = _mongomock_config() + _insert_as_stored_identifiers(cfg.identifierCollection, MINIMAL_CONDENSED_GRAPH) + + rocrate_id = "ark:59853/rocrate-test-pipeline" + response = FairscapeCondensationRequest(cfg).condense_rocrate( + rocrate_id=rocrate_id, + threshold=5, + max_member_ids=0, + owner_email="test@fairscape.org", + ) + + assert response.success, response.error + assert response.statusCode == 201 + assert response.model["condensed_id"] == f"{rocrate_id}-condensed" + assert "stats" in response.model + + # Condensed StoredIdentifier doc was written + stored = cfg.identifierCollection.find_one({"@id": f"{rocrate_id}-condensed"}) + assert stored is not None + metadata = stored["metadata"] + assert "@graph" in metadata + # Back-pointer on the source crate was set + source_doc = cfg.identifierCollection.find_one({"@id": rocrate_id}) + assert source_doc["metadata"]["hasCondensedROCrate"] == { + "@id": f"{rocrate_id}-condensed" + } + + def test_condense_409_when_already_exists(self): + cfg = _mongomock_config() + rocrate_id = "ark:59853/rocrate-test-pipeline" + condensed_id = f"{rocrate_id}-condensed" + cfg.identifierCollection.insert_one( + {"@id": condensed_id, "@type": "Dataset", "metadata": {}} + ) + + response = FairscapeCondensationRequest(cfg).condense_rocrate( + rocrate_id=rocrate_id, + ) + assert response.success is False + assert response.statusCode == 409 + assert "already exists" in response.error["message"] + + def test_condense_404_when_source_missing(self): + cfg = _mongomock_config() + response = FairscapeCondensationRequest(cfg).condense_rocrate( + rocrate_id="ark:59853/missing", + ) + assert response.success is False + assert response.statusCode == 404 + assert "No metadata found" in response.error["message"] + + +# --------------------------------------------------------------------------- +# Interpreter (LLM-driven) end-to-end coverage is not in scope for Phase 1; +# a pydantic_ai.TestModel scaffold is tracked in MIGRATION.md as followup. +# --------------------------------------------------------------------------- + + +class TestInterpretRocrateWrapperSmoke: + """The thin wrapper's control flow around the Interpreter construction + is exercised without running the LLM pipeline — just verify the + task-not-found failure path.""" + + def test_missing_task_raises(self): + cfg = _mongomock_config() + request = FairscapeInterpretationRequest(cfg) + with pytest.raises(ValueError, match="not found"): + request.interpret_rocrate("nonexistent-task") diff --git a/pyproject.toml b/pyproject.toml index 165b656..9976178 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,11 +45,11 @@ dependencies = [ "PyYaml>=6.0.2", "PyGithub", "pydantic-ai[google]>=0.1.0", - "fairscape_interpret", + "fairscape_graph_tools", ] [tool.uv.sources] -fairscape_interpret = { path = "../fairscape_interpret", editable = true } +fairscape_graph_tools = { path = "../fairscape_graph_tools", editable = true } requires-python = ">=3.9" [project.urls] From 6e0c5d8b8c5a9620e4a70cd242215e4bdb11c827 Mon Sep 17 00:00:00 2001 From: jniestroy Date: Tue, 21 Apr 2026 13:05:02 -0400 Subject: [PATCH 20/27] Phase 1: shrink models/evidence_graph.py to shim over shared pkg. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit EvidenceGraph core + pure projection helpers now live in fairscape_graph_tools. This module re-exports EvidenceGraphCreate / EvidenceNode straight and subclasses the shared EvidenceGraph to re-attach the legacy Mongo-aware build_graph method. crud/evidence_graph keeps working unchanged; the subclass goes away in Phase 3 when that CRUD starts calling EvidenceGraphBuilder. EvidenceGraphBuildRequest and list_evidence_graphs_from_db stay here — both are Celery/Mongo-bound and explicitly server-only per the Phase 0 decision log in fairscape_graph_tools/EVIDENCE_GRAPH_MIGRATION.md. Control flow in the subclass's build_graph is identical to the original (output_nodes + start_rocrate_outputs captured pre-condense, same BFS, same projection order) to keep the server byte-identical ahead of the Phase 3 regression test. --- .../fairscape_mds/models/evidence_graph.py | 428 ++++-------------- 1 file changed, 89 insertions(+), 339 deletions(-) diff --git a/mds/src/fairscape_mds/models/evidence_graph.py b/mds/src/fairscape_mds/models/evidence_graph.py index 1da0aaa..925a6e8 100644 --- a/mds/src/fairscape_mds/models/evidence_graph.py +++ b/mds/src/fairscape_mds/models/evidence_graph.py @@ -1,334 +1,77 @@ -from pydantic import BaseModel, Field, ValidationError -from typing import Dict, List, Optional, Any, Union, Set -import pymongo -import datetime -from fairscape_mds.crud.fairscape_response import FairscapeResponse - - -class EvidenceNode: - def __init__(self, id: str, type: str): - self.id = id - self.type = type - self.usedSoftware: Optional[List[str]] = None - self.usedDataset: Optional[List[str]] = None - self.usedSample: Optional[List[str]] = None - self.usedInstrument: Optional[List[str]] = None - self.usedMLModel: Optional[List[str]] = None - self.generatedBy: Optional[str] = None - -class EvidenceGraph(BaseModel): - metadataType: str = Field(default="evi:EvidenceGraph", alias="@type") - guid: str = Field(alias="@id") - owner: str - description: str - name: str = Field(default="Evidence Graph") - outputs: Optional[List[Dict[str, str]]] = Field(default=None) - graph: Optional[Dict[str, Dict[str, Any]]] = Field(default=None, alias="@graph") - condensation_stats: Optional[Dict[str, Any]] = Field(default=None) - - class Config: - extra = 'allow' - populate_by_name = True +"""Server-side shim for evidence-graph models. + +The core Pydantic types (`EvidenceGraph`, `EvidenceNode`, `EvidenceGraphCreate`) +and the pure projection helpers now live in `fairscape_graph_tools`. This +module re-exports them and keeps the Mongo/Celery-bound artifacts that +only matter server-side: + +- `EvidenceGraphBuildRequest` — Celery task envelope (stays here per the + Phase 0 decision log in `EVIDENCE_GRAPH_MIGRATION.md`). +- `list_evidence_graphs_from_db` — Mongo-backed listing helper. +- `EvidenceGraph.build_graph` — a temporary Mongo-aware method kept on a + server-side subclass of the shared `EvidenceGraph` so + `crud/evidence_graph.py` keeps working between Phase 1 and Phase 3. + Phase 3 will rewire that CRUD to call `EvidenceGraphBuilder` and drop + this subclass. +""" - def _flatten_metadata(self, node: Dict) -> Dict: - if "metadata" not in node: - return node - - flattened = {k: v for k, v in node.items() if k != "metadata"} - metadata_content = node["metadata"] - if isinstance(metadata_content, dict): - for key, value in metadata_content.items(): - if key not in flattened: - flattened[key] = value - return flattened - - def _is_rocrate(self, node_type_field: Any) -> bool: - if isinstance(node_type_field, list): - return any("ROCrate" in str(t) for t in node_type_field) - elif isinstance(node_type_field, str): - return "ROCrate" in node_type_field - return False - - def _get_rocrate_outputs(self, node: Dict) -> List[Dict]: - output_fields = ["https://w3id.org/EVI#outputs", "EVI:outputs", "outputs"] +import datetime +from typing import Any, Dict, List, Optional - for field in output_fields: - if field in node: - outputs = node[field] - if isinstance(outputs, list): - return outputs - elif isinstance(outputs, dict): - return [outputs] - return [] +import pymongo +from pydantic import BaseModel, Field + +from fairscape_graph_tools.models.evidence_graph import ( + EvidenceGraph as _SharedEvidenceGraph, + EvidenceGraphCreate, + EvidenceNode, +) +from fairscape_graph_tools.pipeline.condense import condense_evidence_graph_cache +from fairscape_graph_tools.pipeline.evidence_graph import ( + _build_node_from_cache, + _extract_referenced_ids, + _flatten_metadata, + _get_rocrate_outputs, + _is_rocrate, +) - def _extract_referenced_ids(self, node: Dict) -> Set[str]: - referenced_ids = set() +from fairscape_mds.crud.fairscape_response import FairscapeResponse - node_type_field = node.get("@type", "") - current_node_type_str = "" - if isinstance(node_type_field, list): - if "Dataset" in node_type_field: current_node_type_str = "Dataset" - elif "Computation" in node_type_field: current_node_type_str = "Computation" - elif "Sample" in node_type_field: current_node_type_str = "Sample" - elif "Software" in node_type_field: current_node_type_str = "Software" - elif "MLModel" in node_type_field: current_node_type_str = "MLModel" - elif "Experiment" in node_type_field: current_node_type_str = "Experiment" - elif "Activity" in node_type_field: current_node_type_str = "Activity" - elif node_type_field: current_node_type_str = node_type_field[-1] - elif isinstance(node_type_field, str): - current_node_type_str = node_type_field - if "Dataset" in current_node_type_str or \ - "Sample" in current_node_type_str or \ - "Instrument" in current_node_type_str or \ - "Software" in current_node_type_str or \ - "MLModel" in current_node_type_str: - generated_by_info = node.get("generatedBy") - if generated_by_info: - if isinstance(generated_by_info, list) and generated_by_info: - comp_id = generated_by_info[0].get("@id") - if comp_id: - referenced_ids.add(comp_id) - elif isinstance(generated_by_info, dict): - comp_id = generated_by_info.get("@id") - if comp_id: - referenced_ids.add(comp_id) +class EvidenceGraph(_SharedEvidenceGraph): + """Server-side subclass keeping the legacy Mongo-aware `build_graph`. - elif "Computation" in current_node_type_str or \ - "Experiment" in current_node_type_str or \ - "Annotation" in current_node_type_str or \ - "Activity" in current_node_type_str: - for field_name in ["usedDataset", "usedSoftware", "usedSample", "usedInstrument", "usedMLModel"]: - field_info = node.get(field_name) - if field_info: - items = field_info if isinstance(field_info, list) else [field_info] - for item in items: - if isinstance(item, dict) and item.get("@id"): - referenced_ids.add(item.get("@id")) - - return referenced_ids + Phase 3 removes this subclass once `crud/evidence_graph.py` calls + `EvidenceGraphBuilder` instead. + """ - def _process_used_dataset(self, dataset_info: Any, node_cache: Dict[str, Dict]) -> List[Dict[str, str]]: - datasets_to_process = [] - - if isinstance(dataset_info, list): - datasets_to_process = dataset_info - elif isinstance(dataset_info, dict): - datasets_to_process = [dataset_info] - else: - return [] - - result_refs = [] - - for dataset_ref in datasets_to_process: - if not dataset_ref.get("@id"): - continue - - dataset_id = dataset_ref.get("@id") - dataset_node = node_cache.get(dataset_id) - - if dataset_node and "error" not in dataset_node: - node_type = dataset_node.get("@type", "") - - if self._is_rocrate(node_type): - outputs = self._get_rocrate_outputs(dataset_node) - if outputs: - for output_ref in outputs: - if output_ref.get("@id"): - result_refs.append({"@id": output_ref.get("@id")}) - else: - result_refs.append({"@id": dataset_id}) - else: - result_refs.append({"@id": dataset_id}) - else: - result_refs.append({"@id": dataset_id}) - - return result_refs - - def _build_node_from_cache( + def build_graph( self, - node_id: str, - node_cache: Dict[str, Dict], - graph_dict: Dict[str, Dict], - start_rocrate_id: Optional[str] = None, - rocrate_outputs: Optional[List[Dict]] = None - ) -> None: - if node_id in graph_dict: - return - - node = node_cache.get(node_id) - if not node: - graph_dict[node_id] = {"@id": node_id, "error": "not found"} - return - - if "error" in node: - graph_dict[node_id] = node - return - - result_node = { - "@id": node.get("@id"), - "@type": node.get("@type"), - "name": node.get("name"), - "description": node.get("description") - } - - created_by = node.get("createdBy") - if created_by: - result_node["createdBy"] = created_by - - node_type_field = node.get("@type", "") - - if start_rocrate_id and node_id == start_rocrate_id and self._is_rocrate(node_type_field): - if rocrate_outputs: - result_node["hasOutputs"] = rocrate_outputs - - current_node_type_str = "" - if isinstance(node_type_field, list): - if "Dataset" in node_type_field: current_node_type_str = "Dataset" - elif "Computation" in node_type_field: current_node_type_str = "Computation" - elif "Sample" in node_type_field: current_node_type_str = "Sample" - elif "Software" in node_type_field: current_node_type_str = "Software" - elif "MLModel" in node_type_field: current_node_type_str = "MLModel" - elif "Experiment" in node_type_field: current_node_type_str = "Experiment" - elif "Activity" in node_type_field: current_node_type_str = "Activity" - elif node_type_field: current_node_type_str = node_type_field[-1] - elif isinstance(node_type_field, str): - current_node_type_str = node_type_field - - if "Dataset" in current_node_type_str or \ - "Sample" in current_node_type_str or \ - "Instrument" in current_node_type_str or \ - "Software" in current_node_type_str or \ - "MLModel" in current_node_type_str: - generated_by_info = node.get("generatedBy") - if generated_by_info: - if isinstance(generated_by_info, list) and generated_by_info: - comp_id = generated_by_info[0].get("@id") - elif isinstance(generated_by_info, dict): - comp_id = generated_by_info.get("@id") - else: - comp_id = None - - if comp_id: - self._build_node_from_cache(comp_id, node_cache, graph_dict, start_rocrate_id, rocrate_outputs) - result_node["generatedBy"] = {"@id": comp_id} - elif generated_by_info: - result_node["generatedBy"] = generated_by_info - - elif "Computation" in current_node_type_str or \ - "Experiment" in current_node_type_str or \ - "Annotation" in current_node_type_str or \ - "Activity" in current_node_type_str: - used_dataset_info = node.get("usedDataset") - if used_dataset_info: - dataset_refs = self._process_used_dataset(used_dataset_info, node_cache) - if dataset_refs: - result_node["usedDataset"] = dataset_refs - for ref in dataset_refs: - if ref.get("@id"): - self._build_node_from_cache(ref.get("@id"), node_cache, graph_dict, start_rocrate_id, rocrate_outputs) + start_node_id: str, + mongo_collection: pymongo.collection.Collection, + condense: bool = True, + condense_threshold: int = 5, + ): + graph_dict: Dict[str, Dict] = {} + output_nodes: List[Dict[str, str]] = [] - used_software_info = node.get("usedSoftware") - if used_software_info: - software_refs = [] - if isinstance(used_software_info, list): - for item in used_software_info: - if item.get("@id"): - self._build_node_from_cache(item.get("@id"), node_cache, graph_dict, start_rocrate_id, rocrate_outputs) - software_refs.append({"@id": item.get("@id")}) - elif isinstance(used_software_info, dict) and used_software_info.get("@id"): - self._build_node_from_cache(used_software_info.get("@id"), node_cache, graph_dict, start_rocrate_id, rocrate_outputs) - software_refs.append({"@id": used_software_info.get("@id")}) - if software_refs: - result_node["usedSoftware"] = software_refs - - used_sample_info = node.get("usedSample") - if used_sample_info: - sample_refs = [] - if isinstance(used_sample_info, list): - for item in used_sample_info: - if item.get("@id"): - self._build_node_from_cache(item.get("@id"), node_cache, graph_dict, start_rocrate_id, rocrate_outputs) - sample_refs.append({"@id": item.get("@id")}) - elif isinstance(used_sample_info, dict) and used_sample_info.get("@id"): - self._build_node_from_cache(used_sample_info.get("@id"), node_cache, graph_dict, start_rocrate_id, rocrate_outputs) - sample_refs.append({"@id": used_sample_info.get("@id")}) - if sample_refs: - result_node["usedSample"] = sample_refs - - used_instrument_info = node.get("usedInstrument") - if used_instrument_info: - instrument_refs = [] - if isinstance(used_instrument_info, list): - for item in used_instrument_info: - if item.get("@id"): - self._build_node_from_cache(item.get("@id"), node_cache, graph_dict, start_rocrate_id, rocrate_outputs) - instrument_refs.append({"@id": item.get("@id")}) - elif isinstance(used_instrument_info, dict) and used_instrument_info.get("@id"): - self._build_node_from_cache(used_instrument_info.get("@id"), node_cache, graph_dict, start_rocrate_id, rocrate_outputs) - instrument_refs.append({"@id": used_instrument_info.get("@id")}) - if instrument_refs: - result_node["usedInstrument"] = instrument_refs - - used_mlmodel_info = node.get("usedMLModel") - if used_mlmodel_info: - mlmodel_refs = [] - if isinstance(used_mlmodel_info, list): - for item in used_mlmodel_info: - if item.get("@id"): - self._build_node_from_cache(item.get("@id"), node_cache, graph_dict, start_rocrate_id, rocrate_outputs) - mlmodel_refs.append({"@id": item.get("@id")}) - elif isinstance(used_mlmodel_info, dict) and used_mlmodel_info.get("@id"): - self._build_node_from_cache(used_mlmodel_info.get("@id"), node_cache, graph_dict, start_rocrate_id, rocrate_outputs) - mlmodel_refs.append({"@id": used_mlmodel_info.get("@id")}) - if mlmodel_refs: - result_node["usedMLModel"] = mlmodel_refs - - # Preserve DatasetGroup summary fields and recurse into the - # representative dataset so it actually appears in the graph. - # The other members are dropped by condensation by design. - if isinstance(node_type_field, list) and any("DatasetGroup" in str(t) for t in node_type_field): - for field in ("evi:memberCount", "evi:representativeDataset", - "evi:commonFormat", "evi:commonSoftware", "format", - "evi:memberIds"): - if field in node: - result_node[field] = node[field] - - rep_ref = node.get("evi:representativeDataset") - rep_id = None - if isinstance(rep_ref, dict): - rep_id = rep_ref.get("@id") - elif isinstance(rep_ref, str): - rep_id = rep_ref - if rep_id: - self._build_node_from_cache( - rep_id, node_cache, graph_dict, - start_rocrate_id, rocrate_outputs, - ) - - graph_dict[node_id] = result_node - - def build_graph(self, start_node_id: str, mongo_collection: pymongo.collection.Collection, condense: bool = True, condense_threshold: int = 5): - graph_dict = {} - output_nodes = [] - start_node = mongo_collection.find_one({"@id": start_node_id}, {"_id": 0}) - + if not start_node: output_nodes.append({"@id": start_node_id}) graph_dict[start_node_id] = {"@id": start_node_id, "error": "not found"} self.outputs = output_nodes self.graph = graph_dict return - - start_node = self._flatten_metadata(start_node) + + start_node = _flatten_metadata(start_node) node_cache = {start_node_id: start_node} - + node_type = start_node.get("@type", "") - start_rocrate_id = None + start_rocrate_id: Optional[str] = None start_rocrate_outputs: Optional[List[Dict]] = None - if self._is_rocrate(node_type): - rocrate_outputs = self._get_rocrate_outputs(start_node) + if _is_rocrate(node_type): + rocrate_outputs = _get_rocrate_outputs(start_node) start_rocrate_outputs = list(rocrate_outputs) if rocrate_outputs else [] traversal_outputs = list(start_rocrate_outputs) traversal_outputs.append({"@id": start_node_id}) @@ -336,44 +79,42 @@ def build_graph(self, start_node_id: str, mongo_collection: pymongo.collection.C if traversal_outputs: for output_ref in traversal_outputs: if output_ref.get("@id"): - output_id = output_ref.get("@id") - output_nodes.append({"@id": output_id}) + output_nodes.append({"@id": output_ref.get("@id")}) else: output_nodes.append({"@id": start_node_id}) else: output_nodes.append({"@id": start_node_id}) - + current_level = {node_id["@id"] for node_id in output_nodes} - processed_ids = set() - + processed_ids: set = set() + while current_level: - next_level = set() - + next_level: set = set() + ids_to_fetch = current_level - processed_ids if ids_to_fetch: ids_not_in_cache = [nid for nid in ids_to_fetch if nid not in node_cache] - + if ids_not_in_cache: cursor = mongo_collection.find({"@id": {"$in": ids_not_in_cache}}, {"_id": 0}) - fetched = {node["@id"]: self._flatten_metadata(node) for node in cursor} + fetched = {node["@id"]: _flatten_metadata(node) for node in cursor} node_cache.update(fetched) - + for nid in ids_not_in_cache: if nid not in node_cache: node_cache[nid] = {"@id": nid, "error": "not found"} - + for node_id in ids_to_fetch: if node_id not in processed_ids: processed_ids.add(node_id) node = node_cache.get(node_id) if node and "error" not in node: - referenced_ids = self._extract_referenced_ids(node) + referenced_ids = _extract_referenced_ids(node) next_level.update(referenced_ids) - + current_level = next_level if condense: - from fairscape_mds.crud.condensation import condense_evidence_graph_cache self.condensation_stats = condense_evidence_graph_cache( node_cache, condense_threshold ) @@ -381,22 +122,21 @@ def build_graph(self, start_node_id: str, mongo_collection: pymongo.collection.C for output_node in output_nodes: output_id = output_node.get("@id") if output_id: - self._build_node_from_cache(output_id, node_cache, graph_dict, start_rocrate_id, start_rocrate_outputs) - + _build_node_from_cache( + output_id, + node_cache, + graph_dict, + start_rocrate_id, + start_rocrate_outputs, + ) + self.outputs = output_nodes self.graph = graph_dict -class EvidenceGraphCreate(BaseModel): - guid: str = Field(alias="@id") - description: str - name: str = Field(default="Evidence Graph") - - class Config: - populate_by_name = True - - -def list_evidence_graphs_from_db(mongo_collection: pymongo.collection.Collection) -> FairscapeResponse: +def list_evidence_graphs_from_db( + mongo_collection: pymongo.collection.Collection, +) -> FairscapeResponse: from fairscape_mds.models.identifier import StoredIdentifier try: cursor = mongo_collection.find({"@type": "evi:EvidenceGraph"}, {"_id": 0}) @@ -404,7 +144,8 @@ def list_evidence_graphs_from_db(mongo_collection: pymongo.collection.Collection return FairscapeResponse(success=True, statusCode=200, model=graphs) except Exception as e: return FairscapeResponse(success=False, statusCode=500, error={"message": f"Error listing evidence graphs: {str(e)}"}) - + + class EvidenceGraphBuildRequest(BaseModel): guid: str task_type: str = Field(default="EvidenceGraphBuild") @@ -422,3 +163,12 @@ class EvidenceGraphBuildRequest(BaseModel): class Config: populate_by_name = True + + +__all__ = [ + "EvidenceGraph", + "EvidenceGraphBuildRequest", + "EvidenceGraphCreate", + "EvidenceNode", + "list_evidence_graphs_from_db", +] From 93bb7fa21a1a5a2c8b7eed3c973c478a3e3116b2 Mon Sep 17 00:00:00 2001 From: jniestroy Date: Wed, 22 Apr 2026 16:33:41 -0400 Subject: [PATCH 21/27] move evidence graph into graph tools --- mds/src/fairscape_mds/crud/evidence_graph.py | 107 ++---- .../fairscape_mds/crud/interpret_adapters.py | 57 +++ .../fairscape_mds/models/evidence_graph.py | 129 +------ .../fairscape_mds/routers/evidence_graph.py | 8 +- .../tests/crud/test_evidence_graph.py | 353 ++++++++++++++++++ mds/src/fairscape_mds/worker.py | 4 +- 6 files changed, 453 insertions(+), 205 deletions(-) create mode 100644 mds/src/fairscape_mds/tests/crud/test_evidence_graph.py diff --git a/mds/src/fairscape_mds/crud/evidence_graph.py b/mds/src/fairscape_mds/crud/evidence_graph.py index 9af5500..0ff2737 100644 --- a/mds/src/fairscape_mds/crud/evidence_graph.py +++ b/mds/src/fairscape_mds/crud/evidence_graph.py @@ -3,10 +3,13 @@ import logging from fairscape_mds.crud.fairscape_request import FairscapeRequest from fairscape_mds.crud.fairscape_response import FairscapeResponse +from fairscape_mds.crud.interpret_adapters import MongoGraphSource, MongoResultSink from fairscape_mds.models.user import UserWriteModel, Permissions from fairscape_mds.models.evidence_graph import EvidenceGraph, EvidenceGraphCreate from fairscape_mds.models.identifier import StoredIdentifier, PublicationStatusEnum, MetadataTypeEnum +from fairscape_graph_tools.evidence_graph_builder import EvidenceGraphBuilder + logger = logging.getLogger(__name__) class FairscapeEvidenceGraphRequest(FairscapeRequest): @@ -102,11 +105,8 @@ def build_evidence_graph_for_node( requesting_user: UserWriteModel, naan: str, postfix: str, - condense: bool = True, - condense_threshold: int = 5, ) -> FairscapeResponse: node_id = f"ark:{naan}/{postfix}" - evidence_graph_id = f"ark:{naan}/evidence-graph-{postfix}" source_node_data = self.config.identifierCollection.find_one({"@id": node_id}) if not source_node_data: @@ -122,91 +122,44 @@ def build_evidence_graph_for_node( .get("@id") ) if existing_graph_id: - existing_graph_data = self.config.identifierCollection.find_one( - {"@id": existing_graph_id}, {"_id": 0} - ) - if existing_graph_data: - try: - logger.debug("Returning existing graph for %s", existing_graph_id) - stored_identifier = StoredIdentifier.model_validate(existing_graph_data) - return FairscapeResponse( - success=True, - statusCode=200, - model=stored_identifier - ) - except Exception as e: - return FairscapeResponse( - success=False, - statusCode=500, - error={"message": f"Error validating existing EvidenceGraph {existing_graph_id}: {str(e)}"} - ) - - evidence_graph_data_to_validate = { - "@id": evidence_graph_id, - "name": f"Evidence Graph for {node_id}", - "description": f"Automatically generated Evidence Graph for node {node_id}", - "owner": requesting_user.email, - "@type": "evi:EvidenceGraph", - "graph": None - } - - try: - evidence_graph = EvidenceGraph.model_validate(evidence_graph_data_to_validate) - evidence_graph.build_graph(node_id, self.config.identifierCollection, condense=condense, condense_threshold=condense_threshold) - except Exception as e: - return FairscapeResponse( - success=False, - statusCode=500, - error={"message": f"Error building evidence graph: {str(e)}"} - ) - - default_permissions = Permissions( - owner=requesting_user.email, - group=requesting_user.groups[0] if requesting_user.groups else None - ) - - now = datetime.datetime.utcnow() - stored_identifier = StoredIdentifier( - guid=evidence_graph.guid, - metadataType=MetadataTypeEnum.EVIDENCE_GRAPH, - metadata=evidence_graph, - publicationStatus=PublicationStatusEnum.PUBLISHED, - permissions=default_permissions, - distribution=None, - descriptiveStatistics={}, - dateCreated=now, - dateModified=now + existing_response = self.get_evidence_graph(existing_graph_id) + if existing_response.success: + logger.debug("Returning existing graph for %s", existing_graph_id) + return existing_response + # Stale back-pointer -- fall through and rebuild. The builder + # doesn't short-circuit on `hasEvidenceGraph` (Phase 5 decision), + # so the new derived id will be written and the back-pointer + # will be overwritten as part of the sink's upsert. + + source = MongoGraphSource(self.config) + sink = MongoResultSink( + self.config, + owner_email=requesting_user.email, + owner_groups=requesting_user.groups or [], ) + builder = EvidenceGraphBuilder(source, sink) try: - insert_data = stored_identifier.model_dump(by_alias=True, mode="json") - self.config.identifierCollection.insert_one(insert_data) + evidence_graph_id = builder.build( + node_id, + owner_email=requesting_user.email, + name=f"Evidence Graph for {node_id}", + description=f"Automatically generated Evidence Graph for node {node_id}", + ) except pymongo.errors.DuplicateKeyError: return FairscapeResponse( success=False, statusCode=409, - error={"message": f"EvidenceGraph with @id '{evidence_graph_id}' already exists (race condition)."} - ) - except Exception as e: - return FairscapeResponse( - success=False, - statusCode=500, - error={"message": f"Error storing new evidence graph: {str(e)}"} - ) - - try: - self.config.identifierCollection.update_one( - {"@id": node_id}, - {"$set": {"metadata.hasEvidenceGraph": {"@id": stored_identifier.guid}}} + error={"message": f"EvidenceGraph for '{node_id}' already exists (race condition)."} ) except Exception as e: return FairscapeResponse( success=False, statusCode=500, - model=stored_identifier, - error={ - "message": f"EvidenceGraph created but failed to link to source node {node_id}: {str(e)} (graph @id: {stored_identifier.guid})" - } + error={"message": f"Error building evidence graph: {str(e)}"} ) - return FairscapeResponse(success=True, statusCode=201, model=stored_identifier) + response = self.get_evidence_graph(evidence_graph_id) + if response.success: + return FairscapeResponse(success=True, statusCode=201, model=response.model) + return response diff --git a/mds/src/fairscape_mds/crud/interpret_adapters.py b/mds/src/fairscape_mds/crud/interpret_adapters.py index 391473c..fdf890d 100644 --- a/mds/src/fairscape_mds/crud/interpret_adapters.py +++ b/mds/src/fairscape_mds/crud/interpret_adapters.py @@ -8,6 +8,13 @@ - `MongoTaskTracker` -> `TaskTracker` - `ServerSoftwareFetcher` -> `SoftwareFetcher` +`MongoGraphSource` and `MongoResultSink` serve both the +`Interpreter` (annotated-evidence-graph pipeline) and the +`EvidenceGraphBuilder` (standard evidence-graph pipeline). The two +orchestrators share port contracts, so the same adapter classes cover +both — which is why these types have methods for condensed RO-Crates, +AnnotatedEvidenceGraphs, *and* standard EvidenceGraphs. + The shared package has no knowledge of MongoDB, StoredIdentifier, Permissions, Celery, or the `/software/download/` endpoint -- all of that lives here. See `fairscape_graph_tools/ports.py` for the port @@ -34,6 +41,10 @@ from fairscape_graph_tools.models.annotated_computation import AnnotatedComputation from fairscape_graph_tools.models.annotated_evidence_graph import AnnotatedEvidenceGraph +from fairscape_graph_tools.models.evidence_graph import EvidenceGraph +from fairscape_graph_tools.pipeline.evidence_graph import ( + _flatten_metadata as _flatten_for_evidence_graph, +) from fairscape_graph_tools.pipeline.github import ( MAX_SOFTWARE_BYTES, prefetch_software_code, @@ -79,6 +90,20 @@ def find_entity(self, ark_id: str) -> dict | None: return None return _flatten_metadata(doc) + def find_many(self, ark_ids: Iterable[str]) -> dict[str, dict]: + ids = list(ark_ids) + if not ids: + return {} + cursor = self.config.identifierCollection.find( + {"@id": {"$in": ids}}, {"_id": 0} + ) + # Use the pipeline's flatten (lifts `metadata.*` up while keeping + # sibling storage fields) to stay byte-identical with the + # pre-refactor shim BFS in `models/evidence_graph.py`. The + # module-local `_flatten_metadata` strips those sibling fields, + # which is correct for the interpret pipeline but wrong here. + return {doc["@id"]: _flatten_for_evidence_graph(doc) for doc in cursor} + def find_dataset_stats(self, ark_ids: Iterable[str]) -> dict[str, dict]: ids = list(ark_ids) if not ids: @@ -115,9 +140,11 @@ def __init__( config: FairscapeConfig, *, owner_email: str = "system@fairscape.org", + owner_groups: list[str] | None = None, ): self.config = config self.owner_email = owner_email + self.owner_groups = list(owner_groups) if owner_groups else [] self.last_stats: dict = {} def persist_condensed( @@ -148,6 +175,36 @@ def persist_condensed( ) return condensed_id + def persist_evidence_graph( + self, + evidence_graph: EvidenceGraph, + source_node_id: str, + ) -> str: + group = self.owner_groups[0] if self.owner_groups else None + permissions = Permissions(owner=self.owner_email, group=group) + now = datetime.datetime.utcnow() + + stored = StoredIdentifier( + guid=evidence_graph.guid, + metadataType=MetadataTypeEnum.EVIDENCE_GRAPH, + metadata=evidence_graph, + publicationStatus=PublicationStatusEnum.PUBLISHED, + permissions=permissions, + distribution=None, + descriptiveStatistics={}, + dateCreated=now, + dateModified=now, + ) + self.config.identifierCollection.insert_one( + stored.model_dump(by_alias=True, mode="json") + ) + self.config.identifierCollection.update_one( + {"@id": source_node_id}, + {"$set": {"metadata.hasEvidenceGraph": {"@id": evidence_graph.guid}}}, + ) + logger.info(f"Stored EvidenceGraph {evidence_graph.guid}") + return evidence_graph.guid + def persist_aeg( self, aeg: AnnotatedEvidenceGraph, diff --git a/mds/src/fairscape_mds/models/evidence_graph.py b/mds/src/fairscape_mds/models/evidence_graph.py index 925a6e8..9967947 100644 --- a/mds/src/fairscape_mds/models/evidence_graph.py +++ b/mds/src/fairscape_mds/models/evidence_graph.py @@ -1,139 +1,34 @@ """Server-side shim for evidence-graph models. The core Pydantic types (`EvidenceGraph`, `EvidenceNode`, `EvidenceGraphCreate`) -and the pure projection helpers now live in `fairscape_graph_tools`. This -module re-exports them and keeps the Mongo/Celery-bound artifacts that -only matter server-side: +and the pure projection helpers live in `fairscape_graph_tools`. This +module re-exports them straight and keeps the Mongo/Celery-bound +artifacts that only matter server-side: -- `EvidenceGraphBuildRequest` — Celery task envelope (stays here per the +- `EvidenceGraphBuildRequest` -- Celery task envelope (stays here per the Phase 0 decision log in `EVIDENCE_GRAPH_MIGRATION.md`). -- `list_evidence_graphs_from_db` — Mongo-backed listing helper. -- `EvidenceGraph.build_graph` — a temporary Mongo-aware method kept on a - server-side subclass of the shared `EvidenceGraph` so - `crud/evidence_graph.py` keeps working between Phase 1 and Phase 3. - Phase 3 will rewire that CRUD to call `EvidenceGraphBuilder` and drop - this subclass. +- `list_evidence_graphs_from_db` -- Mongo-backed listing helper. + +The Phase 1 temporary `EvidenceGraph` subclass (which carried a legacy +Mongo-aware `build_graph` method) was removed in Phase 3 once +`crud/evidence_graph.py` started calling `EvidenceGraphBuilder`. """ import datetime -from typing import Any, Dict, List, Optional +from typing import Any, Dict, Optional import pymongo from pydantic import BaseModel, Field from fairscape_graph_tools.models.evidence_graph import ( - EvidenceGraph as _SharedEvidenceGraph, + EvidenceGraph, EvidenceGraphCreate, EvidenceNode, ) -from fairscape_graph_tools.pipeline.condense import condense_evidence_graph_cache -from fairscape_graph_tools.pipeline.evidence_graph import ( - _build_node_from_cache, - _extract_referenced_ids, - _flatten_metadata, - _get_rocrate_outputs, - _is_rocrate, -) from fairscape_mds.crud.fairscape_response import FairscapeResponse -class EvidenceGraph(_SharedEvidenceGraph): - """Server-side subclass keeping the legacy Mongo-aware `build_graph`. - - Phase 3 removes this subclass once `crud/evidence_graph.py` calls - `EvidenceGraphBuilder` instead. - """ - - def build_graph( - self, - start_node_id: str, - mongo_collection: pymongo.collection.Collection, - condense: bool = True, - condense_threshold: int = 5, - ): - graph_dict: Dict[str, Dict] = {} - output_nodes: List[Dict[str, str]] = [] - - start_node = mongo_collection.find_one({"@id": start_node_id}, {"_id": 0}) - - if not start_node: - output_nodes.append({"@id": start_node_id}) - graph_dict[start_node_id] = {"@id": start_node_id, "error": "not found"} - self.outputs = output_nodes - self.graph = graph_dict - return - - start_node = _flatten_metadata(start_node) - node_cache = {start_node_id: start_node} - - node_type = start_node.get("@type", "") - start_rocrate_id: Optional[str] = None - start_rocrate_outputs: Optional[List[Dict]] = None - if _is_rocrate(node_type): - rocrate_outputs = _get_rocrate_outputs(start_node) - start_rocrate_outputs = list(rocrate_outputs) if rocrate_outputs else [] - traversal_outputs = list(start_rocrate_outputs) - traversal_outputs.append({"@id": start_node_id}) - start_rocrate_id = start_node_id - if traversal_outputs: - for output_ref in traversal_outputs: - if output_ref.get("@id"): - output_nodes.append({"@id": output_ref.get("@id")}) - else: - output_nodes.append({"@id": start_node_id}) - else: - output_nodes.append({"@id": start_node_id}) - - current_level = {node_id["@id"] for node_id in output_nodes} - processed_ids: set = set() - - while current_level: - next_level: set = set() - - ids_to_fetch = current_level - processed_ids - if ids_to_fetch: - ids_not_in_cache = [nid for nid in ids_to_fetch if nid not in node_cache] - - if ids_not_in_cache: - cursor = mongo_collection.find({"@id": {"$in": ids_not_in_cache}}, {"_id": 0}) - fetched = {node["@id"]: _flatten_metadata(node) for node in cursor} - node_cache.update(fetched) - - for nid in ids_not_in_cache: - if nid not in node_cache: - node_cache[nid] = {"@id": nid, "error": "not found"} - - for node_id in ids_to_fetch: - if node_id not in processed_ids: - processed_ids.add(node_id) - node = node_cache.get(node_id) - if node and "error" not in node: - referenced_ids = _extract_referenced_ids(node) - next_level.update(referenced_ids) - - current_level = next_level - - if condense: - self.condensation_stats = condense_evidence_graph_cache( - node_cache, condense_threshold - ) - - for output_node in output_nodes: - output_id = output_node.get("@id") - if output_id: - _build_node_from_cache( - output_id, - node_cache, - graph_dict, - start_rocrate_id, - start_rocrate_outputs, - ) - - self.outputs = output_nodes - self.graph = graph_dict - - def list_evidence_graphs_from_db( mongo_collection: pymongo.collection.Collection, ) -> FairscapeResponse: @@ -152,8 +47,6 @@ class EvidenceGraphBuildRequest(BaseModel): owner_email: str naan: str postfix: str - condense: bool = Field(default=True) - condense_threshold: int = Field(default=5) status: str = Field(default="PENDING") time_created: datetime.datetime = Field(default_factory=datetime.datetime.utcnow) time_started: Optional[datetime.datetime] = None diff --git a/mds/src/fairscape_mds/routers/evidence_graph.py b/mds/src/fairscape_mds/routers/evidence_graph.py index 594e799..5f652b6 100644 --- a/mds/src/fairscape_mds/routers/evidence_graph.py +++ b/mds/src/fairscape_mds/routers/evidence_graph.py @@ -1,4 +1,4 @@ -from fastapi import APIRouter, Depends, HTTPException, Path, Query, status +from fastapi import APIRouter, Depends, HTTPException, Path, status from fastapi.responses import JSONResponse from typing import Annotated, List, Dict import logging @@ -106,8 +106,6 @@ def initiate_build_evidence_graph_for_node_route( NAAN: Annotated[str, Path(description="NAAN of the node to build graph for")], postfix: Annotated[str, Path(description="Postfix of the node to build graph for")], current_user: Annotated[UserWriteModel, Depends(getCurrentUser)], - condense: bool = Query(default=True, description="Apply condensation to reduce large graphs"), - condense_threshold: int = Query(default=5, ge=2, description="Min group size to trigger condensation"), ): task_guid = str(uuid.uuid4()) @@ -116,8 +114,6 @@ def initiate_build_evidence_graph_for_node_route( "owner_email": current_user.email, "naan": NAAN, "postfix": postfix, - "condense": condense, - "condense_threshold": condense_threshold, "status": "PENDING", } @@ -136,8 +132,6 @@ def initiate_build_evidence_graph_for_node_route( user_email=current_user.email, naan=NAAN, postfix=postfix, - condense=condense, - condense_threshold=condense_threshold, ) return {"message": "EvidenceGraph build process initiated.", "task_id": task_guid, "status_endpoint": f"/evidencegraph/build/status/{task_guid}"} diff --git a/mds/src/fairscape_mds/tests/crud/test_evidence_graph.py b/mds/src/fairscape_mds/tests/crud/test_evidence_graph.py new file mode 100644 index 0000000..6920fd6 --- /dev/null +++ b/mds/src/fairscape_mds/tests/crud/test_evidence_graph.py @@ -0,0 +1,353 @@ +"""Mongomock-backed tests for ``crud/evidence_graph.py``. + +These cover the thin Phase-3 CRUD wrapper around ``EvidenceGraphBuilder``: + +- ``build_evidence_graph_for_node``: 404 on missing source node, + idempotent 200 on a valid pre-existing back-pointer, fresh 201 on + first build, stale back-pointer falls through to rebuild, 409 on a + duplicate-key race. +- quick smokes for ``create_evidence_graph`` / ``get_evidence_graph`` / + ``delete_evidence_graph`` so the un-refactored methods stay green. + +The byte-identical fixture-crate regression (server-side Phase 3 +acceptance) is *not* done here -- mongomock only validates structural +correctness, not that the @graph shape matches a pre-refactor snapshot. +That regression needs a real Mongo + fixture and is tracked in +``EVIDENCE_GRAPH_MIGRATION.md``. +""" + +from __future__ import annotations + +import datetime + +import mongomock +import pytest +from unittest.mock import MagicMock + +from fairscape_mds.crud.evidence_graph import FairscapeEvidenceGraphRequest +from fairscape_mds.models.evidence_graph import EvidenceGraphCreate +from fairscape_mds.models.user import UserWriteModel + + +# --------------------------------------------------------------------------- +# Fixtures +# --------------------------------------------------------------------------- + + +def _mongomock_config() -> MagicMock: + cfg = MagicMock() + client = mongomock.MongoClient() + db = client["fairscape_test"] + cfg.identifierCollection = db["identifier"] + cfg.asyncCollection = db["async"] + cfg.baseUrl = None + cfg.internalUrl = None + return cfg + + +def _user(email: str = "alice@fairscape.org", groups=("team-alpha",)) -> UserWriteModel: + return UserWriteModel.model_validate({ + "email": email, + "firstName": "Alice", + "lastName": "Anderson", + "password": "pw", + "@type": "Person", + "groups": list(groups), + }) + + +def _insert_source_node(cfg, *, naan: str, postfix: str, + extra_metadata: dict | None = None) -> str: + """Insert a minimal Dataset node as a StoredIdentifier doc. Returns + the node @id. The evidence-graph builder's BFS will start here.""" + node_id = f"ark:{naan}/{postfix}" + metadata = { + "@id": node_id, + "@type": "Dataset", + "name": postfix, + "description": "source node for build_evidence_graph_for_node", + } + if extra_metadata: + metadata.update(extra_metadata) + now = datetime.datetime.utcnow() + cfg.identifierCollection.insert_one({ + "@id": node_id, + "@type": "Dataset", + "metadata": metadata, + "permissions": {"owner": "alice@fairscape.org", "group": None}, + "distribution": None, + "dateCreated": now, + "dateModified": now, + }) + return node_id + + +def _insert_chain(cfg, *, naan: str, postfix: str) -> None: + """Insert a Dataset-with-generatedBy -> Computation -> Dataset chain + rooted at `ark:{naan}/{postfix}` so BFS has something to traverse.""" + now = datetime.datetime.utcnow() + nodes = [ + { + "@id": f"ark:{naan}/{postfix}", + "@type": "Dataset", + "metadata": { + "@id": f"ark:{naan}/{postfix}", + "@type": "Dataset", + "name": "output", + "description": "x", + "generatedBy": {"@id": f"ark:{naan}/comp"}, + }, + }, + { + "@id": f"ark:{naan}/comp", + "@type": "Computation", + "metadata": { + "@id": f"ark:{naan}/comp", + "@type": "Computation", + "name": "c", + "usedDataset": [{"@id": f"ark:{naan}/input"}], + "usedSoftware": [{"@id": f"ark:{naan}/sw"}], + }, + }, + { + "@id": f"ark:{naan}/input", + "@type": "Dataset", + "metadata": { + "@id": f"ark:{naan}/input", + "@type": "Dataset", + "name": "input", + }, + }, + { + "@id": f"ark:{naan}/sw", + "@type": "Software", + "metadata": { + "@id": f"ark:{naan}/sw", + "@type": "Software", + "name": "sw", + }, + }, + ] + for n in nodes: + n["permissions"] = {"owner": "alice@fairscape.org", "group": None} + n["distribution"] = None + n["dateCreated"] = now + n["dateModified"] = now + cfg.identifierCollection.insert_many(nodes) + + +# --------------------------------------------------------------------------- +# build_evidence_graph_for_node +# --------------------------------------------------------------------------- + + +class TestBuildEvidenceGraphForNode: + def test_missing_source_node_returns_404(self): + cfg = _mongomock_config() + req = FairscapeEvidenceGraphRequest(cfg) + resp = req.build_evidence_graph_for_node( + _user(), naan="59999", postfix="ghost", + ) + assert resp.success is False + assert resp.statusCode == 404 + assert "not found" in resp.error["message"].lower() + + def test_fresh_build_returns_201_and_persists_evidence_graph(self): + cfg = _mongomock_config() + _insert_chain(cfg, naan="59852", postfix="ds-out") + + resp = FairscapeEvidenceGraphRequest(cfg).build_evidence_graph_for_node( + _user(), naan="59852", postfix="ds-out", + ) + assert resp.success is True + assert resp.statusCode == 201 + + eg_id = "ark:59852/evidence-graph-ds-out" + # The evidence-graph doc landed in identifierCollection. + stored = cfg.identifierCollection.find_one({"@id": eg_id}) + assert stored is not None + assert stored["@type"] == "evi:EvidenceGraph" + assert stored["permissions"]["owner"] == "alice@fairscape.org" + assert stored["permissions"]["group"] == "team-alpha" + + # The response envelope round-tripped through get_evidence_graph. + assert resp.model.guid == eg_id + + # Source node got the back-pointer written. + src = cfg.identifierCollection.find_one({"@id": "ark:59852/ds-out"}) + assert src["metadata"]["hasEvidenceGraph"] == {"@id": eg_id} + + def test_graph_contains_reachable_nodes(self): + cfg = _mongomock_config() + _insert_chain(cfg, naan="59852", postfix="ds-out") + FairscapeEvidenceGraphRequest(cfg).build_evidence_graph_for_node( + _user(), naan="59852", postfix="ds-out", + ) + stored = cfg.identifierCollection.find_one( + {"@id": "ark:59852/evidence-graph-ds-out"} + ) + graph = stored["metadata"]["@graph"] + assert set(graph) == { + "ark:59852/ds-out", + "ark:59852/comp", + "ark:59852/input", + "ark:59852/sw", + } + + def test_idempotent_200_when_back_pointer_and_graph_exist(self): + cfg = _mongomock_config() + _insert_chain(cfg, naan="59852", postfix="ds-out") + req = FairscapeEvidenceGraphRequest(cfg) + + # First call: fresh 201. + first = req.build_evidence_graph_for_node( + _user(), naan="59852", postfix="ds-out", + ) + assert first.statusCode == 201 + # Second call: the back-pointer now points at the existing graph, + # so the CRUD returns the existing envelope with 200. + second = req.build_evidence_graph_for_node( + _user(), naan="59852", postfix="ds-out", + ) + assert second.success is True + assert second.statusCode == 200 + assert second.model.guid == first.model.guid + # Only one evidence-graph doc exists in the collection. + count = cfg.identifierCollection.count_documents( + {"@id": "ark:59852/evidence-graph-ds-out"} + ) + assert count == 1 + + def test_stale_back_pointer_falls_through_to_rebuild(self): + """Source node carries `hasEvidenceGraph` but the referenced doc + isn't actually in the collection -- CRUD rebuilds and returns 201.""" + cfg = _mongomock_config() + _insert_chain( + cfg, naan="59852", postfix="ds-out", + ) + # Seed a dangling back-pointer on the source node. + cfg.identifierCollection.update_one( + {"@id": "ark:59852/ds-out"}, + {"$set": { + "metadata.hasEvidenceGraph": {"@id": "ark:59852/nonexistent-graph"}, + }}, + ) + + resp = FairscapeEvidenceGraphRequest(cfg).build_evidence_graph_for_node( + _user(), naan="59852", postfix="ds-out", + ) + assert resp.success is True + assert resp.statusCode == 201 + # Back-pointer now points at the real graph, not the stale id. + src = cfg.identifierCollection.find_one({"@id": "ark:59852/ds-out"}) + assert src["metadata"]["hasEvidenceGraph"] == { + "@id": "ark:59852/evidence-graph-ds-out", + } + + def test_owner_group_none_when_user_has_no_groups(self): + cfg = _mongomock_config() + _insert_chain(cfg, naan="59852", postfix="ds-out") + + resp = FairscapeEvidenceGraphRequest(cfg).build_evidence_graph_for_node( + _user(email="bob@x", groups=()), + naan="59852", + postfix="ds-out", + ) + assert resp.statusCode == 201 + stored = cfg.identifierCollection.find_one( + {"@id": "ark:59852/evidence-graph-ds-out"} + ) + assert stored["permissions"]["owner"] == "bob@x" + assert stored["permissions"]["group"] is None + + def test_duplicate_key_race_returns_409(self): + cfg = _mongomock_config() + _insert_chain(cfg, naan="59852", postfix="ds-out") + # Pre-plant the evidence-graph id so the sink's insert trips + # DuplicateKeyError. mongomock needs a unique index on @id for + # that to fire. + cfg.identifierCollection.create_index("@id", unique=True) + cfg.identifierCollection.insert_one({ + "@id": "ark:59852/evidence-graph-ds-out", + "metadata": {"@id": "ark:59852/evidence-graph-ds-out"}, + }) + + resp = FairscapeEvidenceGraphRequest(cfg).build_evidence_graph_for_node( + _user(), naan="59852", postfix="ds-out", + ) + assert resp.success is False + assert resp.statusCode == 409 + + +# --------------------------------------------------------------------------- +# Quick smokes for the un-refactored methods +# --------------------------------------------------------------------------- + + +class TestOtherCrudMethods: + def test_create_then_get(self): + cfg = _mongomock_config() + req = FairscapeEvidenceGraphRequest(cfg) + user = _user() + + created = req.create_evidence_graph( + requesting_user=user, + evi_graph_create_model=EvidenceGraphCreate.model_validate({ + "@id": "ark:59852/manual-eg", + "description": "manual", + "name": "Manual EG", + }), + ) + assert created.success is True + assert created.statusCode == 201 + + fetched = req.get_evidence_graph("ark:59852/manual-eg") + assert fetched.success is True + assert fetched.statusCode == 200 + assert fetched.model.guid == "ark:59852/manual-eg" + + def test_delete_checks_top_level_owner_currently_rejects_legit_owner(self): + """`delete_evidence_graph` checks `graph_data.get("owner")` against + the requesting user's email, but `StoredIdentifier` docs only carry + `owner` under `metadata.owner` / `permissions.owner` -- never at + top level. So the check fails even when the user *is* the owner. + + Preserving this as a known quirk test rather than fixing it: Phase 3 + explicitly left `delete_evidence_graph` untouched, and the fix + (reading `graph_data["metadata"]["owner"]` or + `graph_data["permissions"]["owner"]`) is a follow-up separate from + the evidence-graph migration.""" + cfg = _mongomock_config() + req = FairscapeEvidenceGraphRequest(cfg) + owner = _user(email="owner@x") + req.create_evidence_graph( + owner, + EvidenceGraphCreate.model_validate({ + "@id": "ark:59852/owned", + "description": "d", + }), + ) + resp = req.delete_evidence_graph(owner, "ark:59852/owned") + assert resp.statusCode == 403, ( + "If this starts returning 200 the delete owner-check was fixed " + "separately -- flip this assertion." + ) + + def test_create_conflict_on_duplicate_id(self): + cfg = _mongomock_config() + req = FairscapeEvidenceGraphRequest(cfg) + payload = EvidenceGraphCreate.model_validate({ + "@id": "ark:59852/dup", + "description": "d", + }) + req.create_evidence_graph(requesting_user=_user(), evi_graph_create_model=payload) + again = req.create_evidence_graph(requesting_user=_user(), evi_graph_create_model=payload) + assert again.success is False + assert again.statusCode == 409 + + def test_delete_on_missing_doc_returns_404(self): + cfg = _mongomock_config() + req = FairscapeEvidenceGraphRequest(cfg) + resp = req.delete_evidence_graph(_user(), "ark:59852/ghost") + assert resp.success is False + assert resp.statusCode == 404 diff --git a/mds/src/fairscape_mds/worker.py b/mds/src/fairscape_mds/worker.py index 3a48bbc..f9421ae 100644 --- a/mds/src/fairscape_mds/worker.py +++ b/mds/src/fairscape_mds/worker.py @@ -94,7 +94,7 @@ def processROCrate(transactionGUID: str): #Are the guids supposed to be @id? @celeryApp.task(name='fairscape_mds.worker.build_evidence_graph_task', bind=True) -def build_evidence_graph_task(self, task_guid: str, user_email: str, naan: str, postfix: str, condense: bool = True, condense_threshold: int = 5): +def build_evidence_graph_task(self, task_guid: str, user_email: str, naan: str, postfix: str): print(f"Starting Evidence Graph Build Job: Task GUID {task_guid} for ark:{naan}/{postfix}") try: @@ -123,8 +123,6 @@ def build_evidence_graph_task(self, task_guid: str, user_email: str, naan: str, requesting_user=requesting_user, naan=naan, postfix=postfix, - condense=condense, - condense_threshold=condense_threshold, ) if response.success: From 75e57c9128282ab53e5707e09ce138f42da077a4 Mon Sep 17 00:00:00 2001 From: jniestroy Date: Thu, 23 Apr 2026 08:37:49 -0400 Subject: [PATCH 22/27] build with intepert --- Dockerfile | 7 ++++--- build.sh | 0 compose.yaml | 12 ++++++++---- pyproject.toml | 1 + uv.lock | 22 ++++++++++++++++++++++ 5 files changed, 35 insertions(+), 7 deletions(-) mode change 100644 => 100755 build.sh diff --git a/Dockerfile b/Dockerfile index 438edae..4c33aa5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,11 +13,12 @@ RUN sh /uv-installer.sh && rm /uv-installer.sh ENV PATH="/root/.local/bin/:$PATH" # copy source code -COPY mds/src/ /fairscape/src/ +COPY fairscape_server/mds/src/ /fairscape/src/ +COPY fairscape_graph_tools/ /fairscape/fairscape_graph_tools/ WORKDIR /fairscape/src/ -COPY pyproject.toml . -COPY uv.lock . +COPY fairscape_server/pyproject.toml . +COPY fairscape_server/uv.lock . RUN uv sync --locked diff --git a/build.sh b/build.sh old mode 100644 new mode 100755 diff --git a/compose.yaml b/compose.yaml index a718c94..fad4a98 100644 --- a/compose.yaml +++ b/compose.yaml @@ -9,7 +9,9 @@ services: - fairscape-api - mongo fairscape-worker: - build: "./" + build: + context: "../" + dockerfile: "fairscape_server/Dockerfile" # image: ghcr.io/fairscape/mds_python:RELEASE.2025-05-21.v1 env_file: "./deploy/docker_compose.env" environment: @@ -24,7 +26,9 @@ services: - mongo - minio fairscape-api: - build: "./" + build: + context: "../" + dockerfile: "fairscape_server/Dockerfile" # image: ghcr.io/fairscape/mds_python:RELEASE.2025-05-21.v1 env_file: "./deploy/docker_compose.env" ports: @@ -75,8 +79,8 @@ services: image: minio/minio:latest restart: always ports: - - 9000:9000 - - 9001:9001 + - 9010:9000 + - 9011:9001 environment: MINIO_ROOT_USER: miniotestadmin MINIO_ROOT_PASSWORD: miniotestsecret diff --git a/pyproject.toml b/pyproject.toml index 9976178..a9a2274 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,6 +9,7 @@ authors = [ { name = "Tim Clark", email = "twc8q@virginia.edu"} ] license = {file= "LICENSE"} +requires-python = ">=3.9" classifiers = [ "License :: OSI Approved :: MIT License", "Programming Language :: Python", diff --git a/uv.lock b/uv.lock index 099cd30..3b4a1da 100644 --- a/uv.lock +++ b/uv.lock @@ -1062,6 +1062,26 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317, upload-time = "2025-09-01T09:48:08.5Z" }, ] +[[package]] +name = "fairscape-interpret" +version = "0.1.0" +source = { editable = "../fairscape_graph_tools" } +dependencies = [ + { name = "fairscape-models" }, + { name = "httpx" }, + { name = "pydantic" }, + { name = "pydantic-ai", version = "0.8.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pydantic-ai", version = "1.67.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] + +[package.metadata] +requires-dist = [ + { name = "fairscape-models", specifier = ">=1.1.1" }, + { name = "httpx", specifier = ">=0.28.1" }, + { name = "pydantic", specifier = ">=2.5.1" }, + { name = "pydantic-ai", specifier = ">=0.1.0" }, +] + [[package]] name = "fairscape-mds" version = "0.1.0" @@ -1070,6 +1090,7 @@ dependencies = [ { name = "boto3" }, { name = "celery", extra = ["redis"] }, { name = "docker" }, + { name = "fairscape-interpret" }, { name = "fairscape-models" }, { name = "fastapi" }, { name = "google-genai", version = "1.47.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, @@ -1111,6 +1132,7 @@ requires-dist = [ { name = "boto3", specifier = ">=1.34.26" }, { name = "celery", extras = ["redis"], specifier = ">=5.3.6" }, { name = "docker", specifier = ">=7.0.0" }, + { name = "fairscape-interpret", editable = "../fairscape_graph_tools" }, { name = "fairscape-models", specifier = ">=1.1.1" }, { name = "fastapi", specifier = ">=0.109.0" }, { name = "google-genai", specifier = ">=1.0.0" }, From 4cdd5dac4075c2974d24798e644ef99a4793e661 Mon Sep 17 00:00:00 2001 From: jniestroy Date: Wed, 13 May 2026 07:56:31 -0400 Subject: [PATCH 23/27] my latest code --- mds/src/fairscape_mds/routers/interpretation.py | 2 +- preflight.sh | 11 +++++++++++ pyproject.toml | 2 +- uv.lock | 6 +++--- 4 files changed, 16 insertions(+), 5 deletions(-) create mode 100755 preflight.sh diff --git a/mds/src/fairscape_mds/routers/interpretation.py b/mds/src/fairscape_mds/routers/interpretation.py index 991c102..25bcef3 100644 --- a/mds/src/fairscape_mds/routers/interpretation.py +++ b/mds/src/fairscape_mds/routers/interpretation.py @@ -50,7 +50,7 @@ def trigger_interpretation( token: Annotated[str, Depends(OAuthScheme)], NAAN: str, postfix: str, - llm_model: str = Query(default="anthropic:claude-haiku-4-5-20251001", description="PydanticAI model string"), + llm_model: str = Query(default="anthropic:claude-sonnet-4-6", description="PydanticAI model string"), temperature: float = Query(default=0.2, ge=0.0, le=2.0, description="LLM temperature"), persona: str = Query(default="datasci", description="Interpretation persona"), force: bool = Query(default=True, description="Force re-interpretation if one already exists"), diff --git a/preflight.sh b/preflight.sh new file mode 100755 index 0000000..24283ad --- /dev/null +++ b/preflight.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +set -u + +PORTS=(8080 6379 27017 8081 9010 9011) + +echo "Current holders:" +sudo ss -ltnp | grep -E ":($(IFS=\|; echo "${PORTS[*]}"))\b" || echo " (none)" + +for p in "${PORTS[@]}"; do + sudo fuser -k "${p}/tcp" 2>/dev/null || true +done diff --git a/pyproject.toml b/pyproject.toml index a9a2274..c1f042e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,7 +51,7 @@ dependencies = [ [tool.uv.sources] fairscape_graph_tools = { path = "../fairscape_graph_tools", editable = true } -requires-python = ">=3.9" + [project.urls] Homepage = "https://fairscape.github.io" diff --git a/uv.lock b/uv.lock index 3b4a1da..4df8afc 100644 --- a/uv.lock +++ b/uv.lock @@ -1063,7 +1063,7 @@ wheels = [ ] [[package]] -name = "fairscape-interpret" +name = "fairscape-graph-tools" version = "0.1.0" source = { editable = "../fairscape_graph_tools" } dependencies = [ @@ -1090,7 +1090,7 @@ dependencies = [ { name = "boto3" }, { name = "celery", extra = ["redis"] }, { name = "docker" }, - { name = "fairscape-interpret" }, + { name = "fairscape-graph-tools" }, { name = "fairscape-models" }, { name = "fastapi" }, { name = "google-genai", version = "1.47.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, @@ -1132,7 +1132,7 @@ requires-dist = [ { name = "boto3", specifier = ">=1.34.26" }, { name = "celery", extras = ["redis"], specifier = ">=5.3.6" }, { name = "docker", specifier = ">=7.0.0" }, - { name = "fairscape-interpret", editable = "../fairscape_graph_tools" }, + { name = "fairscape-graph-tools", editable = "../fairscape_graph_tools" }, { name = "fairscape-models", specifier = ">=1.1.1" }, { name = "fastapi", specifier = ">=0.109.0" }, { name = "google-genai", specifier = ">=1.0.0" }, From f6799fe3682cb04c5c44f956e94a78c3631eef7a Mon Sep 17 00:00:00 2001 From: jniestroy Date: Mon, 1 Jun 2026 09:03:33 -0400 Subject: [PATCH 24/27] local creation --- Dockerfile.local | 27 +++++++++++++++++++++++++++ compose.local.yaml | 12 ++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 Dockerfile.local create mode 100644 compose.local.yaml diff --git a/Dockerfile.local b/Dockerfile.local new file mode 100644 index 0000000..dca5e13 --- /dev/null +++ b/Dockerfile.local @@ -0,0 +1,27 @@ +FROM python:3.12-slim-bookworm + +RUN apt-get update && apt-get install -y --no-install-recommends curl ca-certificates + +ADD https://astral.sh/uv/install.sh /uv-installer.sh +RUN sh /uv-installer.sh && rm /uv-installer.sh +ENV PATH="/root/.local/bin/:$PATH" + +COPY fairscape_server/mds/src/ /fairscape/src/ +COPY fairscape_graph_tools/ /fairscape/fairscape_graph_tools/ +COPY fairscape_models/ /fairscape/fairscape_models/ + +WORKDIR /fairscape/src/ + +COPY fairscape_server/pyproject.toml . +COPY fairscape_server/uv.lock . +RUN uv sync --locked +RUN uv pip install --editable /fairscape/fairscape_models/ + +ENV PYTHONPATH="/fairscape/src" +ENV PATH="/fairscape/src/.venv/bin:$PATH" +# Prevent uv run from re-syncing on startup (which would overwrite the editable install above) +ENV UV_NO_SYNC=1 + +WORKDIR /fairscape/src/ + +CMD ["uv", "run", "uvicorn", "fairscape_mds.main:app", "--no-access-log", "--host", "0.0.0.0", "--port", "8080"] diff --git a/compose.local.yaml b/compose.local.yaml new file mode 100644 index 0000000..ea4a404 --- /dev/null +++ b/compose.local.yaml @@ -0,0 +1,12 @@ +# Local development override — uses local fairscape_graph_tools and fairscape_models +# Usage: docker compose -f compose.yaml -f compose.local.yaml up +services: + fairscape-worker: + build: + context: "../" + dockerfile: "fairscape_server/Dockerfile.local" + + fairscape-api: + build: + context: "../" + dockerfile: "fairscape_server/Dockerfile.local" From 450388923aebcad8a4b663c37a77d8af14316b47 Mon Sep 17 00:00:00 2001 From: jniestroy Date: Mon, 1 Jun 2026 09:03:44 -0400 Subject: [PATCH 25/27] person defined term --- mds/src/fairscape_mds/models/identifier.py | 25 ++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/mds/src/fairscape_mds/models/identifier.py b/mds/src/fairscape_mds/models/identifier.py index d01262a..a4dbbab 100644 --- a/mds/src/fairscape_mds/models/identifier.py +++ b/mds/src/fairscape_mds/models/identifier.py @@ -20,6 +20,11 @@ from fairscape_models.medical_condition import MedicalCondition from fairscape_models.annotation import Annotation from fairscape_models.activity import Activity +from fairscape_models.person import Person +from fairscape_models.defined_term import DefinedTerm +from fairscape_models.container import Container +from fairscape_models.claim import Claim +from fairscape_models.article import Article from fairscape_models.conversion.models.AIReady import AIReadyScore from fairscape_models.model_card import ModelCard from fairscape_models.fairscape_base import IdentifierValue @@ -55,6 +60,11 @@ class MetadataTypeEnum(Enum): ML_MODEL = ["prov:Entity","https://w3id.org/EVI#MLModel"] ACTIVITY = ["prov:Activity"] ANNOTATION = "https://w3id.org/EVI#Annotation" + PERSON = "Person" + DEFINED_TERM = "DefinedTerm" + CONTAINER = ["prov:Entity", "https://w3id.org/EVI#Container"] + CLAIM = ["prov:Entity", "https://w3id.org/EVI#Claim"] + ARTICLE = ["prov:Entity", "https://w3id.org/EVI#Article"] EVIDENCE_GRAPH = "evi:EvidenceGraph" ANNOTATED_EVIDENCE_GRAPH = ["prov:Entity", "https://w3id.org/EVI#EvidenceGraph", "https://w3id.org/EVI#AnnotatedEvidenceGraph"] ANNOTATED_COMPUTATION = ["prov:Entity", "https://w3id.org/EVI#Annotation", "https://w3id.org/EVI#AnnotatedComputation"] @@ -79,6 +89,11 @@ class MetadataTypeEnum(Enum): EvidenceGraph, AIReadyScore, ModelCard, + Person, + DefinedTerm, + Container, + Claim, + Article, GenericMetadataElem ] @@ -165,6 +180,16 @@ def determineMetadataType(inputType)->MetadataTypeEnum: return MetadataTypeEnum.ANNOTATION elif 'Activity' in inputType: return MetadataTypeEnum.ACTIVITY + elif 'Person' in inputType: + return MetadataTypeEnum.PERSON + elif 'DefinedTerm' in inputType: + return MetadataTypeEnum.DEFINED_TERM + elif 'Container' in inputType: + return MetadataTypeEnum.CONTAINER + elif 'Claim' in inputType: + return MetadataTypeEnum.CLAIM + elif 'Article' in inputType: + return MetadataTypeEnum.ARTICLE else: raise Exception(f"Type not found for value {inputType}") From ea58483de11d323a067c95ad837d35c851895e99 Mon Sep 17 00:00:00 2001 From: jniestroy Date: Tue, 9 Jun 2026 12:29:23 -0400 Subject: [PATCH 26/27] build updates to support lcoal edits --- .gitignore | 1 + Dockerfile | 10 +++++----- Dockerfile.local | 4 +--- Makefile | 8 ++++++-- build.sh | 14 -------------- compose.local.yaml | 7 +++++-- compose.yaml | 10 ++-------- preflight.sh | 11 ----------- uv.lock | 2 +- 9 files changed, 21 insertions(+), 46 deletions(-) delete mode 100755 build.sh delete mode 100755 preflight.sh diff --git a/.gitignore b/.gitignore index ec41a3d..3ac67d4 100644 --- a/.gitignore +++ b/.gitignore @@ -170,3 +170,4 @@ mds/src/fairscape_mds/tests/descriptive_statistics.ipynb /.devcontainer .devcontainer/devcontainer.json CLAUDE.md +preflight.sh \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 4c33aa5..dfcaef6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,13 +13,13 @@ RUN sh /uv-installer.sh && rm /uv-installer.sh ENV PATH="/root/.local/bin/:$PATH" # copy source code -COPY fairscape_server/mds/src/ /fairscape/src/ -COPY fairscape_graph_tools/ /fairscape/fairscape_graph_tools/ +COPY mds/src/ /fairscape/src/ WORKDIR /fairscape/src/ -COPY fairscape_server/pyproject.toml . -COPY fairscape_server/uv.lock . -RUN uv sync --locked +COPY pyproject.toml . +COPY uv.lock . +# --no-sources ignores the [tool.uv.sources] local-path overrides in pyproject, +RUN uv sync --no-sources #RUN export PYTHONPATH="$PYTHONPATH:/fairscape/src" diff --git a/Dockerfile.local b/Dockerfile.local index dca5e13..e571135 100644 --- a/Dockerfile.local +++ b/Dockerfile.local @@ -2,9 +2,7 @@ FROM python:3.12-slim-bookworm RUN apt-get update && apt-get install -y --no-install-recommends curl ca-certificates -ADD https://astral.sh/uv/install.sh /uv-installer.sh -RUN sh /uv-installer.sh && rm /uv-installer.sh -ENV PATH="/root/.local/bin/:$PATH" +RUN pip install uv COPY fairscape_server/mds/src/ /fairscape/src/ COPY fairscape_graph_tools/ /fairscape/fairscape_graph_tools/ diff --git a/Makefile b/Makefile index 51d9032..9d75afe 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,5 @@ -VERSION = RELEASE.2026-06-05.v1 +VERSION = RELEASE.2026-06-09.v1 +IMAGE = ghcr.io/fairscape/mds_python run: cd src/ && python -m fairscape_mds @@ -23,7 +24,10 @@ clean: rm -rf __pycache__ build: - docker build --no-cache -t ghcr.io/fairscape/mds_python:${VERSION} . + docker build --no-cache -f Dockerfile -t $(IMAGE):$(VERSION) . + +build-local: + cd .. && docker build --no-cache -f fairscape_server/Dockerfile.local -t $(IMAGE):$(VERSION) . push: docker push ghcr.io/fairscape/mds_python:${VERSION} diff --git a/build.sh b/build.sh deleted file mode 100755 index 7c45cf5..0000000 --- a/build.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/bash - -VERSION=v1 - -# Generate date in YYYY-MM-DD format -DATE=$(date '+%Y-%m-%d') - -# Build the Docker image -sudo docker build \ - --no-cache \ - -f Dockerfile \ - -t ghcr.io/fairscape/mds_python:RELEASE.${DATE}.${VERSION} . - -sudo docker push ghcr.io/fairscape/mds_python:RELEASE.${DATE}.${VERSION} diff --git a/compose.local.yaml b/compose.local.yaml index ea4a404..01fbe9d 100644 --- a/compose.local.yaml +++ b/compose.local.yaml @@ -1,12 +1,15 @@ -# Local development override — uses local fairscape_graph_tools and fairscape_models -# Usage: docker compose -f compose.yaml -f compose.local.yaml up +# Local development override — builds from the local fairscape_graph_tools and +# fairscape_models checkouts (editable). +# Usage: docker compose -f compose.yaml -f compose.local.yaml up --build services: fairscape-worker: + image: fairscape/mds_python:local build: context: "../" dockerfile: "fairscape_server/Dockerfile.local" fairscape-api: + image: fairscape/mds_python:local build: context: "../" dockerfile: "fairscape_server/Dockerfile.local" diff --git a/compose.yaml b/compose.yaml index fad4a98..a5fd16e 100644 --- a/compose.yaml +++ b/compose.yaml @@ -9,10 +9,7 @@ services: - fairscape-api - mongo fairscape-worker: - build: - context: "../" - dockerfile: "fairscape_server/Dockerfile" - # image: ghcr.io/fairscape/mds_python:RELEASE.2025-05-21.v1 + image: ghcr.io/fairscape/mds_python:RELEASE.2026-06-09.v1 env_file: "./deploy/docker_compose.env" environment: - GEMINI_API_KEY=${GEMINI_API_KEY} @@ -26,10 +23,7 @@ services: - mongo - minio fairscape-api: - build: - context: "../" - dockerfile: "fairscape_server/Dockerfile" - # image: ghcr.io/fairscape/mds_python:RELEASE.2025-05-21.v1 + image: ghcr.io/fairscape/mds_python:RELEASE.2026-06-09.v1 env_file: "./deploy/docker_compose.env" ports: - 8080:8080 diff --git a/preflight.sh b/preflight.sh deleted file mode 100755 index 24283ad..0000000 --- a/preflight.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env bash -set -u - -PORTS=(8080 6379 27017 8081 9010 9011) - -echo "Current holders:" -sudo ss -ltnp | grep -E ":($(IFS=\|; echo "${PORTS[*]}"))\b" || echo " (none)" - -for p in "${PORTS[@]}"; do - sudo fuser -k "${p}/tcp" 2>/dev/null || true -done diff --git a/uv.lock b/uv.lock index 4df8afc..fcf89bd 100644 --- a/uv.lock +++ b/uv.lock @@ -1133,7 +1133,7 @@ requires-dist = [ { name = "celery", extras = ["redis"], specifier = ">=5.3.6" }, { name = "docker", specifier = ">=7.0.0" }, { name = "fairscape-graph-tools", editable = "../fairscape_graph_tools" }, - { name = "fairscape-models", specifier = ">=1.1.1" }, + { name = "fairscape-models", specifier = ">=1.1.2" }, { name = "fastapi", specifier = ">=0.109.0" }, { name = "google-genai", specifier = ">=1.0.0" }, { name = "httpx", specifier = ">=0.28.1" }, From 78bc0f9fcf763e38f391ae46d54f3a4a1771f296 Mon Sep 17 00:00:00 2001 From: jniestroy Date: Tue, 9 Jun 2026 12:36:58 -0400 Subject: [PATCH 27/27] delete code moved to graph tools --- .../models/annotated_computation.py | 44 ----------------- .../models/annotated_evidence_graph.py | 22 --------- mds/src/fairscape_mds/models/identifier.py | 2 +- .../fairscape_mds/models/interpretation.py | 47 ------------------- .../tests/crud/test_interpretation.py | 4 +- 5 files changed, 3 insertions(+), 116 deletions(-) delete mode 100644 mds/src/fairscape_mds/models/annotated_computation.py delete mode 100644 mds/src/fairscape_mds/models/annotated_evidence_graph.py delete mode 100644 mds/src/fairscape_mds/models/interpretation.py diff --git a/mds/src/fairscape_mds/models/annotated_computation.py b/mds/src/fairscape_mds/models/annotated_computation.py deleted file mode 100644 index 4029e33..0000000 --- a/mds/src/fairscape_mds/models/annotated_computation.py +++ /dev/null @@ -1,44 +0,0 @@ -"""Compatibility shim -- re-exports from the shared fairscape_graph_tools package. - -The model definitions now live in fairscape_graph_tools.models.annotated_computation. -This module is kept so existing `from fairscape_mds.models.annotated_computation -import X` paths keep working; new code should import from fairscape_graph_tools directly. -""" - -from fairscape_graph_tools.models.annotated_computation import ( - ANNOTATED_COMPUTATION_TYPE, - AnnotatedComputation, - Assumption, - AssumptionImpact, - CodeAnalysis, - ComputationError, - ComputationReviewStatus, - DatasetSummary, - EvidencePointer, - LLMAssumption, - LLMCodeAnalysis, - LLMComputationAnnotation, - LLMDatasetSummary, - LLMError, - normalize_assumption, - normalize_error, -) - -__all__ = [ - "ANNOTATED_COMPUTATION_TYPE", - "AnnotatedComputation", - "Assumption", - "AssumptionImpact", - "CodeAnalysis", - "ComputationError", - "ComputationReviewStatus", - "DatasetSummary", - "EvidencePointer", - "LLMAssumption", - "LLMCodeAnalysis", - "LLMComputationAnnotation", - "LLMDatasetSummary", - "LLMError", - "normalize_assumption", - "normalize_error", -] diff --git a/mds/src/fairscape_mds/models/annotated_evidence_graph.py b/mds/src/fairscape_mds/models/annotated_evidence_graph.py deleted file mode 100644 index 226d53a..0000000 --- a/mds/src/fairscape_mds/models/annotated_evidence_graph.py +++ /dev/null @@ -1,22 +0,0 @@ -"""Compatibility shim -- re-exports from the shared fairscape_graph_tools package. - -The model definitions now live in fairscape_graph_tools.models.annotated_evidence_graph. -This module is kept so existing `from fairscape_mds.models.annotated_evidence_graph -import X` paths keep working; new code should import from fairscape_graph_tools directly. -""" - -from fairscape_graph_tools.models.annotated_evidence_graph import ( - ANNOTATED_EVIDENCE_GRAPH_TYPE, - AnnotatedEvidenceGraph, - AudiencePerspective, - DataOverview, - GraphAssumption, -) - -__all__ = [ - "ANNOTATED_EVIDENCE_GRAPH_TYPE", - "AnnotatedEvidenceGraph", - "AudiencePerspective", - "DataOverview", - "GraphAssumption", -] diff --git a/mds/src/fairscape_mds/models/identifier.py b/mds/src/fairscape_mds/models/identifier.py index a4dbbab..40d8c6f 100644 --- a/mds/src/fairscape_mds/models/identifier.py +++ b/mds/src/fairscape_mds/models/identifier.py @@ -28,7 +28,7 @@ from fairscape_models.conversion.models.AIReady import AIReadyScore from fairscape_models.model_card import ModelCard from fairscape_models.fairscape_base import IdentifierValue -from fairscape_mds.models.annotated_evidence_graph import AnnotatedEvidenceGraph +from fairscape_graph_tools.models.annotated_evidence_graph import AnnotatedEvidenceGraph import datetime diff --git a/mds/src/fairscape_mds/models/interpretation.py b/mds/src/fairscape_mds/models/interpretation.py deleted file mode 100644 index d640ace..0000000 --- a/mds/src/fairscape_mds/models/interpretation.py +++ /dev/null @@ -1,47 +0,0 @@ -from pydantic import BaseModel, Field -from typing import Optional, Dict, Any, List -import datetime - - -class ComputationProgress(BaseModel): - """Track progress of a single computation annotation.""" - computation_id: str - name: str = "" - status: str = "pending" # pending, processing, done, error - error: Optional[str] = None - - -class InterpretationTask(BaseModel): - guid: str = Field(alias="guid") - task_type: str = Field(default="InterpretROCrate") - rocrate_id: str - owner_email: str - status: str = Field(default="PENDING") - - # Pipeline progress - current_step: str = Field(default="PENDING") - # Steps: CONDENSING, TRAVERSING, PREFETCHING, PROMPTING, SYNTHESIZING, STORING - - # Computation-level progress (for parallel step) - total_computations: int = 0 - completed_computations: int = 0 - computation_details: List[Dict[str, Any]] = Field(default_factory=list) - - # Results - condensed_rocrate_id: Optional[str] = None - annotated_evidence_graph_id: Optional[str] = None - - # Timing - time_created: datetime.datetime = Field(default_factory=datetime.datetime.utcnow) - time_started: Optional[datetime.datetime] = None - time_finished: Optional[datetime.datetime] = None - - # Config - llm_model: str = "gemini-2.5-flash" - llm_temperature: float = 0.2 - persona: str = "datasci" - - error: Optional[Dict[str, Any]] = None - - class Config: - populate_by_name = True diff --git a/mds/src/fairscape_mds/tests/crud/test_interpretation.py b/mds/src/fairscape_mds/tests/crud/test_interpretation.py index 6af87da..2747059 100644 --- a/mds/src/fairscape_mds/tests/crud/test_interpretation.py +++ b/mds/src/fairscape_mds/tests/crud/test_interpretation.py @@ -37,8 +37,8 @@ MongoResultSink, MongoTaskTracker, ) -from fairscape_mds.models.annotated_computation import AnnotatedComputation -from fairscape_mds.models.annotated_evidence_graph import AnnotatedEvidenceGraph +from fairscape_graph_tools.models.annotated_computation import AnnotatedComputation +from fairscape_graph_tools.models.annotated_evidence_graph import AnnotatedEvidenceGraph from fairscape_graph_tools.condenser import Condenser from fairscape_graph_tools.interpreter import Interpreter, InterpretConfig