From 0b7ef1ed1f68e5262a875da88e743185ea8c3643 Mon Sep 17 00:00:00 2001 From: Adam Wright Date: Tue, 26 May 2026 16:56:29 -0400 Subject: [PATCH 01/17] =?UTF-8?q?Add=20entity=E2=86=92reaction=20proxy=20m?= =?UTF-8?q?apping=20for=20decomposed=20species?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Complexes/EntitySets that contain an EntitySet are expanded into virtual variants during generation, so the parent species' stId never appears in stid_to_uuid_mapping.csv. Consumers that know a species by its Reactome stId (notably MP-BioPath key-outputs, which are predominantly Complexes) then can't locate it — even though the reaction that produces it is in the network. This was silently dropping ~35% of benchmark key-output lookups. New output file entity_reaction_proxy_mapping.csv maps each such missing species to the UUIDs of the reaction that produces it (falling back to the reaction that consumes it). Reaction flux is a tight, biologically faithful proxy for "is this species present?" — far better than pointing at the species' terminal components, which for hub proteins span dozens of reaction contexts and discriminate nothing. The primary stid_to_uuid_mapping.csv is untouched (keeps its one-row-per-UUID identity contract). bin/backfill-proxy-mapping.py regenerates the file for the existing catalog without a full re-run. Verified on Signaling_by_WNT: the ubiquitinated-phospho-beta-catenin complex (R-HSA-2130284), previously unresolvable, now maps to its 17 producing-reaction UUIDs. End-to-end accuracy on the MP-BioPath experimental set rose from 38.4% to 55.7%, with keyoutput-not-in-network failures dropping 209 → 17. Co-Authored-By: Claude Opus 4.7 (1M context) --- bin/backfill-proxy-mapping.py | 66 ++++++++++++++++ src/logic_network_generator.py | 104 ++++++++++++++++++++++++++ src/neo4j_connector.py | 96 ++++++++++++++++++++++++ src/pathway_generator.py | 20 +++++ tests/test_logic_network_generator.py | 103 +++++++++++++++++++++++++ 5 files changed, 389 insertions(+) create mode 100644 bin/backfill-proxy-mapping.py diff --git a/bin/backfill-proxy-mapping.py b/bin/backfill-proxy-mapping.py new file mode 100644 index 0000000..3561594 --- /dev/null +++ b/bin/backfill-proxy-mapping.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 +"""Backfill entity_reaction_proxy_mapping.csv for already-generated pathways. + +Regenerating a network from scratch is expensive; this reuses the existing +logic_network.csv + stid_to_uuid_mapping.csv and only adds the new proxy file +(which just needs Neo4j + those two artifacts). Going forward the generator +emits the file itself; this is a one-time catch-up for the existing catalog. +""" +import sys +from pathlib import Path + +import pandas as pd + +from src.neo4j_connector import prefetch_entity_data +from src.logic_network_generator import export_entity_reaction_proxy_mapping + +OUTPUT = Path("output") + + +def backfill(pathway_dir: Path) -> None: + name = pathway_dir.name + if not name.endswith(tuple(f"R-HSA-{n}" for n in [""])) and "R-HSA-" not in name: + return + pid = "R-HSA-" + name.rsplit("R-HSA-", 1)[-1] + net_f = pathway_dir / "logic_network.csv" + map_f = pathway_dir / "stid_to_uuid_mapping.csv" + rc_f = pathway_dir / "cache" / "reaction_connections.csv" + if not (net_f.exists() and map_f.exists()): + print(f" skip {name}: missing artifacts") + return + + network = pd.read_csv(net_f) + mapping = pd.read_csv(map_f) + reactome_id_to_uuid = dict(zip(mapping["uuid"].astype(str), + mapping["stable_id"].astype(str))) + # Approximate reaction_id_map from the mapping (reaction stIds carry UUIDs + # too); only reaction stIds get queried, so entity rows are harmless. + rid_map = mapping.rename(columns={"uuid": "uid", "stable_id": "reactome_id"}) + + # Warm caches with the pathway's reactions for fast terminal-component walks. + if rc_f.exists(): + rc = pd.read_csv(rc_f, dtype=str) + rxn_ids = set(rc["preceding_reaction_id"].dropna()) | \ + set(rc["following_reaction_id"].dropna()) + if rxn_ids: + prefetch_entity_data(list(rxn_ids)) + + out_f = pathway_dir / "entity_reaction_proxy_mapping.csv" + export_entity_reaction_proxy_mapping( + network, rid_map, reactome_id_to_uuid, pid, str(out_f)) + df = pd.read_csv(out_f) + print(f" {name}: {len(df)} rows, " + f"{df['entity_stable_id'].nunique() if len(df) else 0} entities") + + +if __name__ == "__main__": + targets = sys.argv[1:] + if targets: + dirs = [OUTPUT / t for t in targets] + else: + dirs = [d for d in sorted(OUTPUT.iterdir()) if d.is_dir()] + for d in dirs: + try: + backfill(d) + except Exception as e: + print(f" ERROR {d.name}: {e}") diff --git a/src/logic_network_generator.py b/src/logic_network_generator.py index 44d88cd..da5e768 100755 --- a/src/logic_network_generator.py +++ b/src/logic_network_generator.py @@ -1131,3 +1131,107 @@ def export_uuid_to_reactome_mapping( mapping_df.to_csv(output_file, index=False) logger.info(f"Exported UUID to Reactome stable ID mapping with {len(mapping_df)} entries") + + +def export_entity_reaction_proxy_mapping( + pathway_logic_network: pd.DataFrame, + reaction_id_map: pd.DataFrame, + reactome_id_to_uuid: Dict[str, str], + pathway_id: str, + output_file: str, +) -> None: + """Map curated species absent from the UUID mapping to a reaction-flux proxy. + + Complexes/EntitySets that contain an EntitySet are expanded into virtual + variants when the logic network is built (see docs/DESIGN_DECISIONS.md, + "EntitySet expansion produces multiple virtual reactions"). The variants get + their own UUIDs but the *parent's* stId is not preserved anywhere in + ``stid_to_uuid_mapping.csv`` — so a consumer that knows a curated species by + its Reactome stId (e.g. MP-BioPath key-outputs, which are predominantly + Complexes) can't find it, even though the reaction that produces it *is* in + the network. + + The right proxy for "is species S present?" is the flux through the reaction + that produces it: if S is the output of reaction R, then R's node activity is + a direct, tight readout of S's production. Mapping S to its *terminal + components* instead would be far too lax — a hub protein like β-catenin + appears in dozens of reaction contexts, so a max over its component UUIDs + reads "active" almost everywhere and discriminates nothing. + + For each PhysicalEntity that participates in the pathway but is not directly + present in the UUID mapping, we therefore record the UUIDs of the reactions + that **output** it (the producing reactions). If a species has no producing + reaction in the network we fall back to reactions that **consume** it as an + input — its presence is still implied by that reaction's activity. + + The primary ``stid_to_uuid_mapping.csv`` is intentionally left untouched: it + keeps its one-row-per-UUID identity contract, and this supplementary file + carries the (many-to-many) species → proxy-reaction relationship. + + Output CSV columns: + - entity_stable_id: a species stId absent from the UUID mapping + - proxy_uuid: a reaction UUID present in the network whose flux proxies it + - proxy_role: 'producing' (entity is the reaction's output) or + 'consuming' (entity is the reaction's input) + """ + from src.neo4j_connector import ( + get_pathway_participating_entities, + get_pathway_entity_reactions, + ) + + network_uuids: Set[str] = set() + network_uuids.update(pathway_logic_network['source_id'].dropna().unique()) + network_uuids.update(pathway_logic_network['target_id'].dropna().unique()) + + # stable_id of every entity directly addressable in the mapping. + present_stids: Set[str] = set() + if reactome_id_to_uuid: + sample_key = next(iter(reactome_id_to_uuid.keys())) + uuid_keyed = '-' in str(sample_key) + for k, v in reactome_id_to_uuid.items(): + entity_uuid, stid = (k, v) if uuid_keyed else (v, k) + if entity_uuid in network_uuids: + present_stids.add(str(stid)) + + # reaction stId -> [reaction UUIDs present in the network] + reaction_stid_to_uuids: Dict[str, List[str]] = {} + for _, row in reaction_id_map.iterrows(): + ruuid = str(row['uid']) + if ruuid in network_uuids: + reaction_stid_to_uuids.setdefault(str(row['reactome_id']), []).append(ruuid) + + participating = get_pathway_participating_entities(pathway_id) + missing = {e for e in participating if e not in present_stids} + if not missing: + pd.DataFrame(columns=['entity_stable_id', 'proxy_uuid', 'proxy_role']).to_csv( + output_file, index=False) + logger.info("Entity-reaction proxy mapping: no missing species to proxy") + return + + # entity stId -> {'output': [reaction stIds], 'input': [reaction stIds]} + entity_reactions = get_pathway_entity_reactions(pathway_id, list(missing)) + + rows: List[Dict[str, str]] = [] + for entity_stid in missing: + roles = entity_reactions.get(entity_stid, {}) + # Prefer producing reactions; fall back to consuming if none are present. + for role_name, rel_key in (('producing', 'output'), ('consuming', 'input')): + proxy_uuids: List[str] = [] + for rxn_stid in roles.get(rel_key, []): + proxy_uuids.extend(reaction_stid_to_uuids.get(str(rxn_stid), [])) + if proxy_uuids: + for puuid in dict.fromkeys(proxy_uuids): # de-dup, keep order + rows.append({'entity_stable_id': str(entity_stid), + 'proxy_uuid': puuid, + 'proxy_role': role_name}) + break # don't also emit consuming rows once producing matched + + out_df = pd.DataFrame(rows, columns=['entity_stable_id', 'proxy_uuid', 'proxy_role']) + if not out_df.empty: + out_df = out_df.sort_values(['entity_stable_id', 'proxy_uuid']) + out_df.to_csv(output_file, index=False) + n_entities = out_df['entity_stable_id'].nunique() if not out_df.empty else 0 + logger.info( + f"Exported entity-reaction proxy mapping: {len(out_df)} rows " + f"covering {n_entities} of {len(missing)} missing species" + ) diff --git a/src/neo4j_connector.py b/src/neo4j_connector.py index 6cc7f96..a1707da 100755 --- a/src/neo4j_connector.py +++ b/src/neo4j_connector.py @@ -331,6 +331,102 @@ def get_top_level_pathways() -> List[Dict[str, Any]]: ) from e +def get_pathway_participating_entities(pathway_id: str) -> Set[str]: + """Return every PhysicalEntity stId that participates in the pathway's reactions. + + Walks all ReactionLikeEvents under the pathway (via hasEvent) and collects + the stIds of their inputs, outputs, catalysts, and regulators. This includes + the *intact* Complex/EntitySet entities as Reactome curates them — which is + exactly the set that may get decomposed into virtual variants in the logic + network and therefore lose their original stId from the UUID mapping. + + Args: + pathway_id: Reactome pathway stable ID (e.g., "R-HSA-69620") + + Returns: + Set of PhysicalEntity stable IDs. + + Raises: + ConnectionError: If Neo4j database is not accessible + """ + # Explicit, indexed patterns per role. A blanket variable-length match + # (e.g. ``-[*1..3]-``) over these relationship types is orders of magnitude + # slower and can time out on large pathways. + query: str = """ + MATCH (pathway:Pathway {stId: $pathway_id})-[:hasEvent*]->(r:ReactionLikeEvent) + OPTIONAL MATCH (r)-[:input|output]->(io:PhysicalEntity) + OPTIONAL MATCH (r)-[:catalystActivity]->(:CatalystActivity) + -[:physicalEntity]->(cat:PhysicalEntity) + OPTIONAL MATCH (r)-[:regulatedBy]->(:Regulation) + -[:regulator]->(reg:PhysicalEntity) + RETURN COLLECT(DISTINCT io.stId) + + COLLECT(DISTINCT cat.stId) + + COLLECT(DISTINCT reg.stId) AS stids + """ + try: + result = get_graph().run(query, pathway_id=pathway_id).data() + if not result: + return set() + return {s for s in result[0]["stids"] if s} + except Exception as e: + logger.error( + f"Error in get_pathway_participating_entities for {pathway_id}", + exc_info=True, + ) + raise ConnectionError( + f"Failed to query participating entities from Neo4j at " + f"{os.getenv('NEO4J_URL', 'bolt://localhost:7687')}. " + f"Original error: {str(e)}" + ) from e + + +def get_pathway_entity_reactions( + pathway_id: str, entity_ids: List[str] +) -> Dict[str, Dict[str, List[str]]]: + """For each entity, the pathway reactions that output (produce) or input it. + + Args: + pathway_id: Reactome pathway stable ID (e.g., "R-HSA-195721") + entity_ids: PhysicalEntity stIds to look up. + + Returns: + ``{entity_stId: {"output": [reaction_stId, ...], + "input": [reaction_stId, ...]}}`` + Entities with no participating reaction in the pathway are absent. + + Raises: + ConnectionError: If Neo4j database is not accessible + """ + if not entity_ids: + return {} + query: str = """ + MATCH (pathway:Pathway {stId: $pathway_id})-[:hasEvent*]->(r:ReactionLikeEvent) + MATCH (r)-[rel:input|output]->(e:PhysicalEntity) + WHERE e.stId IN $entity_ids + RETURN e.stId AS entity, type(rel) AS rel, + COLLECT(DISTINCT r.stId) AS reactions + """ + try: + result = get_graph().run( + query, pathway_id=pathway_id, entity_ids=list(entity_ids) + ).data() + out: Dict[str, Dict[str, List[str]]] = {} + for row in result: + out.setdefault(row["entity"], {})[row["rel"]] = [ + rid for rid in row["reactions"] if rid + ] + return out + except Exception as e: + logger.error( + f"Error in get_pathway_entity_reactions for {pathway_id}", exc_info=True + ) + raise ConnectionError( + f"Failed to query entity reactions from Neo4j at " + f"{os.getenv('NEO4J_URL', 'bolt://localhost:7687')}. " + f"Original error: {str(e)}" + ) from e + + def get_pathway_name(pathway_id: str) -> str: """Get the display name for a pathway by its stable ID. diff --git a/src/pathway_generator.py b/src/pathway_generator.py index 6141b93..f21c982 100755 --- a/src/pathway_generator.py +++ b/src/pathway_generator.py @@ -8,6 +8,7 @@ from src.decomposed_uid_mapping import decomposed_uid_mapping_column_types from src.logic_network_generator import ( create_pathway_logic_network, + export_entity_reaction_proxy_mapping, export_uuid_to_reactome_mapping, ) from src.neo4j_connector import get_reaction_connections @@ -150,6 +151,25 @@ def generate_pathway_file( logger.error(f"Failed to write stable ID to UUID mapping file {uuid_to_reactome_file}: {e}") # Don't raise - this is supplementary + # Export entity→reaction proxy mapping. Curated species (often Complexes) + # that were expanded into virtual variants lose their own stId from the + # UUID mapping; this file points each such species at the UUIDs of the + # reactions that produce (or, failing that, consume) it, so consumers can + # read reaction flux as a proxy for the species' state. + proxy_mapping_file = pathway_output_dir / "entity_reaction_proxy_mapping.csv" + try: + export_entity_reaction_proxy_mapping( + result.logic_network, + result.reaction_id_map, + result.uuid_mapping, + pathway_id, + str(proxy_mapping_file), + ) + logger.info(f"Successfully exported entity-reaction proxy mapping: {proxy_mapping_file}") + except Exception as e: + logger.error(f"Failed to write entity-reaction proxy mapping file {proxy_mapping_file}: {e}") + # Don't raise - this is supplementary + logger.info(f"Output directory: {pathway_output_dir}") except (ConnectionError, ValueError) as e: diff --git a/tests/test_logic_network_generator.py b/tests/test_logic_network_generator.py index 1fb9eac..fb45d79 100644 --- a/tests/test_logic_network_generator.py +++ b/tests/test_logic_network_generator.py @@ -21,6 +21,7 @@ _register_entity_uuid, _get_or_create_entity_uuid, _resolve_vr_entities, + export_entity_reaction_proxy_mapping, ) @@ -414,3 +415,105 @@ def test_leaf_mints_fresh_uuid_when_entity_is_new_to_network(self): # Each fresh UUID is also used as a source on an assembly edge for u in new_uuids: assert any(e["source_id"] == u and e["edge_type"] == "assembly" for e in edges) + + +class TestEntityReactionProxyMapping: + """Tests for export_entity_reaction_proxy_mapping. + + A curated species (often a Complex containing an EntitySet) gets expanded + into virtual variants during generation, so its own stId never appears in + stid_to_uuid_mapping.csv. This export restores addressability by pointing + the species at the UUIDs of the reaction that produces it. + """ + + def _write(self, tmp_path, network, reaction_id_map, reactome_id_to_uuid, + participating, entity_reactions): + out = tmp_path / "proxy.csv" + with patch('src.neo4j_connector.get_pathway_participating_entities', + return_value=participating), \ + patch('src.neo4j_connector.get_pathway_entity_reactions', + return_value=entity_reactions): + export_entity_reaction_proxy_mapping( + network, reaction_id_map, reactome_id_to_uuid, + "R-HSA-1", str(out), + ) + return pd.read_csv(out) + + # Fake UUIDs must contain a dash: export_*_mapping detects the dict + # orientation with the same `'-' in key` heuristic the rest of the module + # uses, and real position-aware UUIDs always have dashes. + def test_missing_complex_maps_to_producing_reaction(self, tmp_path): + # Network: reaction R1 (uuid u-r1) outputs the expanded variant nodes; + # the parent complex C is absent from the mapping. + network = pd.DataFrame({ + "source_id": ["u-in", "u-r1"], + "target_id": ["u-r1", "u-out"], + }) + reaction_id_map = pd.DataFrame({"uid": ["u-r1"], "reactome_id": ["R-HSA-R1"]}) + # Mapping has the reaction and some leaves, but NOT complex C. + reactome_id_to_uuid = {"u-r1": "R-HSA-R1", "u-in": "R-HSA-IN", "u-out": "R-HSA-OUT"} + + df = self._write( + tmp_path, network, reaction_id_map, reactome_id_to_uuid, + participating={"R-HSA-C", "R-HSA-IN", "R-HSA-OUT"}, + entity_reactions={"R-HSA-C": {"output": ["R-HSA-R1"]}}, + ) + rows = df[df["entity_stable_id"] == "R-HSA-C"] + assert list(rows["proxy_uuid"]) == ["u-r1"] + assert set(rows["proxy_role"]) == {"producing"} + + def test_entity_already_in_mapping_is_skipped(self, tmp_path): + network = pd.DataFrame({"source_id": ["u-r1"], "target_id": ["u-out"]}) + reaction_id_map = pd.DataFrame({"uid": ["u-r1"], "reactome_id": ["R-HSA-R1"]}) + # R-HSA-OUT is directly present, so it must not be proxied. + reactome_id_to_uuid = {"u-r1": "R-HSA-R1", "u-out": "R-HSA-OUT"} + df = self._write( + tmp_path, network, reaction_id_map, reactome_id_to_uuid, + participating={"R-HSA-OUT"}, + entity_reactions={"R-HSA-OUT": {"output": ["R-HSA-R1"]}}, + ) + assert df.empty or "R-HSA-OUT" not in set(df["entity_stable_id"]) + + def test_consuming_fallback_when_no_producer(self, tmp_path): + network = pd.DataFrame({"source_id": ["u-r1"], "target_id": ["u-out"]}) + reaction_id_map = pd.DataFrame({"uid": ["u-r1"], "reactome_id": ["R-HSA-R1"]}) + reactome_id_to_uuid = {"u-r1": "R-HSA-R1", "u-out": "R-HSA-OUT"} + # C is only ever consumed (input) — no producing reaction in-pathway. + df = self._write( + tmp_path, network, reaction_id_map, reactome_id_to_uuid, + participating={"R-HSA-C"}, + entity_reactions={"R-HSA-C": {"input": ["R-HSA-R1"]}}, + ) + rows = df[df["entity_stable_id"] == "R-HSA-C"] + assert list(rows["proxy_uuid"]) == ["u-r1"] + assert set(rows["proxy_role"]) == {"consuming"} + + def test_producing_preferred_over_consuming(self, tmp_path): + network = pd.DataFrame({ + "source_id": ["u-r1", "u-r2"], "target_id": ["u-x", "u-y"], + }) + reaction_id_map = pd.DataFrame( + {"uid": ["u-r1", "u-r2"], "reactome_id": ["R-HSA-R1", "R-HSA-R2"]}) + reactome_id_to_uuid = {"u-r1": "R-HSA-R1", "u-r2": "R-HSA-R2"} + # C is produced by R1 and consumed by R2 — only the producer should win. + df = self._write( + tmp_path, network, reaction_id_map, reactome_id_to_uuid, + participating={"R-HSA-C"}, + entity_reactions={"R-HSA-C": {"output": ["R-HSA-R1"], "input": ["R-HSA-R2"]}}, + ) + rows = df[df["entity_stable_id"] == "R-HSA-C"] + assert list(rows["proxy_uuid"]) == ["u-r1"] + assert set(rows["proxy_role"]) == {"producing"} + + def test_reaction_not_in_network_yields_no_row(self, tmp_path): + # The producing reaction exists in Reactome but its UUID never made it + # into the network (e.g. dropped for having no I/O) — nothing to proxy. + network = pd.DataFrame({"source_id": ["u-a"], "target_id": ["u-b"]}) + reaction_id_map = pd.DataFrame({"uid": [], "reactome_id": []}) + reactome_id_to_uuid = {"u-a": "R-HSA-A", "u-b": "R-HSA-B"} + df = self._write( + tmp_path, network, reaction_id_map, reactome_id_to_uuid, + participating={"R-HSA-C"}, + entity_reactions={"R-HSA-C": {"output": ["R-HSA-MISSING"]}}, + ) + assert df.empty or "R-HSA-C" not in set(df["entity_stable_id"]) From 18b3b0893b4b7d53aa074646d72ba7cd0b7b6bf8 Mon Sep 17 00:00:00 2001 From: Adam Wright Date: Wed, 27 May 2026 18:12:12 -0400 Subject: [PATCH 02/17] Decompose boundary complexes positionally, per occurrence MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root-input / terminal-output membership was decided by a global stId set difference (all_input_eids - all_output_eids). That removed a complex's stId entirely the moment it appeared as a product *anywhere* in the pathway, so its genuine root-input occurrences were never decomposed. Curators routinely perturb individual subunits, so those subunits must be addressable wherever they enter or leave the pathway. _emit_boundary_decomposition_edges now derives roots/terminals positionally from the edge list (source-only = root input, target-only = terminal output) and decomposes each such complex occurrence. Members are not duplicated: a member reuses its existing node (or one freshly-minted UUID) and gets one assembly/dissociation edge per occurrence — so a complex appearing as a root input at N positions yields one member node with N edges into it. Verified on DNA_Double_Strand_Break_Response: boundary edges 7+10 -> 410+637; MDC1 and RNF8 (previously unaddressable, perturbed by curators) now resolve to network nodes. Catalyst/regulator-only proteins (e.g. USP1) remain a separate gap — they never appear as a reaction input/output. Adds unit tests for member sharing across occurrences and for leaving intermediate occurrences intact. Also fixes bin/backfill-proxy-mapping.py to put the repo root on sys.path so it runs from bin/. Co-Authored-By: Claude Opus 4.7 (1M context) --- bin/backfill-proxy-mapping.py | 11 ++- src/logic_network_generator.py | 125 +++++++++++++------------- tests/test_logic_network_generator.py | 74 +++++++++++---- 3 files changed, 131 insertions(+), 79 deletions(-) diff --git a/bin/backfill-proxy-mapping.py b/bin/backfill-proxy-mapping.py index 3561594..cd98f01 100644 --- a/bin/backfill-proxy-mapping.py +++ b/bin/backfill-proxy-mapping.py @@ -6,15 +6,20 @@ (which just needs Neo4j + those two artifacts). Going forward the generator emits the file itself; this is a one-time catch-up for the existing catalog. """ +import os import sys from pathlib import Path import pandas as pd -from src.neo4j_connector import prefetch_entity_data -from src.logic_network_generator import export_entity_reaction_proxy_mapping +# Allow running as `bin/backfill-proxy-mapping.py` — put the repo root on the +# path so `src` imports resolve regardless of cwd. +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) -OUTPUT = Path("output") +from src.neo4j_connector import prefetch_entity_data # noqa: E402 +from src.logic_network_generator import export_entity_reaction_proxy_mapping # noqa: E402 + +OUTPUT = Path(__file__).resolve().parent.parent / "output" def backfill(pathway_dir: Path) -> None: diff --git a/src/logic_network_generator.py b/src/logic_network_generator.py index da5e768..c1ef3a4 100755 --- a/src/logic_network_generator.py +++ b/src/logic_network_generator.py @@ -553,42 +553,47 @@ def _decompose_regulator_entity(entity_id: str) -> List[tuple]: def _emit_boundary_decomposition_edges( pathway_logic_network_data: List[Dict[str, Any]], - root_input_eids: Set[str], - terminal_output_eids: Set[str], - root_input_uuid_cache: Dict[str, str], - terminal_output_uuid_cache: Dict[str, str], reactome_id_to_uuid: Dict[str, str], ) -> None: - """Append synthetic edges that expose leaves of root/terminal complexes. - - For each root-input complex C with components {A, B, ...}, emit - ``A → C``, ``B → C``, ... edges of edge_type='assembly'. For each - terminal-output complex, emit ``C → A``, ``C → B``, ... of - edge_type='dissociation'. Each leaf shares a single UUID across all - boundary contexts so that perturbing a leaf at the assembly side - propagates through any downstream dissociation that reads the same - species. - - Intermediate complexes (those produced by some reaction AND consumed - by another in this pathway) are intentionally NOT expanded — they're - real biological species flowing between reactions, and the AB dimer - is a different molecule from free A and free B. See - docs/DESIGN_DECISIONS.md, "Two layers of decomposition." - - Simple-leaf root/terminal entities (proteins, small molecules) are - skipped: they're already perturbable as themselves. - - A leaf reuses any UUID the entity already has elsewhere in the network - (regular VR inputs/outputs, regulators, catalysts) so that perturbing a - protein in one role propagates through every other role. Without this, - boundary leaves would be disconnected duplicate nodes for the same - biological entity. + """Expose the members of every root-input and terminal-output complex. + + Boundary membership is decided **positionally, per network occurrence** — + not by a global stId set difference. A node is a *root input* if it is a + source but never a target (no reaction produces it); a *terminal output* if + it is a target but never a source (no reaction consumes it). The same + complex stId can appear at several positions: the occurrences that are root + inputs get decomposed, the occurrence sitting intermediate between two + reactions is left intact — exactly as a real species should be. + + (The previous implementation used ``all_input_eids - all_output_eids`` over + the whole pathway, which removed a complex's stId entirely the moment it was + produced *anywhere*, leaving its genuine root-input occurrences undecomposed. + Curator perturbations target individual proteins, so those proteins must be + addressable wherever they enter or leave the pathway.) + + For each root-input complex C with terminal members {A, B, ...}, emit + ``A → C``, ``B → C`` (``edge_type='assembly'``). For each terminal-output + complex, emit ``C → A``, ``C → B`` (``edge_type='dissociation'``). + + Members are **not duplicated**: a member reuses any UUID it already has in + the network, else a single freshly-minted UUID shared across every boundary + occurrence. So if complex C appears as a root input at 15 positions, member A + is one node with 15 assembly edges — one to each occurrence. Perturbing A + then propagates into all of them. """ from src.neo4j_connector import get_labels - # Build stId → existing UUID lookup from everything assigned so far - # (entity registry from VR phases, plus regulator/catalyst UUIDs added - # by append_regulators). reactome_id_to_uuid is keyed by UUID, so invert. + # Positional roots / terminals from the current edge list. + sources: Set[str] = set() + targets: Set[str] = set() + for edge in pathway_logic_network_data: + sources.add(edge["source_id"]) + targets.add(edge["target_id"]) + root_uuids = sources - targets # produced by no reaction in this pathway + terminal_uuids = targets - sources # consumed by no reaction in this pathway + + # stId → existing UUID, so a member reuses the node it already has elsewhere + # (free protein, regulator, catalyst) rather than becoming a disconnected dup. stid_to_existing_uuid: Dict[str, str] = {} for existing_uuid, stid in reactome_id_to_uuid.items(): if stid not in stid_to_existing_uuid: @@ -607,20 +612,22 @@ def _leaf_uuid(leaf_stid: str) -> str: def _is_complex(entity_id: str) -> bool: return "Complex" in get_labels(entity_id) + seen_edges: Set[tuple] = set() assembly_count = 0 - for eid in root_input_eids: - if not _is_complex(eid): + for complex_uuid in root_uuids: + stid = reactome_id_to_uuid.get(complex_uuid) + if not stid or not _is_complex(stid): continue - complex_uuid = root_input_uuid_cache.get(eid) - if not complex_uuid: - continue - leaves = get_terminal_components(eid) - # If the only "leaf" is the complex itself, there's nothing to expose. - if leaves == {str(eid)}: + leaves = get_terminal_components(stid) + if leaves == {str(stid)}: # nothing below the complex to expose continue for leaf in leaves: + leaf_uuid = _leaf_uuid(leaf) + if (leaf_uuid, complex_uuid) in seen_edges: + continue + seen_edges.add((leaf_uuid, complex_uuid)) pathway_logic_network_data.append({ - "source_id": _leaf_uuid(leaf), + "source_id": leaf_uuid, "target_id": complex_uuid, "pos_neg": "pos", "and_or": "and", @@ -630,19 +637,21 @@ def _is_complex(entity_id: str) -> bool: assembly_count += 1 dissociation_count = 0 - for eid in terminal_output_eids: - if not _is_complex(eid): - continue - complex_uuid = terminal_output_uuid_cache.get(eid) - if not complex_uuid: + for complex_uuid in terminal_uuids: + stid = reactome_id_to_uuid.get(complex_uuid) + if not stid or not _is_complex(stid): continue - leaves = get_terminal_components(eid) - if leaves == {str(eid)}: + leaves = get_terminal_components(stid) + if leaves == {str(stid)}: continue for leaf in leaves: + leaf_uuid = _leaf_uuid(leaf) + if (complex_uuid, leaf_uuid) in seen_edges: + continue + seen_edges.add((complex_uuid, leaf_uuid)) pathway_logic_network_data.append({ "source_id": complex_uuid, - "target_id": _leaf_uuid(leaf), + "target_id": leaf_uuid, "pos_neg": "pos", "and_or": "and", "edge_type": "dissociation", @@ -652,9 +661,9 @@ def _is_complex(entity_id: str) -> bool: if assembly_count or dissociation_count: logger.info( - f"Boundary expansion: {assembly_count} assembly edges, " + f"Boundary expansion (positional): {assembly_count} assembly edges, " f"{dissociation_count} dissociation edges, " - f"{len(leaf_uuid_registry)} unique boundary leaves" + f"{len(leaf_uuid_registry)} new member leaves" ) @@ -1008,18 +1017,14 @@ def create_pathway_logic_network( entity_uuid_registry=entity_uuid_registry, ) - # Boundary expansion: root-input and terminal-output complexes get - # synthetic assembly / dissociation edges to their leaf components so - # individual proteins are perturbable / readable at the network - # boundary. Intermediate complexes are deliberately left intact — - # they're the actual biological species flowing between reactions. - # See docs/DESIGN_DECISIONS.md, "Two layers of decomposition." + # Boundary expansion: every root-input and terminal-output complex + # occurrence gets synthetic assembly / dissociation edges to its member + # proteins, so individual subunits are perturbable / readable wherever + # they enter or leave the pathway. Decided positionally per occurrence + # (a complex that is also intermediate elsewhere keeps that intermediate + # node intact). See docs/DESIGN_DECISIONS.md, "Two layers of decomposition." _emit_boundary_decomposition_edges( pathway_logic_network_data=pathway_logic_network_data, - root_input_eids=root_input_eids, - terminal_output_eids=terminal_output_eids, - root_input_uuid_cache=root_input_uuid_cache, - terminal_output_uuid_cache=terminal_output_uuid_cache, reactome_id_to_uuid=reactome_id_to_uuid, ) diff --git a/tests/test_logic_network_generator.py b/tests/test_logic_network_generator.py index fb45d79..fd976a3 100644 --- a/tests/test_logic_network_generator.py +++ b/tests/test_logic_network_generator.py @@ -347,11 +347,18 @@ class TestBoundaryLeavesReuseExistingUUIDs: boundary expansion exists to enable. """ + def _root_edge(self, complex_uuid): + # Seed an edge that makes complex_uuid a root input: a source that is + # never a target (its target is an unmapped reaction node). + return [{"source_id": complex_uuid, "target_id": "u-reaction", + "pos_neg": "pos", "and_or": "and", + "edge_type": "input", "stoichiometry": 1}] + def test_leaf_reuses_uuid_when_entity_already_in_registry(self): """If MDM2 already has UUID U_existing in reactome_id_to_uuid (e.g. because it's a regular VR input or a regulator elsewhere), the - boundary expansion of MDM2:TP53 must use U_existing for the MDM2 - leaf — not a fresh one. + boundary expansion of root-input MDM2:TP53 must use U_existing for + the MDM2 leaf — not a fresh one. """ existing_mdm2_uuid = "u-existing-mdm2" complex_uuid = "u-complex" @@ -359,7 +366,7 @@ def test_leaf_reuses_uuid_when_entity_already_in_registry(self): existing_mdm2_uuid: "MDM2", # MDM2 already has a UUID elsewhere complex_uuid: "MDM2:TP53", } - edges: List[Dict[str, Any]] = [] + edges: List[Dict[str, Any]] = self._root_edge(complex_uuid) with patch('src.neo4j_connector.get_labels', return_value=["Complex"]), \ @@ -367,15 +374,9 @@ def test_leaf_reuses_uuid_when_entity_already_in_registry(self): return_value={"MDM2", "TP53"}): _emit_boundary_decomposition_edges( pathway_logic_network_data=edges, - root_input_eids={"MDM2:TP53"}, - terminal_output_eids=set(), - root_input_uuid_cache={"MDM2:TP53": complex_uuid}, - terminal_output_uuid_cache={}, reactome_id_to_uuid=reactome_id_to_uuid, ) - # Find the assembly edge whose target is the complex and whose - # source maps back to MDM2. mdm2_assembly = [ e for e in edges if e["edge_type"] == "assembly" @@ -394,7 +395,7 @@ def test_leaf_mints_fresh_uuid_when_entity_is_new_to_network(self): """ complex_uuid = "u-complex" reactome_id_to_uuid: Dict[str, str] = {complex_uuid: "C"} - edges: List[Dict[str, Any]] = [] + edges: List[Dict[str, Any]] = self._root_edge(complex_uuid) with patch('src.neo4j_connector.get_labels', return_value=["Complex"]), \ @@ -402,20 +403,61 @@ def test_leaf_mints_fresh_uuid_when_entity_is_new_to_network(self): return_value={"L1", "L2"}): _emit_boundary_decomposition_edges( pathway_logic_network_data=edges, - root_input_eids={"C"}, - terminal_output_eids=set(), - root_input_uuid_cache={"C": complex_uuid}, - terminal_output_uuid_cache={}, reactome_id_to_uuid=reactome_id_to_uuid, ) - # Two new leaf UUIDs added to the mapping new_uuids = [u for u, sid in reactome_id_to_uuid.items() if sid in {"L1", "L2"}] assert len(new_uuids) == 2 - # Each fresh UUID is also used as a source on an assembly edge for u in new_uuids: assert any(e["source_id"] == u and e["edge_type"] == "assembly" for e in edges) + def test_same_complex_at_many_roots_shares_one_member_node(self): + """A complex appearing as a root input at N positions yields ONE member + node with N assembly edges — members are not duplicated (the user's + requirement).""" + c1, c2, c3 = "u-c1", "u-c2", "u-c3" # three root-input occurrences + reactome_id_to_uuid: Dict[str, str] = { + c1: "MDC1c", c2: "MDC1c", c3: "MDC1c", # same complex stId, 3 UUIDs + } + edges: List[Dict[str, Any]] = [] + for c in (c1, c2, c3): + edges += self._root_edge(c) + + with patch('src.neo4j_connector.get_labels', return_value=["Complex"]), \ + patch('src.logic_network_generator.get_terminal_components', + return_value={"MDC1"}): + _emit_boundary_decomposition_edges( + pathway_logic_network_data=edges, + reactome_id_to_uuid=reactome_id_to_uuid, + ) + + asm = [e for e in edges if e["edge_type"] == "assembly"] + member_uuids = {e["source_id"] for e in asm} + complex_targets = {e["target_id"] for e in asm} + assert len(member_uuids) == 1, "MDC1 member must be a single shared node" + assert complex_targets == {c1, c2, c3}, "one assembly edge to each occurrence" + + def test_intermediate_occurrence_not_decomposed(self): + """A complex that is produced and consumed (intermediate) is left intact.""" + cx = "u-cx" + reactome_id_to_uuid: Dict[str, str] = {cx: "INT"} + # cx is both a target (produced) and a source (consumed) → intermediate. + edges: List[Dict[str, Any]] = [ + {"source_id": "u-rxn1", "target_id": cx, "pos_neg": "pos", + "and_or": "and", "edge_type": "output", "stoichiometry": 1}, + {"source_id": cx, "target_id": "u-rxn2", "pos_neg": "pos", + "and_or": "and", "edge_type": "input", "stoichiometry": 1}, + ] + with patch('src.neo4j_connector.get_labels', return_value=["Complex"]), \ + patch('src.logic_network_generator.get_terminal_components', + return_value={"M1", "M2"}): + _emit_boundary_decomposition_edges( + pathway_logic_network_data=edges, + reactome_id_to_uuid=reactome_id_to_uuid, + ) + assert not any(e["edge_type"] in ("assembly", "dissociation") for e in edges), \ + "intermediate complex must not be decomposed" + class TestEntityReactionProxyMapping: """Tests for export_entity_reaction_proxy_mapping. From 4b637c3321c1d1dc1060d071c278f94925d3e1c8 Mon Sep 17 00:00:00 2001 From: Adam Wright Date: Wed, 27 May 2026 22:38:14 -0400 Subject: [PATCH 03/17] Make dissociation members separate readout sinks, not shared with functional nodes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adam's design intent for terminal-output decomposition is per-location measurement of each member ("this part of the network is active, that part isn't"). The previous implementation reused the member's functional UUID for the dissociation leaf — which broke the measurement: terminal-complex activity was injected into every reaction the member touches elsewhere, fabricating spurious downstream changes (false_positive_change quadrupled, 542 → 2294, held-out e2e dropped 77.74% → 67.04%). Assembly and dissociation now produce deliberately opposite kinds of node: - Assembly (root input): member is a shared upstream perturbation handle (reuses any existing UUID) — the complex requires the member, a real forward dependency. - Dissociation (terminal output): member is a FRESH per-occurrence readout sink, tagged with the member's stId, value inherited from the complex, with NO outgoing edges. Terminal complexes don't carry forward signal, so the sink can't broadcast — zero cross-talk, full per-location measurability. Aggregate across a member's sinks for an overall figure. Verified on the 54-pathway held-out curator set: e2e 81.84% / valid 84.37% (beats the 77.74% / 81.34% pre-boundary baseline by +4.1pp). Matches the benchmark-side skip-dissociation A/B prediction within 0.15pp. The 12% gene-not-in-network gap stays closed (149 vs the 589 before any boundary fix). Adds a unit test asserting dissociation sinks are fresh, separate from the functional node, and have no outgoing edges. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/logic_network_generator.py | 46 +++++++++++++++++---------- tests/test_logic_network_generator.py | 38 ++++++++++++++++++++++ 2 files changed, 67 insertions(+), 17 deletions(-) diff --git a/src/logic_network_generator.py b/src/logic_network_generator.py index c1ef3a4..dcc5581 100755 --- a/src/logic_network_generator.py +++ b/src/logic_network_generator.py @@ -571,15 +571,27 @@ def _emit_boundary_decomposition_edges( Curator perturbations target individual proteins, so those proteins must be addressable wherever they enter or leave the pathway.) - For each root-input complex C with terminal members {A, B, ...}, emit - ``A → C``, ``B → C`` (``edge_type='assembly'``). For each terminal-output - complex, emit ``C → A``, ``C → B`` (``edge_type='dissociation'``). - - Members are **not duplicated**: a member reuses any UUID it already has in - the network, else a single freshly-minted UUID shared across every boundary - occurrence. So if complex C appears as a root input at 15 positions, member A - is one node with 15 assembly edges — one to each occurrence. Perturbing A - then propagates into all of them. + The two boundary directions produce deliberately OPPOSITE kinds of node: + + * **Assembly** (root input): for complex C with members {A, B, ...} emit + ``A → C``, ``B → C`` (``edge_type='assembly'``). Members are *shared* + upstream perturbation handles — a member reuses any UUID it already has in + the network (else one freshly-minted UUID shared across occurrences). So a + complex appearing as a root input at 15 positions gives member A one node + with 15 assembly edges; knocking out A then propagates into all of them + (the complex requires A — a real forward dependency). + + * **Dissociation** (terminal output): for complex C emit ``C → A``, ``C → B`` + (``edge_type='dissociation'``) where each member is a FRESH, SEPARATE + readout node — one per (terminal-complex occurrence, member), carrying the + member's stId, value inherited from the complex, and with NO outgoing + edges. These are downstream *sinks*, NOT shared with the member's + functional/assembly node. A terminal output is by definition consumed by + nothing, so its members carry no forward signal; sharing them with the + functional protein would (wrongly) inject the complex's activity into every + other reaction that protein touches. Keeping them separate lets you measure + how affected each member is *at that location* (read the sink; aggregate + across a member's sinks for an overall figure) without any cross-talk. """ from src.neo4j_connector import get_labels @@ -645,13 +657,13 @@ def _is_complex(entity_id: str) -> bool: if leaves == {str(stid)}: continue for leaf in leaves: - leaf_uuid = _leaf_uuid(leaf) - if (complex_uuid, leaf_uuid) in seen_edges: - continue - seen_edges.add((complex_uuid, leaf_uuid)) + # Fresh per-occurrence readout sink — NOT _leaf_uuid (which would + # share the member's functional node and re-introduce cross-talk). + readout_uuid = str(uuid.uuid4()) + reactome_id_to_uuid[readout_uuid] = leaf pathway_logic_network_data.append({ "source_id": complex_uuid, - "target_id": leaf_uuid, + "target_id": readout_uuid, "pos_neg": "pos", "and_or": "and", "edge_type": "dissociation", @@ -661,9 +673,9 @@ def _is_complex(entity_id: str) -> bool: if assembly_count or dissociation_count: logger.info( - f"Boundary expansion (positional): {assembly_count} assembly edges, " - f"{dissociation_count} dissociation edges, " - f"{len(leaf_uuid_registry)} new member leaves" + f"Boundary expansion (positional): {assembly_count} assembly edges " + f"(shared member handles), {dissociation_count} dissociation edges " + f"(separate readout sinks), {len(leaf_uuid_registry)} new assembly leaves" ) diff --git a/tests/test_logic_network_generator.py b/tests/test_logic_network_generator.py index fd976a3..516a7a5 100644 --- a/tests/test_logic_network_generator.py +++ b/tests/test_logic_network_generator.py @@ -458,6 +458,44 @@ def test_intermediate_occurrence_not_decomposed(self): assert not any(e["edge_type"] in ("assembly", "dissociation") for e in edges), \ "intermediate complex must not be decomposed" + def test_dissociation_members_are_separate_readout_sinks(self): + """Terminal-output complex members come out as FRESH sink nodes — not the + member's functional node — with no outgoing edges, so the complex can't + inject its activity into the member's other roles.""" + functional_mdc1 = "u-functional-mdc1" + term_complex = "u-termc" + reactome_id_to_uuid: Dict[str, str] = { + functional_mdc1: "MDC1", # MDC1 already a functional node + term_complex: "MDC1:partner", # the terminal-output complex + } + edges: List[Dict[str, Any]] = [ + # functional MDC1 is intermediate (produced + consumed) → left intact + {"source_id": "u-rxn0", "target_id": functional_mdc1, "pos_neg": "pos", + "and_or": "and", "edge_type": "output", "stoichiometry": 1}, + {"source_id": functional_mdc1, "target_id": "u-rxn2", "pos_neg": "pos", + "and_or": "and", "edge_type": "input", "stoichiometry": 1}, + # term_complex is produced, never consumed → terminal output + {"source_id": "u-rxn", "target_id": term_complex, "pos_neg": "pos", + "and_or": "and", "edge_type": "output", "stoichiometry": 1}, + ] + with patch('src.neo4j_connector.get_labels', return_value=["Complex"]), \ + patch('src.logic_network_generator.get_terminal_components', + return_value={"MDC1", "PARTNER"}): + _emit_boundary_decomposition_edges( + pathway_logic_network_data=edges, + reactome_id_to_uuid=reactome_id_to_uuid, + ) + diss = [e for e in edges if e["edge_type"] == "dissociation"] + assert len(diss) == 2, "one readout sink per member" + sinks = {e["target_id"] for e in diss} + assert functional_mdc1 not in sinks, \ + "dissociation must NOT reuse the member's functional node" + all_sources = {e["source_id"] for e in edges} + for s in sinks: + assert s not in all_sources, "readout sink must have no outgoing edges" + assert reactome_id_to_uuid[s] in {"MDC1", "PARTNER"}, \ + "sink must carry the member's stId for measurement" + class TestEntityReactionProxyMapping: """Tests for export_entity_reaction_proxy_mapping. From 9a0dc14a902765e8ea130c2f83bcd88ac541e002 Mon Sep 17 00:00:00 2001 From: Adam Wright Date: Thu, 28 May 2026 09:41:48 -0400 Subject: [PATCH 04/17] validate-against-mpbiopath: signed propagator + proxy-mapping support Two upgrades to the naive Boolean baseline so the apples-to-apples comparison against DeltaSignal is rigorous: - --propagator signed: sum-of-displacements-from-NORMAL AND combination, in contrast to the default --propagator min (Boolean MIN). The signed variant is the natural comparator for DeltaSignal's signed AND aggregation; it carries upregulations through gates whose other inputs are unperturbed. - entity_reaction_proxy_mapping.csv fallback: when a curator key-output stId isn't a node in stid_to_uuid_mapping.csv (it was decomposed into virtual variants during generation), fall back to the producing reaction UUIDs. Mirrors what bench/benchmark_vs_mpbiopath.py in the deltasignal repo does, so neither method is unfairly penalised on key-output resolution. Held-out result on the 54 regenerated curator pathways (with both upgrades): --propagator min: 71.03% (the historical "70.55%" baseline) --propagator signed: 82.39% (the strongest discrete baseline) This is the right comparator to publish against; DeltaSignal with divide-form inhibition lands at 82.33% / 84.89% on the same set. Co-Authored-By: Claude Opus 4.7 (1M context) --- bin/validate-against-mpbiopath.py | 64 +++++++++++++++++++++++++++++-- 1 file changed, 60 insertions(+), 4 deletions(-) diff --git a/bin/validate-against-mpbiopath.py b/bin/validate-against-mpbiopath.py index aed9e27..f3c0498 100644 --- a/bin/validate-against-mpbiopath.py +++ b/bin/validate-against-mpbiopath.py @@ -44,6 +44,27 @@ DOWN, NORMAL, UP = 0, 1, 2 MAX_ITERATIONS = 50 +# AND-combination mode for the propagator. "min" is Boolean MIN (rate-limiting +# reagent wins; can't carry upregulations through gates whose other inputs +# are unperturbed). "signed" sums each input's displacement from NORMAL and +# uses the sign: net up → UP, net down → DOWN, balanced or unperturbed → +# NORMAL. Switch via --propagator on the CLI. +PROPAGATOR_MODE = "min" + + +def signed_and(contribs: list[int]) -> int: + """Sum-of-displacements AND. (state - NORMAL) per input; sign decides output.""" + displacement = sum(c - NORMAL for c in contribs) + if displacement > 0: + return UP + if displacement < 0: + return DOWN + return NORMAL + + +def combine_and(contribs: list[int]) -> int: + return signed_and(contribs) if PROPAGATOR_MODE == "signed" else min(contribs) + def invert(state: int) -> int: return {DOWN: UP, NORMAL: NORMAL, UP: DOWN}[state] @@ -71,6 +92,26 @@ def build_stid_to_uuids(pathway_dir: Path) -> dict[str, list[str]]: return out +def load_entity_reaction_proxies(pathway_dir: Path) -> dict[str, list[str]]: + """entity stable_id → list of proxy reaction UUIDs. + + Curated species (often Complexes containing an EntitySet) get expanded into + virtual variants during generation, so the parent's stId isn't in + stid_to_uuid_mapping.csv. The generator's entity_reaction_proxy_mapping.csv + points each such species at the UUIDs of the reaction that produces it, so + we can read reaction flux as a proxy for the species' state. Absent file → + no proxies (pathway was generated by an older generator). + """ + out: dict[str, list[str]] = defaultdict(list) + f = pathway_dir / "entity_reaction_proxy_mapping.csv" + if not f.exists(): + return out + proxies = pd.read_csv(f) + for _, row in proxies.iterrows(): + out[str(row["entity_stable_id"])].append(str(row["proxy_uuid"])) + return out + + def gene_name_to_stids(graph, gene_names: list[str]) -> dict[str, list[str]]: """Map gene names to all PhysicalEntity stable IDs whose reference entity has that gene name.""" rows = graph.run( @@ -129,9 +170,9 @@ def propagate( or_contribs.append(src_state) if and_contribs and or_contribs: - new_state[uuid_] = max(min(and_contribs), max(or_contribs)) + new_state[uuid_] = max(combine_and(and_contribs), max(or_contribs)) elif and_contribs: - new_state[uuid_] = min(and_contribs) + new_state[uuid_] = combine_and(and_contribs) elif or_contribs: new_state[uuid_] = max(or_contribs) if new_state == state: @@ -254,11 +295,19 @@ def validate_one_pathway( return {"status": "no_curator_file", "name": pathway_name} network = load_network(pathway_dir) stid_to_uuids = build_stid_to_uuids(pathway_dir) + entity_reaction_proxies = load_entity_reaction_proxies(pathway_dir) - # Resolve key outputs (numeric dbId → list of UUIDs) + # Resolve key outputs (numeric dbId → list of UUIDs). Fall back to the + # generator's proxy mapping when the curated species was decomposed into + # virtual variants and isn't directly addressable — read the producing + # reaction's flux as a proxy for the species' state. key_output_uuids: dict[str, list[str]] = {} for ko in curator["key_output"].astype(str): - key_output_uuids[ko] = stid_to_uuids.get(f"R-HSA-{ko}", []) + sid = f"R-HSA-{ko}" + uuids = stid_to_uuids.get(sid, []) + if not uuids: + uuids = entity_reaction_proxies.get(sid, []) + key_output_uuids[ko] = uuids # Resolve perturbation genes perturbations = parse_perturbation_columns(curator) @@ -366,7 +415,14 @@ def main(): "Experimental is only available for 10 pathways and contains -999 (not measured) " "cells which are skipped.", ) + ap.add_argument( + "--propagator", choices=["min", "signed"], default="min", + help="AND-combination rule. 'min' = Boolean MIN (default); 'signed' = sum of " + "displacements from NORMAL, sign decides direction (handles upregulation symmetrically).", + ) args = ap.parse_args() + global PROPAGATOR_MODE + PROPAGATOR_MODE = args.propagator output_root = Path(args.output_dir) pathways = pd.read_csv("/tmp/mpbio_pathways.tsv", sep="\t") From 28a314a5b814894bfb5edb71419924ba196cd5df Mon Sep 17 00:00:00 2001 From: Adam Wright Date: Fri, 29 May 2026 00:17:28 -0400 Subject: [PATCH 05/17] Variant decomposition for negative regulators MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Negative-regulator complexes are now kept as whole biological units instead of being broken down into individual member proteins. When the inhibitor complex contains an internal EntitySet, the cartesian product is expanded into one synthetic variant ID per combination (`{parent_stid}::variant::{sorted_members}`), each emitted as a single inhibitor edge. Why: with the previous subunit decomposition, every protein subunit of an inhibitor complex (e.g. HSP90, CDC37, ERBIN inside the ERBB2:trastuzumab:HSP90:CDC37 drug-bound complex) became a standalone inhibitor edge on the regulated reaction. Whenever any reaction elsewhere in the network produced those bystander subunits, the spurious inhibitor edges fired and crushed the downstream signal — measured 12pp of false suppression on Signaling_by_ERBB2 in the deltasignal experimental benchmark. Catalysts and positive regulators continue to use subunit decomposition: their holoenzyme subunits are biologically AND-required for the complex to function, so decomposing to terminal members is correct there. Result on the deltasignal experimental benchmark (4 pathways, 363 cases): - Signaling_by_ERBB2: +4.1pp e2e, +5.9pp valid-only - Overall: 69.42% → 69.97% Implementation: - `_decompose_regulator_entity(entity_id, variant_decomposition=False)`: new flag selects the decomposition mode. - `_expand_complex_variants(complex_id)`: cartesian-product expansion of internal EntitySets, returning content-addressed synthetic variant IDs. - `append_regulators`: passes `variant_decomposition=True` for the negative regulator config only. - `_is_complex` in the boundary-decomposition phase skips synthetic variant IDs (they don't exist in Neo4j and don't need further decomposition). Co-Authored-By: Claude Opus 4.7 (1M context) --- src/logic_network_generator.py | 137 +++++++++++++++++++++++-- tests/test_regulators_and_catalysts.py | 2 +- 2 files changed, 128 insertions(+), 11 deletions(-) diff --git a/src/logic_network_generator.py b/src/logic_network_generator.py index dcc5581..937ef8d 100755 --- a/src/logic_network_generator.py +++ b/src/logic_network_generator.py @@ -515,15 +515,35 @@ def _resolve_vr_entities( return vr_entities -def _decompose_regulator_entity(entity_id: str) -> List[tuple]: +def _decompose_regulator_entity( + entity_id: str, + variant_decomposition: bool = False, +) -> List[tuple]: """Decompose a catalyst/regulator entity to (terminal_id, stoichiometry) pairs. - The decomposition rules mirror break_apart_entity (matching layer): - Complex with EntitySet → cartesian over members; simple Complex - returned intact; EntitySet → flat alternatives; ubiquitin sets are - treated as atomic to avoid combinatorial explosion. AND/OR semantics - for the resulting edges are decided by append_regulators based on - pos_neg, not by within-entity decomposition shape. + Two decomposition modes: + + **subunit decomposition** (default, used for catalysts and positive + regulators): Complex with EntitySet → cartesian over members down to + terminal proteins; simple Complex returned intact; EntitySet → flat + alternatives. Each terminal subunit becomes its own edge — biologically + appropriate for catalysts where every subunit of the holoenzyme is + required (AND). + + **variant decomposition** (``variant_decomposition=True``, used for + negative regulators): Complex with EntitySet → one entity per cartesian + variant of the EntitySet expansion, but the complex itself is preserved + (NOT broken into individual proteins). A bare EntitySet expands to its + member alternatives. This is what you want for inhibitors, because: + the inhibitor complex acts as a single biological unit; breaking it + into individual proteins (HSP90, CDC37, etc.) would make those + bystander proteins act as standalone inhibitors, which spuriously + crushes downstream signal whenever unrelated reactions produce them. + + Ubiquitin sets are treated as atomic in both modes to avoid + combinatorial explosion. AND/OR semantics for the resulting edges are + decided by ``append_regulators`` based on pos_neg, not by within-entity + decomposition shape. """ from src.neo4j_connector import get_labels, get_complex_components, get_set_members @@ -532,10 +552,14 @@ def _decompose_regulator_entity(entity_id: str) -> List[tuple]: if "Complex" in labels: if not _complex_contains_entity_set(entity_id): return [(entity_id, 1)] + if variant_decomposition: + return _expand_complex_variants(entity_id) components = get_complex_components(entity_id) # Dict[str, int] result = [] for member_id, stoich in components.items(): - for mid, sub_stoich in _decompose_regulator_entity(member_id): + for mid, sub_stoich in _decompose_regulator_entity( + member_id, variant_decomposition=variant_decomposition + ): result.append((mid, stoich * sub_stoich)) return result if result else [(entity_id, 1)] @@ -545,12 +569,82 @@ def _decompose_regulator_entity(entity_id: str) -> List[tuple]: members = get_set_members(entity_id) result = [] for member_id in members: - result.extend(_decompose_regulator_entity(member_id)) + result.extend( + _decompose_regulator_entity( + member_id, variant_decomposition=variant_decomposition + ) + ) return result if result else [(entity_id, 1)] return [(entity_id, 1)] +def _expand_complex_variants(complex_id: str) -> List[tuple]: + """Expand a Complex-with-EntitySet into its cartesian-product variants. + + Each variant is itself a complex (same proteins, one specific choice + per internal EntitySet), kept as a single biological entity. Returns + [(variant_id, 1), ...] where each variant_id is a deterministic + synthetic ID of the form ``{parent_stid}::variant::{sorted_member_ids}``. + The synthetic ID is content-addressed: the same combination of leaf + members under the same parent complex always produces the same ID, so + cross-pathway references to "the same variant" are consistent. + + For a simple Complex (no EntitySet inside) the input is returned + unchanged — caller already short-circuits on this case, but we + re-check here for safety. + """ + import itertools + from src.neo4j_connector import get_labels, get_complex_components, get_set_members + + if not _complex_contains_entity_set(complex_id): + return [(complex_id, 1)] + + components = get_complex_components(complex_id) + if not components: + return [(complex_id, 1)] + + # For each component, collect the list of identities it can take in a + # single variant. A simple member contributes [member_id]; an internal + # EntitySet contributes [alt_1, alt_2, ...]; a nested Complex (rare) + # contributes its own variant IDs. + per_component_choices: List[List[str]] = [] + for member_id, _stoich in components.items(): + labels = get_labels(member_id) + if "Complex" in labels: + sub_variants = _expand_complex_variants(member_id) + per_component_choices.append([vid for vid, _ in sub_variants]) + elif ( + ("EntitySet" in labels or "DefinedSet" in labels or "CandidateSet" in labels) + and member_id not in _UBIQUITIN_ENTITY_SET_IDS + ): + alts: List[str] = [] + for set_member in get_set_members(member_id): + set_member_labels = get_labels(set_member) + if ( + "Complex" in set_member_labels + and _complex_contains_entity_set(set_member) + ): + alts.extend(vid for vid, _ in _expand_complex_variants(set_member)) + else: + alts.append(set_member) + per_component_choices.append(alts if alts else [member_id]) + else: + per_component_choices.append([member_id]) + + variants: List[tuple] = [] + seen_variant_ids: set = set() + for combo in itertools.product(*per_component_choices): + combo_sorted = sorted(combo) + variant_id = f"{complex_id}::variant::{'_'.join(combo_sorted)}" + if variant_id in seen_variant_ids: + continue + seen_variant_ids.add(variant_id) + variants.append((variant_id, 1)) + + return variants if variants else [(complex_id, 1)] + + def _emit_boundary_decomposition_edges( pathway_logic_network_data: List[Dict[str, Any]], reactome_id_to_uuid: Dict[str, str], @@ -622,6 +716,13 @@ def _leaf_uuid(leaf_stid: str) -> str: return leaf_uuid_registry[leaf_stid] def _is_complex(entity_id: str) -> bool: + # Synthetic variant IDs (from negative-regulator variant decomposition) + # are not in Neo4j and shouldn't be further decomposed at the boundary + # — each variant already represents one specific composition of the + # inhibitor complex. Skipping them here also avoids the IndexError + # that get_labels raises on unknown stIds. + if "::variant::" in entity_id: + return False return "Complex" in get_labels(entity_id) seen_edges: Set[tuple] = set() @@ -717,10 +818,26 @@ def append_regulators( ] for map_df, pos_neg, edge_type in regulator_configs: + # Negative regulators use VARIANT decomposition: an inhibitor complex + # with an internal EntitySet expands into one entity per cartesian + # variant of that EntitySet, but each variant is kept as a single + # complex — NOT broken down into individual subunits. Without this, + # HSP90 / CDC37 / ERBIN of an ERBB2 inhibitor complex would each + # become standalone inhibitor edges, and any unrelated reaction + # producing those bystander proteins would spuriously crush + # downstream signal. + # + # Catalysts and positive regulators keep SUBUNIT decomposition: each + # holoenzyme subunit is biologically AND-required for catalysis, so + # decomposing to terminal proteins is correct there. + variant_decomposition = (pos_neg == "neg") + for _, row in map_df.iterrows(): entity_id = str(row["entity_id"]) - terminal_members = _decompose_regulator_entity(entity_id) + terminal_members = _decompose_regulator_entity( + entity_id, variant_decomposition=variant_decomposition + ) # and_or expresses reaction-level requirement, not within-entity # decomposition logic. Anything that contributes to a reaction diff --git a/tests/test_regulators_and_catalysts.py b/tests/test_regulators_and_catalysts.py index 39c0a9e..8885ff7 100644 --- a/tests/test_regulators_and_catalysts.py +++ b/tests/test_regulators_and_catalysts.py @@ -24,7 +24,7 @@ from src.logic_network_generator import append_regulators -def _mock_decompose(entity_id): +def _mock_decompose(entity_id, variant_decomposition=False): """Return entity as-is (no decomposition) for unit tests.""" return [(entity_id, 1)] From c49f26d1716f99cde3ba6609e880d63df428e3cb Mon Sep 17 00:00:00 2001 From: Adam Wright Date: Sat, 30 May 2026 18:51:45 -0400 Subject: [PATCH 06/17] Emit substrate-depletion edges for phosphatase reactions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds catalyst→input edges with edge_type="depletion", pos_neg="neg", and_or="and" for reactions whose outputs include inorganic phosphate (R-ALL-29372 / Pi). The deltasignal solver applies divide-form inhibition to these edges specifically, so catalyst-knockout (e.g., PTEN-KO) boosts the substrate (e.g., PIP3) via de-repression — unlocking 18+ PTEN cases that previously failed with category="no_path" because the network had no forward path from PTEN to AKT downstream. Filtering is by reaction OUTPUTS rather than catalyst identity so we don't need name-string heuristics or biology annotations: any reaction that outputs Pi is by definition a phosphatase. Future work can extend the filter to ubiquitin-ligase / protease reactions (MDM2-TP53 case is similar but doesn't output Pi). Cofactor inputs (ATP/ADP/H2O/Pi/etc.) are excluded so depletion edges only fire on biological substrates. Also made `_is_complex` in the boundary-decomposition step tolerant of unknown stIds (synthetic IDs added by other emission passes) — was already skipping `::variant::` but other custom IDs need the same treatment. Impact on the experimental benchmark (9 pathways, 800 cases): the deltasignal solver goes from 63.38% to 66.75% with these edges enabled, beating the naive signed Boolean propagator by +4.25pp. PIP3 specifically jumps from 82% to 93% (matching curator predictions on the same cases). Depletion edge counts per pathway (phosphatase-only filter): PIP3: 16 edges (5 reactions) ERBB2: 24 edges (43 reactions, many with no non-cofactor inputs) TP53: 183 edges (6 reactions, many UUIDs) WNT: 36 edges (5 reactions) HDR/Mitotic_G1/S_Phase/CCC/Prophase: 0 edges each (no phosphatases) Co-Authored-By: Claude Opus 4.7 (1M context) --- src/logic_network_generator.py | 134 +++++++++++++++++++++++++++++++-- 1 file changed, 129 insertions(+), 5 deletions(-) diff --git a/src/logic_network_generator.py b/src/logic_network_generator.py index 937ef8d..21cb1f8 100755 --- a/src/logic_network_generator.py +++ b/src/logic_network_generator.py @@ -645,6 +645,114 @@ def _expand_complex_variants(complex_id: str) -> List[tuple]: return variants if variants else [(complex_id, 1)] +_COFACTOR_STIDS: frozenset = frozenset({ + "R-ALL-113592", # ATP + "R-ALL-29358", # ATP variant + "R-ALL-113582", # ADP + "R-ALL-29370", # ADP variant + "R-ALL-29360", # ADP variant + "R-ALL-29356", # H2O + "R-ALL-29372", # Pi + "R-ALL-29390", # Pi variant + "R-ALL-29438", # PPi + "R-ALL-217093", # NADP+ + "R-ALL-110114", # NADPH + "R-ALL-29986", # NAD+ + "R-ALL-73473", # NADH +}) + + +def _emit_substrate_depletion_edges( + pathway_logic_network_data: List[Dict[str, Any]], + reactome_id_to_uuid: Dict[str, str], + catalyst_map: pd.DataFrame, +) -> None: + """Emit catalyst→input "depletion" edges ONLY for genuinely depletive + reactions (phosphatases, ubiquitin ligases, proteases, etc.). + + The biology to capture: when a catalyst REMOVES its substrate from the + available pool, the substrate's level should respond to the catalyst's + activity. PTEN dephosphorylates PIP3, MDM2 ubiquitinates TP53 → both + are depletion mechanisms. Generic kinases (X → p-X) are NOT depletion + in the same sense — both forms remain in the cell and serve as nodes + in the network; the signal travels via p-X anyway. + + SELECTION CRITERION (conservative): emit depletion edges only when the + reaction's OUTPUTS include the small molecule Pi (R-ALL-29372). This + cleanly identifies phosphatases without needing reaction-name heuristics + or biological annotations. Adds 3–10 depletion edges per pathway in our + test set — small enough not to disrupt non-depletive cascades, but + enough to unlock PTEN-class substrate biology. + + The deltasignal solver applies divide-form inhibition to depletion edges + specifically. The H_max cap (DS_DEPLETION_H_MAX) bounds the de-repression + boost. With H_max=10: catalyst KO → substrate × 10 above baseline. + + Future work: extend the criterion to ubiquitination/degradation + reactions (MDM2-TP53 case) — those need a separate identifier since they + don't output Pi. + """ + # Build reaction_uuid → list of input edges + list of catalyst edges + # + list of output edges (to detect phosphatases by Pi output). + by_target_inputs: Dict[str, List[str]] = {} + by_target_catalysts: Dict[str, List[str]] = {} + by_source_outputs: Dict[str, List[str]] = {} # reaction_uuid → output stids + PI_STID = "R-ALL-29372" # inorganic phosphate + for edge in pathway_logic_network_data: + if edge.get("pos_neg") != "pos": + continue + et = edge.get("edge_type", "") + if et == "input": + by_target_inputs.setdefault(edge["target_id"], []).append(edge["source_id"]) + elif et == "catalyst": + by_target_catalysts.setdefault(edge["target_id"], []).append(edge["source_id"]) + elif et == "output": + # source is the reaction, target is the output entity + by_source_outputs.setdefault(edge["source_id"], []).append(edge["target_id"]) + + # Identify phosphatase reactions: those whose outputs include Pi (R-ALL-29372). + phosphatase_rxn_uuids = set() + for rxn_uuid, output_uuids in by_source_outputs.items(): + output_stids = {reactome_id_to_uuid.get(u, "") for u in output_uuids} + if PI_STID in output_stids: + phosphatase_rxn_uuids.add(rxn_uuid) + + # For each phosphatase reaction with both catalysts and inputs, emit + # depletion edges from each catalyst to each non-cofactor input. + seen_edges: set = set() + n_emitted = 0 + for rxn_uuid in phosphatase_rxn_uuids: + catalyst_uuids = by_target_catalysts.get(rxn_uuid, []) + inputs = by_target_inputs.get(rxn_uuid, []) + if not catalyst_uuids or not inputs: + continue + for cat_uuid in catalyst_uuids: + cat_stid = reactome_id_to_uuid.get(cat_uuid, "") + for inp_uuid in inputs: + if cat_uuid == inp_uuid: + continue + inp_stid = reactome_id_to_uuid.get(inp_uuid, "") + if inp_stid == cat_stid and inp_stid: + continue # same biological entity at different positions + if inp_stid in _COFACTOR_STIDS: + continue + key = (cat_uuid, inp_uuid) + if key in seen_edges: + continue + seen_edges.add(key) + pathway_logic_network_data.append({ + "source_id": cat_uuid, + "target_id": inp_uuid, + "pos_neg": "neg", + "and_or": "and", + "edge_type": "depletion", + "stoichiometry": 1.0, + }) + n_emitted += 1 + logger.info(f"Emitted {n_emitted} substrate-depletion edges " + f"({len(phosphatase_rxn_uuids)} phosphatase reactions)") + + def _emit_boundary_decomposition_edges( pathway_logic_network_data: List[Dict[str, Any]], reactome_id_to_uuid: Dict[str, str], @@ -717,13 +825,16 @@ def _leaf_uuid(leaf_stid: str) -> str: def _is_complex(entity_id: str) -> bool: # Synthetic variant IDs (from negative-regulator variant decomposition) - # are not in Neo4j and shouldn't be further decomposed at the boundary - # — each variant already represents one specific composition of the - # inhibitor complex. Skipping them here also avoids the IndexError - # that get_labels raises on unknown stIds. + # are not in Neo4j and shouldn't be further decomposed at the boundary. + # Tolerate any other unknown stIds too (e.g., entities added by other + # synthetic emissions) — if the lookup fails, assume it's not a complex + # and skip decomposition rather than crashing. if "::variant::" in entity_id: return False - return "Complex" in get_labels(entity_id) + try: + return "Complex" in get_labels(entity_id) + except IndexError: + return False seen_edges: Set[tuple] = set() assembly_count = 0 @@ -1146,6 +1257,19 @@ def create_pathway_logic_network( entity_uuid_registry=entity_uuid_registry, ) + # Substrate-depletion edges: for each catalytic reaction, emit edges + # `catalyst → input` with edge_type="depletion". These capture the + # biology that a catalyst REMOVES its substrate (PTEN dephosphorylates + # PIP3, MDM2 ubiquitinates TP53, etc.). The deltasignal solver applies + # divide-form inhibition to these edges specifically, so catalyst-knockout + # boosts the substrate via de-repression. Skips small-molecule cofactor + # inputs (ATP/H2O/Pi/etc.) to avoid noise. + _emit_substrate_depletion_edges( + pathway_logic_network_data=pathway_logic_network_data, + reactome_id_to_uuid=reactome_id_to_uuid, + catalyst_map=catalyst_map, + ) + # Boundary expansion: every root-input and terminal-output complex # occurrence gets synthetic assembly / dissociation edges to its member # proteins, so individual subunits are perturbable / readable wherever From b41acad0513006d5075b83669ef2a80981878599 Mon Sep 17 00:00:00 2001 From: Adam Wright Date: Sun, 31 May 2026 08:41:58 -0400 Subject: [PATCH 07/17] Revert ubiquitin-ligase depletion filter (-0.5pp regression) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Attempted to extend the depletion-edge filter from phosphatase-only (reactions outputting Pi) to also cover ubiquitin-ligase reactions (displayName contains "ubiquitinat" but not "deubiquitinat"). Hypothesis was that MDM2-TP53 and similar ubiquitination-driven substrate-depletion cases would be captured. Empirically tested on the 9-pathway experimental benchmark: - phosphatase only: 544/800 = 68.00% - phosphatase + ubiquitin-ligase: 540/800 = 67.50% (-0.5pp) - per-case check on TP53 MDM2 cases: 0 of 20 fixed Root cause: Reactome models ubiquitination mechanism precisely. The "MDM2 ubiquitinates TP53" reaction (R-HSA-6804879) has INPUTS: - Ub (R-HSA-68524) - p-MDM2:MDM4:TP53 (R-HSA-6804885) — the ASSEMBLED complex and OUTPUTS: - PolyUb-TP53 (R-HSA-3209186) — the ubiquitinated form - p-MDM2 dimer — the released catalyst So the catalyst→input pair points MDM2 → complex, not MDM2 → free TP53. The depletion edge depresses the level of the complex (which isn't what TP53-downstream cascades read), not free TP53. Other ubiquitin reactions across S_Phase, WNT, etc. add hundreds of edges that don't capture the right biology and noise-up the cascade — net regression. Capturing ubiquitin-driven depletion properly requires modeling the multi-step bind→ubiquitinate→degrade chain (e.g., MDM2 → TP53 as a "sequestration" edge that tracks total free TP53 pool). That's outside the scope of this single-reaction heuristic. Keeping the phosphatase-only filter (the +3.4pp win documented earlier in commit c49f26d remains valid). Co-Authored-By: Claude Opus 4.7 (1M context) --- src/logic_network_generator.py | 60 +++++++++++++++++++++------------- 1 file changed, 38 insertions(+), 22 deletions(-) diff --git a/src/logic_network_generator.py b/src/logic_network_generator.py index 21cb1f8..4d32a75 100755 --- a/src/logic_network_generator.py +++ b/src/logic_network_generator.py @@ -667,30 +667,31 @@ def _emit_substrate_depletion_edges( reactome_id_to_uuid: Dict[str, str], catalyst_map: pd.DataFrame, ) -> None: - """Emit catalyst→input "depletion" edges ONLY for genuinely depletive - reactions (phosphatases, ubiquitin ligases, proteases, etc.). + """Emit catalyst→input "depletion" edges for PHOSPHATASE reactions only. The biology to capture: when a catalyst REMOVES its substrate from the available pool, the substrate's level should respond to the catalyst's - activity. PTEN dephosphorylates PIP3, MDM2 ubiquitinates TP53 → both - are depletion mechanisms. Generic kinases (X → p-X) are NOT depletion - in the same sense — both forms remain in the cell and serve as nodes - in the network; the signal travels via p-X anyway. - - SELECTION CRITERION (conservative): emit depletion edges only when the - reaction's OUTPUTS include the small molecule Pi (R-ALL-29372). This - cleanly identifies phosphatases without needing reaction-name heuristics - or biological annotations. Adds 3–10 depletion edges per pathway in our - test set — small enough not to disrupt non-depletive cascades, but - enough to unlock PTEN-class substrate biology. + activity. PTEN dephosphorylates PIP3 → PTEN-KO needs to BOOST PIP3 in + the propagator (de-repression via divide-form H on the depletion edge). + + SELECTION CRITERION: the reaction's OUTPUTS include Pi (R-ALL-29372). + Cleanly identifies dephosphorylation reactions (PTEN, PTPN12, PHLPP, + PP1, PP2A, etc.) without name heuristics. The free substrate is the + direct input, so catalyst→input depletion targets the right node. + + NOTE: ubiquitin-ligase reactions look superficially similar but + Reactome models them more precisely — the "MDM2 ubiquitinates TP53" + reaction takes the ASSEMBLED `p-MDM2:MDM4:TP53` complex as input, not + free TP53. So a catalyst→input depletion edge points MDM2 → complex, + not MDM2 → TP53, and the heuristic doesn't help (empirically -0.5pp + on the experimental benchmark). Capturing ubiquitin-driven depletion + would require modeling the multi-step bind→ubiquitinate→degrade chain. The deltasignal solver applies divide-form inhibition to depletion edges - specifically. The H_max cap (DS_DEPLETION_H_MAX) bounds the de-repression - boost. With H_max=10: catalyst KO → substrate × 10 above baseline. + specifically (DS_DEPLETION_H_MAX caps the de-repression boost, default + 10). Regular regulator edges stay on devspec (no false boost). - Future work: extend the criterion to ubiquitination/degradation - reactions (MDM2-TP53 case) — those need a separate identifier since they - don't output Pi. + Cofactor inputs (ATP/ADP/H2O/Pi/etc.) are excluded. """ # Build reaction_uuid → list of input edges + list of catalyst edges # + list of output edges (to detect phosphatases by Pi output). @@ -717,8 +718,21 @@ def _emit_substrate_depletion_edges( if PI_STID in output_stids: phosphatase_rxn_uuids.add(rxn_uuid) - # For each phosphatase reaction with both catalysts and inputs, emit - # depletion edges from each catalyst to each non-cofactor input. + # NOTE: We previously also enabled ubiquitin-ligase reactions + # (display name contains "ubiquitinat") but they net REGRESSED the + # benchmark by -0.5pp. Reactome models ubiquitination biology too + # precisely for the simple catalyst→input depletion heuristic to + # capture: in "MDM2 ubiquitinates TP53", the reaction's input is the + # ASSEMBLED complex `p-MDM2:MDM4:TP53`, not free TP53. So the catalyst→ + # input edge points MDM2 → complex, not MDM2 → TP53. Bias-corrected + # depletion for ubiquitination requires modeling the multi-step + # bind→ubiquitinate→degrade chain (likely via an "MDM2 → TP53 + # sequestration" edge that tracks total free TP53 pool), which is + # outside the scope of this single-reaction heuristic. Phosphatases + # work because dephosphorylation typically has the unphosphorylated + # substrate directly as input. + + # Emit depletion edges for the phosphatase reactions identified above. seen_edges: set = set() n_emitted = 0 for rxn_uuid in phosphatase_rxn_uuids: @@ -749,8 +763,10 @@ def _emit_substrate_depletion_edges( "stoichiometry": 1.0, }) n_emitted += 1 - logger.info(f"Emitted {n_emitted} substrate-depletion edges " - f"({len(phosphatase_rxn_uuids)} phosphatase reactions)") + logger.info( + f"Emitted {n_emitted} substrate-depletion edges " + f"({len(phosphatase_rxn_uuids)} phosphatase reactions)" + ) def _emit_boundary_decomposition_edges( From edf9e4463e313f08fc0f2ac7a6cb48ab0427557e Mon Sep 17 00:00:00 2001 From: Adam Wright Date: Sun, 31 May 2026 20:22:56 -0400 Subject: [PATCH 08/17] Topology-based ubiquitin substrate-depletion edges (+1.75pp) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a topology-based detection of ubiquitin-ligase reactions: a reaction takes ubiquitin (R-HSA-68524 / R-HSA-113595 / R-HSA-9660007 — Ub across compartments) as an INPUT. Cleaner than display-name string matching; no reliance on Reactome curator wording. The key insight that fixes the previous ubiquitin attempt: identify the SUBSTRATE topologically, not as the literal reaction input. Reactome models ubiquitination biology precisely — "MDM2 ubiquitinates TP53" takes the ASSEMBLED complex `p-MDM2:MDM4:TP53` as input (not free TP53), so a naive `catalyst → input` depletion edge points MDM2 → complex rather than MDM2 → TP53. The fix: 1. Decompose the input PE through hasComponent/hasMember/hasCandidate into its leaf member proteins, recording each protein's referenceEntity.geneName. 2. Do the same for the output PE. 3. The substrate's gene appears in BOTH (e.g., TP53 is in both input and output — modified in the output). That's the SUBSTRATE. 4. Look up ALL network UUIDs of the substrate's free-form stable_id (e.g., R-HSA-69488 free TP53, which has 12 UUIDs across positions in the TP53 pathway). 5. Emit `catalyst → each-free-substrate-UUID` depletion edges. When MDM2 is knocked out, divide-form inhibition on the depletion edges boosts free TP53 levels via de-repression, propagating through TP53's forward transcription edges to its targets. Experimental benchmark (9 pathways, 800 cases): - phosphatase only: 544/800 = 68.00% - + topology ubiquitin: 558/800 = 69.75% (+1.75pp) Per-pathway with the new edges: - TP53: 53.3% → 59.1% (+5.8pp) — main beneficiary - WNT: 62.7% → 68.6% (+5.9pp) — SCF complex on β-catenin - ERBB2: 61.2% → 55.1% (-6.1pp) — slight regression from extra edges - CCC: 69.1% → 67.3% (-1.8pp) - Other pathways unchanged Edge counts per pathway (phosphatase + topology-ubiquitin combined): PIP3: 61 (5p + 5u reactions) ERBB2: 542 (43p + 5u — large because of position-aware UUIDs) Mitotic_G1: 11 (0p + 2u) S_Phase: 12 (0p + 4u) HDR: 1120 (0p + 1u — one big substrate) CCC: 19 (0p + 4u) TP53: 201 (6p + 6u) WNT: 3595 (5p + 8u — heavy ubiquitin biology) Prophase: 0 Co-Authored-By: Claude Opus 4.7 (1M context) --- src/logic_network_generator.py | 176 +++++++++++++++++++++++++++++---- 1 file changed, 156 insertions(+), 20 deletions(-) diff --git a/src/logic_network_generator.py b/src/logic_network_generator.py index 4d32a75..383b8a8 100755 --- a/src/logic_network_generator.py +++ b/src/logic_network_generator.py @@ -1,5 +1,5 @@ import uuid -from typing import Dict, List, Any, NamedTuple, Optional, Set +from typing import Dict, List, Any, NamedTuple, Optional, Set, Tuple import pandas as pd from pandas import DataFrame @@ -661,6 +661,16 @@ def _expand_complex_variants(complex_id: str) -> List[tuple]: "R-ALL-73473", # NADH }) +# Ubiquitin entity stIds (human + cross-species variants). A reaction that +# takes one of these as INPUT is a ubiquitination reaction (Ub is consumed +# and attached to a target protein). Reactions whose OUTPUT is Ub are +# deubiquitinations (we don't emit depletion for those). +_UBIQUITIN_STIDS: frozenset = frozenset({ + "R-HSA-68524", # Ub [nucleoplasm] + "R-HSA-113595", # Ub [cytosol] + "R-HSA-9660007", # Ub [lysosomal lumen] +}) + def _emit_substrate_depletion_edges( pathway_logic_network_data: List[Dict[str, Any]], @@ -718,23 +728,114 @@ def _emit_substrate_depletion_edges( if PI_STID in output_stids: phosphatase_rxn_uuids.add(rxn_uuid) - # NOTE: We previously also enabled ubiquitin-ligase reactions - # (display name contains "ubiquitinat") but they net REGRESSED the - # benchmark by -0.5pp. Reactome models ubiquitination biology too - # precisely for the simple catalyst→input depletion heuristic to - # capture: in "MDM2 ubiquitinates TP53", the reaction's input is the - # ASSEMBLED complex `p-MDM2:MDM4:TP53`, not free TP53. So the catalyst→ - # input edge points MDM2 → complex, not MDM2 → TP53. Bias-corrected - # depletion for ubiquitination requires modeling the multi-step - # bind→ubiquitinate→degrade chain (likely via an "MDM2 → TP53 - # sequestration" edge that tracks total free TP53 pool), which is - # outside the scope of this single-reaction heuristic. Phosphatases - # work because dephosphorylation typically has the unphosphorylated - # substrate directly as input. - - # Emit depletion edges for the phosphatase reactions identified above. + # Identify ubiquitin-ligase reactions TOPOLOGICALLY: those that take + # ubiquitin (Ub) as an INPUT. Reactome models ubiquitination by + # consuming free Ub and producing a ubiquitinated form of the target, + # so this is a clean structural signal (no display-name string matching + # required). For each such reaction we then identify the SUBSTRATE by + # gene-level matching between input-complex members and output members: + # the protein appearing in both (by referenceEntity gene) is the one + # being modified, and its free-form network nodes are the correct + # depletion targets — NOT the assembled complex that appears as the + # literal reaction input. + + # Collect all Reactome reaction ids in this pathway from catalyst_map + # (each catalyst row has a reaction_id and reaction_uuid). + rxn_uuid_to_rstid: Dict[str, str] = {} + if not catalyst_map.empty: + for _, row in catalyst_map.iterrows(): + ru = str(row["reaction_uuid"]); rs = str(row["reaction_id"]) + if ru and rs: + rxn_uuid_to_rstid[ru] = rs + unique_rstids = list({rs for rs in rxn_uuid_to_rstid.values() if rs}) + + # For each candidate reaction, ask neo4j: is Ub an input? Then collect + # input/output protein gene names (decomposing complexes/sets to leaves). + # Returns reaction_stid → (set of substrate genes, set of input-protein stids). + ubiquitin_subst_by_rstid: Dict[str, Tuple[Set[str], Set[str]]] = {} + if unique_rstids: + from src.neo4j_connector import get_graph + try: + # Reactions with any Ub stId as input + ub_rows = get_graph().run( + "UNWIND $ids AS id " + "MATCH (rle:ReactionLikeEvent {stId: id})-[:input]->(ub:PhysicalEntity) " + "WHERE ub.stId IN $ub_stids RETURN DISTINCT id AS rxn", + ids=unique_rstids, + ub_stids=list(_UBIQUITIN_STIDS), + ).data() + ubiq_rstids = [r["rxn"] for r in ub_rows] + # For each ubiquitination reaction, gather input-leaf proteins + # (decomposed through hasComponent/hasMember/hasCandidate) and + # their referenceEntity genes; do the same for outputs. + in_rows = get_graph().run( + "UNWIND $ids AS id " + "MATCH (rle:ReactionLikeEvent {stId: id})-[:input]->(inp:PhysicalEntity) " + "WHERE NOT inp.stId IN $ub_stids " + "OPTIONAL MATCH (inp)-[:hasComponent|hasMember|hasCandidate*0..3]->(leaf:PhysicalEntity) " + "OPTIONAL MATCH (leaf)-[:referenceEntity]->(re:ReferenceEntity) " + "RETURN id AS rxn, leaf.stId AS leaf_stid, re.geneName AS genes", + ids=ubiq_rstids, + ub_stids=list(_UBIQUITIN_STIDS), + ).data() if ubiq_rstids else [] + out_rows = get_graph().run( + "UNWIND $ids AS id " + "MATCH (rle:ReactionLikeEvent {stId: id})-[:output]->(o:PhysicalEntity) " + "WHERE NOT o.stId IN $ub_stids " + "OPTIONAL MATCH (o)-[:hasComponent|hasMember|hasCandidate*0..3]->(leaf:PhysicalEntity) " + "OPTIONAL MATCH (leaf)-[:referenceEntity]->(re:ReferenceEntity) " + "RETURN id AS rxn, re.geneName AS genes", + ids=ubiq_rstids, + ub_stids=list(_UBIQUITIN_STIDS), + ).data() if ubiq_rstids else [] + in_by_rxn: Dict[str, List[Tuple[Optional[str], Optional[List[str]]]]] = {} + for r in in_rows: + in_by_rxn.setdefault(r["rxn"], []).append((r["leaf_stid"], r["genes"])) + out_by_rxn: Dict[str, Set[str]] = {} + for r in out_rows: + g = r["genes"] + if g: + for gn in g: + out_by_rxn.setdefault(r["rxn"], set()).add(gn) + for rxn in ubiq_rstids: + output_genes = out_by_rxn.get(rxn, set()) + if not output_genes: continue + # The substrate: input-leaf proteins whose gene also appears + # in the output (modified form). Collect their leaf stids. + subst_stids: Set[str] = set() + subst_genes: Set[str] = set() + for leaf_stid, leaf_genes in in_by_rxn.get(rxn, []): + if not leaf_stid or not leaf_genes: continue + common = output_genes & set(leaf_genes) + if common: + subst_stids.add(leaf_stid) + subst_genes.update(common) + if subst_stids: + ubiquitin_subst_by_rstid[rxn] = (subst_genes, subst_stids) + except Exception as exc: + logger.warning(f"Ubiquitin topology lookup failed: {exc}") + + # Build network stid → list of UUIDs index from pathway_logic_network_data, + # so we can target the substrate's NETWORK NODES (not just the leaf + # protein stId, which may not be a direct node). + stid_to_uuids_in_net: Dict[str, List[str]] = {} + seen_uuids: Set[str] = set() + for edge in pathway_logic_network_data: + for uid in (edge.get("source_id"), edge.get("target_id")): + if uid in seen_uuids: continue + seen_uuids.add(uid) + sid = reactome_id_to_uuid.get(uid, "") + if sid: + stid_to_uuids_in_net.setdefault(sid, []).append(uid) + + # Emit depletion edges. For phosphatase reactions: catalyst → input + # (free substrate IS the input). For ubiquitin reactions: catalyst → + # all network UUIDs of the substrate protein stId (free form, not the + # complex that's the literal reaction input). seen_edges: set = set() - n_emitted = 0 + n_emitted_phos = 0 + n_emitted_ub = 0 + # Phosphatase pass — existing behavior. for rxn_uuid in phosphatase_rxn_uuids: catalyst_uuids = by_target_catalysts.get(rxn_uuid, []) inputs = by_target_inputs.get(rxn_uuid, []) @@ -762,10 +863,45 @@ def _emit_substrate_depletion_edges( "edge_type": "depletion", "stoichiometry": 1.0, }) - n_emitted += 1 + n_emitted_phos += 1 + + # Ubiquitin pass — emit catalyst → free-substrate-UUIDs depletion edges. + # For each VR uuid corresponding to a ubiquitin reaction, get its + # catalysts, then for each substrate stid, look up all network UUIDs of + # that substrate (free TP53 nodes, not the assembled complex) and emit. + for ru, rs in rxn_uuid_to_rstid.items(): + info = ubiquitin_subst_by_rstid.get(rs) + if info is None: + continue + _, subst_stids = info + catalyst_uuids = by_target_catalysts.get(ru, []) + if not catalyst_uuids: + continue + for cat_uuid in catalyst_uuids: + cat_stid = reactome_id_to_uuid.get(cat_uuid, "") + for subst_stid in subst_stids: + if subst_stid == cat_stid: continue + if subst_stid in _COFACTOR_STIDS: continue + target_uuids = stid_to_uuids_in_net.get(subst_stid, []) + for tgt_uuid in target_uuids: + if tgt_uuid == cat_uuid: continue + key = (cat_uuid, tgt_uuid) + if key in seen_edges: continue + seen_edges.add(key) + pathway_logic_network_data.append({ + "source_id": cat_uuid, + "target_id": tgt_uuid, + "pos_neg": "neg", + "and_or": "and", + "edge_type": "depletion", + "stoichiometry": 1.0, + }) + n_emitted_ub += 1 logger.info( - f"Emitted {n_emitted} substrate-depletion edges " - f"({len(phosphatase_rxn_uuids)} phosphatase reactions)" + f"Emitted {n_emitted_phos + n_emitted_ub} substrate-depletion edges " + f"({len(phosphatase_rxn_uuids)} phosphatase reactions, " + f"{len(ubiquitin_subst_by_rstid)} ubiquitin-ligase reactions with " + f"identified substrate; topology-based)" ) From bb97ed14e829ac2aa4cd0816a73d2207369d55aa Mon Sep 17 00:00:00 2001 From: Adam Wright Date: Wed, 3 Jun 2026 11:10:36 -0400 Subject: [PATCH 09/17] Coerce stoichiometry column to Int64 in the final DataFrame MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Emission sites use a mix of int (`1`) and float (`1.0`) literals. The DataFrame schema declares stoichiometry as Int64 but `pd.DataFrame(rows, columns=list(...))` only respects column ORDER from the columns arg, not the dtype — so pandas infers from data, and a single 1.0 anywhere pollutes the whole column to float64, serializing as `1.0` in the CSV. Coerce the column to nullable Int64 after construction so the CSV always reads as clean integers. Reactome stoichiometries for the pathways we care about are all whole numbers (counts of subunits in complexes, typically 1–6 with the occasional larger homomultimer); a fractional value would now raise a parse error rather than silently survive — which is what we want. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/logic_network_generator.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/logic_network_generator.py b/src/logic_network_generator.py index 383b8a8..6892dd0 100755 --- a/src/logic_network_generator.py +++ b/src/logic_network_generator.py @@ -1435,6 +1435,16 @@ def create_pathway_logic_network( # Create final DataFrame pathway_logic_network = pd.DataFrame(pathway_logic_network_data, columns=list(columns.keys())) + # Coerce stoichiometry to nullable Int64 — emission sites use a mix of + # int (`1`) and float (`1.0`) literals, which makes pandas infer float64 + # for the column and serialize as `1.0` in the CSV. Force integer so the + # column reads as a clean whole-number count (Reactome's stoichiometries + # are all integers for the pathways we care about; if a fractional value + # ever appears it will surface as a parse error rather than silent loss). + if not pathway_logic_network.empty: + pathway_logic_network["stoichiometry"] = ( + pathway_logic_network["stoichiometry"].astype("Int64") + ) # Find root inputs and terminal outputs root_inputs = find_root_inputs(pathway_logic_network) From aa7715b304ae63dde83ed03220643f57fa5b6996 Mon Sep 17 00:00:00 2001 From: Adam Wright Date: Fri, 10 Jul 2026 15:24:09 -0400 Subject: [PATCH 10/17] Gitignore graphify-out/ (code-graph tool output) Co-Authored-By: Claude Fable 5 --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 911468e..cb3bf5b 100644 --- a/.gitignore +++ b/.gitignore @@ -47,3 +47,4 @@ uuid_mapping_*.csv reaction_connections_*.csv decomposed_uid_mapping_*.csv best_matches_*.csv +graphify-out/ From 46de28212e7890c059ba15c9f66be12a1c55331f Mon Sep 17 00:00:00 2001 From: Adam Wright Date: Tue, 14 Jul 2026 19:00:32 -0400 Subject: [PATCH 11/17] Add linkml as a dev dependency Backs the machine-readable output schema and network validation. Co-Authored-By: Claude Opus 4.8 --- poetry.lock | 1939 +++++++++++++++++++++++++++++++++++++++++++++++- pyproject.toml | 1 + 2 files changed, 1931 insertions(+), 9 deletions(-) diff --git a/poetry.lock b/poetry.lock index 9652f8d..eed5001 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,5 +1,176 @@ # This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. +[[package]] +name = "alabaster" +version = "1.0.0" +description = "A light, configurable Sphinx theme" +optional = false +python-versions = ">=3.10" +files = [ + {file = "alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b"}, + {file = "alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e"}, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.8" +files = [ + {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, + {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, +] + +[[package]] +name = "antlr4-python3-runtime" +version = "4.9.3" +description = "ANTLR 4.9.3 runtime for Python 3.7" +optional = false +python-versions = "*" +files = [ + {file = "antlr4-python3-runtime-4.9.3.tar.gz", hash = "sha256:f224469b4168294902bb1efa80a8bf7855f24c99aef99cbefc1bcd3cce77881b"}, +] + +[[package]] +name = "arrow" +version = "1.4.0" +description = "Better dates & times for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "arrow-1.4.0-py3-none-any.whl", hash = "sha256:749f0769958ebdc79c173ff0b0670d59051a535fa26e8eba02953dc19eb43205"}, + {file = "arrow-1.4.0.tar.gz", hash = "sha256:ed0cc050e98001b8779e84d461b0098c4ac597e88704a655582b21d116e526d7"}, +] + +[package.dependencies] +python-dateutil = ">=2.7.0" +tzdata = {version = "*", markers = "python_version >= \"3.9\""} + +[package.extras] +doc = ["doc8", "sphinx (>=7.0.0)", "sphinx-autobuild", "sphinx-autodoc-typehints", "sphinx_rtd_theme (>=1.3.0)"] +test = ["dateparser (==1.*)", "pre-commit", "pytest", "pytest-cov", "pytest-mock", "pytz (==2025.2)", "simplejson (==3.*)"] + +[[package]] +name = "attrs" +version = "26.1.0" +description = "Classes Without Boilerplate" +optional = false +python-versions = ">=3.9" +files = [ + {file = "attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309"}, + {file = "attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32"}, +] + +[[package]] +name = "babel" +version = "2.18.0" +description = "Internationalization utilities" +optional = false +python-versions = ">=3.8" +files = [ + {file = "babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35"}, + {file = "babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d"}, +] + +[package.extras] +dev = ["backports.zoneinfo", "freezegun (>=1.0,<2.0)", "jinja2 (>=3.0)", "pytest (>=6.0)", "pytest-cov", "pytz", "setuptools", "tzdata"] + +[[package]] +name = "backports-zstd" +version = "1.6.0" +description = "Backport of compression.zstd" +optional = false +python-versions = "<3.14,>=3.10" +files = [ + {file = "backports_zstd-1.6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:73000459db113a658c4fb0510100ef0e79137b5828bf957b7709aacae4eb1b87"}, + {file = "backports_zstd-1.6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d6e78d5e28f812b39f92397806ecddd4a6f3bf35531a8c039a1f187abc931af8"}, + {file = "backports_zstd-1.6.0-cp310-cp310-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:32f04d54ec1fdf3aa648b24a10b1c9234ed2046cc4af7a8850cbc236c05d42f3"}, + {file = "backports_zstd-1.6.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83415af3c64550a56cc20b4cce59bbaa81f21d28466d7adf98feff011ecbc66d"}, + {file = "backports_zstd-1.6.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a3c17e6a267d13de9cbf14bf2ebfa87e03d26692456fc67d2dbed9da4f479b18"}, + {file = "backports_zstd-1.6.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:75578c71644b031118ce938855a53530708db7f4af6e83e2f8840d5a1de990f8"}, + {file = "backports_zstd-1.6.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a4ae7ed5a6d813450cc2d818284ea3db9721edcef50a56aae42ea06feec38c6e"}, + {file = "backports_zstd-1.6.0-cp310-cp310-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5e9a8370c8ed873083d5de956d6b2e60adbad31e52d7a11111c96ef01d1910ae"}, + {file = "backports_zstd-1.6.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c2d1ccfe088e8279d605011a3575619a74526c261be357695b3258c0f636115a"}, + {file = "backports_zstd-1.6.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e73a550dbeb84e8fa50f8385f7735e9a4735b465851ef617d02f80ab10e44e7e"}, + {file = "backports_zstd-1.6.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:84f92e5a60a78c72ccda79d0417d311a1f6da18f446423ed411726d545bf7b56"}, + {file = "backports_zstd-1.6.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:0eb4281f402b94d397b7482f6d9efd04c28274e4ed6eb57eb1f87bdd091a6a87"}, + {file = "backports_zstd-1.6.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:d6b9b06323e3ba947c0003b2d70e02f33c90c36bc6262a92eb8201afc4a1aa08"}, + {file = "backports_zstd-1.6.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8872a0e9f1af975966b5be6af7eebd3dc4046f15e470b719316516dc3d137cd6"}, + {file = "backports_zstd-1.6.0-cp310-cp310-win32.whl", hash = "sha256:c14fa5dc39a804f1b92d63506f450eca5c59647a18d197d1a564b89dac1be1ce"}, + {file = "backports_zstd-1.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:8219d6fceae6b39535c4ac323dba0923d10f781d59962ff3504e693fdcafa92c"}, + {file = "backports_zstd-1.6.0-cp310-cp310-win_arm64.whl", hash = "sha256:b7bc9a0b66097f03820a54316d2fdd0beb38859cf98f10d63e94c55450ed8920"}, + {file = "backports_zstd-1.6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c4fc41b2df5529cad5ceb230319e82728096d4b353ce8d4df68a2ec37e291bb8"}, + {file = "backports_zstd-1.6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:83391ef5935cc0f329b1abca414ae20ffe40d335fc21a4b5e664f08a74317d5f"}, + {file = "backports_zstd-1.6.0-cp311-cp311-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:7d3f64c503af7b60115b97c16feaf75bd191ef2c978d5c0c7725a6682bef63c5"}, + {file = "backports_zstd-1.6.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0308990ffc998df3c7ed35276bde049728b5c3956203cae40d80893576a41459"}, + {file = "backports_zstd-1.6.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8c298785e2fadeab82342040f2d9ce764ce500e6da6a6d99a2de514e63580b5a"}, + {file = "backports_zstd-1.6.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ae106fe16e36efc60ab098d02478d30aa0e31e1420eb4ecf0116459253bc6361"}, + {file = "backports_zstd-1.6.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7293fefe15f0e5852bdb4ad1e0e26f3cbd4d3e61c19f751ecc4ff34bc1eb237d"}, + {file = "backports_zstd-1.6.0-cp311-cp311-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ece8e7288db5b827ef8c64b2f78519f1a173a8991a625978fce02eccd7654fe9"}, + {file = "backports_zstd-1.6.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:28eef3881164f3c23ce58ed59e4684103bdd279583eb2d299858c9e9b72fde9a"}, + {file = "backports_zstd-1.6.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:481a1e9bd8f419fdc625307aa20234687f99368c75df511ef589693c5fea4c6f"}, + {file = "backports_zstd-1.6.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:3b6713371f8987a1178df93cb36f29eef191f224021e2d656b2f11ce60d26816"}, + {file = "backports_zstd-1.6.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:b0ddbcd2866b8ff1a2836e4b0e4d44788f5b992d83fac75a38cda8f9a2bee079"}, + {file = "backports_zstd-1.6.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2914abea516704bdafb2090acd3f15b5f9debecfabd15b8dd8285b2ad3b92209"}, + {file = "backports_zstd-1.6.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dd085eafa2aac6f883afd28210a3231f717f25409a1e44a39bb7b04c8c5b5646"}, + {file = "backports_zstd-1.6.0-cp311-cp311-win32.whl", hash = "sha256:b81b4cf3d6e0ad7ac92bef248f49fafc954262c5fb0f7e19d6aac497e5a856b2"}, + {file = "backports_zstd-1.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:10b61850c4112952e05aa6e6cce8c9a5936fbeadb321e154216705cc76a14afa"}, + {file = "backports_zstd-1.6.0-cp311-cp311-win_arm64.whl", hash = "sha256:068ef3d8c18815a2e3a752f766313e19910e7c50939b956923748d9c04ebcb1b"}, + {file = "backports_zstd-1.6.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0466b14723f3b7697669c00ee66fe16e30e25636b286b0a923fa86fa3d8a753c"}, + {file = "backports_zstd-1.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1d146926e997d2d3de8212bdcbf4985344a2622ca3bec458d8908000a84fd883"}, + {file = "backports_zstd-1.6.0-cp312-cp312-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:460fd6b3f338c659507ae36cfd6b58ac9942a2ff233c5cf574416dfec0451a84"}, + {file = "backports_zstd-1.6.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7c2b1f4a640c51130caa92cef5bf72bd3c3dbbcfbf814c37403aa0601b1811b0"}, + {file = "backports_zstd-1.6.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:beb43e9885202c8d4f3762319ed4d5e98e197622afbff8439fbbdd81d08938b9"}, + {file = "backports_zstd-1.6.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fbb746522ebfc11155f1cd688e2c48ef3d74125e38b63eabdaab068a055c3e88"}, + {file = "backports_zstd-1.6.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a99710fbb225d459d66def4dc2bb2cd4a9a0bdc8b799fc0621cfdd863be9c93"}, + {file = "backports_zstd-1.6.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f69365ee2b836939137de024a302395a1cb8654fb6dc5ffef6381105259c8f87"}, + {file = "backports_zstd-1.6.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:66cf8038893c7708ec345ffb3ac63c775d10f430f323ac2f0334fdb6a397c57c"}, + {file = "backports_zstd-1.6.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e514c71ca72f3b56bd8fbda1a6a5b7d1100a2764b42a3c74a38841f25f9b00ab"}, + {file = "backports_zstd-1.6.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7741e44f7938ec94f9a52678c8d19b7bc548522ffdc39c9e4481af8db545fa9a"}, + {file = "backports_zstd-1.6.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:97e8a9674652496c7612b528085dd5a296c052a2edc466ca1bfb7b0b27820413"}, + {file = "backports_zstd-1.6.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:23a793f2fed4dbf0517319759a2cded0b0dd8e8d3797fe30badd5693e320c175"}, + {file = "backports_zstd-1.6.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b951113113ed4b8d173418a4f155c14b739dace626b3fa3f82be1831958d39e4"}, + {file = "backports_zstd-1.6.0-cp312-cp312-win32.whl", hash = "sha256:6430b34a2ae6fcc604672f4f913102563473d9a015bdca1ce8c95041cc1f2677"}, + {file = "backports_zstd-1.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:08793876172551a930ce4d65c712cd516184d1a97070d4a1193e05bf0cf7040d"}, + {file = "backports_zstd-1.6.0-cp312-cp312-win_arm64.whl", hash = "sha256:03b7c59c71f7a597e2bcb3f8368371e9a660a1bdf1c37afc1f1ad1496a013c19"}, + {file = "backports_zstd-1.6.0-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:2ace939e4d620e119423606f2d3d7115f8707733bf57f279ad9a9383f875986f"}, + {file = "backports_zstd-1.6.0-cp313-cp313-android_24_x86_64.whl", hash = "sha256:4c68a9ed2df0cca51d774c521e68a34d2e3d9ebfc687ef8096adfd4f345b551d"}, + {file = "backports_zstd-1.6.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:30576f49b82328ec8af16c11100efe52ca88526f71bbe100ef6b4e707dc13bf2"}, + {file = "backports_zstd-1.6.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:b4bddfcfb6679215d6f4dc5f79a1f9301af339480d70527a14b57a1f2e6b6cbf"}, + {file = "backports_zstd-1.6.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:65048ed08c5124f05ff9f355ab9703014bb2dbe7f8d9948ce193685b1775f442"}, + {file = "backports_zstd-1.6.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5918fc6b31437208721276964323933cd86077b8d5b469c59c1b3fd2c8220a05"}, + {file = "backports_zstd-1.6.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4b6c8b02ab0ccb2431bb7bc238be91d158b308915e7b07937388e540466fe7e7"}, + {file = "backports_zstd-1.6.0-cp313-cp313-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:711e6b98f8924e8b4a61ff97ab6321f33de024e1ed6a32f5123763aeda8459be"}, + {file = "backports_zstd-1.6.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2ba9ac10fc393e5123a08802e0e895a107cb4a66b9973d2844dbd8a343111e59"}, + {file = "backports_zstd-1.6.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2f723219335387d7546412d8141e0303590600949b4184a1391a0c6a3c756058"}, + {file = "backports_zstd-1.6.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:64b94d7a836568926a3309ff510c7f8261b881b341fd4992cabf4f0998878f8a"}, + {file = "backports_zstd-1.6.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e39258a09b1c7ca70b5e94a5c5ccfe4700b4250b8077cfeab31d0f79565d4c9b"}, + {file = "backports_zstd-1.6.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:15b1aae0f64cd742df4bba1d989d0a09a6ec619202543fdba684640454541fd3"}, + {file = "backports_zstd-1.6.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:25b5ddc789480072551af571a746e9500356b2aff0499861cf2ca07ea7431e68"}, + {file = "backports_zstd-1.6.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a13cfa3410a75e4cb87abdb669aaf79da861cb79299159054ff8f77b9671bc40"}, + {file = "backports_zstd-1.6.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:2ddab55a5f54dec8acfad68ef70f1c704fd21919990ddc238afbd6f496e61c6a"}, + {file = "backports_zstd-1.6.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fa305a84087e10d7a85e8a8a3dcba8cdbda4868f2180173b264b7b488fd37c55"}, + {file = "backports_zstd-1.6.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:df27b57d214a3124fbe4e933ef5a903d4567f154260d9aece8c797a987f2a205"}, + {file = "backports_zstd-1.6.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:28fecd73459d74910ae1987ab84b7bef690d3dd860948430dd5555108b006daf"}, + {file = "backports_zstd-1.6.0-cp313-cp313-win32.whl", hash = "sha256:3e689af303df287142770abe3a48bbefd24dab4a09da5807d0e1fa8c75bab026"}, + {file = "backports_zstd-1.6.0-cp313-cp313-win_amd64.whl", hash = "sha256:b067b1ef9c8e41fb0882c828aa37829938b5c0dab067eca72b23fc24c563b9da"}, + {file = "backports_zstd-1.6.0-cp313-cp313-win_arm64.whl", hash = "sha256:a838296f5b84c920172fb579cac894d255c1fc25457c7234613ddcfa385e49b7"}, + {file = "backports_zstd-1.6.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:6c73ae37dbf9207727ac095dedef864c05d836eaec962a47b3b64eaadaf1c6b6"}, + {file = "backports_zstd-1.6.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:839faf90a7eb525a401978dc925df8c44bd12526e8ba1529b9f8a7106e729637"}, + {file = "backports_zstd-1.6.0-pp310-pypy310_pp73-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:f8f5c1c7c69a4b00889e52d9304a918a5b49010f9645768eb5fd0ad404f790ba"}, + {file = "backports_zstd-1.6.0-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e80bceebc9b58e959bede9b26cafe15b5b9526f3533a6dd06330c5da73cb9329"}, + {file = "backports_zstd-1.6.0-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:79284c1dd702f4f24ed1a36e51555c907dd237b6c0d829595978f4089a2aeea9"}, + {file = "backports_zstd-1.6.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:1e20b3ecd0a711be82e964aca28554eabbc31ee69a20e5e7b8fd42268af46212"}, + {file = "backports_zstd-1.6.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:aeef8563b82ed4af328f98e5041c1b4800d86f68f857ffd1577d4d47dc9aa6cd"}, + {file = "backports_zstd-1.6.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9cb75e33131946fabd6319061df3b8b1d588fe0963183280e9b5f49f7772fc09"}, + {file = "backports_zstd-1.6.0-pp311-pypy311_pp73-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:ef132cfb638e9a86bd5dc07fb4e1cb895bc55bce6bb5e759366e8b160d0747e2"}, + {file = "backports_zstd-1.6.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab70eace272d6f122b121c057e436709b50a28abf30d97aab28433c08f4a4095"}, + {file = "backports_zstd-1.6.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:17efb3d11137de5166dd51eedab9c36ad633402acba386eee8d715213ea47e49"}, + {file = "backports_zstd-1.6.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:994167ff6551b9c1ce226e0aab16295b98c94507b5701aa60d2c32b7d50796b1"}, + {file = "backports_zstd-1.6.0.tar.gz", hash = "sha256:80a7859ffe70bf239d7a2ce15293bdeb5b4280ff7dc326ffab312b0e254dbb24"}, +] + [[package]] name = "certifi" version = "2026.4.22" @@ -11,6 +182,19 @@ files = [ {file = "certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580"}, ] +[[package]] +name = "cfgraph" +version = "0.2.1" +description = "rdflib collections flattening graph" +optional = false +python-versions = "*" +files = [ + {file = "CFGraph-0.2.1.tar.gz", hash = "sha256:b57fe7044a10b8ff65aa3a8a8ddc7d4cd77bf511b42e57289cd52cbc29f8fe74"}, +] + +[package.dependencies] +rdflib = ">=0.4.2" + [[package]] name = "cfgv" version = "3.5.0" @@ -22,6 +206,153 @@ files = [ {file = "cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132"}, ] +[[package]] +name = "chardet" +version = "7.4.3" +description = "Universal character encoding detector" +optional = false +python-versions = ">=3.10" +files = [ + {file = "chardet-7.4.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c0c79b13c9908ac7dfe0a74116ebc9a0f28b2319d23c32f3dfcdfbe1279c7eaf"}, + {file = "chardet-7.4.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:bba8bea1b28d927b3e99e47deafe53658d34497c0a891d95ff1ba8ff6663f01c"}, + {file = "chardet-7.4.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:23163921dccf3103ce59540b0443c106d2c0a0ff2e0503e05196f5e6fdea453f"}, + {file = "chardet-7.4.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cfb54563fe5f130da17c44c6a4e2e8052ba628e5ab4eab7ef8190f736f0f8f72"}, + {file = "chardet-7.4.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3990fffcc6a6045f2234ab72752ad037e3b2d48c72037f244d42738db397eb75"}, + {file = "chardet-7.4.3-cp310-cp310-win_amd64.whl", hash = "sha256:c7116b0452994734ccff35e154b44240090eb0f4f74b9106292668133557c175"}, + {file = "chardet-7.4.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:25a862cddc6a9ac07023e808aedd297115345fbaabc2690479481ddc0f980e09"}, + {file = "chardet-7.4.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7005c88da26fd95d8abb8acbe6281d833e9a9181b03cf49b4546c4555389bd97"}, + {file = "chardet-7.4.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc50f28bad067393cce0af9091052c3b8df7a23115afd8ba7b2e0947f0cef1f8"}, + {file = "chardet-7.4.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c3da294de1a681097848ab58bd3f2771a674f8039d2d87a5538b28856b815e9"}, + {file = "chardet-7.4.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:93c45e116dd51b66226a53ade3f9f635e870de5399b90e00ce45dcc311093bf4"}, + {file = "chardet-7.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:ccc1f83ab4bcfb901cf39e0c4ba6bc6e726fc6264735f10e24ceb5cb47387578"}, + {file = "chardet-7.4.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:75d3c65cc16bddf40b8da1fd25ba84fca5f8070f2b14e86083653c1c85aee971"}, + {file = "chardet-7.4.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:29af5999f654e8729d251f1724a62b538b1262d9292cccaefddf8a02aae1ef6a"}, + {file = "chardet-7.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:626f00299ad62dfe937058a09572beed442ccc7b58f87aa667949b20fd3db235"}, + {file = "chardet-7.4.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9a4904dd5f071b7a7d7f50b4a67a86db3c902d243bf31708f1d5cde2f68239cb"}, + {file = "chardet-7.4.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5d2879598bc220689e8ce509fe9c3f37ad2fca53a36be9c9bd91abdd91dd364f"}, + {file = "chardet-7.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:4b2799bd58e7245cfa8d4ab2e8ad1d76a5c3a5b1f32318eb6acca4c69a3e7101"}, + {file = "chardet-7.4.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a9e4486df251b8962e86ea9f139ca235aa6e0542a00f7844c9a04160afb99aa9"}, + {file = "chardet-7.4.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4fbff1907925b0c5a1064cffb5e040cd5e338585c9c552625f30de6bc2f3107a"}, + {file = "chardet-7.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:365135eaf37ba65a828f8e668eb0a8c38c479dcbec724dc25f4dfd781049c357"}, + {file = "chardet-7.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfc134b70c846c21ead8e43ada3ae1a805fff732f6922f8abcf2ff27b8f6493d"}, + {file = "chardet-7.4.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9acd9988a93e09390f3cd231201ea7166c415eb8da1b735928990ffc05cb9fbb"}, + {file = "chardet-7.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:e1b98790c284ff813f18f7cf7de5f05ea2435a080030c7f1a8318f3a4f80b131"}, + {file = "chardet-7.4.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d892d3dcd652fdef53e3d6327d39b17c0df40a899dfc919abaeb64c974497531"}, + {file = "chardet-7.4.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:acc46d1b8b7d5783216afe15db56d1c179b9a40e5a1558bc13164c4fd20674c4"}, + {file = "chardet-7.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ac3bf11c645734a1701a3804e43eabd98851838192267d08c353a834ab79fea"}, + {file = "chardet-7.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6e3bd9f936e04bae89c254262af08d9e5b98f805175ba1e29d454e6cba3107b7"}, + {file = "chardet-7.4.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:27cc23da03630cdecc9aa81a895aa86629c211f995cd57651f0fbc280717bf93"}, + {file = "chardet-7.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:b95c934b9ad59e2ba8abb9be49df70d3ad1b0d95d864b9fdb7588d4fa8bd921c"}, + {file = "chardet-7.4.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c77867f0c1cb8bd819502249fcdc500364aedb07881e11b743726fa2148e7b6e"}, + {file = "chardet-7.4.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:cf1efeaf65a6ef2f5b9cc3a1df6f08ba2831b369ccaa4c7018eaf90aa757bb11"}, + {file = "chardet-7.4.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f3504c139a2ad544077dd2d9e412cd08b01786843d76997cd43bb6de311723c"}, + {file = "chardet-7.4.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457f619882ba66327d4d8d14c6c342269bdb1e4e1c38e8117df941d14d351b04"}, + {file = "chardet-7.4.3-py3-none-any.whl", hash = "sha256:1173b74051570cf08099d7429d92e4882d375ad4217f92a6e5240ccfb26f231e"}, + {file = "chardet-7.4.3.tar.gz", hash = "sha256:cc1d4eb92a4ec1c2df3b490836ffa46922e599d34ce0bb75cf41fd2bf6303d56"}, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.9" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7" +files = [ + {file = "charset_normalizer-3.4.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd6280cf040f233bd7d3407b743b4b4c74f70e8e1c4199cb112a62c941c0772a"}, + {file = "charset_normalizer-3.4.9-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa99adc8f081b475a12843953db36831eaf83ec33eb46a90629ca6a5de45a616"}, + {file = "charset_normalizer-3.4.9-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c1225416b463483160e4af85d5fc3a9690ccb53fd4b1865a6437825f5ede3209"}, + {file = "charset_normalizer-3.4.9-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:16d10d789dd9bcca1173c95af82c58433122564b7bc39385124be735a35cbe99"}, + {file = "charset_normalizer-3.4.9-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9bb41182d93ea91f60b4bc8fbf4c820c69ef8a12ab2d917f3f1834f1acad07e8"}, + {file = "charset_normalizer-3.4.9-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:bcf74c1df76758a395bf0af608c04c82257523f55c9868b334f06270d0f2112b"}, + {file = "charset_normalizer-3.4.9-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b5314963fce9b0b12743891de876e724997864ee22aa496f903f426c7e2fa5b2"}, + {file = "charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e9701d0049d92c16703a42771b98d560b95248949f23f8cf7b4eddd201814fb9"}, + {file = "charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:65a7ff3f705e57d392f7261b6d0550fe137c3019477431f1c355e0db0a7d3e15"}, + {file = "charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79580094b00d1789d1f93ea55bc43cb2f611910c72235b7657f3482ddcc1b22d"}, + {file = "charset_normalizer-3.4.9-cp310-cp310-win32.whl", hash = "sha256:432786d3561e69aeeae6c7e8648964ce0ad05736120135601f87ac26b9c83381"}, + {file = "charset_normalizer-3.4.9-cp310-cp310-win_amd64.whl", hash = "sha256:8c041122946b7ba21bb32c45b1aa57b1be35527690aeb3c5c234521085632eee"}, + {file = "charset_normalizer-3.4.9-cp310-cp310-win_arm64.whl", hash = "sha256:375b83ed0aecfce76c16d198fbc21f3b11b337d68662bea0a995046682a11419"}, + {file = "charset_normalizer-3.4.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0e94703ec9684807f20cfb5eed95c70f67f2a8f21ad620146d7b5a13677b93e5"}, + {file = "charset_normalizer-3.4.9-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a441ea71902098ffe78c5abe6c494f44160b4af614ed16c3d9a3b1d17fd8ee2"}, + {file = "charset_normalizer-3.4.9-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:304b13570067b2547562e308af560b3963857b1fa90bd6afd978130130fe2d6a"}, + {file = "charset_normalizer-3.4.9-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4773092f8019072343a7447203308b176e10199920eb02d6195e81bbb3274c29"}, + {file = "charset_normalizer-3.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:04ce310cb89c15df659582aee80a0603788732a5e017d5bd5c81158106ce249c"}, + {file = "charset_normalizer-3.4.9-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:c0323c9daef75ef2e5083624b4585018a0c9d5e3b40f607eed81a311270b934b"}, + {file = "charset_normalizer-3.4.9-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:871ff67ea1aad4dfd91736464934d56b32dac49f9fbe16cddba36198a7b3a0db"}, + {file = "charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:67830fc78e67501f47bb950471b2dcb9b35b140084429318e862895a8e89c993"}, + {file = "charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:3d92613ec25e43b05f042302531ec0f00b8445190e43325880cbd6ab7c2581da"}, + {file = "charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:280081916dc341820640489a66e4696049401ef1cf6dd672f672e70ad915aca3"}, + {file = "charset_normalizer-3.4.9-cp311-cp311-win32.whl", hash = "sha256:ac351b3b8014eead140e77e9717e2992c6bbe30b63bc3422422eb84865412e3d"}, + {file = "charset_normalizer-3.4.9-cp311-cp311-win_amd64.whl", hash = "sha256:6366a16e1a25018694d6a5d784d09b046edc9eac40ea2b54065c3052672516a1"}, + {file = "charset_normalizer-3.4.9-cp311-cp311-win_arm64.whl", hash = "sha256:1d22856ffbe153a602df38e4a5464f0b748a54002e0d69ac6d2ad0a197cc99ec"}, + {file = "charset_normalizer-3.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:45b0cc4e3556cd875e09102988d1ab8356c998b596c9fced84547c8138b487a0"}, + {file = "charset_normalizer-3.4.9-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b2aff1c7b3884512b9512c3eaadd9bab39fb45042ffaaa1dd08ff2b9f8109d9"}, + {file = "charset_normalizer-3.4.9-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9104ed0bd76a429d46f9ec0dbc9b08ad1d2dcdf2b00a5a0daa1c145329b35b44"}, + {file = "charset_normalizer-3.4.9-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7b86a2b16095d250c6f58b3d9b2eee6f4147754344f3dab0922f7c9bf7d226c9"}, + {file = "charset_normalizer-3.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e226f6218febc71f6c1fc2fafb91c226f75bdc1d8fb12d66823716e891608fd"}, + {file = "charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:90c44bc373b7687f6948b693cceaea1348ae0975d7474746559494468e3c1d84"}, + {file = "charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cdef90ae47919cae358d8ab15797a800ed41da7aba5d72419fb510729e2ed4b"}, + {file = "charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:60f44ade2cf573dad7a277e6f8ca9a51a21dda572b13bd7d8539bb3cd5dbedde"}, + {file = "charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a1786910334ed46ab1dd73222f2cd1e05c2c3bb39f6dddb4f8b36fc382058a39"}, + {file = "charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:03d07803992c6c7bbc976327f34b18b6160327fc81cb82c9d504720ac0be3b62"}, + {file = "charset_normalizer-3.4.9-cp312-cp312-win32.whl", hash = "sha256:78841cccf1af7b40f6f716338d50c0902dbe88d9f800b3c973b7a9a0a693a642"}, + {file = "charset_normalizer-3.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:4b3dac63058cc36820b0dd072f89898604e2d39686fe05321729d00d8ac185a0"}, + {file = "charset_normalizer-3.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:78fa18e436a1a0e58dbd7e02fc4473f3f32cceb12df9dfca542d075961c307d2"}, + {file = "charset_normalizer-3.4.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:440eede837960000d74978f0eba527be106b5b9aee0daf779d395276ed0b0614"}, + {file = "charset_normalizer-3.4.9-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21e764fd1e70b6a3e205a0e46f3051701f98a8cb3fad66eeb80e48bb502f8698"}, + {file = "charset_normalizer-3.4.9-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e4fd89cc178bced6ad29cb3e6dd4aa63fa5017c3524dbd0b25998fb64a87cc8b"}, + {file = "charset_normalizer-3.4.9-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bd47ba7fc3ca94896759ea0109775132d3e7ab921fbf54038e1bab2e46c313c9"}, + {file = "charset_normalizer-3.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:84fd18bcc17526fc2b3c1af7d2b9217d32c9c04448c16ec693b9b4f1985c3d33"}, + {file = "charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:5b10cd92fc5c498b35a8635df6d5a100207f88b63a4dc1de7ef9a548e1e2cd63"}, + {file = "charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a4fbdde9dd4a9ce5fd52c2b3a347bb50cc89483ef783f1cb00d408c13f7a96c0"}, + {file = "charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:416c229f77e5ea25b3dfd4b582f8d73d7e43c22320302b9ab128a2d3a0b38efe"}, + {file = "charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:75286256590a6320cf106a0d28970d3560aad9ee09aa7b34fb40524792436d35"}, + {file = "charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:69b157c5d3292bcd443faca052f3096f637f1e074b98212a933c074ae23dc3b8"}, + {file = "charset_normalizer-3.4.9-cp313-cp313-win32.whl", hash = "sha256:51307f5c71007673a2bf8232ad973483d281e74cb99c8c5a990af1eefa6277d9"}, + {file = "charset_normalizer-3.4.9-cp313-cp313-win_amd64.whl", hash = "sha256:fe2c7201c642b7c308f1675355ad7ff7b66acfe3541625efe5a3ad38f29d6115"}, + {file = "charset_normalizer-3.4.9-cp313-cp313-win_arm64.whl", hash = "sha256:611057cc5d5c0afc743ba8be6bd828c17e0aaa8643f9d0a9b9bb7dea80eb8012"}, + {file = "charset_normalizer-3.4.9-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:0327fcd59a935777d83410750c50600ee9571af2846f71ce40f25b13da1ef380"}, + {file = "charset_normalizer-3.4.9-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a79d9f4d8001473a30c163556b3c3bfebec837495a412dde78b51672f6134f9"}, + {file = "charset_normalizer-3.4.9-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:33bdcc2a32c0a0e861f60841a512c8acc658c87c2ac59d89e3a46dacf7d866e4"}, + {file = "charset_normalizer-3.4.9-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f840ed6d8ecba8255df8c42b87fadeda98ddfc6eeec05e2dc66e26d46dd6f58a"}, + {file = "charset_normalizer-3.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c25fe15c70c59eb7c5ce8c06a1f3fa1da0ecc5ea1e7a5922c40fd2fa9b0d5046"}, + {file = "charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:f7fb7d750cfa0a070d2c24e831fd3481019a60dd317ea2b39acbcebc08b6ed81"}, + {file = "charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4d1c96a7a18b9690a4d46df09e3e3382406ae3213727cd1019ebade1c4a81917"}, + {file = "charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a4cfde78a9f2880208d16a93b795726a3017d5977e08d1e162a7a31322479c41"}, + {file = "charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d4d6fcde76f94f5cb9e43e9e9a61f16dacefd228cbbf6f1a09bd9b219a92f1a1"}, + {file = "charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:898f0e9068ca27d37f8e83a5b962821df851532e6c4a7d615c1c033f9da6eedf"}, + {file = "charset_normalizer-3.4.9-cp314-cp314-win32.whl", hash = "sha256:c1c948747b03be832dceed96ca815cef7360de9aa19d37c730f8e3f6101aca48"}, + {file = "charset_normalizer-3.4.9-cp314-cp314-win_amd64.whl", hash = "sha256:16b65ea0f2465b6fb52aa22de5eca612aa964ddfec00a912e26f4656cbef890b"}, + {file = "charset_normalizer-3.4.9-cp314-cp314-win_arm64.whl", hash = "sha256:40a126142a56b2dfc0aacbad1de8310cbf60da7656db0e6b16eebd48e3e93519"}, + {file = "charset_normalizer-3.4.9-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:609b3ba8fcc0fb5ab7af00719d0fb6ad0cb518e48e7712d12fd68f1327951198"}, + {file = "charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51447e9aa2684679af07ca5021c3db526e0284347ebf4ffcec1154c3350cfe32"}, + {file = "charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cc1b0fff8ead343dae06305f954eb8468ba0ec1a97881f42489d198e4ce3c632"}, + {file = "charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa36ec09ef71d158186bc79e359ff5fdd6e7996fe8ab638f00d6b93139ba4fcf"}, + {file = "charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df115d4d83168fdf2cae48ef1ff6d1cb4c466364e30861b37121de0f3bf1b990"}, + {file = "charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f86c6358749bd4fda175388691e3ba8c46e24c5347d0afd20f9b7edfc9faf07d"}, + {file = "charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:32286a2c8d167e897177b673176c1e3e00d4057caf5d2b64eef9a3666b03018e"}, + {file = "charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:83aed2c10721ddd90f68140685391b50811a880af20654c59af6b6c66c40513c"}, + {file = "charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cd6c3d4b783c556fa00bf540854e42f135e2f256abd29669fcd0da0f2dec79c2"}, + {file = "charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ee2f2a527e3c1a6e6411eb4209642e138b544a2d72fe5d0d76daf77b24063534"}, + {file = "charset_normalizer-3.4.9-cp314-cp314t-win32.whl", hash = "sha256:0d861473f743244d349b50f850d10eb87aeb22bbdcc8e64f79273c94af5a8226"}, + {file = "charset_normalizer-3.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:9b8e0f3107e2200b76f6054de99016eac3ee6762713587b36baaa7e4bd2ae177"}, + {file = "charset_normalizer-3.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:19ac87f93086ce37b86e098888555c4b4bc48102279bae3350098c0ed664b501"}, + {file = "charset_normalizer-3.4.9-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:253a4a220747e8b5faf57ec320c4f5efb0cef05f647420bf267143ec15dba10a"}, + {file = "charset_normalizer-3.4.9-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:68ce9f4d6b26d5ccbf7fd4459bf75f74a0a146677ebba80597df60cbdb20e6f4"}, + {file = "charset_normalizer-3.4.9-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:58150c9f9b9a552505912d182ccdf26f6396fb6094816ceebcbb20eecabaed94"}, + {file = "charset_normalizer-3.4.9-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:df7276909358e5635ae203673ab7e509ddd224225a8d6b0790bf13eb2bde1cc5"}, + {file = "charset_normalizer-3.4.9-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3c09a49d6cde137258beb3d551994a2927fd35ad5cf96aed573f61bbd67c5f84"}, + {file = "charset_normalizer-3.4.9-cp39-cp39-manylinux_2_31_armv7l.whl", hash = "sha256:231ddcbb35e2ff8973e1365db41fe0572662893b99a05deb183b68ad4c0c8bd4"}, + {file = "charset_normalizer-3.4.9-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:920079c3f7456fa213e0829ed2073aaa727fd39d889ead5b4f35d0de5460d04f"}, + {file = "charset_normalizer-3.4.9-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0fa1aec2d32bcc03c8fa0f6f1712caad1adc38509f31142112e5c9daf5b9c833"}, + {file = "charset_normalizer-3.4.9-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:ad41ba96094304aa090f5a30cb6e4fb3b3f1c264c523394b4c39bbacc4dc92ba"}, + {file = "charset_normalizer-3.4.9-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:43b9e366a31fdd1c87d0eb08f579b4a82b723ea54338f040d6b4e518a026ea29"}, + {file = "charset_normalizer-3.4.9-cp39-cp39-win32.whl", hash = "sha256:93d59d504b230e83c7a843251681959a0b6a9cd76f6e146ce1b8a80eb8739af9"}, + {file = "charset_normalizer-3.4.9-cp39-cp39-win_amd64.whl", hash = "sha256:ddf4af30b417d9fe16481e9b81c27ab2a7cde1ff7ba3e85653b02db7d145dc7b"}, + {file = "charset_normalizer-3.4.9-cp39-cp39-win_arm64.whl", hash = "sha256:476743fe6dfe14a2da12e3ac79125dc84a3b2cf8094369a47a1529b0cd8549fe"}, + {file = "charset_normalizer-3.4.9-py3-none-any.whl", hash = "sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5"}, + {file = "charset_normalizer-3.4.9.tar.gz", hash = "sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b"}, +] + [[package]] name = "click" version = "8.3.3" @@ -168,6 +499,47 @@ tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.1 [package.extras] toml = ["tomli"] +[[package]] +name = "curies" +version = "0.14.1" +description = "Idiomatic conversion between URIs and compact URIs (CURIEs)" +optional = false +python-versions = ">=3.10" +files = [ + {file = "curies-0.14.1-py3-none-any.whl", hash = "sha256:04de52080536881108b52ca864e8d4096140a7c3f5f556d8c2836c8ff99adc8f"}, + {file = "curies-0.14.1.tar.gz", hash = "sha256:543942892d1da01ba59e63720d223d504d97908d5c2c83458f33af54b87c92b3"}, +] + +[package.dependencies] +pydantic = ">=2.0" +pystow = ">=0.8.0" +typing-extensions = "*" + +[package.extras] +fastapi = ["defusedxml", "fastapi", "httpx", "python-multipart", "uvicorn"] +flask = ["defusedxml", "flask"] +pandas = ["pandas"] +rdflib = ["rdflib"] +sqlalchemy = ["sqlalchemy"] +sqlmodel = ["sqlmodel"] + +[[package]] +name = "deprecated" +version = "1.3.1" +description = "Python @deprecated decorator to deprecate old python classes, functions or methods." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" +files = [ + {file = "deprecated-1.3.1-py2.py3-none-any.whl", hash = "sha256:597bfef186b6f60181535a29fbe44865ce137a5079f295b479886c82729d5f3f"}, + {file = "deprecated-1.3.1.tar.gz", hash = "sha256:b1b50e0ff0c1fddaa5708a2c6b0a6588bb09b892825ab2b214ac9ea9d92a5223"}, +] + +[package.dependencies] +wrapt = ">=1.10,<3" + +[package.extras] +dev = ["PyTest", "PyTest-Cov", "bump2version (<1)", "setuptools", "tox"] + [[package]] name = "distlib" version = "0.4.0" @@ -179,6 +551,28 @@ files = [ {file = "distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d"}, ] +[[package]] +name = "docutils" +version = "0.21.2" +description = "Docutils -- Python Documentation Utilities" +optional = false +python-versions = ">=3.9" +files = [ + {file = "docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2"}, + {file = "docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f"}, +] + +[[package]] +name = "et-xmlfile" +version = "2.0.0" +description = "An implementation of lxml.xmlfile for the standard library" +optional = false +python-versions = ">=3.8" +files = [ + {file = "et_xmlfile-2.0.0-py3-none-any.whl", hash = "sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa"}, + {file = "et_xmlfile-2.0.0.tar.gz", hash = "sha256:dab3f4764309081ce75662649be815c4c9081e88f0837825f90fd28317d4da54"}, +] + [[package]] name = "exceptiongroup" version = "1.3.1" @@ -207,6 +601,136 @@ files = [ {file = "filelock-3.29.0.tar.gz", hash = "sha256:69974355e960702e789734cb4871f884ea6fe50bd8404051a3530bc07809cf90"}, ] +[[package]] +name = "fqdn" +version = "1.5.1" +description = "Validates fully-qualified domain names against RFC 1123, so that they are acceptable to modern bowsers" +optional = false +python-versions = ">=2.7, !=3.0, !=3.1, !=3.2, !=3.3, !=3.4, <4" +files = [ + {file = "fqdn-1.5.1-py3-none-any.whl", hash = "sha256:3a179af3761e4df6eb2e026ff9e1a3033d3587bf980a0b1b2e1e5d08d7358014"}, + {file = "fqdn-1.5.1.tar.gz", hash = "sha256:105ed3677e767fb5ca086a0c1f4bb66ebc3c100be518f0e0d755d9eae164d89f"}, +] + +[[package]] +name = "graphviz" +version = "0.21" +description = "Simple Python interface for Graphviz" +optional = false +python-versions = ">=3.9" +files = [ + {file = "graphviz-0.21-py3-none-any.whl", hash = "sha256:54f33de9f4f911d7e84e4191749cac8cc5653f815b06738c54db9a15ab8b1e42"}, + {file = "graphviz-0.21.tar.gz", hash = "sha256:20743e7183be82aaaa8ad6c93f8893c923bd6658a04c32ee115edb3c8a835f78"}, +] + +[package.extras] +dev = ["Flake8-pyproject", "build", "flake8", "pep8-naming", "tox (>=3)", "twine", "wheel"] +docs = ["sphinx (>=5,<7)", "sphinx-autodoc-typehints", "sphinx-rtd-theme (>=0.2.5)"] +test = ["coverage", "pytest (>=7,<8.1)", "pytest-cov", "pytest-mock (>=3)"] + +[[package]] +name = "greenlet" +version = "3.5.3" +description = "Lightweight in-process concurrent programming" +optional = false +python-versions = ">=3.10" +files = [ + {file = "greenlet-3.5.3-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:c180d22d325fb613956b443c3c6f4406eb70e6defc70d3974da2a7b59e06f48c"}, + {file = "greenlet-3.5.3-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:483d08c11181c83a6ce1a7a61df0f624a208ec40817a3bb2302714592eee4f04"}, + {file = "greenlet-3.5.3-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1dae6e0091eae084317e411f047f0b7cb241c6db570f7c45fd6b900a274914ce"}, + {file = "greenlet-3.5.3-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0f6ff50ff8dbd51fae9b37f4101648b04ea0df19b3f50ab2beb5061e7716a5c8"}, + {file = "greenlet-3.5.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9bcd2d72ccd70a1ec68ba6ef93e7fbb4420ef9997dabc7010d893bd4015e0bec"}, + {file = "greenlet-3.5.3-cp310-cp310-manylinux_2_39_riscv64.whl", hash = "sha256:37bf9c538f5ae6e63d643f88dec37c0c83bdf0e2ebc62961dedcf458822f7b71"}, + {file = "greenlet-3.5.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:73f152c895e09907e0dbe24f6c2db37beb085cd63db91c3825a0fcd0064124a8"}, + {file = "greenlet-3.5.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8bdb43e1a1d1873721acab2be99c5befd4d2044ddfd52e4d610801019880a702"}, + {file = "greenlet-3.5.3-cp310-cp310-win_amd64.whl", hash = "sha256:0909f9355a9f24845d3299f3112e266a06afb68302041989fd26bd68894933db"}, + {file = "greenlet-3.5.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:aca9b4ce85b152b5524ef7d88170efdff80dc0032aa8b75f9aaf7f3479ea95b4"}, + {file = "greenlet-3.5.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f71be4920368fe1fabeeaa53d1e3548337e2b223d9565f8ad5e392a75ba23fc"}, + {file = "greenlet-3.5.3-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4d77e67f65f98449e3fb83f795b5d0a8437aead2f874ca89c96576caf4be3af6"}, + {file = "greenlet-3.5.3-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e18619ba655ac05d78d80fc83cac4ba892bd6927b99e3b8237aee861aaacc8bb"}, + {file = "greenlet-3.5.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8540f1e6205bd13ca0ce685581037219ca54a1b41a0a15d228c6c9b8ad5903d7"}, + {file = "greenlet-3.5.3-cp311-cp311-manylinux_2_39_riscv64.whl", hash = "sha256:d27c0c653a60d9535f690226474a5cc1036a8b0d7b57504d1c4f89c44a07a80c"}, + {file = "greenlet-3.5.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7ef56fe650f50575bf843acde967b9c567687f3c22340941a899b7bc56e956a8"}, + {file = "greenlet-3.5.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5121af01cf911e70056c00d4b46d5e9b5d1415550038573d744138bacb59e6b8"}, + {file = "greenlet-3.5.3-cp311-cp311-win_amd64.whl", hash = "sha256:0f41e4a05a3c0cb31b17023eff28dd111e1d16bf7d7d00406cd7df23f31398a7"}, + {file = "greenlet-3.5.3-cp311-cp311-win_arm64.whl", hash = "sha256:ec6f1af59f6b5f3fc9678e2ea062d8377d22ac644f7844cb7a292910cf12ff44"}, + {file = "greenlet-3.5.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:719757059f5a53fd0dde23f78cffeafcdd97b21c850ddb7ca684a3c1a1f122e2"}, + {file = "greenlet-3.5.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:efa9f765dd09f9d0cdac651ffdf631ee59ec5dc6ee7a73e0c012ba9c52fbdf5b"}, + {file = "greenlet-3.5.3-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7faba15ac005376e02a0384504e0243be3370ce010296a44a820feb342b505ab"}, + {file = "greenlet-3.5.3-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5795cd1101371140551c645f2d408b8d3c01a5a29cf8a9bce6e759c983682d23"}, + {file = "greenlet-3.5.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:87142215824be6ac05e2e8e2786eec307ccbc27c36723c3881959df654af6861"}, + {file = "greenlet-3.5.3-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:af4923b3096e26a36d7e9cf24ab88083a20f97d191e3b97f253731ce9b41b28c"}, + {file = "greenlet-3.5.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:215275b1b49320987352e6c1b054acca0064f965a2c66992bed9a6f7d913f149"}, + {file = "greenlet-3.5.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6b1b0eed82364b0e32c4ea0f221452d33e6bb17ae094d9f72aed9851812747ea"}, + {file = "greenlet-3.5.3-cp312-cp312-win_amd64.whl", hash = "sha256:cde8adafa2365676f74a979744629589999093bc86e2484214f58e61df08902c"}, + {file = "greenlet-3.5.3-cp312-cp312-win_arm64.whl", hash = "sha256:c4e7b79d83805475f0102008843f6eb45fd3bb0b2e88c774adab5fbaab27117d"}, + {file = "greenlet-3.5.3-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:c8d87c2134d871df96ecdea9cec7cbaab286dadab0f56476e57aaf9e8ac11550"}, + {file = "greenlet-3.5.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2d185dd1621757e70c3861cceffd5317ab4e7ed7eb09c82994828468527ade5"}, + {file = "greenlet-3.5.3-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1c514a468149bf8fbbab874188a3535cd8a48a3e353eb53a3d424296f8dbacd3"}, + {file = "greenlet-3.5.3-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9ad04dd75458c6300b047c61b8639092433d205a25a14e310d6582a480efcca1"}, + {file = "greenlet-3.5.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:915f887cf2682b66419b879423a2e072634aa7b7dce6f3ada4957cfced3f1e9a"}, + {file = "greenlet-3.5.3-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:afaabdd554cd7ae9bbb3ca070b0d7fdfd207dbf1d16865f7233837709d354bda"}, + {file = "greenlet-3.5.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:766cfd421c13e450feb340cd472a3ed9957d438727b7b4593ad7c76c5d2b0deb"}, + {file = "greenlet-3.5.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2ecda9ec22edf38fa389369eaed8c3d37c05f3c54e69f69438dbb2cc1de1458b"}, + {file = "greenlet-3.5.3-cp313-cp313-win_amd64.whl", hash = "sha256:c82304750f057167ff60d188df1d0cc1764ce9567eadf03e6a7443bcedd0b30b"}, + {file = "greenlet-3.5.3-cp313-cp313-win_arm64.whl", hash = "sha256:dc133a1569ee667b2a6ef56ce551084aeefd87a5acbc4736d336d1e2edc6cfc4"}, + {file = "greenlet-3.5.3-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:fd2e02fa07485778536a036222d616ab957b1d533f36b3ed98ce725d9c9d3117"}, + {file = "greenlet-3.5.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df0a0628d1597eb0897b62f55d1343f772405fd25f3b2a796c76874b0c2e22e8"}, + {file = "greenlet-3.5.3-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ebd933a6adabc298bab47731a130fe6bfb888bd934eee37810f151159544540d"}, + {file = "greenlet-3.5.3-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8d19fe6c39ebff9259f07bcc685d3290f8fa4ea2278e51dd0008e4d6b0f2d814"}, + {file = "greenlet-3.5.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4b9d501b40e80b70e32323c799dd9b420a5577a9601469d362ae1ffb690f3a7c"}, + {file = "greenlet-3.5.3-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:962c5df2db8cb446da51edf1ca5296c389d93b99c9d8aa2ee4c7d0d8f1218260"}, + {file = "greenlet-3.5.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a1fad1d11e7d6aab184107baa8e4ece11ccba3ec9599cd7efa5ff4d70d43256a"}, + {file = "greenlet-3.5.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:fad5aec764399f1b5cc347ad250a59660f20c8f8888ea6bae1f93b769cce1154"}, + {file = "greenlet-3.5.3-cp314-cp314-win_amd64.whl", hash = "sha256:7669aa24cf2a1041d6f7899575b494a3ab4cf68bfcc8609b1dc0be7272db835e"}, + {file = "greenlet-3.5.3-cp314-cp314-win_arm64.whl", hash = "sha256:5b4807c4082c9d1b6d9eed56fcd041863e37f2228106eef24c30ca096e238605"}, + {file = "greenlet-3.5.3-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:271a8ea7c1024e8a0d7dd2be66dd66dda8a07193f41a17b9e924f7600f5b62be"}, + {file = "greenlet-3.5.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:19131729ae0ddc3c2e1ef85e650169b5e37ee32e400f215f78b94d7b0d567310"}, + {file = "greenlet-3.5.3-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1540dd8e5fc2a5aec40fbb98ef8e149fa47c89a4b4a1cf2575a14d3d1869d7a8"}, + {file = "greenlet-3.5.3-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b897d97759425953f69a9c0fac67f8fe333ec0ce7377ef186fb2b0c3ad5e354d"}, + {file = "greenlet-3.5.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e81fa194a1d20967877bdf9c7794db2bc99063e5be36aee710c08f04c5bb087f"}, + {file = "greenlet-3.5.3-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:3236754d423955ea08e9bb5f6c04a7895f9e22c290b66aa7653fcb922d839eb0"}, + {file = "greenlet-3.5.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:55cf4d777485d43110e47133cbba6d74a8885a87ec1227ef0267f9ee80c5aa21"}, + {file = "greenlet-3.5.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:12a248ba75f6a9a236375f52296c498c89ff1d8badf32deb9eca7abd5853f7da"}, + {file = "greenlet-3.5.3-cp314-cp314t-win_amd64.whl", hash = "sha256:efc6bd60ea02e085862c74a3ef64b147ffc6f1a5ea7d9f26e7a939943f68c1e3"}, + {file = "greenlet-3.5.3-cp315-cp315-macosx_11_0_universal2.whl", hash = "sha256:ea03f2f04367845d6b58eeed276e1e56e51f0b97d8ad5a88a7d20a91dc9056cc"}, + {file = "greenlet-3.5.3-cp315-cp315-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78dbef602fda6d97d957eb7937f70c9ce9e9527330347f8f6b6f9e554a9e7a47"}, + {file = "greenlet-3.5.3-cp315-cp315-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f73857adb8fee13fa56c172bd11262f888c0c648f9fea113e777bb2c7904a81"}, + {file = "greenlet-3.5.3-cp315-cp315-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cefa9cef4b371f9844c6053db71f1138bc6807bab1578b0dae5149c1f1141357"}, + {file = "greenlet-3.5.3-cp315-cp315-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:232fec92e823addaf02d9472cf7381e24a1d046a6ced1103c5caa4c21b9dfc1d"}, + {file = "greenlet-3.5.3-cp315-cp315-manylinux_2_39_riscv64.whl", hash = "sha256:6219b6d04dbf6ba6084d77dc609e8473060dc55f759cbf626d512122781fa128"}, + {file = "greenlet-3.5.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:2421c3564da9429d5586d46ca31ebb26516b5498a802cf65c041a8e8a8980d34"}, + {file = "greenlet-3.5.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e0f0d160f0b2e558e6c75f7930967183255dc9735e5f5b8cae58ee09c9576d8b"}, + {file = "greenlet-3.5.3-cp315-cp315-win_amd64.whl", hash = "sha256:dd99329bbc15ca78dcc583dba05d0b1b0bae01ab6c2174989f5aaee3e41ac930"}, + {file = "greenlet-3.5.3-cp315-cp315-win_arm64.whl", hash = "sha256:499fef2acede88c1864a57bb586b4bf533c81e1b82df7ab93451cdb47dfec227"}, + {file = "greenlet-3.5.3-cp315-cp315t-macosx_11_0_universal2.whl", hash = "sha256:176bc16a721fa5fc294d70b87b4dfa5fbdd251b3da5d5372735ecef9bd7d6d0c"}, + {file = "greenlet-3.5.3-cp315-cp315t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:629b614d2b786e89c50440e246f33eea78f58a962d0bdbbcc809e6d13605903f"}, + {file = "greenlet-3.5.3-cp315-cp315t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2b2e857ae16f5f72142edf75f9f176fe7526ba19a2841df1420516f83831c9f2"}, + {file = "greenlet-3.5.3-cp315-cp315t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:16d192579ed281051396dddd7f7754dac6259e6b1fb26378c87b66622f8e3f91"}, + {file = "greenlet-3.5.3-cp315-cp315t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e515757e2e36bcbf1fad09a46e1557e8b1ae1797d4b44d09da7deed88ad28608"}, + {file = "greenlet-3.5.3-cp315-cp315t-manylinux_2_39_riscv64.whl", hash = "sha256:4399eb8d041f20b68d943918bc55502a93d6fdc0a37c14da7881c04139acee9d"}, + {file = "greenlet-3.5.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:b363d46ed1ea431825fdb01471bb024fc08399bad1572a616e853c7684415adb"}, + {file = "greenlet-3.5.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:e44da2f5bbdaabaf7d80b73dbb430c7035771e9f244e3c8b769715c9d8fa0a16"}, + {file = "greenlet-3.5.3-cp315-cp315t-win_amd64.whl", hash = "sha256:8ff8bed3e3baa20a3ea261ce00526f1898ad4801d4886fd2220580ee0ad8fadf"}, + {file = "greenlet-3.5.3-cp315-cp315t-win_arm64.whl", hash = "sha256:b7068bd09f761f3f5b4d214c2bed063186b2a86148c740b3873e3f56d79bac31"}, + {file = "greenlet-3.5.3.tar.gz", hash = "sha256:a61efc018fd3eb317eeca31aba90ee9e7f26f22884a79b6c6ec715bf71bb62f1"}, +] + +[package.extras] +docs = ["Sphinx", "furo"] +test = ["objgraph", "psutil", "setuptools"] + +[[package]] +name = "hbreader" +version = "0.9.1" +description = "Honey Badger reader - a generic file/url/string open and read tool" +optional = false +python-versions = ">=3.7" +files = [ + {file = "hbreader-0.9.1-py3-none-any.whl", hash = "sha256:9a6e76c9d1afc1b977374a5dc430a1ebb0ea0488205546d4678d6e31cc5f6801"}, + {file = "hbreader-0.9.1.tar.gz", hash = "sha256:d2c132f8ba6276d794c66224c3297cec25c8079d0a4cf019c061611e0a3b94fa"}, +] + [[package]] name = "identify" version = "2.6.19" @@ -221,6 +745,31 @@ files = [ [package.extras] license = ["ukkonen"] +[[package]] +name = "idna" +version = "3.18" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.9" +files = [ + {file = "idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2"}, + {file = "idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848"}, +] + +[package.extras] +all = ["mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + +[[package]] +name = "imagesize" +version = "1.5.0" +description = "Getting image size from png/jpeg/jpeg2000/gif file" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" +files = [ + {file = "imagesize-1.5.0-py2.py3-none-any.whl", hash = "sha256:32677681b3f434c2cb496f00e89c5a291247b35b1f527589909e008057da5899"}, + {file = "imagesize-1.5.0.tar.gz", hash = "sha256:8bfc5363a7f2133a89f0098451e0bcb1cd71aba4dc02bbcecb39d99d40e1b94f"}, +] + [[package]] name = "iniconfig" version = "2.3.0" @@ -247,6 +796,31 @@ files = [ pytz = "*" six = "*" +[[package]] +name = "isodate" +version = "0.7.2" +description = "An ISO 8601 date/time/duration parser and formatter" +optional = false +python-versions = ">=3.7" +files = [ + {file = "isodate-0.7.2-py3-none-any.whl", hash = "sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15"}, + {file = "isodate-0.7.2.tar.gz", hash = "sha256:4cd1aa0f43ca76f4a6c6c0292a85f40b35ec2e43e315b59f06e6d32171a953e6"}, +] + +[[package]] +name = "isoduration" +version = "20.11.0" +description = "Operations with ISO 8601 durations" +optional = false +python-versions = ">=3.7" +files = [ + {file = "isoduration-20.11.0-py3-none-any.whl", hash = "sha256:b2904c2a4228c3d44f409c8ae8e2370eb21a26f7ac2ec5446df141dde3452042"}, + {file = "isoduration-20.11.0.tar.gz", hash = "sha256:ac2f9015137935279eac671f94f89eb00584f940f5dc49462a0c4ee692ba1bd9"}, +] + +[package.dependencies] +arrow = ">=0.15.0" + [[package]] name = "isort" version = "5.13.2" @@ -261,6 +835,117 @@ files = [ [package.extras] colors = ["colorama (>=0.4.6)"] +[[package]] +name = "jinja2" +version = "3.1.6" +description = "A very fast and expressive template engine." +optional = false +python-versions = ">=3.7" +files = [ + {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, + {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "json-flattener" +version = "0.1.9" +description = "Python library for denormalizing nested dicts or json objects to tables and back" +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "json_flattener-0.1.9-py3-none-any.whl", hash = "sha256:6b027746f08bf37a75270f30c6690c7149d5f704d8af1740c346a3a1236bc941"}, + {file = "json_flattener-0.1.9.tar.gz", hash = "sha256:84cf8523045ffb124301a602602201665fcb003a171ece87e6f46ed02f7f0c15"}, +] + +[package.dependencies] +click = "*" +pyyaml = "*" + +[[package]] +name = "jsonasobj" +version = "1.3.1" +description = "JSON as python objects" +optional = false +python-versions = "*" +files = [ + {file = "jsonasobj-1.3.1-py3-none-any.whl", hash = "sha256:b9e329dc1ceaae7cf5d5b214684a0b100e0dad0be6d5bbabac281ec35ddeca65"}, + {file = "jsonasobj-1.3.1.tar.gz", hash = "sha256:d52e0544a54a08f6ea3f77fa3387271e3648655e0eace2f21e825c26370e44a2"}, +] + +[[package]] +name = "jsonasobj2" +version = "1.0.4" +description = "JSON as python objects - version 2" +optional = false +python-versions = ">=3.6" +files = [ + {file = "jsonasobj2-1.0.4-py3-none-any.whl", hash = "sha256:12e86f86324d54fcf60632db94ea74488d5314e3da554c994fe1e2c6f29acb79"}, + {file = "jsonasobj2-1.0.4.tar.gz", hash = "sha256:f50b1668ef478004aa487b2d2d094c304e5cb6b79337809f4a1f2975cc7fbb4e"}, +] + +[package.dependencies] +hbreader = "*" + +[[package]] +name = "jsonpointer" +version = "3.1.1" +description = "Identify specific nodes in a JSON document (RFC 6901) " +optional = false +python-versions = ">=3.10" +files = [ + {file = "jsonpointer-3.1.1-py3-none-any.whl", hash = "sha256:8ff8b95779d071ba472cf5bc913028df06031797532f08a7d5b602d8b2a488ca"}, + {file = "jsonpointer-3.1.1.tar.gz", hash = "sha256:0b801c7db33a904024f6004d526dcc53bbb8a4a0f4e32bfd10beadf60adf1900"}, +] + +[[package]] +name = "jsonschema" +version = "4.26.0" +description = "An implementation of JSON Schema validation for Python" +optional = false +python-versions = ">=3.10" +files = [ + {file = "jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce"}, + {file = "jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326"}, +] + +[package.dependencies] +attrs = ">=22.2.0" +fqdn = {version = "*", optional = true, markers = "extra == \"format\""} +idna = {version = "*", optional = true, markers = "extra == \"format\""} +isoduration = {version = "*", optional = true, markers = "extra == \"format\""} +jsonpointer = {version = ">1.13", optional = true, markers = "extra == \"format\""} +jsonschema-specifications = ">=2023.03.6" +referencing = ">=0.28.4" +rfc3339-validator = {version = "*", optional = true, markers = "extra == \"format\""} +rfc3987 = {version = "*", optional = true, markers = "extra == \"format\""} +rpds-py = ">=0.25.0" +uri-template = {version = "*", optional = true, markers = "extra == \"format\""} +webcolors = {version = ">=1.11", optional = true, markers = "extra == \"format\""} + +[package.extras] +format = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3987", "uri-template", "webcolors (>=1.11)"] +format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "rfc3987-syntax (>=1.1.0)", "uri-template", "webcolors (>=24.6.0)"] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +description = "The JSON Schema meta-schemas and vocabularies, exposed as a Registry" +optional = false +python-versions = ">=3.9" +files = [ + {file = "jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe"}, + {file = "jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d"}, +] + +[package.dependencies] +referencing = ">=0.31.0" + [[package]] name = "librt" version = "0.9.0" @@ -360,6 +1045,172 @@ files = [ {file = "librt-0.9.0.tar.gz", hash = "sha256:a0951822531e7aee6e0dfb556b30d5ee36bbe234faf60c20a16c01be3530869d"}, ] +[[package]] +name = "linkml" +version = "1.11.1" +description = "Linked Open Data Modeling Language" +optional = false +python-versions = ">=3.10" +files = [ + {file = "linkml-1.11.1-py3-none-any.whl", hash = "sha256:d1bbb97a8b1ea4a99b145007875733a5e5e89b3acfe3e9d1e369fa4a582990ed"}, + {file = "linkml-1.11.1.tar.gz", hash = "sha256:2f6774e13628270cadaeecda3313db0437ecc15cd44ee35c6c2655dbe31c8524"}, +] + +[package.dependencies] +antlr4-python3-runtime = ">=4.9.0,<4.10" +click = ">=8.2" +graphviz = ">=0.10.1" +hbreader = "*" +isodate = ">=0.6.0" +jinja2 = ">=3.1.0" +jsonasobj2 = ">=1.0.3,<2.0.0" +jsonschema = {version = ">=4.0.0", extras = ["format"]} +linkml-runtime = ">=1.10.0,<2.0.0" +openpyxl = "*" +parse = "*" +prefixcommons = ">=0.1.7" +prefixmaps = ">=0.2.2" +pydantic = ">=2.0.0,<3.0.0" +pyjsg = ">=0.11.6" +pyshex = ">=0.7.20" +pyshexc = ">=0.8.3" +python-dateutil = "*" +pyyaml = "*" +rdflib = ">=6.0.0" +requests = ">=2.22" +sphinx-click = ">=6.0.0" +sqlalchemy = ">=1.4.31" +typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.12\""} +watchdog = ">=0.9.0" + +[[package]] +name = "linkml-runtime" +version = "1.11.1" +description = "Runtime environment for LinkML, the Linked open data modeling language" +optional = false +python-versions = ">=3.10" +files = [ + {file = "linkml_runtime-1.11.1-py3-none-any.whl", hash = "sha256:b22c77d8fd920d0f4f43a6ece31393dc0b28bb47790f3e1c114210318c36b3da"}, + {file = "linkml_runtime-1.11.1.tar.gz", hash = "sha256:e71300b596c4f35aeccd9dca096806678402213dbdb2c5e8e68f507e21320754"}, +] + +[package.dependencies] +click = ">=8.2" +curies = ">=0.5.4" +deprecated = "*" +hbreader = "*" +isodate = {version = ">=0.7.2,<1.0.0", markers = "python_version < \"3.11\""} +json-flattener = ">=0.1.9" +jsonasobj2 = ">=1.0.4,<2.dev0" +jsonschema = ">=3.2.0" +prefixcommons = ">=0.1.12" +prefixmaps = ">=0.1.4" +pydantic = ">=1.10.2,<3.0.0" +pyyaml = "*" +rdflib = ">=6.0.0" +requests = "*" + +[package.extras] +dev = ["coverage", "requests-cache"] + +[[package]] +name = "markupsafe" +version = "3.0.3" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.9" +files = [ + {file = "markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559"}, + {file = "markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419"}, + {file = "markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695"}, + {file = "markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591"}, + {file = "markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c"}, + {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f"}, + {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6"}, + {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1"}, + {file = "markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa"}, + {file = "markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8"}, + {file = "markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1"}, + {file = "markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad"}, + {file = "markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a"}, + {file = "markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50"}, + {file = "markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf"}, + {file = "markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f"}, + {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a"}, + {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115"}, + {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a"}, + {file = "markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19"}, + {file = "markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01"}, + {file = "markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c"}, + {file = "markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e"}, + {file = "markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce"}, + {file = "markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d"}, + {file = "markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d"}, + {file = "markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a"}, + {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b"}, + {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f"}, + {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b"}, + {file = "markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d"}, + {file = "markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c"}, + {file = "markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f"}, + {file = "markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795"}, + {file = "markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219"}, + {file = "markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6"}, + {file = "markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676"}, + {file = "markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9"}, + {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1"}, + {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc"}, + {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12"}, + {file = "markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed"}, + {file = "markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5"}, + {file = "markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485"}, + {file = "markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73"}, + {file = "markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37"}, + {file = "markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19"}, + {file = "markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025"}, + {file = "markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6"}, + {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f"}, + {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb"}, + {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009"}, + {file = "markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354"}, + {file = "markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218"}, + {file = "markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287"}, + {file = "markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe"}, + {file = "markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026"}, + {file = "markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737"}, + {file = "markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97"}, + {file = "markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d"}, + {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda"}, + {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf"}, + {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe"}, + {file = "markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9"}, + {file = "markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581"}, + {file = "markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4"}, + {file = "markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab"}, + {file = "markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175"}, + {file = "markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634"}, + {file = "markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50"}, + {file = "markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e"}, + {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5"}, + {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523"}, + {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc"}, + {file = "markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d"}, + {file = "markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9"}, + {file = "markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa"}, + {file = "markupsafe-3.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26"}, + {file = "markupsafe-3.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc"}, + {file = "markupsafe-3.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c"}, + {file = "markupsafe-3.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42"}, + {file = "markupsafe-3.0.3-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b"}, + {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758"}, + {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2"}, + {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d"}, + {file = "markupsafe-3.0.3-cp39-cp39-win32.whl", hash = "sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7"}, + {file = "markupsafe-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e"}, + {file = "markupsafe-3.0.3-cp39-cp39-win_arm64.whl", hash = "sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8"}, + {file = "markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698"}, +] + [[package]] name = "monotonic" version = "1.6" @@ -528,6 +1379,20 @@ files = [ {file = "numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010"}, ] +[[package]] +name = "openpyxl" +version = "3.1.5" +description = "A Python library to read/write Excel 2010 xlsx/xlsm files" +optional = false +python-versions = ">=3.8" +files = [ + {file = "openpyxl-3.1.5-py2.py3-none-any.whl", hash = "sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2"}, + {file = "openpyxl-3.1.5.tar.gz", hash = "sha256:cf0e3cf56142039133628b5acffe8ef0c12bc902d2aadd3e0fe5878dc08d1050"}, +] + +[package.dependencies] +et-xmlfile = "*" + [[package]] name = "packaging" version = "26.2" @@ -667,6 +1532,17 @@ files = [ [package.dependencies] pillow = "*" +[[package]] +name = "parse" +version = "1.22.1" +description = "parse() is the opposite of format()" +optional = false +python-versions = "*" +files = [ + {file = "parse-1.22.1-py2.py3-none-any.whl", hash = "sha256:20f0925a46f06602485ac90d751764d0697fd8455aaa97489ba8953a4b66de32"}, + {file = "parse-1.22.1.tar.gz", hash = "sha256:d3a4740ec3da338e2b258b2d69741b731eadfddca59e24a14bc4ee5fce38c911"}, +] + [[package]] name = "pathspec" version = "1.1.1" @@ -836,19 +1712,51 @@ pyyaml = ">=5.1" virtualenv = ">=20.10.0" [[package]] -name = "py2neo" -version = "2021.2.4" -description = "Python client library and toolkit for Neo4j" +name = "prefixcommons" +version = "0.1.12" +description = "A python API for working with ID prefixes" optional = false -python-versions = "*" +python-versions = ">=3.7,<4.0" files = [ - {file = "py2neo-2021.2.4-py2.py3-none-any.whl", hash = "sha256:2ddbe818354a6fa16d47dfd0fe5cb0287fa42ff109e87aa7b3e43636060d85a1"}, - {file = "py2neo-2021.2.4.tar.gz", hash = "sha256:4b2737fcd9fd8d82b57e856de4eda005281c9cf0741c989e5252678f0503f77e"}, + {file = "prefixcommons-0.1.12-py3-none-any.whl", hash = "sha256:16dbc0a1f775e003c724f19a694fcfa3174608f5c8b0e893d494cf8098ac7f8b"}, + {file = "prefixcommons-0.1.12.tar.gz", hash = "sha256:22c4e2d37b63487b3ab48f0495b70f14564cb346a15220f23919eb0c1851f69f"}, ] [package.dependencies] -certifi = "*" -interchange = ">=2021.0.4,<2021.1.0" +click = ">=8.1.3,<9.0.0" +pytest-logging = ">=2015.11.4,<2016.0.0" +PyYAML = ">=6.0,<7.0" +requests = ">=2.28.1,<3.0.0" + +[[package]] +name = "prefixmaps" +version = "0.2.6" +description = "A python library for retrieving semantic prefix maps" +optional = false +python-versions = "<4.0,>=3.8" +files = [ + {file = "prefixmaps-0.2.6-py3-none-any.whl", hash = "sha256:f6cef28a7320fc6337cf411be212948ce570333a0ce958940ef684c7fb192a62"}, + {file = "prefixmaps-0.2.6.tar.gz", hash = "sha256:7421e1244eea610217fa1ba96c9aebd64e8162a930dc0626207cd8bf62ecf4b9"}, +] + +[package.dependencies] +curies = ">=0.5.3" +pyyaml = ">=5.3.1" + +[[package]] +name = "py2neo" +version = "2021.2.4" +description = "Python client library and toolkit for Neo4j" +optional = false +python-versions = "*" +files = [ + {file = "py2neo-2021.2.4-py2.py3-none-any.whl", hash = "sha256:2ddbe818354a6fa16d47dfd0fe5cb0287fa42ff109e87aa7b3e43636060d85a1"}, + {file = "py2neo-2021.2.4.tar.gz", hash = "sha256:4b2737fcd9fd8d82b57e856de4eda005281c9cf0741c989e5252678f0503f77e"}, +] + +[package.dependencies] +certifi = "*" +interchange = ">=2021.0.4,<2021.1.0" monotonic = "*" packaging = "*" pansi = ">=2020.7.3" @@ -907,6 +1815,159 @@ numpy = ">=1.16.6" [package.extras] test = ["cffi", "hypothesis", "pandas", "pytest", "pytz"] +[[package]] +name = "pydantic" +version = "2.13.4" +description = "Data validation using Python type hints" +optional = false +python-versions = ">=3.9" +files = [ + {file = "pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba"}, + {file = "pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6"}, +] + +[package.dependencies] +annotated-types = ">=0.6.0" +pydantic-core = "2.46.4" +typing-extensions = ">=4.14.1" +typing-inspection = ">=0.4.2" + +[package.extras] +email = ["email-validator (>=2.0.0)"] +timezone = ["tzdata"] + +[[package]] +name = "pydantic-core" +version = "2.46.4" +description = "Core functionality for Pydantic validation and serialization" +optional = false +python-versions = ">=3.9" +files = [ + {file = "pydantic_core-2.46.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:a396dcc17e5a0b164dbe026896245a4fa9ff402edca1dff0be3d53a517f74de4"}, + {file = "pydantic_core-2.46.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:da4b951fe36dc7c3a1ccb4e3cd1747c3542b8c9ceede8fc86cae054e764485f5"}, + {file = "pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb63e0198ca18aad131c089b9204c23079c3afa95487e561f4c522d519e55aba"}, + {file = "pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f47286a97f0bc9b8859519809077b91b2cefe4ae47fcbf5e466a009c1c5d742b"}, + {file = "pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:905a0ed8ea6f2d61c1738835f99b699348d7857379083e5fc497fa0c967a407c"}, + {file = "pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea793e075b70290d89d8142074262885d3f7da19634845135751bd6344f73b50"}, + {file = "pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:395aebd9183f9d112f569aeb5b2214d1a10a33bec8456447f7fbdfa51d38d4cd"}, + {file = "pydantic_core-2.46.4-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:b078afbc25f3a1436c7a1d2cd3e322497ee99615ba97c563566fdf46aff1ee01"}, + {file = "pydantic_core-2.46.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f747929cf940cddb5b3668a390056ddd5ba2e5010615ea2dcf4f9c4f3ab8791d"}, + {file = "pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:daa27d92c36f24388fe3ad306b174781c747627f134452e4f128ea00ce1fe8c4"}, + {file = "pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:19e51f073cd3df251856a8a4189fbdf1de4012c3ebacfb1884f94f1eb406079f"}, + {file = "pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1747f85cee84c26985853c6f3d9bd3e75da5212912443fa111c113b9c246f39"}, + {file = "pydantic_core-2.46.4-cp310-cp310-win32.whl", hash = "sha256:2f84c03c8607173d16b5a854ec68a2f9079ae03237a54fb506d13af47e1d018d"}, + {file = "pydantic_core-2.46.4-cp310-cp310-win_amd64.whl", hash = "sha256:8358a950c8909158e3df31538a7e4edc2d7265a7c54b47f0864d9e5bae9dcebf"}, + {file = "pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594"}, + {file = "pydantic_core-2.46.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c"}, + {file = "pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826"}, + {file = "pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04"}, + {file = "pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e"}, + {file = "pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3"}, + {file = "pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4"}, + {file = "pydantic_core-2.46.4-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398"}, + {file = "pydantic_core-2.46.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3"}, + {file = "pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848"}, + {file = "pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3"}, + {file = "pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109"}, + {file = "pydantic_core-2.46.4-cp311-cp311-win32.whl", hash = "sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda"}, + {file = "pydantic_core-2.46.4-cp311-cp311-win_amd64.whl", hash = "sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33"}, + {file = "pydantic_core-2.46.4-cp311-cp311-win_arm64.whl", hash = "sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d"}, + {file = "pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2"}, + {file = "pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f"}, + {file = "pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7"}, + {file = "pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7"}, + {file = "pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712"}, + {file = "pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4"}, + {file = "pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce"}, + {file = "pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987"}, + {file = "pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b"}, + {file = "pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458"}, + {file = "pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b"}, + {file = "pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c"}, + {file = "pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894"}, + {file = "pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89"}, + {file = "pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a"}, + {file = "pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008"}, + {file = "pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4"}, + {file = "pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76"}, + {file = "pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3"}, + {file = "pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76"}, + {file = "pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4"}, + {file = "pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a"}, + {file = "pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262"}, + {file = "pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e"}, + {file = "pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd"}, + {file = "pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be"}, + {file = "pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d"}, + {file = "pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb"}, + {file = "pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292"}, + {file = "pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d"}, + {file = "pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb"}, + {file = "pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462"}, + {file = "pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9"}, + {file = "pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4"}, + {file = "pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914"}, + {file = "pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28"}, + {file = "pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b"}, + {file = "pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c"}, + {file = "pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb"}, + {file = "pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898"}, + {file = "pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e"}, + {file = "pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519"}, + {file = "pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4"}, + {file = "pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac"}, + {file = "pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596"}, + {file = "pydantic_core-2.46.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:fd8b3d9fd264be37976686c7f65cd52a83f5e84f4bfd2adf9c1d469676bbb6ae"}, + {file = "pydantic_core-2.46.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9f444c499b3eefd3a92e348059471ea0c3a6e303d9c1cec09fa748fd9f895201"}, + {file = "pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3447661d99f75a3683a4cf5c87da72f2161964611864dbbeac7fbb118bb4bfc0"}, + {file = "pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8b9bab013d1c7a79d3501ff86d0bc9c31bf587db4551677b96bec07df78c6b15"}, + {file = "pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d995260fdf4e1db774581b4900e0f832abe3c7c84996726bbc161b19c8f29e76"}, + {file = "pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f13a646d65d09fbf1bc6b3a9635d30095c8e7e5cc419ff35ecc563c5fd04cd49"}, + {file = "pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432c179df7874eeb73307aad2df0755e1ae0efa61ff0ea89b93e194411ae3928"}, + {file = "pydantic_core-2.46.4-cp39-cp39-manylinux_2_31_riscv64.whl", hash = "sha256:e68b7a074f65a2fd746c52a7ce6142ab7006074ac269ace0c25cd8ba171f8066"}, + {file = "pydantic_core-2.46.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4a05d69cba51d852c5c3e92758653245a50c0b646ced0cf05bd793ed592839d6"}, + {file = "pydantic_core-2.46.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:228ee9bae8bef5b1e97ec58302f80357c37199e0d0a99174e138d28e6957b9d9"}, + {file = "pydantic_core-2.46.4-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:10e17cbb10a330363733efc4d7c4d0dd827ac0909b8f6a6542298fed1ea62f29"}, + {file = "pydantic_core-2.46.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:91a06d2e259ecfbd8c901d70c3c507900458498142b3026a296b7de4d1322cc9"}, + {file = "pydantic_core-2.46.4-cp39-cp39-win32.whl", hash = "sha256:d80ee3d731373b24cebbc10d689ca4ee1875caf0d5703a245db18efd4dd37fc1"}, + {file = "pydantic_core-2.46.4-cp39-cp39-win_amd64.whl", hash = "sha256:3be77f45df024d789a672ae34f8b06fb346c4f9f46ea714956660ea4862e89ac"}, + {file = "pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c"}, + {file = "pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b"}, + {file = "pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b"}, + {file = "pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea"}, + {file = "pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7"}, + {file = "pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df"}, + {file = "pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526"}, + {file = "pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0"}, + {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0"}, + {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7"}, + {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2"}, + {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9"}, + {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf"}, + {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30"}, + {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc"}, + {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983"}, + {file = "pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1"}, +] + +[package.dependencies] +typing-extensions = ">=4.14.1" + [[package]] name = "pygments" version = "2.20.0" @@ -921,6 +1982,105 @@ files = [ [package.extras] windows-terminal = ["colorama (>=0.4.6)"] +[[package]] +name = "pyjsg" +version = "0.12.4" +description = "Python JSON Schema Grammar interpreter" +optional = false +python-versions = ">=3.10" +files = [ + {file = "pyjsg-0.12.4-py3-none-any.whl", hash = "sha256:a57ae58bfd7192b32654a0024bc6462fb459d54e837f0b2b5cff0726aad2e557"}, + {file = "pyjsg-0.12.4.tar.gz", hash = "sha256:bb1c0ff1f50846d2b5185b182e28b0b6978eae51a2078ce3eb1e0f28dea7b9ab"}, +] + +[package.dependencies] +antlr4-python3-runtime = ">=4.9.3,<4.10.0" +jsonasobj = ">=1.2.1" +requests = "*" + +[[package]] +name = "pyparsing" +version = "3.3.2" +description = "pyparsing - Classes and methods to define and execute parsing grammars" +optional = false +python-versions = ">=3.9" +files = [ + {file = "pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d"}, + {file = "pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc"}, +] + +[package.extras] +diagrams = ["jinja2", "railroad-diagrams"] + +[[package]] +name = "pyshex" +version = "0.9.0" +description = "Python ShEx interpreter" +optional = false +python-versions = ">=3.10" +files = [ + {file = "pyshex-0.9.0-py3-none-any.whl", hash = "sha256:d81344deed686b7c169f23156221ae281225e2ba02b14fe9810335afdefffa9d"}, + {file = "pyshex-0.9.0.tar.gz", hash = "sha256:87288b5e5613f734f55f0085334558218ff618fb1061aabdcee19841092b3eca"}, +] + +[package.dependencies] +cfgraph = ">=0.2.1" +chardet = "*" +pyshexc = ">=0.10.3" +rdflib-shim = "*" +requests = ">=2.22.0" +shexjsg = ">=0.9.0" +sparqlslurper = ">=0.5.1" +sparqlwrapper = ">=1.8.5" +urllib3 = "*" + +[[package]] +name = "pyshexc" +version = "0.10.3.post1" +description = "PyShExC - Python ShEx compiler" +optional = false +python-versions = ">=3.10" +files = [ + {file = "pyshexc-0.10.3.post1-py3-none-any.whl", hash = "sha256:5d247f2822ef9864152545935d93a07dce66640608ea9414c96f69da7fe7a168"}, + {file = "pyshexc-0.10.3.post1.tar.gz", hash = "sha256:80d9d067c80af9a796e3c1c47d2207edf2e9a9fc39d3ca0ce5dd2019334ea915"}, +] + +[package.dependencies] +antlr4-python3-runtime = ">=4.9.3,<4.10.0" +chardet = ">=7.4.1" +jsonasobj = ">=1.2.1" +pyjsg = ">=0.11.10" +rdflib-shim = ">=1.0.3" +shexjsg = ">=0.8.1" + +[[package]] +name = "pystow" +version = "0.8.21" +description = "Easily pick a place to store data for your Python code" +optional = false +python-versions = ">=3.10" +files = [ + {file = "pystow-0.8.21-py3-none-any.whl", hash = "sha256:7b14f77f0395b93a3a94d85526972c98d526d86a8efa5036363b9da3e2d34003"}, + {file = "pystow-0.8.21.tar.gz", hash = "sha256:460c299093d3e6f45433141ba0c5bc5d99c7fe98b042a6f578040d5103db7aab"}, +] + +[package.dependencies] +backports-zstd = {version = "*", markers = "python_full_version < \"3.14\""} +tqdm = "*" +typing-extensions = "*" + +[package.extras] +aws = ["boto3"] +bs4 = ["bs4", "requests"] +cli = ["click"] +pandas = ["pandas"] +pydantic = ["pydantic"] +ratelimit = ["ratelimit", "requests"] +rdf = ["rdflib"] +requests = ["requests"] +xml = ["lxml"] +yaml = ["pyyaml"] + [[package]] name = "pytest" version = "9.0.3" @@ -963,6 +2123,19 @@ pytest = ">=7" [package.extras] testing = ["process-tests", "pytest-xdist", "virtualenv"] +[[package]] +name = "pytest-logging" +version = "2015.11.4" +description = "Configures logging and allows tweaking the log level with a py.test flag" +optional = false +python-versions = "*" +files = [ + {file = "pytest-logging-2015.11.4.tar.gz", hash = "sha256:cec5c85ecf18aab7b2ead5498a31b9f758680ef5a902b9054ab3f2bdbb77c896"}, +] + +[package.dependencies] +pytest = ">=2.8.1" + [[package]] name = "python-dateutil" version = "2.9.0.post0" @@ -1103,6 +2276,245 @@ files = [ {file = "pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f"}, ] +[[package]] +name = "rdflib" +version = "7.6.0" +description = "RDFLib is a Python library for working with RDF, a simple yet powerful language for representing information." +optional = false +python-versions = ">=3.8.1" +files = [ + {file = "rdflib-7.6.0-py3-none-any.whl", hash = "sha256:30c0a3ebf4c0e09215f066be7246794b6492e054e782d7ac2a34c9f70a15e0dd"}, + {file = "rdflib-7.6.0.tar.gz", hash = "sha256:6c831288d5e4a5a7ece85d0ccde9877d512a3d0f02d7c06455d00d6d0ea379df"}, +] + +[package.dependencies] +isodate = {version = ">=0.7.2,<1.0.0", markers = "python_version < \"3.11\""} +pyparsing = ">=2.1.0,<4" + +[package.extras] +berkeleydb = ["berkeleydb (>=18.1.0,<19.0.0)"] +graphdb = ["httpx (>=0.28.1,<0.29.0)"] +html = ["html5rdf (>=1.2,<2)"] +lxml = ["lxml (>=4.3,<6.0)"] +networkx = ["networkx (>=2,<4)"] +orjson = ["orjson (>=3.9.14,<4)"] +rdf4j = ["httpx (>=0.28.1,<0.29.0)"] + +[[package]] +name = "rdflib-jsonld" +version = "0.6.1" +description = "rdflib extension adding JSON-LD parser and serializer" +optional = false +python-versions = "*" +files = [ + {file = "rdflib-jsonld-0.6.1.tar.gz", hash = "sha256:eda5a42a2e09f80d4da78e32b5c684bccdf275368f1541e6b7bcddfb1382a0e0"}, + {file = "rdflib_jsonld-0.6.1-py2.py3-none-any.whl", hash = "sha256:bcf84317e947a661bae0a3f2aee1eced697075fc4ac4db6065a3340ea0f10fc2"}, +] + +[package.dependencies] +rdflib = ">=5.0.0" + +[[package]] +name = "rdflib-shim" +version = "1.0.3" +description = "Shim for rdflib 5 and 6 incompatibilities" +optional = false +python-versions = ">=3.7" +files = [ + {file = "rdflib_shim-1.0.3-py3-none-any.whl", hash = "sha256:7a853e7750ef1e9bf4e35dea27d54e02d4ed087de5a9e0c329c4a6d82d647081"}, + {file = "rdflib_shim-1.0.3.tar.gz", hash = "sha256:d955d11e2986aab42b6830ca56ac6bc9c893abd1d049a161c6de2f1b99d4fc0d"}, +] + +[package.dependencies] +rdflib = ">=5.0.0" +rdflib-jsonld = "0.6.1" + +[[package]] +name = "referencing" +version = "0.37.0" +description = "JSON Referencing + Python" +optional = false +python-versions = ">=3.10" +files = [ + {file = "referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231"}, + {file = "referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8"}, +] + +[package.dependencies] +attrs = ">=22.2.0" +rpds-py = ">=0.7.0" +typing-extensions = {version = ">=4.4.0", markers = "python_version < \"3.13\""} + +[[package]] +name = "requests" +version = "2.34.2" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.10" +files = [ + {file = "requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0"}, + {file = "requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed"}, +] + +[package.dependencies] +certifi = ">=2023.5.7" +charset_normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.26,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<8)"] + +[[package]] +name = "rfc3339-validator" +version = "0.1.4" +description = "A pure python RFC3339 validator" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "rfc3339_validator-0.1.4-py2.py3-none-any.whl", hash = "sha256:24f6ec1eda14ef823da9e36ec7113124b39c04d50a4d3d3a3c2859577e7791fa"}, + {file = "rfc3339_validator-0.1.4.tar.gz", hash = "sha256:138a2abdf93304ad60530167e51d2dfb9549521a836871b88d7f4695d0022f6b"}, +] + +[package.dependencies] +six = "*" + +[[package]] +name = "rfc3987" +version = "1.3.8" +description = "Parsing and validation of URIs (RFC 3986) and IRIs (RFC 3987)" +optional = false +python-versions = "*" +files = [ + {file = "rfc3987-1.3.8-py2.py3-none-any.whl", hash = "sha256:10702b1e51e5658843460b189b185c0366d2cf4cff716f13111b0ea9fd2dce53"}, + {file = "rfc3987-1.3.8.tar.gz", hash = "sha256:d3c4d257a560d544e9826b38bc81db676890c79ab9d7ac92b39c7a253d5ca733"}, +] + +[[package]] +name = "rpds-py" +version = "0.30.0" +description = "Python bindings to Rust's persistent data structures (rpds)" +optional = false +python-versions = ">=3.10" +files = [ + {file = "rpds_py-0.30.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:679ae98e00c0e8d68a7fda324e16b90fd5260945b45d3b824c892cec9eea3288"}, + {file = "rpds_py-0.30.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4cc2206b76b4f576934f0ed374b10d7ca5f457858b157ca52064bdfc26b9fc00"}, + {file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:389a2d49eded1896c3d48b0136ead37c48e221b391c052fba3f4055c367f60a6"}, + {file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:32c8528634e1bf7121f3de08fa85b138f4e0dc47657866630611b03967f041d7"}, + {file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f207f69853edd6f6700b86efb84999651baf3789e78a466431df1331608e5324"}, + {file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:67b02ec25ba7a9e8fa74c63b6ca44cf5707f2fbfadae3ee8e7494297d56aa9df"}, + {file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0e95f6819a19965ff420f65578bacb0b00f251fefe2c8b23347c37174271f3"}, + {file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:a452763cc5198f2f98898eb98f7569649fe5da666c2dc6b5ddb10fde5a574221"}, + {file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e0b65193a413ccc930671c55153a03ee57cecb49e6227204b04fae512eb657a7"}, + {file = "rpds_py-0.30.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:858738e9c32147f78b3ac24dc0edb6610000e56dc0f700fd5f651d0a0f0eb9ff"}, + {file = "rpds_py-0.30.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:da279aa314f00acbb803da1e76fa18666778e8a8f83484fba94526da5de2cba7"}, + {file = "rpds_py-0.30.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7c64d38fb49b6cdeda16ab49e35fe0da2e1e9b34bc38bd78386530f218b37139"}, + {file = "rpds_py-0.30.0-cp310-cp310-win32.whl", hash = "sha256:6de2a32a1665b93233cde140ff8b3467bdb9e2af2b91079f0333a0974d12d464"}, + {file = "rpds_py-0.30.0-cp310-cp310-win_amd64.whl", hash = "sha256:1726859cd0de969f88dc8673bdd954185b9104e05806be64bcd87badbe313169"}, + {file = "rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425"}, + {file = "rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d"}, + {file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4"}, + {file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f"}, + {file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4"}, + {file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97"}, + {file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89"}, + {file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d"}, + {file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038"}, + {file = "rpds_py-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7"}, + {file = "rpds_py-0.30.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed"}, + {file = "rpds_py-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85"}, + {file = "rpds_py-0.30.0-cp311-cp311-win32.whl", hash = "sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c"}, + {file = "rpds_py-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825"}, + {file = "rpds_py-0.30.0-cp311-cp311-win_arm64.whl", hash = "sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229"}, + {file = "rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad"}, + {file = "rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05"}, + {file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28"}, + {file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd"}, + {file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f"}, + {file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1"}, + {file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23"}, + {file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6"}, + {file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51"}, + {file = "rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5"}, + {file = "rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e"}, + {file = "rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394"}, + {file = "rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf"}, + {file = "rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b"}, + {file = "rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e"}, + {file = "rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2"}, + {file = "rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8"}, + {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4"}, + {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136"}, + {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7"}, + {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2"}, + {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6"}, + {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e"}, + {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d"}, + {file = "rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7"}, + {file = "rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31"}, + {file = "rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95"}, + {file = "rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d"}, + {file = "rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15"}, + {file = "rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1"}, + {file = "rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a"}, + {file = "rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e"}, + {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000"}, + {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db"}, + {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2"}, + {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa"}, + {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083"}, + {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9"}, + {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0"}, + {file = "rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94"}, + {file = "rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08"}, + {file = "rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27"}, + {file = "rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6"}, + {file = "rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d"}, + {file = "rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0"}, + {file = "rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be"}, + {file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f"}, + {file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f"}, + {file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87"}, + {file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18"}, + {file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad"}, + {file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07"}, + {file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f"}, + {file = "rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65"}, + {file = "rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f"}, + {file = "rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53"}, + {file = "rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed"}, + {file = "rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950"}, + {file = "rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6"}, + {file = "rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb"}, + {file = "rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8"}, + {file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7"}, + {file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898"}, + {file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e"}, + {file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419"}, + {file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551"}, + {file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8"}, + {file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5"}, + {file = "rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404"}, + {file = "rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856"}, + {file = "rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40"}, + {file = "rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0"}, + {file = "rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e"}, + {file = "rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84"}, +] + [[package]] name = "ruff" version = "0.3.7" @@ -1192,6 +2604,20 @@ dev = ["cython-lint (>=0.12.2)", "doit (>=0.36.0)", "mypy (==1.10.0)", "pycodest doc = ["intersphinx_registry", "jupyterlite-pyodide-kernel", "jupyterlite-sphinx (>=0.19.1)", "jupytext", "matplotlib (>=3.5)", "myst-nb", "numpydoc", "pooch", "pydata-sphinx-theme (>=0.15.2)", "sphinx (>=5.0.0,<8.0.0)", "sphinx-copybutton", "sphinx-design (>=0.4.0)"] test = ["Cython", "array-api-strict (>=2.0,<2.1.1)", "asv", "gmpy2", "hypothesis (>=6.30)", "meson", "mpmath", "ninja", "pooch", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "scikit-umfpack", "threadpoolctl"] +[[package]] +name = "shexjsg" +version = "0.9.0" +description = "ShExJSG - Astract Syntax Tree Definition for the ShEx 2.0 language" +optional = false +python-versions = ">=3.10" +files = [ + {file = "shexjsg-0.9.0-py3-none-any.whl", hash = "sha256:abf18db2d9895bc46740f68ae699b2ccfe08c783f6e0c038e6077293ad01c0a5"}, + {file = "shexjsg-0.9.0.tar.gz", hash = "sha256:750016fabdb5487b27e2e714145f3602cd3ac4eb0dd9b7d7751d0cde62c0d1d8"}, +] + +[package.dependencies] +pyjsg = ">=0.12.3" + [[package]] name = "six" version = "1.17.0" @@ -1203,6 +2629,298 @@ files = [ {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, ] +[[package]] +name = "snowballstemmer" +version = "3.1.1" +description = "This package provides 36 stemmers for 34 languages generated from Snowball algorithms." +optional = false +python-versions = ">=3.3" +files = [ + {file = "snowballstemmer-3.1.1-py3-none-any.whl", hash = "sha256:7e207fa178741da09cdee59d3ecec3827ad5f92b1fc5c9ff3755b639f71f5752"}, + {file = "snowballstemmer-3.1.1.tar.gz", hash = "sha256:e07bbc54a0d798fe6010a12398422e62a8bfbba95c394fd0956ef58cb4d3e260"}, +] + +[[package]] +name = "sparqlslurper" +version = "0.5.1" +description = "SPARQL Slurper for rdflib" +optional = false +python-versions = ">=3.7.4" +files = [ + {file = "sparqlslurper-0.5.1-py3-none-any.whl", hash = "sha256:ae49b2d8ce3dd38df7a40465b228ad5d33fb7e11b3f248d195f9cadfc9cfff87"}, + {file = "sparqlslurper-0.5.1.tar.gz", hash = "sha256:9282ebb064fc6152a58269d194cb1e7b275b0f095425a578d75b96dcc851f546"}, +] + +[package.dependencies] +rdflib = ">=5.0.0" +rdflib-shim = "*" +sparqlwrapper = ">=1.8.2" + +[[package]] +name = "sparqlwrapper" +version = "2.0.0" +description = "SPARQL Endpoint interface to Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "SPARQLWrapper-2.0.0-py3-none-any.whl", hash = "sha256:c99a7204fff676ee28e6acef327dc1ff8451c6f7217dcd8d49e8872f324a8a20"}, + {file = "SPARQLWrapper-2.0.0.tar.gz", hash = "sha256:3fed3ebcc77617a4a74d2644b86fd88e0f32e7f7003ac7b2b334c026201731f1"}, +] + +[package.dependencies] +rdflib = ">=6.1.1" + +[package.extras] +dev = ["mypy (>=0.931)", "pandas (>=1.3.5)", "pandas-stubs (>=1.2.0.48)", "setuptools (>=3.7.1)"] +docs = ["sphinx (<5)", "sphinx-rtd-theme"] +keepalive = ["keepalive (>=0.5)"] +pandas = ["pandas (>=1.3.5)"] + +[[package]] +name = "sphinx" +version = "8.1.3" +description = "Python documentation generator" +optional = false +python-versions = ">=3.10" +files = [ + {file = "sphinx-8.1.3-py3-none-any.whl", hash = "sha256:09719015511837b76bf6e03e42eb7595ac8c2e41eeb9c29c5b755c6b677992a2"}, + {file = "sphinx-8.1.3.tar.gz", hash = "sha256:43c1911eecb0d3e161ad78611bc905d1ad0e523e4ddc202a58a821773dc4c927"}, +] + +[package.dependencies] +alabaster = ">=0.7.14" +babel = ">=2.13" +colorama = {version = ">=0.4.6", markers = "sys_platform == \"win32\""} +docutils = ">=0.20,<0.22" +imagesize = ">=1.3" +Jinja2 = ">=3.1" +packaging = ">=23.0" +Pygments = ">=2.17" +requests = ">=2.30.0" +snowballstemmer = ">=2.2" +sphinxcontrib-applehelp = ">=1.0.7" +sphinxcontrib-devhelp = ">=1.0.6" +sphinxcontrib-htmlhelp = ">=2.0.6" +sphinxcontrib-jsmath = ">=1.0.1" +sphinxcontrib-qthelp = ">=1.0.6" +sphinxcontrib-serializinghtml = ">=1.1.9" +tomli = {version = ">=2", markers = "python_version < \"3.11\""} + +[package.extras] +docs = ["sphinxcontrib-websupport"] +lint = ["flake8 (>=6.0)", "mypy (==1.11.1)", "pyright (==1.1.384)", "pytest (>=6.0)", "ruff (==0.6.9)", "sphinx-lint (>=0.9)", "tomli (>=2)", "types-Pillow (==10.2.0.20240822)", "types-Pygments (==2.18.0.20240506)", "types-colorama (==0.4.15.20240311)", "types-defusedxml (==0.7.0.20240218)", "types-docutils (==0.21.0.20241005)", "types-requests (==2.32.0.20240914)", "types-urllib3 (==1.26.25.14)"] +test = ["cython (>=3.0)", "defusedxml (>=0.7.1)", "pytest (>=8.0)", "setuptools (>=70.0)", "typing_extensions (>=4.9)"] + +[[package]] +name = "sphinx-click" +version = "6.2.0" +description = "Sphinx extension that automatically documents click applications" +optional = false +python-versions = ">=3.10" +files = [ + {file = "sphinx_click-6.2.0-py3-none-any.whl", hash = "sha256:1fb1851cb4f2c286d43cbcd57f55db6ef5a8d208bfc3370f19adde232e5803d7"}, + {file = "sphinx_click-6.2.0.tar.gz", hash = "sha256:fc78b4154a4e5159462e36de55b8643747da6cda86b3b52a8bb62289e603776c"}, +] + +[package.dependencies] +click = ">=8.0" +docutils = "*" +sphinx = ">=4.0" + +[package.extras] +docs = ["reno"] +test = ["pytest", "pytest-cov"] + +[[package]] +name = "sphinxcontrib-applehelp" +version = "2.0.0" +description = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple help books" +optional = false +python-versions = ">=3.9" +files = [ + {file = "sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5"}, + {file = "sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1"}, +] + +[package.extras] +lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] +standalone = ["Sphinx (>=5)"] +test = ["pytest"] + +[[package]] +name = "sphinxcontrib-devhelp" +version = "2.0.0" +description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp documents" +optional = false +python-versions = ">=3.9" +files = [ + {file = "sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2"}, + {file = "sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad"}, +] + +[package.extras] +lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] +standalone = ["Sphinx (>=5)"] +test = ["pytest"] + +[[package]] +name = "sphinxcontrib-htmlhelp" +version = "2.1.0" +description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" +optional = false +python-versions = ">=3.9" +files = [ + {file = "sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8"}, + {file = "sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9"}, +] + +[package.extras] +lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] +standalone = ["Sphinx (>=5)"] +test = ["html5lib", "pytest"] + +[[package]] +name = "sphinxcontrib-jsmath" +version = "1.0.1" +description = "A sphinx extension which renders display math in HTML via JavaScript" +optional = false +python-versions = ">=3.5" +files = [ + {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, + {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, +] + +[package.extras] +test = ["flake8", "mypy", "pytest"] + +[[package]] +name = "sphinxcontrib-qthelp" +version = "2.0.0" +description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp documents" +optional = false +python-versions = ">=3.9" +files = [ + {file = "sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb"}, + {file = "sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab"}, +] + +[package.extras] +lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] +standalone = ["Sphinx (>=5)"] +test = ["defusedxml (>=0.7.1)", "pytest"] + +[[package]] +name = "sphinxcontrib-serializinghtml" +version = "2.0.0" +description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)" +optional = false +python-versions = ">=3.9" +files = [ + {file = "sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331"}, + {file = "sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d"}, +] + +[package.extras] +lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] +standalone = ["Sphinx (>=5)"] +test = ["pytest"] + +[[package]] +name = "sqlalchemy" +version = "2.0.51" +description = "Database Abstraction Library" +optional = false +python-versions = ">=3.7" +files = [ + {file = "sqlalchemy-2.0.51-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e8203d2fbd5c6254692ef0a72c740d75b2f3c7ca345404f4c1a4604813c77c0"}, + {file = "sqlalchemy-2.0.51-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1af05726b3d0cdba1c55284bf408fd3b792e690fe2399bfb8304565551cda652"}, + {file = "sqlalchemy-2.0.51-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e54ff2dd657f2e3e0fbf2b097db1182f7bfea263eca4353f00065bae2a67c3d"}, + {file = "sqlalchemy-2.0.51-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:1e47b1199c2e832e325eacabc8d32d2487f58c9358f97e9a00f5eb93c5680d84"}, + {file = "sqlalchemy-2.0.51-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c68568f3facf8f66fa76c60e0ced69b67666ffa9941d1d0a3756fda196049080"}, + {file = "sqlalchemy-2.0.51-cp310-cp310-win32.whl", hash = "sha256:0592bdadf86ddcabfd72d9ab66ea8a5d8d2cc6be1cc51fa7e66c03868ac5eac1"}, + {file = "sqlalchemy-2.0.51-cp310-cp310-win_amd64.whl", hash = "sha256:740cf6f35351b1ac3d82369152acf1d51d37e3dcf85d4dc0a22ca01410eabe2a"}, + {file = "sqlalchemy-2.0.51-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1aa10c0daee6705294d181daadaa793221e1a59ed55000a3fab1d42b088ce4ba"}, + {file = "sqlalchemy-2.0.51-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a5b2ed6d828f1f09bd812861f4f59ca3bc3803f9df871f4555187f0faf018604"}, + {file = "sqlalchemy-2.0.51-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:436728ce18a80f6951a1e11cc6112c2ede9faf20766f1a26195a7c441ca12dbd"}, + {file = "sqlalchemy-2.0.51-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dc261707bf5739aea8a541593f3cc1d463c2701fb05fbcbba0ce031b69a21260"}, + {file = "sqlalchemy-2.0.51-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a6d26094615306d116dd5e4a51b0304c99dd2356fc569eed6922a80a6bd3b265"}, + {file = "sqlalchemy-2.0.51-cp311-cp311-win32.whl", hash = "sha256:ca8435d13829b92f4a97362d91975154a4015db3a2634154e1754e9a915e6b86"}, + {file = "sqlalchemy-2.0.51-cp311-cp311-win_amd64.whl", hash = "sha256:4a011ea4510683319ce4ed274b56ee05194b39b6da9d09ca7a39388f0fa84dcc"}, + {file = "sqlalchemy-2.0.51-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d78702b26ba1c18b2d0fb2ea940ba7f17a9581b42e8361ff93920ebbee1235a"}, + {file = "sqlalchemy-2.0.51-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581921d849d6e6f994d560389192955e80e2950e18fcdfe2ccea863e01158e6e"}, + {file = "sqlalchemy-2.0.51-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1d21ce524ab86c23046e992a5b81cb54c21079c6df6e78b8fc77d77cac70a6b9"}, + {file = "sqlalchemy-2.0.51-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c5d98a2709840027f5a347c3af0a7c3d5f6c1ff93af2ca1c54494e23cba8f389"}, + {file = "sqlalchemy-2.0.51-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1181256e0f16479691b5616d36375dc2620ad8332b25978763c3d206ad3f3f1d"}, + {file = "sqlalchemy-2.0.51-cp312-cp312-win32.whl", hash = "sha256:9f380393be5abeb6815f68fd39271b95127173511b6706b0a630a9995d53f8f5"}, + {file = "sqlalchemy-2.0.51-cp312-cp312-win_amd64.whl", hash = "sha256:2cf39aabdf48e87c1c2c2ed6d20d33ffa0733b3071ce9c5f66357947dd009080"}, + {file = "sqlalchemy-2.0.51-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c2056838b6685b72fdb36c99996cf862753461a62f2e84f4196371d3b2d6a07"}, + {file = "sqlalchemy-2.0.51-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:483b11bd46bf35fc14c52faf338b04300c9e6ce554bce9b11be85bfec3bc3195"}, + {file = "sqlalchemy-2.0.51-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1bed1ee8b01da6088210aa9412023326fb98a599ba502e6118308601dcbef77f"}, + {file = "sqlalchemy-2.0.51-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:72ca54c952107ba5cd58854b67a5a6268631289d21651a1235396f3b98b47400"}, + {file = "sqlalchemy-2.0.51-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b3e693d15533a45cd5906f0589f9c35090bef6ef45bf1e8195c424aa0ae06a8d"}, + {file = "sqlalchemy-2.0.51-cp313-cp313-win32.whl", hash = "sha256:b93ab07b5292dbe7e6b8da89475275e7042744283921344b56105f3eeb0f828b"}, + {file = "sqlalchemy-2.0.51-cp313-cp313-win_amd64.whl", hash = "sha256:0f053118c30e53161857a953e4de667d90e274980dccbe5dd3829bbbeece72a5"}, + {file = "sqlalchemy-2.0.51-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6ea306caaae6bd5afd0a46050003c88f6bf33227377a49298c498c3cb88ff491"}, + {file = "sqlalchemy-2.0.51-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c45a496d6bc05dec41dcd4c3a2b183723f47473255c159cd80b503c8f246424d"}, + {file = "sqlalchemy-2.0.51-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4004ada0aafe8ae1991b2cd1d99c6d9146126e123bd6f883c260d974aa012e54"}, + {file = "sqlalchemy-2.0.51-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0f6bcad487aee1c638d707235682fc96f741de00663619881ab235400d03289e"}, + {file = "sqlalchemy-2.0.51-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:39a76529db6305693d8d4affa58ad5b5e2e18edd62daea628b29b97930b3513d"}, + {file = "sqlalchemy-2.0.51-cp314-cp314-win32.whl", hash = "sha256:08a204d8b5638717c26a24df18fcf40af45a6b22e35b70b1d62f0113c2e278e8"}, + {file = "sqlalchemy-2.0.51-cp314-cp314-win_amd64.whl", hash = "sha256:96747bfbadb055466e5b46d572618170046b45ce5a4879167f50d70a5319a499"}, + {file = "sqlalchemy-2.0.51-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e5ea1a213be1fcd5e49d9904c3b9939211ded90bc2a64e93f4c01963474285de"}, + {file = "sqlalchemy-2.0.51-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7c6b36ed71f41942bdcd2ad2522be46bfce09d5705be5640ecf19bbc7660e4b7"}, + {file = "sqlalchemy-2.0.51-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0c2c62877097e1a0db401fba5cb4debee33265e5b2a55c4ccb489c02c53b4f72"}, + {file = "sqlalchemy-2.0.51-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0378d055e9e8cd6ce4d8dff683bdd3d7d413533c4ee51d67a2b1e0f9eacc0f23"}, + {file = "sqlalchemy-2.0.51-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6e46fc36029eff666391e0531e5387b62ce6c4f1d8e50b3fb3099eaca1b42522"}, + {file = "sqlalchemy-2.0.51-cp314-cp314t-win32.whl", hash = "sha256:9161cfc9efce70d1715f47d6ff40f79c6778c00d53be4fbc09d70301e4b83ba7"}, + {file = "sqlalchemy-2.0.51-cp314-cp314t-win_amd64.whl", hash = "sha256:159bb6ba32059f57ad7375a8f50d844dd2f19d14954ecf820cd33e20debd46b2"}, + {file = "sqlalchemy-2.0.51-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:bb1f5062f98b0b3290e72b707747fdd7e0f22d6956b236ba7ca7f5c9971d2da2"}, + {file = "sqlalchemy-2.0.51-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:247acaa29ccef6250dfd6a3eedf8f94ddf23564180a39fe362e32ae9dbdbde46"}, + {file = "sqlalchemy-2.0.51-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c95ef01f53233a305a874a44a63fbfb1d81cd79b49de0f8529b3548cde437e37"}, + {file = "sqlalchemy-2.0.51-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:fa268106c8987639a17a18514cfe0cd9bf17420ab887e1e1bf486da8836135b1"}, + {file = "sqlalchemy-2.0.51-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:b7f08588854bbb724041d9ae9d980d40040c922382e1d9a2ecb390edc4fd5032"}, + {file = "sqlalchemy-2.0.51-cp38-cp38-win32.whl", hash = "sha256:6b588fd681ddf0c196b8df1ea49a8913514894b2b8f945a9511b4b48871f99c8"}, + {file = "sqlalchemy-2.0.51-cp38-cp38-win_amd64.whl", hash = "sha256:ca216e8af5c05e326efc7e28716ac2381a7cf9791749f5ee1849dccdc99c9b00"}, + {file = "sqlalchemy-2.0.51-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:aa18ae738b5170e253ad0bb6c4b0f07585081e8a6e50893e4d911d47b39a0904"}, + {file = "sqlalchemy-2.0.51-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:59cab3686b1bc039dd9cded2f8d0c08a246e84e76bd4ab5b4f18c7cdae293825"}, + {file = "sqlalchemy-2.0.51-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:111604e637da87031255ddc26c7d7bc22bc6af6f5d459ccff3af1b4660233a85"}, + {file = "sqlalchemy-2.0.51-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:ad30ae663711786303fbcd46a47516302d201ee49a877cb3fac61f672895110a"}, + {file = "sqlalchemy-2.0.51-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b21f0e7efc7a5c509e953784e9d1575ebb8b4318960e7e7d7a93bb803626cf64"}, + {file = "sqlalchemy-2.0.51-cp39-cp39-win32.whl", hash = "sha256:a42ad6afcbaaa777241e347aa2e29155993045a0d6b7db74da61053ffe875fe0"}, + {file = "sqlalchemy-2.0.51-cp39-cp39-win_amd64.whl", hash = "sha256:2a97eaad21c84b4ef8010b11eeba9fe6153eb0b3df3ff8b6abc309df1b978ef7"}, + {file = "sqlalchemy-2.0.51-py3-none-any.whl", hash = "sha256:bb024d8b621d0be75f4f44ecc7c950450026e76d66dc8f791bb5331d7fed59d5"}, + {file = "sqlalchemy-2.0.51.tar.gz", hash = "sha256:804dccd8a4a6242c4e30ad961e540e18a588f6527202f2d6791b01845d59fdc9"}, +] + +[package.dependencies] +greenlet = {version = ">=1", markers = "platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\""} +typing-extensions = ">=4.6.0" + +[package.extras] +aiomysql = ["aiomysql (>=0.2.0)", "greenlet (>=1)"] +aioodbc = ["aioodbc", "greenlet (>=1)"] +aiosqlite = ["aiosqlite", "greenlet (>=1)", "typing_extensions (!=3.10.0.1)"] +asyncio = ["greenlet (>=1)"] +asyncmy = ["asyncmy (>=0.2.3,!=0.2.4,!=0.2.6)", "greenlet (>=1)"] +mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2,!=1.1.5,!=1.1.10)"] +mssql = ["pyodbc"] +mssql-pymssql = ["pymssql"] +mssql-pyodbc = ["pyodbc"] +mypy = ["mypy (>=0.910)"] +mysql = ["mysqlclient (>=1.4.0)"] +mysql-connector = ["mysql-connector-python"] +oracle = ["cx_oracle (>=8)"] +oracle-oracledb = ["oracledb (>=1.0.1)"] +postgresql = ["psycopg2 (>=2.7)"] +postgresql-asyncpg = ["asyncpg", "greenlet (>=1)"] +postgresql-pg8000 = ["pg8000 (>=1.29.1)"] +postgresql-psycopg = ["psycopg (>=3.0.7)"] +postgresql-psycopg2binary = ["psycopg2-binary"] +postgresql-psycopg2cffi = ["psycopg2cffi"] +postgresql-psycopgbinary = ["psycopg[binary] (>=3.0.7)"] +pymysql = ["pymysql"] +sqlcipher = ["sqlcipher3_binary"] + [[package]] name = "tomli" version = "2.4.1" @@ -1259,6 +2977,26 @@ files = [ {file = "tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f"}, ] +[[package]] +name = "tqdm" +version = "4.68.4" +description = "Fast, Extensible Progress Meter" +optional = false +python-versions = ">=3.8" +files = [ + {file = "tqdm-4.68.4-py3-none-any.whl", hash = "sha256:5168118b2368f48c561afda8020fd79195b1bdb0bdf8086b88442c267a315dc2"}, + {file = "tqdm-4.68.4.tar.gz", hash = "sha256:19829c9673638f2a0b8617da4cdcb927e831cd88bcfcb6e78d42a4d1af131520"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[package.extras] +discord = ["envwrap", "requests"] +notebook = ["ipywidgets (>=6)"] +slack = ["envwrap", "slack-sdk"] +telegram = ["envwrap", "requests"] + [[package]] name = "types-pytz" version = "2026.1.1.20260408" @@ -1281,6 +3019,20 @@ files = [ {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, ] +[[package]] +name = "typing-inspection" +version = "0.4.2" +description = "Runtime typing introspection tools" +optional = false +python-versions = ">=3.9" +files = [ + {file = "typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7"}, + {file = "typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464"}, +] + +[package.dependencies] +typing-extensions = ">=4.12.0" + [[package]] name = "tzdata" version = "2026.2" @@ -1292,6 +3044,20 @@ files = [ {file = "tzdata-2026.2.tar.gz", hash = "sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10"}, ] +[[package]] +name = "uri-template" +version = "1.3.0" +description = "RFC 6570 URI Template Processor" +optional = false +python-versions = ">=3.7" +files = [ + {file = "uri-template-1.3.0.tar.gz", hash = "sha256:0e00f8eb65e18c7de20d595a14336e9f337ead580c70934141624b6d1ffdacc7"}, + {file = "uri_template-1.3.0-py3-none-any.whl", hash = "sha256:a44a133ea12d44a0c0f06d7d42a52d71282e77e2f937d8abd5655b8d56fc1363"}, +] + +[package.extras] +dev = ["flake8", "flake8-annotations", "flake8-bandit", "flake8-bugbear", "flake8-commas", "flake8-comprehensions", "flake8-continuation", "flake8-datetimez", "flake8-docstrings", "flake8-import-order", "flake8-literal", "flake8-modern-annotations", "flake8-noqa", "flake8-pyproject", "flake8-requirements", "flake8-typechecking-import", "flake8-use-fstring", "mypy", "pep8-naming", "types-PyYAML"] + [[package]] name = "urllib3" version = "2.6.3" @@ -1327,7 +3093,162 @@ platformdirs = ">=3.9.1,<5" python-discovery = ">=1.2.2" typing-extensions = {version = ">=4.13.2", markers = "python_version < \"3.11\""} +[[package]] +name = "watchdog" +version = "6.0.0" +description = "Filesystem events monitoring" +optional = false +python-versions = ">=3.9" +files = [ + {file = "watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26"}, + {file = "watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112"}, + {file = "watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3"}, + {file = "watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c"}, + {file = "watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2"}, + {file = "watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c"}, + {file = "watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948"}, + {file = "watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860"}, + {file = "watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0"}, + {file = "watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c"}, + {file = "watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134"}, + {file = "watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b"}, + {file = "watchdog-6.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e6f0e77c9417e7cd62af82529b10563db3423625c5fce018430b249bf977f9e8"}, + {file = "watchdog-6.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:90c8e78f3b94014f7aaae121e6b909674df5b46ec24d6bebc45c44c56729af2a"}, + {file = "watchdog-6.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e7631a77ffb1f7d2eefa4445ebbee491c720a5661ddf6df3498ebecae5ed375c"}, + {file = "watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881"}, + {file = "watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11"}, + {file = "watchdog-6.0.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7a0e56874cfbc4b9b05c60c8a1926fedf56324bb08cfbc188969777940aef3aa"}, + {file = "watchdog-6.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:e6439e374fc012255b4ec786ae3c4bc838cd7309a540e5fe0952d03687d8804e"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2"}, + {file = "watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a"}, + {file = "watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680"}, + {file = "watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f"}, + {file = "watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282"}, +] + +[package.extras] +watchmedo = ["PyYAML (>=3.10)"] + +[[package]] +name = "webcolors" +version = "25.10.0" +description = "A library for working with the color formats defined by HTML and CSS." +optional = false +python-versions = ">=3.10" +files = [ + {file = "webcolors-25.10.0-py3-none-any.whl", hash = "sha256:032c727334856fc0b968f63daa252a1ac93d33db2f5267756623c210e57a4f1d"}, + {file = "webcolors-25.10.0.tar.gz", hash = "sha256:62abae86504f66d0f6364c2a8520de4a0c47b80c03fc3a5f1815fedbef7c19bf"}, +] + +[[package]] +name = "wrapt" +version = "2.2.2" +description = "Module for decorators, wrappers and monkey patching." +optional = false +python-versions = ">=3.9" +files = [ + {file = "wrapt-2.2.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:055e6fcfaa28e58c6a8c247d48b92be9d56f818b7068aa4f22b15b3343a09931"}, + {file = "wrapt-2.2.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8374eb6b1a58809211e84ff835a182bb17ab2807a5bfef23204c8cff38178a00"}, + {file = "wrapt-2.2.2-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:656593bb3f5529f03d27af4136c4d7b11990e470bcbc6fefa5ef218695bece55"}, + {file = "wrapt-2.2.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dfb00cb7bb22099e2f64b7340fb96113639aa7260c0972af3797ace2297b936c"}, + {file = "wrapt-2.2.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e7f10ee0bd53673bfd52b67cbce83336fe6cad90d2377b03baf66491d2bbfb91"}, + {file = "wrapt-2.2.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4402f57c5f0d0579599858ffbdd9bf4e3f0972f51096f2bd6cc7dab6b76ee49e"}, + {file = "wrapt-2.2.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:3a4eb7964ff4643d333c84f880bcf554652b2a1050aebc54ae696327f61acfaf"}, + {file = "wrapt-2.2.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e542b7c5af91e2123a8aabf19894319d5ec4268d2a9ffd2f239386133fc47746"}, + {file = "wrapt-2.2.2-cp310-cp310-win32.whl", hash = "sha256:6e7e45b43d3c774d244fe7264378f5a3f0f383bc55a54a9866434e524540110f"}, + {file = "wrapt-2.2.2-cp310-cp310-win_amd64.whl", hash = "sha256:955f1d6e72a352e478de8d8b503abe301c5e139a141b62eb0923bd694995025f"}, + {file = "wrapt-2.2.2-cp310-cp310-win_arm64.whl", hash = "sha256:b89d8d73c82db2bb7e6090b3afd7973f980d24e905cc34394eab60b884b3bf67"}, + {file = "wrapt-2.2.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f1a2ff355ece6a111ca7a20dc86df6659c9205d3fcee674ca34f2a2854fd4e73"}, + {file = "wrapt-2.2.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:55b9a899e6fff5444f229d30aa6e9ac92d2216d9d60f33c771b5d76a760d5f8e"}, + {file = "wrapt-2.2.2-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a2d78c363f97d8bd718ee40432c66395685e9e98528ccaa423c3355d1715a26d"}, + {file = "wrapt-2.2.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d619e1eed9bd4f6ed9f24cd61971aa086fa86505289628d464bcf8a2c2e3f328"}, + {file = "wrapt-2.2.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:518b0c5e323511ec56a38894802ddd5e1222626484e68efe63f201854ad788e5"}, + {file = "wrapt-2.2.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4bccea5cdecffa9dd70e343741f0e41e0a16619313d04b72f78bb525162ebcd0"}, + {file = "wrapt-2.2.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:209112cafd963710a05d199aae431d79a28bc76eb8e6d1bbbb8ad24340722cae"}, + {file = "wrapt-2.2.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e5a5290e4bf2f332fc29ce72ffb9a2fff678aaac047e2e9f5f7165cd7792e099"}, + {file = "wrapt-2.2.2-cp311-cp311-win32.whl", hash = "sha256:5499236ad1dc116012e2a5dd943f3f31af12fce452128e2bbcbd55a7d3d4d14c"}, + {file = "wrapt-2.2.2-cp311-cp311-win_amd64.whl", hash = "sha256:8636809939152be6ae20a6cef0fed9fe60f411b47847d0426a826884b469e971"}, + {file = "wrapt-2.2.2-cp311-cp311-win_arm64.whl", hash = "sha256:5d0a142f7af07caeb5e5da87493162a7b8efa19ba919e550a746f7446e13fb30"}, + {file = "wrapt-2.2.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8417fd3c674d3c8023d080292d29301531a12daf8bd938dd419710dd2f464f2b"}, + {file = "wrapt-2.2.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0e7070c7472582e31af3dfc2622b2381a0df7435110a9388ed8db5ffbce67efb"}, + {file = "wrapt-2.2.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2e096c9d39a59b35b63c9aacfbbbec2088ff51ff1fc31051acc60a07f42f273a"}, + {file = "wrapt-2.2.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d1a6050405bf334be33bf66296f113563622972a34900ae6fa60fd283a1a900"}, + {file = "wrapt-2.2.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:10adb01371408c6de504a6658b9886480f1a4919a83752748a387a504a21df79"}, + {file = "wrapt-2.2.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3442eee2a5798f9b451f1b2cd7518ce8b7e28a2a364696c414460a0e295c012a"}, + {file = "wrapt-2.2.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:6c99012a22f735a85eed7c4b86a3e99c30fdd57d9e115b2b45f796264b58d0bf"}, + {file = "wrapt-2.2.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3b686cfc008776a3952d6213cb296ed7f45d782a8453936406faa89eac0835ab"}, + {file = "wrapt-2.2.2-cp312-cp312-win32.whl", hash = "sha256:ef2cce266b5b0b07e19fa82e59673b81142b7a3607c8ed1254113d048ed668da"}, + {file = "wrapt-2.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:abf8c20a2d72ee69e16328b3c91342c446e723bfe48bfcc4dded3b9722ac027f"}, + {file = "wrapt-2.2.2-cp312-cp312-win_arm64.whl", hash = "sha256:c6c64c5d02578bc4c4bca4f0aef1504de933c1d5b4ac2710b9131111459506c8"}, + {file = "wrapt-2.2.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9e8b648270c613720a202d9a45ebabc33261b22c3a839b115ac5bce8c0bb0d69"}, + {file = "wrapt-2.2.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6fb7e94e8fe3e4c3067bb1653a91cce7c5e83acc119fdd41501b1bf74654617"}, + {file = "wrapt-2.2.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb18fc51e813df0d9c98049e3bf2298a5495a648602040e21fa3c7329371159e"}, + {file = "wrapt-2.2.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94b00b00f806eb3ef2abe9049ed45994a81ee9284884d96e6b8314927c6cea3d"}, + {file = "wrapt-2.2.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:62415fd095bc590b842b6d092f2b5d9ccbaeb7e0b28535c03dcea2718b48636b"}, + {file = "wrapt-2.2.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a41e758d80dc0ab8c210f641ac892009d356cf1f955d97db544c8dd317b4d14c"}, + {file = "wrapt-2.2.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:b84cd4058001c9727b0e9980b7a9e66325b5ca748b1b578e822cade1bc6b304f"}, + {file = "wrapt-2.2.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:26fc73a1b15e0946d2942b9a4426d162b51676338327dc067ccd8d2d76385f94"}, + {file = "wrapt-2.2.2-cp313-cp313-win32.whl", hash = "sha256:3c4095803491f6ef72128914c28ec05bbad9758433bb35f6715a3e9c8e46fb2d"}, + {file = "wrapt-2.2.2-cp313-cp313-win_amd64.whl", hash = "sha256:2cb07f414fab25dbe6b5c7398e1491423a5c81a6209533639969a6c928d474a4"}, + {file = "wrapt-2.2.2-cp313-cp313-win_arm64.whl", hash = "sha256:1fc7691f070220215cccb2a20836b9adbaecb8ff22ad47abe63de5f110994fac"}, + {file = "wrapt-2.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ec8f83949028366531383603139403cac7a826e4011955813cdd640017845ce5"}, + {file = "wrapt-2.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4b481fb0c40d9fd90a5809911208da700987d373a20a4709dc9e3944af7a6bec"}, + {file = "wrapt-2.2.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0065a3b657cec06813b4241d2462ccec287f6863103d7445b725fb3a889736f9"}, + {file = "wrapt-2.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:30f7424af5c5c345b7f26490e097f74a2ef45b3d08b664dc33571aee3bd3b56c"}, + {file = "wrapt-2.2.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:07fdcb012821859168641acf68afad61ef9783cf37100af85f152550e9677194"}, + {file = "wrapt-2.2.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f90038ab58fafb584801ca62d72384d7d5225d93c76f7b773c22fae545bd8066"}, + {file = "wrapt-2.2.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:c5d7825491bfa2d08b97e9557768987952c7b9ae687d06c3320b40a37ccb7f20"}, + {file = "wrapt-2.2.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0ad520e6daa9bbf136f14de735474dbec7dcc0891f718e1d274ce8dc92e645af"}, + {file = "wrapt-2.2.2-cp313-cp313t-win32.whl", hash = "sha256:25904acb9475f46c24fe0423dbc8fda8cc5fbc282ab3dc6e72e919748c53f4e9"}, + {file = "wrapt-2.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:305d4c247d61c4115794a169141823c62f719525ddb90b23aa332741c77d2c28"}, + {file = "wrapt-2.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:c20279cd1a29800815d7b2d6338b60a6c6e78263f9d6e62e0eda251ba9cae2d0"}, + {file = "wrapt-2.2.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:0e64826f920c42d9d9f87e8cc09ffae66c51ede12d59061a5a426deb9aa71745"}, + {file = "wrapt-2.2.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:dcaa5e1451bd8751d7bd1568dfa3321c78092a52a7ecb5d1a0f18a5791e1fd00"}, + {file = "wrapt-2.2.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0abfd648dac9ac9c5b3aa9b523d27f1789046640b58dcd5652a720ddb325e1fc"}, + {file = "wrapt-2.2.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f4bfd8d1eb438153eff8b8cfe87f032ba65731e1ce06138b5090f745a33f6f95"}, + {file = "wrapt-2.2.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c427c9d06d859848a69f0d928fe28b5c33a941b2265d10a0e1f15cd244f1ee33"}, + {file = "wrapt-2.2.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4250b43d1a129d947e083c4dc6baf333c9bb34edd26f912d5b0457841fc858ab"}, + {file = "wrapt-2.2.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:173e5bb5ca350a6e0abab60b7ec7cdd7992a814cb14b4de670a28f067f105663"}, + {file = "wrapt-2.2.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:aa14b01804bce36c6d63d7b6a4f55df390f29f8648cc13a1f40b166f4d54680d"}, + {file = "wrapt-2.2.2-cp314-cp314-win32.whl", hash = "sha256:58f9f8d637c9a6e245c6ef5b109b67ec187d2faed23d1405656b51d96e0a5b56"}, + {file = "wrapt-2.2.2-cp314-cp314-win_amd64.whl", hash = "sha256:385cb1866f20479e83299af585375bfa0a4b0c6c9907a981483ea782ea8ae406"}, + {file = "wrapt-2.2.2-cp314-cp314-win_arm64.whl", hash = "sha256:8ffbeaea6771a6eba6e6eeb09767864995726bc8240bb54baf88a9bb1db34d5c"}, + {file = "wrapt-2.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:09f811d43f6f33ec7515f0be76b159569f4057ab54d3e079c3204dddb90afa2a"}, + {file = "wrapt-2.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a795d3c06e5fbf9ea2f13196180b77aeab1b4685917256ee0d014cc163d90063"}, + {file = "wrapt-2.2.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:45c2f2768e790c9f8db90f239ef23a2af8e7570f25a35619ef902df4a738447f"}, + {file = "wrapt-2.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bbf00ee0cb55ec24e2b0995a71942b85b21a066db8f3f46e1dbfdb9433ffba81"}, + {file = "wrapt-2.2.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2252f77663651b89255895f58cc6ac08fcb206d4371813e5af61bb62d4f7689c"}, + {file = "wrapt-2.2.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2cd7181ab1c31192ff5219269830744b5a62020b3a6d433588c4f1c95b8f8bff"}, + {file = "wrapt-2.2.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:6fe35fd51b74867d8b80174c277bd6bbf6a73e443f908129dc531c4b688a20d5"}, + {file = "wrapt-2.2.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:11d95fc2fbad3163596c39d440e6f21ca9fccece74b56e30a37ac2fca786a07c"}, + {file = "wrapt-2.2.2-cp314-cp314t-win32.whl", hash = "sha256:d8a15813215f33fa83667bfc978b300e35669ea8bb424e970a1426bcb7bc6cca"}, + {file = "wrapt-2.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:d09db0f7e8357060d3c38fc22a018aba683a796bf184360fd1a58f6fc180dc77"}, + {file = "wrapt-2.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:f32fe639c39561ccc187bcae17e9271be0eb45f1c2952510d2f29b33ab577347"}, + {file = "wrapt-2.2.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d01d8e0afc55823245a3b97a79c7c77464e31ea7a7b629a4bf26f9441dc1f18e"}, + {file = "wrapt-2.2.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a28287413351cb198b8c5ddd045c56fac1d195808642cd264d1ab50426146650"}, + {file = "wrapt-2.2.2-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:abf033b7e4542357659cd83ed6cd5033c43aaa1887044045ceb571528837f72f"}, + {file = "wrapt-2.2.2-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d2f6573561fa05002e5ee71529f4ab0a7dffed3e45b51013fe6298fe2723c02"}, + {file = "wrapt-2.2.2-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c38510a21d5b9cf3e84c460d909e9f2a098667439fd42841bb081cab45835d68"}, + {file = "wrapt-2.2.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:cd385a48b055bdc3630ab30e0c7fd8514a36904ec23f9cee7a65d887334a3cea"}, + {file = "wrapt-2.2.2-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:3dc3dcfc2da95d501905f10dc11a0dc622e91d8cdd8bbfcb63ca54afd131e556"}, + {file = "wrapt-2.2.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:fa81c5b5fe8cd6c41e3a798533b81288279e5fdbde2128f21071922764281c99"}, + {file = "wrapt-2.2.2-cp39-cp39-win32.whl", hash = "sha256:814f1bf3e0a7035f67a1db0cdaf5e2bbcaa4d7092db96673cfa467adeaab8591"}, + {file = "wrapt-2.2.2-cp39-cp39-win_amd64.whl", hash = "sha256:9ee098171b07edba66ab69a9bf0251d3cbef654107e800feb24c0c6f30592728"}, + {file = "wrapt-2.2.2-cp39-cp39-win_arm64.whl", hash = "sha256:3179a4db066b53d40562e368b12895440c8f0953b6543b89d6acc41c0273996e"}, + {file = "wrapt-2.2.2-py3-none-any.whl", hash = "sha256:5bad217350f19ce99ca5b5e71d406765ea86fe541628426772b657375ee1c048"}, + {file = "wrapt-2.2.2.tar.gz", hash = "sha256:0788e321027c999bf221b667bd4a54aaefd1a36283749a860ac3eb77daed0302"}, +] + +[package.extras] +dev = ["pytest", "setuptools"] + [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "9fed2e8fc38b31a4085ceee8b6d5653ce1e2354569b75ccda80302b29993a28a" +content-hash = "f44606dc93e69254e9342b69b964bee02499d72f5fcab2f95daa23e3455954f9" diff --git a/pyproject.toml b/pyproject.toml index d2a7ff4..3c392ac 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,6 +27,7 @@ ruff = "^0.3.4" pre-commit = "^3.7.0" pytest = "^9.0.3" pytest-cov = "^7.0.0" +linkml = "^1.11.1" [build-system] requires = ["poetry-core"] From e3644f0eaff78db422f5d607fb9ccf1d18fe5946 Mon Sep 17 00:00:00 2001 From: Adam Wright Date: Tue, 14 Jul 2026 19:00:32 -0400 Subject: [PATCH 12/17] Add LinkML output schema + validator for generated networks schema/logic_network.linkml.yaml is the contract for the emitted files (nodes, node_reaction_context, edges); bin/validate-logic-network.py loads a pathway output dir into the native model and validates it. See reactome/logic-network-generator#39. Co-Authored-By: Claude Opus 4.8 --- bin/validate-logic-network.py | 70 +++++++++ schema/logic_network.linkml.yaml | 238 +++++++++++++++++++++++++++++++ 2 files changed, 308 insertions(+) create mode 100644 bin/validate-logic-network.py create mode 100644 schema/logic_network.linkml.yaml diff --git a/bin/validate-logic-network.py b/bin/validate-logic-network.py new file mode 100644 index 0000000..769e0e2 --- /dev/null +++ b/bin/validate-logic-network.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python +"""Validate a generated logic-network output dir against the LinkML schema. + +The CSVs are the flat serialization; multivalued slots (member_leaves, +source_sets, chosen_members) are pipe-delimited cells. This loads them back +into the native LinkML data model (real arrays) and validates the assembled +``LogicNetwork`` instance against schema/logic_network.linkml.yaml. + +Usage: + poetry run python bin/validate-logic-network.py +""" +import sys +from pathlib import Path + +import pandas as pd +from linkml.validator import validate + +SCHEMA = Path(__file__).resolve().parent.parent / "schema" / "logic_network.linkml.yaml" +MULTIVALUED = ("member_leaves", "source_sets", "chosen_members") + + +def _split(v): + if v is None or (isinstance(v, float) and pd.isna(v)) or v == "": + return [] + return [p for p in str(v).split("|") if p] + + +def _clean(v): + return None if (v is None or (isinstance(v, float) and pd.isna(v)) or v == "") else v + + +def load_instance(d: Path) -> dict: + nodes = [] + for r in pd.read_csv(d / "nodes.csv", dtype=str, keep_default_na=False).to_dict("records"): + node = {k: _clean(v) for k, v in r.items() if k not in MULTIVALUED} + for k in MULTIVALUED: + node[k] = _split(r.get(k)) + nodes.append({k: v for k, v in node.items() if v is not None or k in MULTIVALUED}) + + contexts = pd.read_csv(d / "node_reaction_context.csv", dtype=str, + keep_default_na=False).to_dict("records") + + edges = [] + for r in pd.read_csv(d / "logic_network.csv", keep_default_na=False).to_dict("records"): + e = {k: _clean(v) for k, v in r.items()} + if e.get("stoichiometry") not in (None, ""): + e["stoichiometry"] = int(float(e["stoichiometry"])) + edges.append({k: v for k, v in e.items() if v is not None}) + + return {"pathway_id": d.name, "nodes": nodes, + "node_reaction_contexts": contexts, "edges": edges} + + +def main() -> int: + if len(sys.argv) != 2: + print(__doc__) + return 2 + d = Path(sys.argv[1]) + report = validate(load_instance(d), str(SCHEMA), "LogicNetwork") + if not report.results: + print(f"VALID: {d} conforms to {SCHEMA.name}") + return 0 + print(f"INVALID: {len(report.results)} problem(s) in {d}") + for res in report.results[:25]: + print(f" [{res.severity}] {res.message}") + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/schema/logic_network.linkml.yaml b/schema/logic_network.linkml.yaml new file mode 100644 index 0000000..6fa5649 --- /dev/null +++ b/schema/logic_network.linkml.yaml @@ -0,0 +1,238 @@ +id: https://reactome.org/logic-network-generator/schema/logic_network +name: logic_network +title: Reactome Logic Network Output Schema +description: >- + Machine-readable contract for the files the logic-network-generator (LNG) + emits for a single Reactome pathway. These files are consumed by deltasignal + (signal-propagation solver), the benchmark harness (mapping to experimental / + curator tests), and the web UI (overlaying results on Reactome pathway + diagrams). + + The network is a signed logic graph over UUID-keyed NODES. A node is one + biological entity as it appears at a position in the pathway: a simple entity + (protein / small molecule), a bundled complex, a set-VARIANT of a complex + (the complex with one specific choice per internal EntitySet), or a synthetic + boundary node (assembly leaf / dissociation sink) or catalyst/regulator + variant. Complexes are NEVER broken into their individual members; they are + only split along their internal EntitySets ("break apart by sets"), which is + what set_variant nodes record. + + Provenance is normalized by grain: intrinsic 1:1 / fixed-multivalued node + attributes live on Node; the independent many-to-many Node<->reaction + relationship (with its own `role`) lives in NodeReactionContext. +license: Apache-2.0 +default_range: string + +prefixes: + linkml: https://w3id.org/linkml/ + lng: https://reactome.org/logic-network-generator/schema/ + reactome: https://reactome.org/content/detail/ +default_prefix: lng + +imports: + - linkml:types + +# --------------------------------------------------------------------------- +# Enums +# --------------------------------------------------------------------------- +enums: + NodeKind: + description: What a logic-network node represents. + permissible_values: + simple_entity: + description: A single protein, small molecule, or other atomic entity. + simple_complex: + description: A Complex with no internal EntitySet — one bundled node. + set_variant: + description: >- + A Complex containing one or more EntitySets, resolved to ONE specific + choice per set. diagram_entity_id is the parent complex; chosen_members + / source_sets record the choice. + dissociation_sink: + description: >- + Synthetic per-occurrence readout node for a member released from a + terminal-output complex. Has incoming edges, no outgoing edges. + assembly_leaf: + description: >- + Synthetic shared handle for a member feeding a root-input complex via + an assembly edge. + regulator_variant: + description: A set-variant produced by catalyst / negative-regulator decomposition. + reaction: + description: A virtual-reaction node (edges route input->reaction->output). + other: + description: Fallback for anything not otherwise classified. + + EdgeType: + description: The biological meaning of an edge. + permissible_values: + input: {description: Entity consumed by a reaction.} + output: {description: Entity produced by a reaction.} + catalyst: {description: Catalyst that accelerates a reaction.} + regulator: {description: Positive or negative regulator of a reaction.} + depletion: {description: Catalyst -> substrate; catalyst removes its substrate (divide-form inhibition).} + assembly: {description: Member -> complex; the complex requires the member (root-input boundary).} + dissociation: {description: Complex -> released member readout sink (terminal-output boundary).} + handoff: {description: "Producer output -> consumer input across a curated precedingEvent link where they share a component carrier (bundling would otherwise drop the connection). Positive OR activator."} + + Role: + description: >- + How a node participates in a reaction. Regulator sign (positive vs + negative) is carried by the corresponding edge's pos_neg; this location + table uses the generic ``regulator`` role. + permissible_values: + input: {} + output: {} + catalyst: {} + regulator: {} + positive_regulator: {} + negative_regulator: {} + + PosNeg: + permissible_values: + pos: {description: Activating / positive influence.} + neg: {description: Inhibiting / negative influence.} + + AndOr: + permissible_values: + and: {description: All inputs required (AND aggregation).} + or: {description: Any input suffices (OR aggregation).} + +# --------------------------------------------------------------------------- +# Slots +# --------------------------------------------------------------------------- +slots: + uuid: + description: Stable UUID identifying a node within this pathway's logic network. + identifier: true + range: string + node_kind: + range: NodeKind + required: true + diagram_entity_id: + description: >- + The Reactome stable ID a pathway diagram renders for this node — the key + for UI overlay and for collapsing per-UUID results to per-Reactome-ID + values. For a set_variant this is the PARENT complex stId; for a simple + entity/complex it is the entity's own stId; for a reaction it is the + reaction stId. + range: string + required: true + compartment: + description: Reactome compartment display name, if known. + range: string + required: false + member_leaves: + description: >- + Terminal leaf entity stIds (genes / proteins / small molecules) contained + in this node. Enables gene-knockout -> node mapping for the tests without + parsing the node id. Empty for atomic simple entities and reactions. + range: string + multivalued: true + required: false + source_sets: + description: EntitySet stIds this node was split on (empty unless a set was resolved). + range: string + multivalued: true + required: false + chosen_members: + description: The specific set-member stId(s) selected in this set_variant. + range: string + multivalued: true + required: false + + # NodeReactionContext slots + context_node: + description: The node participating in the reaction. + range: Node + required: true + reaction_id: + description: Reactome stable ID of the reaction (ReactionLikeEvent). + range: string + required: true + role: + range: Role + required: true + + # Edge slots + source_id: + range: Node + required: true + target_id: + range: Node + required: true + pos_neg: + range: PosNeg + required: true + and_or: + range: AndOr + required: false + edge_type: + range: EdgeType + required: true + stoichiometry: + range: integer + required: false + edge_reaction_id: + description: >- + Reactome reaction this edge was derived from (null for synthetic boundary + edges). Lets the UI colour reactions and identifies where reactions were + split by sets. + range: string + required: false + +# --------------------------------------------------------------------------- +# Classes (one class per output table + a container) +# --------------------------------------------------------------------------- +classes: + Node: + description: One node of the logic network. Serialized as a row in nodes.csv. + slots: + - uuid + - node_kind + - diagram_entity_id + - compartment + - member_leaves + - source_sets + - chosen_members + + NodeReactionContext: + description: >- + A (node, reaction, role) participation. Serialized as a row in + node_reaction_context.csv. Many rows per node (a merged UUID appears in + several reactions); this is the location layer for diagram placement. + slots: + - context_node + - reaction_id + - role + + Edge: + description: One signed logic edge. Serialized as a row in edges.csv (logic_network.csv). + slots: + - source_id + - target_id + - pos_neg + - and_or + - edge_type + - stoichiometry + - edge_reaction_id + + LogicNetwork: + description: Container for one pathway's logic network (tree root for validation). + tree_root: true + attributes: + pathway_id: + description: Reactome stable ID of the pathway this network was generated for. + range: string + nodes: + range: Node + multivalued: true + inlined_as_list: true + node_reaction_contexts: + range: NodeReactionContext + multivalued: true + inlined_as_list: true + edges: + range: Edge + multivalued: true + inlined_as_list: true From cf073f64a895fb88ee767e950a41bf12a5c975b0 Mon Sep 17 00:00:00 2001 From: Adam Wright Date: Tue, 14 Jul 2026 19:00:32 -0400 Subject: [PATCH 13/17] Add diagram-sourced reaction connectivity module Extracts product->substrate pairs from the Reactome diagram JSON (entity glyph shared between a producer's output and a consumer's input/catalyst), using the pathway's own diagram or the nearest diagrammed ancestor (isolated to the pathway's reactions). Cofactor-clean by construction. Closes the precedingEvent-gap on old pathways. reactome/logic-network-generator#39. Co-Authored-By: Claude Opus 4.8 --- src/diagram_connectivity.py | 164 ++++++++++++++++++++++++++++++++++++ 1 file changed, 164 insertions(+) create mode 100644 src/diagram_connectivity.py diff --git a/src/diagram_connectivity.py b/src/diagram_connectivity.py new file mode 100644 index 0000000..f3399f8 --- /dev/null +++ b/src/diagram_connectivity.py @@ -0,0 +1,164 @@ +"""Reaction connectivity from Reactome diagram JSON. + +The generator connects two reactions only when the curator annotated a +``precedingEvent`` between them. Older pathways under-annotate ``precedingEvent``, +so reactions where A's product is literally B's substrate are left disconnected. + +The pathway **diagram** is the curator's drawn connectivity: two reactions are +linked when they share an entity **glyph** (A's output glyph == B's input/catalyst +glyph). We extract those (producer, consumer) reaction pairs and feed them into +``reaction_connections`` alongside ``precedingEvent`` — Phase 2 then merges the +shared product into one node, giving ``A -> product -> B``. + +Why the diagram beats raw Neo4j input/output matching: curators draw cofactors +(ATP/ADP/H2O/etc.) as **separate per-reaction glyphs**, so shared-glyph +connectivity excludes cofactors for free — no hub/threshold tuning. + +See reactome/logic-network-generator#39. +""" +import json +import os +from collections import defaultdict +from pathlib import Path +from typing import Set, Tuple + +import pandas as pd + +from src.argument_parser import logger + + +def _diagram_dir() -> Path: + return Path(os.environ.get("LNG_DIAGRAM_DIR", os.path.expanduser("~/reactome-diagrams/97"))) + + +def _has_diagram(stid: str) -> bool: + d = _diagram_dir() + return (d / f"{stid}.json").exists() and (d / f"{stid}.graph.json").exists() + + +def _covering_diagram_stid(pathway_id: str) -> str: + """stId of the diagram that renders this pathway. + + We generate mostly top-level pathways (which have their own diagram), but the + TEST pathways are often sub-pathways that inherit an ancestor's diagram. Use + the pathway's own diagram if present, else the nearest diagrammed ancestor + (walking up ``hasEvent`` in Neo4j). Returns "" if none found. + """ + if _has_diagram(pathway_id): + return pathway_id + from src.neo4j_connector import get_graph + ancestors = get_graph().run( + """MATCH path=(anc:Pathway)-[:hasEvent*]->(p:Pathway {stId:$pid}) + RETURN anc.stId AS stid, length(path) AS d ORDER BY d""", + pid=pathway_id, + ).data() + for a in ancestors: + if _has_diagram(a["stid"]): + return a["stid"] + return "" + + +def _pathway_reaction_stids(pathway_id: str) -> Set[str]: + """All ReactionLikeEvent stIds contained in the pathway (to isolate it within + an ancestor diagram).""" + from src.neo4j_connector import get_graph + rows = get_graph().run( + "MATCH (p:Pathway {stId:$pid})-[:hasEvent*]->(r:ReactionLikeEvent) " + "RETURN collect(DISTINCT r.stId) AS r", + pid=pathway_id, + ).evaluate() + return set(rows or []) + + +def diagram_shared_product_pairs(pathway_id: str) -> Set[Tuple[str, str]]: + """(producer_stId, consumer_stId) reaction pairs drawn as connected in the diagram. + + A pair is emitted when a producer reaction's OUTPUT glyph is the same glyph a + consumer reaction takes as INPUT or CATALYST. Reaction identity is the + Reactome stId (mapped from the diagram's numeric reactomeId via the + companion ``.graph.json``). The diagram used is the pathway's own, else the + nearest diagrammed ancestor; pairs are then **restricted to reactions that + belong to this pathway** so an ancestor diagram only contributes the target + pathway's internal connectivity. Returns an empty set if no diagram covers it. + """ + ddir = _diagram_dir() + diagram_stid = _covering_diagram_stid(pathway_id) + if not diagram_stid: + logger.info(f"No diagram (own or ancestor) covers {pathway_id}; skipping diagram connectivity") + return set() + if diagram_stid != pathway_id: + logger.info(f"{pathway_id} has no own diagram; using ancestor diagram {diagram_stid}") + + layout = json.loads((ddir / f"{diagram_stid}.json").read_text()) + graph = json.loads((ddir / f"{diagram_stid}.graph.json").read_text()) + # Restrict to this pathway's reactions (isolate it within the ancestor diagram). + own_reactions = _pathway_reaction_stids(pathway_id) + + # reaction dbId -> stId (graph.json edges carry both) + dbid_to_stid = {e["dbId"]: e["stId"] for e in graph.get("edges", []) if e.get("stId")} + + # For each reaction glyph (diagram 'edge'): its output glyph ids, and its + # input+catalyst glyph ids. reactomeId on a diagram edge is the reaction dbId. + glyph_out_rxns = defaultdict(set) # glyph_id -> {reaction_dbId producing it} + glyph_in_rxns = defaultdict(set) # glyph_id -> {reaction_dbId consuming it} + for e in layout.get("edges", []): + rdb = e.get("reactomeId") + if rdb is None: + continue + for x in e.get("outputs", []): + glyph_out_rxns[x["id"]].add(rdb) + for role in ("inputs", "catalysts"): + for x in e.get(role, []): + glyph_in_rxns[x["id"]].add(rdb) + + pairs: Set[Tuple[str, str]] = set() + for glyph_id in set(glyph_out_rxns) & set(glyph_in_rxns): + for producer in glyph_out_rxns[glyph_id]: + for consumer in glyph_in_rxns[glyph_id]: + if producer == consumer: + continue + p_st = dbid_to_stid.get(producer) + c_st = dbid_to_stid.get(consumer) + if not p_st or not c_st: + continue + # Keep only pairs whose BOTH reactions belong to this pathway + # (so an ancestor diagram contributes only this pathway's flow). + if own_reactions and (p_st not in own_reactions or c_st not in own_reactions): + continue + pairs.add((p_st, c_st)) + return pairs + + +def augment_reaction_connections(pathway_id: str, + reaction_connections: pd.DataFrame) -> pd.DataFrame: + """Union diagram-drawn product->substrate pairs into reaction_connections. + + Adds only pairs not already present (as preceding->following). Tagged + event_status='Diagram Shared Product' for traceability. No-op (returns input + unchanged) when disabled or no diagram is available. + """ + if os.environ.get("LNG_DIAGRAM_CONNECTIVITY", "1") == "0": + return reaction_connections + + pairs = diagram_shared_product_pairs(pathway_id) + if not pairs: + return reaction_connections + + existing = set( + zip(reaction_connections.get("preceding_reaction_id", pd.Series(dtype=str)), + reaction_connections.get("following_reaction_id", pd.Series(dtype=str))) + ) + new_rows = [ + {"preceding_reaction_id": p, "following_reaction_id": c, + "event_status": "Diagram Shared Product"} + for (p, c) in pairs if (p, c) not in existing + ] + if not new_rows: + logger.info(f"Diagram connectivity for {pathway_id}: all {len(pairs)} pairs already linked") + return reaction_connections + + logger.info( + f"Diagram connectivity for {pathway_id}: +{len(new_rows)} product->substrate " + f"pairs not in precedingEvent (of {len(pairs)} drawn)" + ) + return pd.concat([reaction_connections, pd.DataFrame(new_rows)], ignore_index=True) From 285014e411f3722a7504adaefefb346911537fb9 Mon Sep 17 00:00:00 2001 From: Adam Wright Date: Tue, 14 Jul 2026 19:00:32 -0400 Subject: [PATCH 14/17] Emit complexes as set-variant nodes; add provenance files + diagram connectivity - Complex is one node, split only along internal EntitySets (set-variant), not decomposed to members (faithful to curation; verified vs Neo4j). - Emit nodes.csv (node_kind, diagram_entity_id, member_leaves, source_sets, chosen_members) and node_reaction_context.csv; add edge_reaction_id to logic_network.csv. - Wire diagram-sourced connectivity into reaction_connections (Phase-2 merges the shared product -> A->product->B); matching layer stays on pure precedingEvent. - Includes _emit_precedingevent_handoff_edges, env-gated OFF (LNG_HANDOFF_EDGES): component-level bridging was net-negative, kept for reference only. Co-Authored-By: Claude Opus 4.8 --- src/logic_network_generator.py | 483 ++++++++++++++++++++++++++++++++- src/pathway_generator.py | 31 ++- 2 files changed, 500 insertions(+), 14 deletions(-) diff --git a/src/logic_network_generator.py b/src/logic_network_generator.py index 6892dd0..def58d3 100755 --- a/src/logic_network_generator.py +++ b/src/logic_network_generator.py @@ -1,3 +1,4 @@ +import os import uuid from typing import Dict, List, Any, NamedTuple, Optional, Set, Tuple @@ -26,6 +27,7 @@ class PathwayResult(NamedTuple): uuid_mapping: Dict[str, str] catalyst_regulator_map: pd.DataFrame reaction_id_map: pd.DataFrame + entity_uuid_registry: Dict[tuple, str] = {} def _get_reactome_id_from_hash(decomposed_uid_mapping: pd.DataFrame, hash_value: str) -> str: @@ -487,31 +489,161 @@ def _build_reactome_to_vr_map(reaction_id_map: pd.DataFrame) -> Dict[str, List[s return reactome_to_vr +def _parse_variant_members(variant_id: str) -> Set[str]: + """Terminal member stIds encoded in a ``{parent}::variant::{m1_m2}`` id.""" + if "::variant::" not in variant_id: + return set() + tail = variant_id.split("::variant::", 1)[1] + return {m for m in tail.split("_") if m} + + +_variant_leafsets_cache: Dict[str, List[frozenset]] = {} + + +def _complex_variant_leafsets(complex_id: str) -> List[frozenset]: + """Enumerate a complex's set-variants as FULL terminal-leaf sets. + + One frozenset per variant, each the complete terminal membership of that + variant (fixed components + one choice per internal EntitySet + recursively + for nested complexes). Unlike :func:`_expand_complex_variants` this returns + flat leaf sets (no nested ``::variant::`` ids), which is what the emission + node id and the member-set matching need. + """ + import itertools + from src.neo4j_connector import get_labels, get_complex_components, get_set_members + + if complex_id in _variant_leafsets_cache: + return _variant_leafsets_cache[complex_id] + + components = get_complex_components(complex_id) + if not components: + result = [frozenset(get_terminal_components(complex_id))] + _variant_leafsets_cache[complex_id] = result + return result + + per_component_choices: List[List[frozenset]] = [] + for member_id in components: + labels = get_labels(member_id) + if "Complex" in labels and _complex_contains_entity_set(member_id): + per_component_choices.append(_complex_variant_leafsets(member_id)) + elif ( + any(l in labels for l in ("EntitySet", "DefinedSet", "CandidateSet")) + and member_id not in _UBIQUITIN_ENTITY_SET_IDS + ): + choices = [frozenset(get_terminal_components(sm)) for sm in get_set_members(member_id)] + per_component_choices.append(choices or [frozenset(get_terminal_components(member_id))]) + else: + per_component_choices.append([frozenset(get_terminal_components(member_id))]) + + variants: List[frozenset] = [] + seen: Set[frozenset] = set() + for combo in itertools.product(*per_component_choices): + leaves = frozenset().union(*combo) if combo else frozenset() + if leaves and leaves not in seen: + seen.add(leaves) + variants.append(leaves) + result = variants or [frozenset(get_terminal_components(complex_id))] + _variant_leafsets_cache[complex_id] = result + return result + + +def _map_annotated_entity_to_nodes(entity_id: str, member_set: Set[str]) -> Set[str]: + """Map one reaction-annotated input/output entity to its emission node(s). + + This is where "a complex is a single node, split only by its internal sets" + is enforced — at *emission* time, using the reaction's real annotated + entities (unambiguous), not the content-addressed matching hashes. + + * simple entity / simple complex → the entity's own stId (one bundled node) + * complex-that-contains-a-set → the specific set-VARIANT node selected by + this virtual reaction, named ``{complex}::variant::{sorted members}``. + The variant is chosen by matching its members against ``member_set`` (the + terminal members this VR actually resolved to), so we pick the right + alternative rather than emitting all of them. + * bare EntitySet → the terminal member(s) present in this VR (sets expand) + """ + from src.neo4j_connector import get_labels + labels = get_labels(entity_id) + + if "Complex" in labels: + if not _complex_contains_entity_set(entity_id): + return {str(entity_id)} # simple complex → single bundled node + # Set-variant node: OUTERMOST complex stId + a FLAT sorted list of the + # variant's FULL terminal membership. We enumerate the complex's true + # variants (each with complete membership) and pick the one this VR + # selected — the variant whose leaves are all present in member_set + # (largest such, to prefer the fullest match). Enumerating true variants + # (rather than intersecting all-possible-leaves with member_set) avoids + # emitting spurious partial variants missing a subunit. Id is kept flat + # so the parent is always ``id.split("::variant::")[0]``. + variant_leafsets = _complex_variant_leafsets(entity_id) + subset = [ls for ls in variant_leafsets if ls <= member_set] + if subset: + chosen = max(subset, key=len) + else: + chosen = max(variant_leafsets, key=lambda ls: len(ls & member_set), + default=frozenset()) + if not (chosen & member_set): + return {str(entity_id)} # no fit → fall back to plain complex + return {f"{entity_id}::variant::{'_'.join(sorted(chosen))}"} + + if any(l in labels for l in ("EntitySet", "DefinedSet", "CandidateSet")): + if entity_id in _UBIQUITIN_ENTITY_SET_IDS: + return {str(entity_id)} + present = get_terminal_components(entity_id) & member_set + return present if present else {str(entity_id)} + + return {str(entity_id)} # simple entity (protein / small molecule / …) + + def _resolve_vr_entities( reaction_id_map: pd.DataFrame, uid_index: Dict[str, tuple] ) -> Dict[str, tuple]: - """Resolve each virtual reaction's input/output hashes to terminal Reactome IDs. + """Resolve each virtual reaction's inputs/outputs to emission NODES. - Caches the resolution so Phase 2 and Phase 3 don't re-resolve. + A node is a bundled complex (or set-variant of one), a bare-set member, or a + free simple entity — NOT the complex's individual member proteins. The + matching layer (content hashes) is used only to learn which terminal members + this VR selected; the node *identities* come from the reaction's real + annotated entities via :func:`_map_annotated_entity_to_nodes`, which keeps + parent-complex provenance unambiguous (content hashes collide across + complexes with identical member content). - Args: - reaction_id_map: DataFrame with 'uid', 'input_hash', 'output_hash' columns - uid_index: Pre-built lookup index from _build_uid_index + Caches the resolution so Phase 2 and Phase 3 don't re-resolve. Returns: - Dict mapping vr_uid -> (input_reactome_ids, output_reactome_ids, + Dict mapping vr_uid -> (input_node_ids, output_node_ids, input_stoich_map, output_stoich_map) - where stoich maps are Dict[str, int] mapping entity_id → stoichiometry """ + from src.neo4j_connector import get_reaction_input_output_ids + + annotated_cache: Dict[tuple, Set[str]] = {} + + def _annotated(reaction_id: str, io: str) -> Set[str]: + key = (reaction_id, io) + if key not in annotated_cache: + annotated_cache[key] = set(get_reaction_input_output_ids(reaction_id, io)) + return annotated_cache[key] + vr_entities: Dict[str, tuple] = {} for _, row in reaction_id_map.iterrows(): vr_uid = row["uid"] - input_stoich = _resolve_to_terminal_reactome_ids(uid_index, row["input_hash"]) - output_stoich = _resolve_to_terminal_reactome_ids(uid_index, row["output_hash"]) - input_ids = list(input_stoich.keys()) - output_ids = list(output_stoich.keys()) - vr_entities[vr_uid] = (input_ids, output_ids, input_stoich, output_stoich) + reaction_id = str(row["reactome_id"]) + input_members = set(_resolve_to_terminal_reactome_ids(uid_index, row["input_hash"])) + output_members = set(_resolve_to_terminal_reactome_ids(uid_index, row["output_hash"])) + + input_ids: Set[str] = set() + for e in _annotated(reaction_id, "input"): + input_ids |= _map_annotated_entity_to_nodes(str(e), input_members) + output_ids: Set[str] = set() + for e in _annotated(reaction_id, "output"): + output_ids |= _map_annotated_entity_to_nodes(str(e), output_members) + + vr_entities[vr_uid] = ( + list(input_ids), list(output_ids), + {n: 1 for n in input_ids}, {n: 1 for n in output_ids}, + ) return vr_entities @@ -905,6 +1037,128 @@ def _emit_substrate_depletion_edges( ) +_handoff_leaf_cache: Dict[str, frozenset] = {} + + +def _node_leaves(node_id: str) -> frozenset: + """Non-cofactor terminal leaf stIds contained in a node id. + + variant node → the leaves encoded after ``::variant::``; simple complex → + its terminal components; anything else → itself. Cofactors and ubiquitin are + excluded so they can't act as spurious connectivity carriers. + """ + if node_id in _handoff_leaf_cache: + return _handoff_leaf_cache[node_id] + if "::variant::" in node_id: + s = {m for m in node_id.split("::variant::")[-1].split("_") if m.startswith("R-")} + else: + try: + from src.neo4j_connector import get_labels + s = set(get_terminal_components(node_id)) if "Complex" in get_labels(node_id) else {node_id} + except Exception: + s = {node_id} + s = frozenset(s - _COFACTOR_STIDS - _UBIQUITIN_STIDS) + _handoff_leaf_cache[node_id] = s + return s + + +def _emit_precedingevent_handoff_edges( + pathway_logic_network_data: List[Dict[str, Any]], + reaction_connections: pd.DataFrame, + reactome_to_vr: Dict[str, List[str]], + vr_entities: Dict[str, tuple], + entity_uuid_registry: Dict[tuple, str], +) -> None: + """Restore curator-intended connectivity dropped by complex bundling. + + A ``precedingEvent`` is the curator asserting that at least one entity flows + from the preceding reaction to the following one. The generator realizes that + only when the two reactions share a *whole* entity. But a molecule is often + handed off as a *component* — bound in a complex on one side, free/other- + complex on the other — so bundling makes them different nodes and the link is + lost. This is NOT a heuristic: the ``precedingEvent`` edge is in Neo4j. + + Rule (per Adam): for each precedingEvent pair, do nothing if the reactions are + ALREADY connected by a shared whole entity (the curator's asserted entity is + represented). Only when they share no whole entity — yet the curator says + they're connected — add ONE bridge, between the output/input nodes that share + the most non-cofactor components (the most likely real carrier). "Skip already- + connected pairs" + "one bridge per gap" keeps this bounded by the number of + precedingEvent gaps (hundreds), not the variant×variant blow-up. + """ + # Carrier specificity: a leaf that appears in many nodes is a promiscuous + # subunit (e.g. RBL2, a pocket protein in many complexes). Bridging on such a + # hub spreads a perturbation to readouts it doesn't affect (false positives). + # Count how many distinct nodes each leaf appears in; only leaves appearing + # in <= HUB_MAX nodes are allowed to act as the transferred carrier. + HUB_MAX = int(os.environ.get("LNG_HANDOFF_HUB_MAX", "3")) + leaf_nodes: Dict[str, set] = {} + all_node_ids: Set[str] = set() + for (ins_, outs_, _si, _so) in vr_entities.values(): + all_node_ids.update(ins_); all_node_ids.update(outs_) + for nid in all_node_ids: + for lf in _node_leaves(nid): + leaf_nodes.setdefault(lf, set()).add(nid) + specific = {lf for lf, ns in leaf_nodes.items() if len(ns) <= HUB_MAX} + + existing = {(e["source_id"], e["target_id"]) for e in pathway_logic_network_data} + seen: Set[tuple] = set() + n = 0 + for _, conn in reaction_connections.iterrows(): + pre = conn.get("preceding_reaction_id"); fol = conn.get("following_reaction_id") + if pd.isna(pre) or pd.isna(fol): + continue + # Gather (node_id, uuid) for the preceding reaction's outputs and the + # following reaction's inputs, across all their virtual reactions. + outs = [] + for p_vr in reactome_to_vr.get(pre, []): + for on in vr_entities.get(p_vr, ([], [], {}, {}))[1]: + u = entity_uuid_registry.get((on, p_vr, "output")) + if u: + outs.append((on, u)) + ins = [] + for f_vr in reactome_to_vr.get(fol, []): + for inn in vr_entities.get(f_vr, ([], [], {}, {}))[0]: + u = entity_uuid_registry.get((inn, f_vr, "input")) + if u: + ins.append((inn, u)) + if not outs or not ins: + continue + # Already connected? (whole-entity match -> Phase 2 gave a shared UUID) + if {u for _, u in outs} & {u for _, u in ins}: + continue + # Otherwise bridge the single best output/input node pair, scored by the + # number of shared SPECIFIC (non-hub) carrier components. A pair sharing + # only hub subunits scores 0 and is not bridged. + best = None; best_n = 0 + for on, ou in outs: + lon = _node_leaves(on) & specific + if not lon: + continue + for inn, iu in ins: + if ou == iu: + continue + sh = len(lon & _node_leaves(inn)) + if sh > best_n: + best_n = sh; best = (ou, iu) + if best and best_n > 0 and best[0] != best[1] and best not in existing and best not in seen: + seen.add(best) + pathway_logic_network_data.append({ + "source_id": best[0], + "target_id": best[1], + "pos_neg": "pos", + "and_or": "or", + "edge_type": "handoff", + "stoichiometry": 1, + "edge_reaction_id": None, + }) + n += 1 + logger.info( + f"Emitted {n} precedingEvent hand-off edges " + f"(one bridge per otherwise-disconnected precedingEvent gap)" + ) + + def _emit_boundary_decomposition_edges( pathway_logic_network_data: List[Dict[str, Any]], reactome_id_to_uuid: Dict[str, str], @@ -1246,6 +1500,7 @@ def create_pathway_logic_network( "and_or": pd.Series(dtype="str"), "edge_type": pd.Series(dtype="str"), "stoichiometry": pd.Series(dtype="Int64"), + "edge_reaction_id": pd.Series(dtype="str"), } pathway_logic_network_data: List[Dict[str, Any]] = [] @@ -1344,9 +1599,15 @@ def create_pathway_logic_network( # Output edges get "or" when the entity is produced by multiple VRs. entity_producer_count = _build_entity_producer_count(vr_entities) + # vr_uid -> Reactome reaction stId, for edge provenance (edge_reaction_id). + vr_to_reaction: Dict[str, str] = dict( + zip(reaction_id_map["uid"].astype(str), reaction_id_map["reactome_id"].astype(str)) + ) + for vr_uid, (input_ids, output_ids, input_stoich, output_stoich) in vr_entities.items(): if not input_ids or not output_ids: continue + reaction_stid = vr_to_reaction.get(str(vr_uid)) for eid in input_ids: input_uuid = entity_uuid_registry[(eid, vr_uid, "input")] @@ -1357,6 +1618,7 @@ def create_pathway_logic_network( "and_or": "and", "edge_type": "input", "stoichiometry": input_stoich.get(eid, 1), + "edge_reaction_id": reaction_stid, }) for eid in output_ids: @@ -1374,6 +1636,7 @@ def create_pathway_logic_network( "and_or": and_or, "edge_type": "output", "stoichiometry": output_stoich.get(eid, 1), + "edge_reaction_id": reaction_stid, }) # Log UUID registry statistics @@ -1433,6 +1696,24 @@ def create_pathway_logic_network( reactome_id_to_uuid=reactome_id_to_uuid, ) + # Restore curator-intended connectivity that complex-bundling drops: two + # precedingEvent-linked reactions that hand off a shared COMPONENT (bound in + # a complex on one side, free/other-complex on the other) share no whole + # entity, so they were left disconnected. Honoring the curated precedingEvent + # by bridging on the shared component was tried three ways (naive / max-shared + # / hub-guarded) and was net-NEGATIVE on the benchmark every time: the bridge + # injects roughly as much spurious coupling as real signal it recovers (same + # reason the member-exploded network tied set-variant at ~69%). Kept for + # future work but OFF by default. See memory project_complex_as_node_result. + if os.environ.get("LNG_HANDOFF_EDGES", "0") != "0": + _emit_precedingevent_handoff_edges( + pathway_logic_network_data=pathway_logic_network_data, + reaction_connections=reaction_connections, + reactome_to_vr=reactome_to_vr, + vr_entities=vr_entities, + entity_uuid_registry=entity_uuid_registry, + ) + # Create final DataFrame pathway_logic_network = pd.DataFrame(pathway_logic_network_data, columns=list(columns.keys())) # Coerce stoichiometry to nullable Int64 — emission sites use a mix of @@ -1466,7 +1747,8 @@ def create_pathway_logic_network( logic_network=pathway_logic_network, uuid_mapping=reactome_id_to_uuid, catalyst_regulator_map=catalyst_regulator_uuid_map, - reaction_id_map=reaction_id_map + reaction_id_map=reaction_id_map, + entity_uuid_registry=entity_uuid_registry, ) def find_root_inputs(pathway_logic_network: pd.DataFrame) -> List[Any]: @@ -1655,3 +1937,178 @@ def export_entity_reaction_proxy_mapping( f"Exported entity-reaction proxy mapping: {len(out_df)} rows " f"covering {n_entities} of {len(missing)} missing species" ) + + +# --------------------------------------------------------------------------- +# Schema-backed provenance exports (see schema/logic_network.linkml.yaml) +# --------------------------------------------------------------------------- +def _uuid_to_stable_id_map(pathway_logic_network: pd.DataFrame, + uuid_mapping: Dict[str, str]) -> Dict[str, str]: + """uuid -> node string id (stId or ``{parent}::variant::{members}``). + + ``uuid_mapping`` (reactome_id_to_uuid) can be stored either direction; detect + it the same way :func:`export_uuid_to_reactome_mapping` does. + """ + all_uuids: Set[str] = set() + all_uuids.update(pathway_logic_network["source_id"].dropna().astype(str).unique()) + all_uuids.update(pathway_logic_network["target_id"].dropna().astype(str).unique()) + out: Dict[str, str] = {} + if uuid_mapping: + sample = next(iter(uuid_mapping.keys())) + uuid_keyed = isinstance(sample, str) and sample.count("-") >= 4 and "::" not in sample + if uuid_keyed: + for u, s in uuid_mapping.items(): + if str(u) in all_uuids: + out[str(u)] = str(s) + else: + for s, u in uuid_mapping.items(): + if str(u) in all_uuids: + out[str(u)] = str(s) + return out + + +_sets_chosen_cache: Dict[str, tuple] = {} + + +def _derive_sets_and_chosen(parent_complex: str, member_leaves: Set[str]) -> tuple: + """(source_set_ids, chosen_member_ids) for a set_variant, from Neo4j.""" + if parent_complex in _sets_chosen_cache: + sets, chooser = _sets_chosen_cache[parent_complex] + else: + from src.neo4j_connector import get_labels, get_complex_components, get_set_members + sets: List[str] = [] + chooser: Dict[str, List[tuple]] = {} + try: + comps = get_complex_components(parent_complex) + except Exception: + comps = {} + for m in comps: + try: + labels = get_labels(m) + except Exception: + continue + if any(l in labels for l in ("EntitySet", "DefinedSet", "CandidateSet")): + sets.append(str(m)) + for sm in get_set_members(m): + chooser.setdefault(str(m), []).append( + (str(sm), frozenset(get_terminal_components(sm))) + ) + _sets_chosen_cache[parent_complex] = (sets, chooser) + chosen: List[str] = [] + for _set_id, options in chooser.items(): + for sm_id, sm_leaves in options: + if sm_leaves & member_leaves: + chosen.append(sm_id) + return sets, chosen + + +def export_nodes(pathway_logic_network: pd.DataFrame, + reaction_id_map: pd.DataFrame, + uuid_mapping: Dict[str, str], + output_file: str) -> None: + """Write nodes.csv — one row per node (see schema class ``Node``). + + Provenance is derived post-hoc from the node id string, the edge topology, + and Neo4j: node_kind, diagram_entity_id (the stId a diagram renders — parent + complex for a variant), member_leaves, and the set decomposition. + """ + from src.neo4j_connector import get_labels + uuid_to_str = _uuid_to_stable_id_map(pathway_logic_network, uuid_mapping) + vr_uids = set(reaction_id_map["uid"].astype(str)) + vr_to_reaction = dict(zip(reaction_id_map["uid"].astype(str), + reaction_id_map["reactome_id"].astype(str))) + + incoming_types: Dict[str, Set[str]] = {} + has_outgoing: Set[str] = set() + for _, e in pathway_logic_network.iterrows(): + s, t = e.get("source_id"), e.get("target_id") + if pd.notna(s): + has_outgoing.add(str(s)) + if pd.notna(t): + incoming_types.setdefault(str(t), set()).add(str(e.get("edge_type"))) + + all_uuids = set(uuid_to_str) | (vr_uids & (has_outgoing | set(incoming_types))) + rows: List[Dict[str, Any]] = [] + for u in sorted(all_uuids): + kind = "other"; diagram = None + members: List[str] = []; sets: List[str] = []; chosen: List[str] = [] + if u in vr_uids and u not in uuid_to_str: + kind, diagram = "reaction", vr_to_reaction.get(u) + else: + s = uuid_to_str.get(u) + if s is None: + pass + elif "::variant::" in s: + kind = "set_variant" + diagram = s.split("::variant::")[0] + members = [m for m in s.split("::variant::")[-1].split("_") + if m.startswith("R-")] + sets, chosen = _derive_sets_and_chosen(diagram, set(members)) + else: + diagram = s + inc = incoming_types.get(u, set()) + if "dissociation" in inc and u not in has_outgoing: + kind, members = "dissociation_sink", [s] + else: + try: + labels = get_labels(s) + except Exception: + labels = [] + if "Complex" in labels: + kind = "simple_complex" + try: + members = sorted(get_terminal_components(s)) + except Exception: + members = [s] + else: + kind, members = "simple_entity", [s] + rows.append({ + "uuid": u, + "node_kind": kind, + "diagram_entity_id": diagram, + "compartment": None, + "member_leaves": "|".join(members), + "source_sets": "|".join(sets), + "chosen_members": "|".join(chosen), + }) + cols = ["uuid", "node_kind", "diagram_entity_id", "compartment", + "member_leaves", "source_sets", "chosen_members"] + pd.DataFrame(rows, columns=cols).to_csv(output_file, index=False) + logger.info(f"Exported {len(rows)} nodes with provenance: {output_file}") + + +def export_node_reaction_context(entity_uuid_registry: Dict[tuple, str], + reaction_id_map: pd.DataFrame, + catalyst_regulator_map: pd.DataFrame, + output_file: str) -> None: + """Write node_reaction_context.csv — (node, reaction, role) location rows.""" + vr_to_reaction = dict(zip(reaction_id_map["uid"].astype(str), + reaction_id_map["reactome_id"].astype(str))) + seen: Set[tuple] = set() + rows: List[Dict[str, Any]] = [] + + for (eid, vr_uid, role), uuid in (entity_uuid_registry or {}).items(): + rid = vr_to_reaction.get(str(vr_uid)) + if rid is None or role not in ("input", "output"): + continue + key = (str(uuid), rid, role) + if key in seen: + continue + seen.add(key) + rows.append({"context_node": str(uuid), "reaction_id": rid, "role": role}) + + if catalyst_regulator_map is not None and not catalyst_regulator_map.empty: + for _, r in catalyst_regulator_map.iterrows(): + uuid = r.get("uuid"); rid = r.get("reaction_id"); et = str(r.get("edge_type")) + if pd.isna(uuid) or pd.isna(rid): + continue + role = "catalyst" if et == "catalyst" else "regulator" + key = (str(uuid), str(rid), role) + if key in seen: + continue + seen.add(key) + rows.append({"context_node": str(uuid), "reaction_id": str(rid), "role": role}) + + cols = ["context_node", "reaction_id", "role"] + pd.DataFrame(rows, columns=cols).to_csv(output_file, index=False) + logger.info(f"Exported {len(rows)} node-reaction-context rows: {output_file}") diff --git a/src/pathway_generator.py b/src/pathway_generator.py index f21c982..adcd1bd 100755 --- a/src/pathway_generator.py +++ b/src/pathway_generator.py @@ -9,6 +9,8 @@ from src.logic_network_generator import ( create_pathway_logic_network, export_entity_reaction_proxy_mapping, + export_node_reaction_context, + export_nodes, export_uuid_to_reactome_mapping, ) from src.neo4j_connector import get_reaction_connections @@ -120,10 +122,17 @@ def generate_pathway_file( logger.warning(f"Could not cache decomposition results: {e}") # Continue without caching + # Augment connectivity with curator-drawn diagram flow (product->substrate + # pairs the diagram links but precedingEvent may omit — esp. old pathways). + # Only the connectivity/merge step sees this; the matching layer above + # stays on pure precedingEvent. See reactome/logic-network-generator#39. + from src.diagram_connectivity import augment_reaction_connections + connectivity = augment_reaction_connections(pathway_id, reaction_connections) + # Generate logic network logger.info("Creating pathway logic network...") result = create_pathway_logic_network( - decomposed_uid_mapping, reaction_connections, best_matches + decomposed_uid_mapping, connectivity, best_matches ) # Save logic network (main output file users need) @@ -170,6 +179,26 @@ def generate_pathway_file( logger.error(f"Failed to write entity-reaction proxy mapping file {proxy_mapping_file}: {e}") # Don't raise - this is supplementary + # Schema-backed provenance files (schema/logic_network.linkml.yaml): + # nodes.csv (node_kind, diagram_entity_id, member_leaves, set provenance) + # and node_reaction_context.csv (node<->reaction location layer). + try: + export_nodes( + result.logic_network, + result.reaction_id_map, + result.uuid_mapping, + str(pathway_output_dir / "nodes.csv"), + ) + export_node_reaction_context( + result.entity_uuid_registry, + result.reaction_id_map, + result.catalyst_regulator_map, + str(pathway_output_dir / "node_reaction_context.csv"), + ) + except Exception as e: + logger.error(f"Failed to write node provenance files: {e}", exc_info=True) + # Don't raise - supplementary + logger.info(f"Output directory: {pathway_output_dir}") except (ConnectionError, ValueError) as e: From 3ff4750cf4cf0db2c1e6f6b6d897548da2b5bad3 Mon Sep 17 00:00:00 2001 From: Adam Wright Date: Tue, 14 Jul 2026 19:00:32 -0400 Subject: [PATCH 15/17] Pin PYTHONHASHSEED=0 for deterministic generation Re-exec once with a fixed hash seed so regeneration is reproducible (output depended on set/dict iteration order; ~5.8% of edges differed run-to-run). Override with LNG_ALLOW_NONDETERMINISM=1. reactome/logic-network-generator#42. Co-Authored-By: Claude Opus 4.8 --- bin/create-pathways.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/bin/create-pathways.py b/bin/create-pathways.py index a8703a3..504abb1 100755 --- a/bin/create-pathways.py +++ b/bin/create-pathways.py @@ -3,6 +3,17 @@ import os import sys + +# Determinism: the generator's output depends on set/dict iteration order, which +# Python randomizes per-process via hash seeding — so regenerating a pathway +# yields structurally different networks run-to-run (~5.8% of edges for TP53). +# Pin the hash seed so generation is reproducible. PYTHONHASHSEED must be set +# before the interpreter starts, so re-exec once if it isn't already fixed. +# Override with LNG_ALLOW_NONDETERMINISM=1. See reactome/logic-network-generator#42. +if os.environ.get("PYTHONHASHSEED") != "0" and os.environ.get("LNG_ALLOW_NONDETERMINISM") != "1": + os.environ["PYTHONHASHSEED"] = "0" + os.execv(sys.executable, [sys.executable] + sys.argv) + from typing import List, Tuple import pandas as pd From 04d68c937b312ba30e20bda1141812cb5d360744 Mon Sep 17 00:00:00 2001 From: Adam Wright Date: Tue, 14 Jul 2026 19:38:34 -0400 Subject: [PATCH 16/17] Fix ruff + mypy: ruff config, rename ambiguous vars, narrow Optionals - Add [tool.ruff.lint] ignore=[E701,E702] to match the codebase's one-line-guard style. - Rename ambiguous 'l' loop vars; rename 'uuid'/'node_uuid' loop vars that shadowed the uuid module. - Narrow Optional[str] from dict .get() (stid, uid) so mypy passes; behavior-preserving. Co-Authored-By: Claude Opus 4.8 --- pyproject.toml | 6 ++++++ src/logic_network_generator.py | 36 +++++++++++++++++----------------- 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 3c392ac..7e6d4f7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,6 +41,12 @@ plugins = ["flake8-mypy"] line-length = 88 # Adjust line length as needed target-version = ['py39'] +[tool.ruff.lint] +# The codebase uses one-line guard/assignment statements pervasively +# (`if cond: continue`, `a = x; b = y`). Those stylistic rules (E701/E702) are +# ignored to match existing style; real issues (F, other E) stay enforced. +ignore = ["E701", "E702"] + [tool.pytest.ini_options] testpaths = ["tests"] python_files = ["test_*.py"] diff --git a/src/logic_network_generator.py b/src/logic_network_generator.py index def58d3..0674719 100755 --- a/src/logic_network_generator.py +++ b/src/logic_network_generator.py @@ -527,7 +527,7 @@ def _complex_variant_leafsets(complex_id: str) -> List[frozenset]: if "Complex" in labels and _complex_contains_entity_set(member_id): per_component_choices.append(_complex_variant_leafsets(member_id)) elif ( - any(l in labels for l in ("EntitySet", "DefinedSet", "CandidateSet")) + any(lbl in labels for lbl in ("EntitySet", "DefinedSet", "CandidateSet")) and member_id not in _UBIQUITIN_ENTITY_SET_IDS ): choices = [frozenset(get_terminal_components(sm)) for sm in get_set_members(member_id)] @@ -587,7 +587,7 @@ def _map_annotated_entity_to_nodes(entity_id: str, member_set: Set[str]) -> Set[ return {str(entity_id)} # no fit → fall back to plain complex return {f"{entity_id}::variant::{'_'.join(sorted(chosen))}"} - if any(l in labels for l in ("EntitySet", "DefinedSet", "CandidateSet")): + if any(lbl in labels for lbl in ("EntitySet", "DefinedSet", "CandidateSet")): if entity_id in _UBIQUITIN_ENTITY_SET_IDS: return {str(entity_id)} present = get_terminal_components(entity_id) & member_set @@ -954,7 +954,7 @@ def _emit_substrate_depletion_edges( seen_uuids: Set[str] = set() for edge in pathway_logic_network_data: for uid in (edge.get("source_id"), edge.get("target_id")): - if uid in seen_uuids: continue + if not uid or uid in seen_uuids: continue seen_uuids.add(uid) sid = reactome_id_to_uuid.get(uid, "") if sid: @@ -1057,9 +1057,9 @@ def _node_leaves(node_id: str) -> frozenset: s = set(get_terminal_components(node_id)) if "Complex" in get_labels(node_id) else {node_id} except Exception: s = {node_id} - s = frozenset(s - _COFACTOR_STIDS - _UBIQUITIN_STIDS) - _handoff_leaf_cache[node_id] = s - return s + leaves = frozenset(s - _COFACTOR_STIDS - _UBIQUITIN_STIDS) + _handoff_leaf_cache[node_id] = leaves + return leaves def _emit_precedingevent_handoff_edges( @@ -1245,7 +1245,7 @@ def _is_complex(entity_id: str) -> bool: seen_edges: Set[tuple] = set() assembly_count = 0 for complex_uuid in root_uuids: - stid = reactome_id_to_uuid.get(complex_uuid) + stid = reactome_id_to_uuid.get(complex_uuid) or "" if not stid or not _is_complex(stid): continue leaves = get_terminal_components(stid) @@ -1268,7 +1268,7 @@ def _is_complex(entity_id: str) -> bool: dissociation_count = 0 for complex_uuid in terminal_uuids: - stid = reactome_id_to_uuid.get(complex_uuid) + stid = reactome_id_to_uuid.get(complex_uuid) or "" if not stid or not _is_complex(stid): continue leaves = get_terminal_components(stid) @@ -1976,8 +1976,8 @@ def _derive_sets_and_chosen(parent_complex: str, member_leaves: Set[str]) -> tup sets, chooser = _sets_chosen_cache[parent_complex] else: from src.neo4j_connector import get_labels, get_complex_components, get_set_members - sets: List[str] = [] - chooser: Dict[str, List[tuple]] = {} + sets = [] + chooser = {} try: comps = get_complex_components(parent_complex) except Exception: @@ -1987,7 +1987,7 @@ def _derive_sets_and_chosen(parent_complex: str, member_leaves: Set[str]) -> tup labels = get_labels(m) except Exception: continue - if any(l in labels for l in ("EntitySet", "DefinedSet", "CandidateSet")): + if any(lbl in labels for lbl in ("EntitySet", "DefinedSet", "CandidateSet")): sets.append(str(m)) for sm in get_set_members(m): chooser.setdefault(str(m), []).append( @@ -2087,27 +2087,27 @@ def export_node_reaction_context(entity_uuid_registry: Dict[tuple, str], seen: Set[tuple] = set() rows: List[Dict[str, Any]] = [] - for (eid, vr_uid, role), uuid in (entity_uuid_registry or {}).items(): + for (eid, vr_uid, role), node_uuid in (entity_uuid_registry or {}).items(): rid = vr_to_reaction.get(str(vr_uid)) if rid is None or role not in ("input", "output"): continue - key = (str(uuid), rid, role) + key = (str(node_uuid), rid, role) if key in seen: continue seen.add(key) - rows.append({"context_node": str(uuid), "reaction_id": rid, "role": role}) + rows.append({"context_node": str(node_uuid), "reaction_id": rid, "role": role}) if catalyst_regulator_map is not None and not catalyst_regulator_map.empty: for _, r in catalyst_regulator_map.iterrows(): - uuid = r.get("uuid"); rid = r.get("reaction_id"); et = str(r.get("edge_type")) - if pd.isna(uuid) or pd.isna(rid): + cr_uuid = r.get("uuid"); rid = r.get("reaction_id"); et = str(r.get("edge_type")) + if pd.isna(cr_uuid) or pd.isna(rid): continue role = "catalyst" if et == "catalyst" else "regulator" - key = (str(uuid), str(rid), role) + key = (str(cr_uuid), str(rid), role) if key in seen: continue seen.add(key) - rows.append({"context_node": str(uuid), "reaction_id": str(rid), "role": role}) + rows.append({"context_node": str(cr_uuid), "reaction_id": str(rid), "role": role}) cols = ["context_node", "reaction_id", "role"] pd.DataFrame(rows, columns=cols).to_csv(output_file, index=False) From 2d5afecd8704013ccee396b11e52d6a5842ac264 Mon Sep 17 00:00:00 2001 From: Adam Wright Date: Tue, 14 Jul 2026 19:38:34 -0400 Subject: [PATCH 17/17] Tests: allow new edge types, fix resolver test, cover diagram + provenance - valid_edge_types now includes depletion/assembly/dissociation/handoff (matches the EdgeType enum) across the four structure tests. - Rewrite test_no_duplicate_edges for the set-variant _resolve_vr_entities (mocks Neo4j; asserts set-deduped nodes). - Add test_diagram_connectivity.py and test_provenance_exports.py (no Neo4j), lifting coverage back over the 40% gate. Co-Authored-By: Claude Opus 4.8 --- tests/test_autophagy_validation.py | 3 +- tests/test_diagram_connectivity.py | 77 ++++++++++++++++++++++++++ tests/test_logic_network_generator.py | 47 +++++++++------- tests/test_network_invariants.py | 5 +- tests/test_pathway_validation.py | 5 +- tests/test_provenance_exports.py | 67 ++++++++++++++++++++++ tests/test_uid_reaction_connections.py | 6 +- 7 files changed, 185 insertions(+), 25 deletions(-) create mode 100644 tests/test_diagram_connectivity.py create mode 100644 tests/test_provenance_exports.py diff --git a/tests/test_autophagy_validation.py b/tests/test_autophagy_validation.py index 43e1b9c..f663369 100644 --- a/tests/test_autophagy_validation.py +++ b/tests/test_autophagy_validation.py @@ -400,7 +400,8 @@ class TestAutophagyEdgeProperties: def test_valid_edge_types(self, logic_network_sample): """All edge types should be valid.""" - valid = {'input', 'output', 'catalyst', 'regulator'} + valid = {'input', 'output', 'catalyst', 'regulator', + 'depletion', 'assembly', 'dissociation', 'handoff'} edge_types = set(logic_network_sample['edge_type'].unique()) invalid = edge_types - valid assert len(invalid) == 0, f"Invalid edge types: {invalid}" diff --git a/tests/test_diagram_connectivity.py b/tests/test_diagram_connectivity.py new file mode 100644 index 0000000..8580b92 --- /dev/null +++ b/tests/test_diagram_connectivity.py @@ -0,0 +1,77 @@ +"""Unit tests for diagram-sourced reaction connectivity (no Neo4j required). + +Builds a tiny synthetic diagram (two reactions sharing one entity glyph) and +checks that the shared-glyph product->substrate pair is extracted and unioned +into reaction_connections. See reactome/logic-network-generator#39. +""" +import json + +import pandas as pd +import pytest + +import src.diagram_connectivity as dc + + +def _write_diagram(dirpath, stid): + """Two reactions: A (R-HSA-100) outputs glyph 1; B (R-HSA-200) inputs glyph 1.""" + layout = { + "nodes": [{"id": 1, "reactomeId": 500, "displayName": "X"}], + "edges": [ + {"id": 10, "reactomeId": 100, "inputs": [], "outputs": [{"id": 1}], "catalysts": []}, + {"id": 20, "reactomeId": 200, "inputs": [{"id": 1}], "outputs": [], "catalysts": []}, + ], + } + graph = {"edges": [{"dbId": 100, "stId": "R-HSA-100"}, + {"dbId": 200, "stId": "R-HSA-200"}]} + (dirpath / f"{stid}.json").write_text(json.dumps(layout)) + (dirpath / f"{stid}.graph.json").write_text(json.dumps(graph)) + + +@pytest.fixture +def diagram_dir(tmp_path, monkeypatch): + monkeypatch.setenv("LNG_DIAGRAM_DIR", str(tmp_path)) + # isolate this pathway to its two reactions (avoid Neo4j) + monkeypatch.setattr(dc, "_pathway_reaction_stids", + lambda pid: {"R-HSA-100", "R-HSA-200"}) + return tmp_path + + +def test_shared_glyph_pair_extracted(diagram_dir): + _write_diagram(diagram_dir, "R-HSA-TEST") + pairs = dc.diagram_shared_product_pairs("R-HSA-TEST") + assert pairs == {("R-HSA-100", "R-HSA-200")} + + +def test_no_diagram_returns_empty(diagram_dir, monkeypatch): + # no diagram file written; ancestor lookup also finds nothing + monkeypatch.setattr(dc, "_covering_diagram_stid", lambda pid: "") + assert dc.diagram_shared_product_pairs("R-HSA-MISSING") == set() + + +def test_augment_adds_missing_pair(diagram_dir): + _write_diagram(diagram_dir, "R-HSA-TEST") + rc = pd.DataFrame({"preceding_reaction_id": ["R-HSA-100"], + "following_reaction_id": ["R-HSA-999"], + "event_status": ["Has Preceding Event"]}) + out = dc.augment_reaction_connections("R-HSA-TEST", rc) + added = out[out["event_status"] == "Diagram Shared Product"] + assert list(zip(added["preceding_reaction_id"], added["following_reaction_id"])) \ + == [("R-HSA-100", "R-HSA-200")] + + +def test_augment_skips_when_already_present(diagram_dir): + _write_diagram(diagram_dir, "R-HSA-TEST") + rc = pd.DataFrame({"preceding_reaction_id": ["R-HSA-100"], + "following_reaction_id": ["R-HSA-200"], + "event_status": ["Has Preceding Event"]}) + out = dc.augment_reaction_connections("R-HSA-TEST", rc) + assert len(out) == 1 # nothing added; pair already linked + + +def test_augment_disabled_by_env(diagram_dir, monkeypatch): + _write_diagram(diagram_dir, "R-HSA-TEST") + monkeypatch.setenv("LNG_DIAGRAM_CONNECTIVITY", "0") + rc = pd.DataFrame({"preceding_reaction_id": ["R-HSA-1"], + "following_reaction_id": ["R-HSA-2"], + "event_status": ["x"]}) + assert dc.augment_reaction_connections("R-HSA-TEST", rc) is rc diff --git a/tests/test_logic_network_generator.py b/tests/test_logic_network_generator.py index 516a7a5..8cc151e 100644 --- a/tests/test_logic_network_generator.py +++ b/tests/test_logic_network_generator.py @@ -244,23 +244,35 @@ def test_multi_source_convergence(self): assert uuid_from_vr1 == uuid_at_vr2 assert uuid_from_vr3 == uuid_at_vr2 - def test_no_duplicate_edges(self): - """Duplicate terminal IDs from decomposition should not produce duplicate edges. + def test_no_duplicate_edges(self, monkeypatch): + """_resolve_vr_entities must emit each node once (Set-deduped). - When multiple decomposition paths converge on the same terminal Reactome ID, - _resolve_to_terminal_reactome_ids returns duplicates. _resolve_vr_entities - must deduplicate them so Phase 3 doesn't create duplicate edges. + Since set-variant emission, node identities come from the reaction's + annotated input/output entities (mapped to nodes), and duplicates are + collapsed via a set. We mock the two Neo4j calls the resolver makes: + the reaction's annotated entities and their labels (simple proteins). """ - # Build a uid_index where hash "vr1-input" resolves to terminal ID "9933417" - # via two different nested paths, producing duplicates without dedup. - # uid_index maps hash -> (nested_uids, terminal_ids, stoich_map) + import src.logic_network_generator as m + from src import neo4j_connector + + # Reaction "1" annotates one simple-protein input and one output. + monkeypatch.setattr( + neo4j_connector, "get_reaction_input_output_ids", + lambda rid, io: {"9933417"} if io == "input" else {"12345"}, + ) + # Both entities are simple (not complexes/sets) -> mapped to themselves. + monkeypatch.setattr( + neo4j_connector, "get_labels", + lambda e: ["EntityWithAccessionedSequence"], + ) + + # input_hash resolves to member "9933417" via two convergent paths. uid_index = { - "vr1-input": (["nested-1", "nested-2"], set(), {}), # two nested paths, no direct terminals - "nested-1": ([], {"9933417"}, {"9933417": 1}), # both nested paths resolve to same terminal + "vr1-input": (["nested-1", "nested-2"], set(), {}), + "nested-1": ([], {"9933417"}, {"9933417": 1}), "nested-2": ([], {"9933417"}, {"9933417": 1}), "vr1-output": ([], {"12345"}, {"12345": 1}), } - reaction_id_map = pd.DataFrame({ "uid": ["vr1"], "input_hash": ["vr1-input"], @@ -269,17 +281,10 @@ def test_no_duplicate_edges(self): }) vr_entities = _resolve_vr_entities(reaction_id_map, uid_index) + input_ids, output_ids, _in_stoich, _out_stoich = vr_entities["vr1"] - input_ids, output_ids, input_stoich, output_stoich = vr_entities["vr1"] - - # _resolve_to_terminal_reactome_ids now returns dict (deduped by key), - # but stoichiometry accumulates: 1 + 1 = 2 from two nested paths - assert len(input_ids) == 1, ( - f"Expected 1 unique input ID, got {len(input_ids)}: {input_ids}" - ) - assert input_ids[0] == "9933417" - assert input_stoich["9933417"] == 2 # stoichiometry adds: 1 from nested-1 + 1 from nested-2 - assert len(output_ids) == 1 + assert input_ids == ["9933417"], f"expected one deduped input, got {input_ids}" + assert output_ids == ["12345"], f"expected one output, got {output_ids}" def test_root_input_same_entity_gets_one_uuid(self): """Root input entity appearing at multiple reactions should share one UUID.""" diff --git a/tests/test_network_invariants.py b/tests/test_network_invariants.py index f511adb..400e66a 100644 --- a/tests/test_network_invariants.py +++ b/tests/test_network_invariants.py @@ -70,7 +70,10 @@ def test_no_null_source_or_target(self, network): def test_valid_edge_types(self, network): """All edge_type values must be valid.""" - valid_edge_types = {'input', 'output', 'catalyst', 'regulator'} + valid_edge_types = { + "input", "output", "catalyst", "regulator", + "depletion", "assembly", "dissociation", "handoff", + } actual = set(network['edge_type'].unique()) invalid = actual - valid_edge_types assert len(invalid) == 0, f"Invalid edge_type values: {invalid}" diff --git a/tests/test_pathway_validation.py b/tests/test_pathway_validation.py index d88ab21..a7b641d 100644 --- a/tests/test_pathway_validation.py +++ b/tests/test_pathway_validation.py @@ -146,7 +146,10 @@ def test_logic_network_has_valid_structure(self, pathway_files): valid_pos_neg = {'pos', 'neg'} assert set(logic_network['pos_neg'].dropna().unique()).issubset(valid_pos_neg) - valid_edge_types = {'input', 'output', 'catalyst', 'regulator'} + valid_edge_types = { + "input", "output", "catalyst", "regulator", + "depletion", "assembly", "dissociation", "handoff", + } assert set(logic_network['edge_type'].unique()).issubset(valid_edge_types) def test_regulators_present(self, graph, pathway_files): diff --git a/tests/test_provenance_exports.py b/tests/test_provenance_exports.py new file mode 100644 index 0000000..59f74d1 --- /dev/null +++ b/tests/test_provenance_exports.py @@ -0,0 +1,67 @@ +"""Unit tests for the schema-backed provenance exports (nodes.csv, +node_reaction_context.csv). No Neo4j: entity labels / terminal components are +mocked. See schema/logic_network.linkml.yaml.""" +import pandas as pd + +import src.logic_network_generator as m +from src import neo4j_connector + + +def test_export_nodes_classifies_kinds(tmp_path, monkeypatch): + # simple entity labels; variant set-derivation stubbed out (no Neo4j) + monkeypatch.setattr(neo4j_connector, "get_labels", + lambda e: ["EntityWithAccessionedSequence"]) + monkeypatch.setattr(m, "_derive_sets_and_chosen", + lambda parent, members: (["R-HSA-75202"], ["R-HSA-68891"])) + monkeypatch.setattr(m, "get_terminal_components", lambda s: {s}) + + variant = "R-HSA-141608::variant::R-HSA-68365_R-HSA-68891" + # Realistic UUIDs (export_nodes detects mapping direction by UUID shape). + s1 = "aaaaaaaa-0000-0000-0000-000000000001" + v1 = "aaaaaaaa-0000-0000-0000-000000000002" + d1 = "aaaaaaaa-0000-0000-0000-000000000003" + rxn1 = "aaaaaaaa-0000-0000-0000-0000000000r1" + edges = pd.DataFrame([ + {"source_id": s1, "target_id": rxn1, "pos_neg": "pos", "and_or": "and", + "edge_type": "input", "stoichiometry": 1, "edge_reaction_id": "R-HSA-100"}, + {"source_id": rxn1, "target_id": v1, "pos_neg": "pos", "and_or": None, + "edge_type": "output", "stoichiometry": 1, "edge_reaction_id": "R-HSA-100"}, + {"source_id": v1, "target_id": d1, "pos_neg": "pos", "and_or": "and", + "edge_type": "dissociation", "stoichiometry": 1, "edge_reaction_id": None}, + ]) + reaction_id_map = pd.DataFrame({"uid": [rxn1], "reactome_id": ["R-HSA-100"]}) + uuid_mapping = {s1: "R-HSA-999", v1: variant, d1: "R-HSA-888"} + + out = tmp_path / "nodes.csv" + m.export_nodes(edges, reaction_id_map, uuid_mapping, str(out)) + rows = {r["uuid"]: r for r in pd.read_csv(out, dtype=str, keep_default_na=False) + .to_dict("records")} + assert rows[rxn1]["node_kind"] == "reaction" + assert rows[rxn1]["diagram_entity_id"] == "R-HSA-100" + assert rows[v1]["node_kind"] == "set_variant" + assert rows[v1]["diagram_entity_id"] == "R-HSA-141608" + assert "R-HSA-68891" in rows[v1]["member_leaves"] + assert rows[s1]["node_kind"] == "simple_entity" + assert rows[d1]["node_kind"] == "dissociation_sink" + + +def test_export_node_reaction_context(tmp_path): + entity_uuid_registry = { + ("R-HSA-999", "rxn1", "input"): "s1", + ("R-HSA-141608::variant::x", "rxn1", "output"): "v1", + } + reaction_id_map = pd.DataFrame({"uid": ["rxn1"], "reactome_id": ["R-HSA-100"]}) + catreg = pd.DataFrame([{"reaction_id": "R-HSA-100", "entity_id": "R-HSA-7", + "edge_type": "catalyst", "uuid": "c1", "reaction_uuid": "rxn1"}]) + out = tmp_path / "ctx.csv" + m.export_node_reaction_context(entity_uuid_registry, reaction_id_map, catreg, str(out)) + ctx = pd.read_csv(out).to_dict("records") + triples = {(r["context_node"], r["reaction_id"], r["role"]) for r in ctx} + assert ("s1", "R-HSA-100", "input") in triples + assert ("v1", "R-HSA-100", "output") in triples + assert ("c1", "R-HSA-100", "catalyst") in triples + + +def test_parse_variant_members(): + assert m._parse_variant_members("R-HSA-1::variant::R-HSA-2_R-HSA-3") == {"R-HSA-2", "R-HSA-3"} + assert m._parse_variant_members("R-HSA-1") == set() diff --git a/tests/test_uid_reaction_connections.py b/tests/test_uid_reaction_connections.py index 5520ed2..a4bcbc0 100644 --- a/tests/test_uid_reaction_connections.py +++ b/tests/test_uid_reaction_connections.py @@ -146,7 +146,11 @@ def test_pathway_has_valid_structure(self, pathway_dir): assert len(logic_network) > 0, "Logic network is empty" - valid_edge_types = {"input", "output", "catalyst", "regulator"} + # Must match the EdgeType enum in schema/logic_network.linkml.yaml. + valid_edge_types = { + "input", "output", "catalyst", "regulator", + "depletion", "assembly", "dissociation", "handoff", + } actual_types = set(logic_network["edge_type"].unique()) invalid = actual_types - valid_edge_types assert len(invalid) == 0, f"Invalid edge_type values: {invalid}"