diff --git a/.github/workflows/label-predicate-changes.yml b/.github/workflows/label-predicate-changes.yml index fd663254..c5040742 100644 --- a/.github/workflows/label-predicate-changes.yml +++ b/.github/workflows/label-predicate-changes.yml @@ -12,21 +12,17 @@ jobs: steps: - name: Check out code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: 3.9 + - name: Install uv + uses: astral-sh/setup-uv@v6 - - name: Install dependencies - run: | - pip install -r requirements.txt - pip install PyGithub + - name: Set up Python + run: uv python install 3.12 - name: Run predicate check env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ISSUE_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number }} run: | - python .github/scripts/Bio_QC_check.py \ No newline at end of file + uv run --with requests python .github/scripts/Bio_QC_check.py \ No newline at end of file diff --git a/.github/workflows/pypi.yml b/.github/workflows/pypi.yml new file mode 100644 index 00000000..d724336d --- /dev/null +++ b/.github/workflows/pypi.yml @@ -0,0 +1,35 @@ +name: Publish Python 🐍 distribution 📦 to PyPI and TestPyPI + +on: + release: + types: [published] + +jobs: + test-pypi: + name: Publish to TestPyPI + runs-on: ubuntu-latest + environment: + name: release + permissions: + id-token: write + steps: + - uses: actions/checkout@v4 + - uses: astral-sh/setup-uv@v6 + - run: uv build + - run: uv publish --trusted-publishing always + env: + UV_PUBLISH_URL: https://test.pypi.org/legacy/ + pypi: + name: Publish to PyPI + needs: + - test-pypi + runs-on: ubuntu-latest + environment: + name: release + permissions: + id-token: write + steps: + - uses: actions/checkout@v4 + - uses: astral-sh/setup-uv@v6 + - run: uv build + - run: uv publish --trusted-publishing always diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6fe4924b..1b126e2a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -7,9 +7,6 @@ jobs: test: name: test runs-on: ubuntu-latest - strategy: - matrix: - python-version: [3.9] steps: - name: Checkout the repository uses: actions/checkout@v4 @@ -24,15 +21,14 @@ jobs: echo "ORION_STORAGE=$PWD/tests/workspace/storage" >> $GITHUB_ENV echo "ORION_GRAPHS=$PWD/tests/workspace/graphs" >> $GITHUB_ENV - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 - with: - python-version: ${{ matrix.python-version }} + - name: Install uv + uses: astral-sh/setup-uv@v6 + + - name: Set up Python + run: uv python install 3.12 - name: Install dependencies - run: | - if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + run: uv sync --extra robokop --group dev - name: Run pytest - run: | - python -m pytest tests/ + run: uv run pytest tests/ diff --git a/.gitignore b/.gitignore index 5f1d0ea7..9bae7b70 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,8 @@ **/__pycache__ *.pycache .env -.idea \ No newline at end of file +.idea +.DS_Store +.pytest_cache/ +dist/ +*.egg-info/ \ No newline at end of file diff --git a/.python-version b/.python-version new file mode 100644 index 00000000..e4fba218 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/Common/merging.py b/Common/merging.py deleted file mode 100644 index 53071510..00000000 --- a/Common/merging.py +++ /dev/null @@ -1,329 +0,0 @@ -import os -import jsonlines -import secrets -from xxhash import xxh64_hexdigest -from Common.biolink_utils import BiolinkUtils -from Common.biolink_constants import * -from Common.utils import quick_json_loads, quick_json_dumps, chunk_iterator, LoggingUtil - -NODE_PROPERTIES_THAT_SHOULD_BE_SETS = {SYNONYMS, NODE_TYPES, SYNONYM} -EDGE_PROPERTIES_THAT_SHOULD_BE_SETS = {AGGREGATOR_KNOWLEDGE_SOURCES, PUBLICATIONS, XREFS} - -NODE_ENTITY_TYPE = 'node' -EDGE_ENTITY_TYPE = 'edge' - -bmt = BiolinkUtils() - -logger = LoggingUtil.init_logging("ORION.Common.merging", - line_format='medium', - log_file_path=os.environ['ORION_LOGS']) - - -def node_key_function(node): - return node['id'] - - -def edge_key_function(edge, custom_key_attributes=None): - qualifiers = [f'{key}{value}' for key, value in edge.items() if bmt.is_qualifier(key)] - standard_attributes = (f'{edge[SUBJECT_ID]}{edge[PREDICATE]}{edge[OBJECT_ID]}' - f'{edge.get(PRIMARY_KNOWLEDGE_SOURCE, "")}{"".join(qualifiers)}') - if custom_key_attributes: - custom_attributes = [edge[attr] if attr in edge else '' for attr in custom_key_attributes] - return xxh64_hexdigest(f'{standard_attributes}{"".join(custom_attributes)}') - else: - return xxh64_hexdigest(standard_attributes) - - -def entity_merging_function(entity_1, entity_2, properties_that_are_sets): - # for every property of entity 2 - for key, entity_2_value in entity_2.items(): - # if entity 1 also has the property and entity_2_value is not null/empty: - # concatenate values if one is a list, otherwise ignore the property from entity 2 - if (key in entity_1) and entity_2_value: - entity_1_value = entity_1[key] - entity_1_is_list = isinstance(entity_1_value, list) - entity_2_is_list = isinstance(entity_2_value, list) - if entity_1_is_list and entity_2_is_list: - # if they're both lists just combine them - entity_1_value.extend(entity_2_value) - elif entity_1_is_list: - # if 1 is a list and 2 isn't, append the value of 2 to the list from 1 - entity_1_value.append(entity_2_value) - elif entity_2_is_list: - if entity_1_value: - # if 2 is a list and 1 has a value, add the value of 1 to the list from 2 - entity_1[key] = [entity_1_value] + entity_2_value - else: - # if 2 is a list and 1 doesn't have a value, just use the list from 2 - entity_1[key] = entity_2_value - # else: - # if neither is a list, do nothing (keep the value from 1) - if (entity_1_is_list or entity_2_is_list) and (key in properties_that_are_sets): - entity_1[key] = list(set(entity_1[key])) - else: - # if entity 1 doesn't have the property, add the property from entity 2 - entity_1[key] = entity_2_value - return entity_1 - - -class GraphMerger: - - def __init__(self): - self.merged_node_counter = 0 - self.merged_edge_counter = 0 - - def merge_nodes(self, nodes_iterable): - raise NotImplementedError - - def merge_edges(self, edges_iterable, additional_edge_attributes=None, add_edge_id=False): - raise NotImplementedError - - def merge_node(self, node): - raise NotImplementedError - - def merge_edge(self, edge, additional_edge_attributes=None, add_edge_id=False): - raise NotImplementedError - - def flush(self): - pass - - def get_merged_nodes_jsonl(self): - raise NotImplementedError - - def get_merged_edges_jsonl(self): - raise NotImplementedError - - -class DiskGraphMerger(GraphMerger): - - def __init__(self, temp_directory: str = None, chunk_size: int = 10_000_000): - - super().__init__() - - self.chunk_size = chunk_size - self.probably_unique_temp_file_key = secrets.token_hex(6) - - self.additional_edge_attributes = None - self.add_edge_id = False - - self.temp_node_file_paths = [] - self.current_node_chunk = 0 - - self.temp_edge_file_paths = [] - self.current_edge_chunk = 0 - - self.temp_directory = temp_directory - self.temp_file_paths = { - NODE_ENTITY_TYPE: [], - EDGE_ENTITY_TYPE: [] - } - self.entity_buffers = { - NODE_ENTITY_TYPE: [], - EDGE_ENTITY_TYPE: [] - } - - def merge_node(self, node): - self.entity_buffers[NODE_ENTITY_TYPE].append(node) - if len(self.entity_buffers[NODE_ENTITY_TYPE]) >= self.chunk_size: - self.flush_node_buffer() - - def merge_nodes(self, nodes): - return self.merge_entities(nodes, - NODE_ENTITY_TYPE, - node_key_function) - - def merge_edge(self, edge, additional_edge_attributes=None, add_edge_id=False): - self.entity_buffers[EDGE_ENTITY_TYPE].append(edge) - if len(self.entity_buffers[EDGE_ENTITY_TYPE]) >= self.chunk_size: - self.flush_edge_buffer() - - def merge_edges(self, edges, additional_edge_attributes=None, add_edge_id=False): - logger.info(f'additional_edge_attributes: {additional_edge_attributes}, add_edge_id: {add_edge_id}') - self.additional_edge_attributes = additional_edge_attributes - self.add_edge_id = add_edge_id - sorting_function = lambda edge: edge_key_function(edge, custom_key_attributes=additional_edge_attributes) - return self.merge_entities(edges, - EDGE_ENTITY_TYPE, - sorting_function) - - def merge_entities(self, entities, entity_type, entity_sorting_function): - entity_counter = 0 - for chunk_of_entities in chunk_iterator(entities, self.chunk_size): - current_chunk_size = len(chunk_of_entities) - entity_counter += current_chunk_size - if current_chunk_size == self.chunk_size: - # this is a full chunk of the max size, go ahead and process it - self.sort_and_write_entities(chunk_of_entities, - entity_sorting_function, - entity_type) - else: - # this chunk is smaller than the max chunk size, add it to the buffer - self.entity_buffers[entity_type].extend(chunk_of_entities) - # if the buffer is full/overfull process it - if len(self.entity_buffers[entity_type]) >= self.chunk_size: - self.sort_and_write_entities(self.entity_buffers[entity_type][:self.chunk_size], - entity_sorting_function, - entity_type) - self.entity_buffers[entity_type] = self.entity_buffers[entity_type][self.chunk_size:] - return entity_counter - - def sort_and_write_entities(self, - entities, - entity_sorting_function, - entity_type): - entities.sort(key=entity_sorting_function) - temp_file_name = f'{entity_type}_{secrets.token_hex(6)}.temp' - temp_file_path = os.path.join(self.temp_directory, temp_file_name) - with jsonlines.open(temp_file_path, 'w', compact=True) as jsonl_writer: - jsonl_writer.write_all(entities) - self.temp_file_paths[entity_type].append(temp_file_path) - - def get_merged_nodes_jsonl(self): - self.flush_node_buffer() - for node in self.get_merged_entities(file_paths=self.temp_file_paths[NODE_ENTITY_TYPE], - sorting_key_function=node_key_function, - merge_function=entity_merging_function, - entity_type=NODE_ENTITY_TYPE): - yield f'{quick_json_dumps(node)}\n' - for file_path in self.temp_file_paths[NODE_ENTITY_TYPE]: - os.remove(file_path) - - def flush_node_buffer(self): - if not self.entity_buffers[NODE_ENTITY_TYPE]: - return - self.sort_and_write_entities(self.entity_buffers[NODE_ENTITY_TYPE], - node_key_function, - NODE_ENTITY_TYPE) - self.entity_buffers[NODE_ENTITY_TYPE] = [] - - def get_merged_edges_jsonl(self): - self.flush_edge_buffer() - sorting_function = lambda e: edge_key_function(e, custom_key_attributes=self.additional_edge_attributes) - for edge in self.get_merged_entities(file_paths=self.temp_file_paths[EDGE_ENTITY_TYPE], - sorting_key_function=sorting_function, - merge_function=entity_merging_function, - entity_type=EDGE_ENTITY_TYPE, - add_edge_id=self.add_edge_id): - yield f'{quick_json_dumps(edge)}\n' - for file_path in self.temp_file_paths[EDGE_ENTITY_TYPE]: - os.remove(file_path) - - def flush_edge_buffer(self): - if not self.entity_buffers[EDGE_ENTITY_TYPE]: - return - self.sort_and_write_entities(self.entity_buffers[EDGE_ENTITY_TYPE], - edge_key_function, - EDGE_ENTITY_TYPE) - self.entity_buffers[EDGE_ENTITY_TYPE] = [] - - def get_merged_entities(self, - file_paths, - sorting_key_function, - merge_function, - entity_type, - add_edge_id=False): - - if not file_paths: - logger.error('get_merged_entities called but no file_paths were provided! Empty source?') - return - - file_handlers = [open(file_path) for file_path in file_paths] - json_readers = {i: jsonlines.Reader(file_handler) for i, file_handler in enumerate(file_handlers)} - - first_lines = {i: json_reader.read() for i, json_reader in json_readers.items()} - next_entities = {i: (sorting_key_function(value), value) for i, value in first_lines.items()} - - min_key = min([key for key, entity in next_entities.values()], default=None) - while min_key is not None: - merged_entity = None - for i in list(next_entities.keys()): - next_key, next_entity = next_entities[i] - while next_key == min_key: - if merged_entity: - if entity_type == NODE_ENTITY_TYPE: - merged_entity = merge_function(merged_entity, next_entity, NODE_PROPERTIES_THAT_SHOULD_BE_SETS) - self.merged_node_counter += 1 - else: - merged_entity = merge_function(merged_entity, next_entity, EDGE_PROPERTIES_THAT_SHOULD_BE_SETS) - self.merged_edge_counter += 1 - else: - merged_entity = next_entity - try: - next_entity = json_readers[i].read() - next_key = sorting_key_function(next_entity) - next_entities[i] = next_key, next_entity - except EOFError: - next_key, next_entity = None, None - del(next_entities[i]) - json_readers[i].close() - file_handlers[i].close() - - # Add the id attribute if add_edge_id is True - if entity_type == EDGE_ENTITY_TYPE and add_edge_id and merged_entity and min_key: - merged_entity["id"] = min_key - - yield merged_entity - min_key = min([key for key, entity in next_entities.values()], default=None) - - def flush(self): - self.flush_node_buffer() - self.flush_edge_buffer() - - -class MemoryGraphMerger(GraphMerger): - - def __init__(self): - super().__init__() - self.nodes = {} - self.edges = {} - - # merge a list of nodes (dictionaries not kgxnode objects!) into the existing set - def merge_nodes(self, nodes): - node_count = 0 - for node in nodes: - node_count += 1 - self.merge_node(node) - return node_count - - def merge_node(self, node): - node_key = node['id'] - if node_key in self.nodes: - self.merged_node_counter += 1 - previous_node = self.nodes[node_key] - merged_node = entity_merging_function(previous_node, - node, - NODE_PROPERTIES_THAT_SHOULD_BE_SETS) - self.nodes[node_key] = merged_node - else: - self.nodes[node_key] = node - - # merge a list of edges (dictionaries not kgxedge objects!) into the existing list - def merge_edges(self, edges, additional_edge_attributes=None, add_edge_id=False): - edge_count = 0 - for edge in edges: - edge_count += 1 - self.merge_edge(edge, additional_edge_attributes=additional_edge_attributes, add_edge_id=add_edge_id) - return edge_count - - def merge_edge(self, edge, additional_edge_attributes=None, add_edge_id=False): - edge_key = edge_key_function(edge, custom_key_attributes=additional_edge_attributes) - if edge_key in self.edges: - self.merged_edge_counter += 1 - merged_edge = entity_merging_function(quick_json_loads(self.edges[edge_key]), - edge, - EDGE_PROPERTIES_THAT_SHOULD_BE_SETS) - if add_edge_id is True: - merged_edge[EDGE_ID] = edge_key - self.edges[edge_key] = quick_json_dumps(merged_edge) - else: - if add_edge_id is True: - edge[EDGE_ID] = edge_key - self.edges[edge_key] = quick_json_dumps(edge) - - def get_merged_nodes_jsonl(self): - for node in self.nodes.values(): - yield f'{quick_json_dumps(node)}\n' - - def get_merged_edges_jsonl(self): - for edge in self.edges.values(): - yield f'{edge}\n' diff --git a/Dockerfile b/Dockerfile index 1eba12e2..e347e4ef 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,20 +1,43 @@ -# A docker container with neo4j, java and python for Data Services -FROM neo4j:5.25.1-community-bullseye +# A docker container with neo4j, java and python for ORION -RUN apt-get update \ - && apt-get -y install python3 \ - && apt-get -y install python-is-python3 \ - && apt-get -y install python3-pip \ - && apt-get -y install git \ - && apt-get -y install vim +# Stage 1: Source Neo4j binaries from the official image +FROM neo4j:5.25.1-community-bullseye AS neo4j-source -COPY ./requirements.txt /ORION/requirements.txt +# Stage 2: Build on Python 3.12 +FROM python:3.12-bookworm -RUN pip3 install -r /ORION/requirements.txt +# Install Java 17 (required by Neo4j) and other utilities +RUN apt-get update \ + && apt-get -y install openjdk-17-jre-headless git vim \ + && rm -rf /var/lib/apt/lists/* + +# Create neo4j user/group matching the official image (UID/GID 7474) +RUN groupadd -r -g 7474 neo4j && useradd -r -u 7474 -g neo4j -m -d /var/lib/neo4j neo4j + +# Copy Neo4j installation from the official image +COPY --from=neo4j-source /var/lib/neo4j /var/lib/neo4j +COPY --from=neo4j-source /startup /startup +ENV NEO4J_HOME="/var/lib/neo4j" \ + PATH="/var/lib/neo4j/bin:${PATH}" + +# Create data/logs directories with symlinks matching the official Neo4j image layout +RUN mkdir -p /data /logs /var/lib/neo4j/run \ + && ln -sf /data /var/lib/neo4j/data \ + && ln -sf /logs /var/lib/neo4j/logs \ + && chmod -R 777 /var/lib/neo4j /data /logs + +EXPOSE 7474 7473 7687 + +# Install uv +COPY --from=ghcr.io/astral-sh/uv:0.10.9 /uv /uvx /usr/local/bin/ COPY . /ORION/. -RUN chmod -R 777 /ORION +# Install project dependencies from pyproject.toml including optional robokop deps +# uv pip doesn't read [tool.uv.sources], so install the intermine git fork explicitly +RUN uv pip install --system git+https://github.com/EvanDietzMorris/intermine-ws-python.git +RUN cd /ORION && uv pip install --system ".[robokop]" +RUN chmod -R 777 /ORION -ENV PYTHONPATH "$PYTHONPATH:/ORION" \ No newline at end of file +ENV PYTHONPATH="$PYTHONPATH:/ORION" \ No newline at end of file diff --git a/README-CONTRIBUTER.md b/README-CONTRIBUTER.md index 71c55b94..bf4eb9d9 100644 --- a/README-CONTRIBUTER.md +++ b/README-CONTRIBUTER.md @@ -3,7 +3,7 @@ ORION welcomes contributions to the code base. Implementing parsers for new data ### For Developers -To add a new data source to ORION, create a new parser. Each parser extends the SourceDataLoader interface in Common/loader_interface.py. +To add a new data source to ORION, create a new parser. Each parser extends the SourceDataLoader interface in orion/loader_interface.py. To implement the interface you will need to write a class that fulfills the following. @@ -35,8 +35,8 @@ Implement get_data(). This function should retrieve any source data files. The f Implement parse_data(). This function should parse the data files and populate lists of node and edge objects: self.final_node_list (kgxnode), self.final_edge_list (kgxedge). -Finally, add your source to the list of sources in Common/data_sources.py. The source ID string here should match the one specified in the new parser. Also add your source to the SOURCE_DATA_LOADER_CLASS_IMPORTS dictionary, mapping it to the new parser class. +Finally, add your source to the list of sources in orion/data_sources.py. The source ID string here should match the one specified in the new parser. Also add your source to the SOURCE_DATA_LOADER_CLASS_IMPORTS dictionary, mapping it to the new parser class. -Now you can use that source ID in a graph spec to include your new source in a graph, or as the source id using load_manager.py. +Now you can use that source ID in a graph spec to include your new source in a graph, or as the source id using `orion-ingest`. Always run the pytest tests after altering the codebase. \ No newline at end of file diff --git a/README.md b/README.md index d3a6d7d5..41fa5ec3 100644 --- a/README.md +++ b/README.md @@ -2,202 +2,148 @@ ### Operational Routine for the Ingest and Output of Networks -This package takes data sets from various sources and converts them into Knowledge Graphs. +ORION ingests data from knowledge sources and converts them into [Biolink Model](https://biolink.github.io/biolink-model/) knowledge graphs in [KGX](https://github.com/biolink/kgx) format. -Each data source will go through the following pipeline before it can be included in a graph: +Each data source goes through the following pipeline: -1. Fetch (retrieve an original data source) -2. Parse (convert the data source into KGX files) -3. Normalize (use normalization services to convert identifiers and ontology terms to preferred synonyms) -4. Supplement (add supplementary knowledge specific to that source) +1. **Fetch** - retrieve the original data source +2. **Parse** - transform the data into KGX files +3. **Normalize** - use normalization services to convert identifiers and ontology terms to preferred synonyms +4. **Supplement** - add supplementary knowledge specific to that source -To build a graph use a Graph Spec yaml file to specify the sources you want. Some examples live in `graph_specs` folder. +Sources are defined in a Graph Spec yaml file (see examples in the `graph_specs/` directory). ORION automatically runs each specified source through the pipeline and merges them into a Knowledge Graph. -ORION will automatically run each data source specified through the necessary pipeline. Then it will merge the specified sources into a Knowledge Graph. +### Installation -### Installing and Configuring ORION +ORION requires [uv](https://docs.astral.sh/uv/) for dependency management. -Create a parent directory: - -``` -mkdir ~/ORION_root -``` - -Clone the code repository: - -``` -cd ~/ORION_root +```bash git clone https://github.com/RobokopU24/ORION.git +cd ORION +uv sync --extra robokop ``` -Next create directories where data sources, graphs, and logs will be stored. - -**ORION_STORAGE** - for storing data sources +The core library is also available on PyPI (`pip install robokop-orion`), but the full repository is needed to utilize ingest modules from the [ROBOKOP](https://robokop.renci.org/) project. -**ORION_GRAPHS** - for storing knowledge graphs +### CLI Commands -**ORION_LOGS** - for storing logs +After installation, the following commands are available (prefix with `uv run` if not using a uv-managed shell): -You can do this manually, or use the script indicated below to set up a default workspace. +| Command | Description | +|---|-------------------------------------------------------| +| `orion-build` | Build complete knowledge graphs from a Graph Spec | +| `orion-ingest` | Run the ingest pipeline for individual data sources | +| `orion-merge` | Merge KGX node/edge files | +| `orion-meta-kg` | Generate MetaKG and test data files | +| `orion-redundant-kg` | Generate edge files with redundant biolink predicates | +| `orion-ac` | Generate AnswerCoalesce files | +| `orion-neo4j-dump` | Generate Neo4j database dumps | +| `orion-memgraph-dump` | Generate Memgraph database dumps | -Option 1: Use this script to create the directories and set the environment variables: +### Configuring ORION -``` -cd ~/ORION_root/ORION/ -source ./set_up_test_env.sh -``` +ORION uses three directories for its data, configured via environment variables: -Option 2: Create three directories and set environment variables specifying paths to the locations of those directories. +| Variable | Purpose | +|---|--------------------------------------| +| `ORION_STORAGE` | Data ingest pipeline storage | +| `ORION_GRAPHS` | Knowledge graph outputs | +| `ORION_LOGS` | Log files | -``` -mkdir ~/ORION_root/storage/ -export ORION_STORAGE=~/ORION_root/storage/ +You can set these up manually or use the provided script: -mkdir ~/ORION_root/graphs/ -export ORION_GRAPHS=~/ORION_root/graphs/ - -mkdir ~/ORION_root/logs/ -export ORION_LOGS=~/ORION_root/logs/ +```bash +source ./set_up_test_env.sh ``` -#### Specify Graph Spec file. - -Next create or select a Graph Spec yaml file, where the content of knowledge graphs to be built is specified. +#### Graph Spec -Set either of the following environment variables, but not both: +A Graph Spec yaml file defines which sources to include in a knowledge graph. Set one of the following environment variables (not both): -Option 1: ORION_GRAPH_SPEC - the name of a Graph Spec file located in the graph_specs directory of ORION - -``` +```bash +# Option 1: Name of a file in the graph_specs/ directory export ORION_GRAPH_SPEC=example-graph-spec.yaml -``` - -Option 2: ORION_GRAPH_SPEC_URL - a URL pointing to a Graph Spec yaml file -``` +# Option 2: URL pointing to a Graph Spec yaml file export ORION_GRAPH_SPEC_URL=https://stars.renci.org/var/data_services/graph_specs/default-graph-spec.yaml ``` -#### Building graph - -To build a custom graph, alter a Graph Spec file, which is composed of a list of graphs. - -For each graph, specify: +Here is a simple Graph Spec example: -**graph_id** - a unique identifier string for the graph, with no spaces - -**sources** - a list of sources identifiers for data sources to include in the graph - -See the full list of data sources and their identifiers in the [data sources file](https://github.com/RobokopU24/ORION/blob/master/Common/data_sources.py). - -Here is a simple example. - -``` +```yaml graphs: - graph_id: Example_Graph graph_name: Example Graph graph_description: A free text description of what is in the graph. output_format: neo4j sources: - - source_id: CTD + - source_id: DrugCentral - source_id: HGNC ``` -There are variety of ways to further customize a knowledge graph. The following are parameters you can set for a particular data source. Mostly, these parameters are used to indicate that you'd like to use a previously built version of a data source or a specific normalization of a source. If you specify versions that are not the latest, and haven't previously built a data source or graph with those versions, it probably won't work. - -**source_version** - the version of the data source, as determined by ORION +See the full list of data sources and their identifiers in the [data sources file](https://github.com/RobokopU24/ORION/blob/master/orion/data_sources.py). -**parsing_version** - the version of the parsing code in ORION for this source +#### Graph Spec Parameters -**merge_strategy** - used to specify alternative merge strategies +The following parameters can be set per data source: -The following are parameters you can set for the entire graph, or for an individual data source: +- **merge_strategy** - alternative merge strategies +- **strict_normalization** - whether to discard nodes that fail to normalize (true/false) +- **conflation** - whether to conflate genes with proteins and chemicals with drugs (true/false) -**node_normalization_version** - the version of the node normalizer API (see: https://nodenormalization-sri.renci.org/openapi.json) +The following can be set at the graph level: -**edge_normalization_version** - the version of biolink model used to normalize predicates and validate the KG +- **add_edge_id** - whether to add unique identifiers to edges (true/false) +- **edge_id_type** - if add_edge_id is true, the type of identifier can be specified (uuid or orion) -**strict_normalization** - True or False specifying whether to discard nodes, node types, and edges connected to those nodes when they fail to normalize +See the `graph_specs/` directory for more examples. -**conflation** - True or False flag specifying whether to conflate genes with proteins and chemicals with drugs +### Running with Docker -For example, we could customize the previous example: +Build the image: -``` -graphs: - - graph_id: Example_Graph - graph_name: Example Graph - graph_description: A free text description of what is in the graph. - output_format: neo4j - sources: - - source_id: CTD - - source_id: HGNC -``` - -See the `graph_specs` directory for more examples. - -### Running ORION - -Install Docker to create and run the necessary containers. - -Use the following command to build the necessary images. - -``` +```bash docker compose build ``` -To build every graph in your Graph Spec use the following command. This runs the command: `python /ORION/Common/build_manager.py all` on the image. +Build all graphs in the configured Graph Spec: -``` +```bash docker compose up ``` -#### Building specific graphs - -To build an individual graph use `build_manager.py` with a graph_id from the Graph Spec. The script merges data sources into complete graphs. - -Usage: `build_manager.py [-h] graph_id` -positional arguments: -`graph_id` : ID of the graph to build. Must match an ID from the configured Graph Spec. +Build a specific graph: -Example command to create a graph from a Graph Spec with graph_id: Example_Graph: - -``` -docker compose run --rm orion python /ORION/Common/build_manager.py Example_Graph +```bash +docker compose run --rm orion orion-build Example_Graph ``` -#### Run ORION Pipeline on a single data source. - -To run the ORION pipeline for a single data source and transform it into KGX files, you can use the `load_manager` script. +Run the ingest pipeline for a single data source: -``` -optional arguments: - -h, --help : show this help message and exit - -t, --test_mode : Test mode will process a small sample version of the data. - -f, --fresh_start_mode : Fresh start mode will ignore previous states and overwrite previous data. - -l, --lenient_normalization : Lenient normalization mode will allow nodes that do not normalize to persist in the finalized kgx files. +```bash +docker compose run --rm orion orion-ingest DrugCentral ``` -Example command to convert data source CTD to KGX files. +See available data sources and options: -``` -docker compose run --rm orion python /ORION/Common/load_manager.py CTD +```bash +docker compose run --rm orion orion-ingest -h ``` -To see the available arguments and a list of supported data sources: +### Development -``` -docker compose run --rm orion python /ORION/Common/load_manager.py -h -``` +Install dev dependencies with [uv](https://docs.astral.sh/uv/): -#### Testing and Troubleshooting +```bash +uv sync --extra robokop --group dev +``` -If you are experiencing issues or errors you may want to run tests: +Run tests: -``` -docker-compose run --rm orion pytest /ORION +```bash +uv run pytest tests/ ``` -#### Contributing to ORION +### Contributing -Contributions are welcome, see the [Contributer README](README-CONTRIBUTER.md). +Contributions are welcome, see the [Contributor README](README-CONTRIBUTER.md). \ No newline at end of file diff --git a/celery_worker.py b/celery_worker.py index 26f2fdf7..f21944ee 100644 --- a/celery_worker.py +++ b/celery_worker.py @@ -23,16 +23,12 @@ # so that the data ingestion task can be picked up from the task queue @celery_app.task(name="orion.data_ingestion", queue="orion") def run_build_manager(task_data): - # Ensure we're in the correct directory (ORION root) - pwd_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'Common') - os.chdir(pwd_path) - print(f'pwd_path: {pwd_path}', flush=True) print(f'task_data: {task_data}', flush=True) - # Run build_manager.py as a subprocess with the provided config + # Run orion-build as a subprocess with the provided config os.environ["ORION_GRAPH_SPEC"] = task_data["graph_spec_filename"] # no need to catch CalledProcessError exception, but rather let it propogate to Celery task handling result = subprocess.run( - ["python", "build_manager.py", task_data["graph_id"], "--graph_specs_dir", + ["orion-build", task_data["graph_id"], "--graph_specs_dir", os.getenv('SHARED_SOURCE_DATA_PATH', None)], capture_output=True, text=True, diff --git a/cli/merge_kgs.py b/cli/merge_kgs.py deleted file mode 100644 index 4c5a3270..00000000 --- a/cli/merge_kgs.py +++ /dev/null @@ -1,90 +0,0 @@ -import argparse -import sys -import os -import json -from datetime import datetime - -from Common.kgx_file_merger import KGXFileMerger -from Common.kgxmodel import GraphSpec, GraphSource - - -# given a list of kgx jsonl node files and edge files, -# create a simple GraphSpec and use KGXFileMerge to merge the files into one node file and one edge file -def merge_kgx_files(output_dir: str, nodes_files: list = None, edges_files: list = None): - if not nodes_files: - nodes_files = [] - else: - for node_file in nodes_files: - if 'node' not in node_file: - print('All node files must contain the text "node" in their file name.') - return False - - if not edges_files: - edges_files = [] - else: - for edge_file in edges_files: - if 'edge' not in edge_file: - print(f'All edge files must contain the text "edge" in their file name. This file does not: {edge_file}') - return False - - current_time = datetime.now() - timestamp = current_time.strftime("%Y/%m/%d %H:%M:%S") - # TODO it'd be nice to make this something reproducible from the inputs - version = timestamp.replace('/', '_').replace(':', '_').replace(' ', '_') - graph_source = GraphSource(id='cli_merge', - file_paths=nodes_files + edges_files) - graph_spec = GraphSpec( - graph_id='cli_merge', - graph_name='', - graph_description=f'Merged on {timestamp}', - graph_url='', - graph_version=version, - graph_output_format='jsonl', - sources=[graph_source], - subgraphs=[] - ) - file_merger = KGXFileMerger(graph_spec=graph_spec, - output_directory=output_dir, - nodes_output_filename=f'{version}_nodes.jsonl', - edges_output_filename=f'{version}_edges.jsonl') - file_merger.merge() - - merge_metadata = file_merger.get_merge_metadata() - if "merge_error" in merge_metadata: - print(f'Merge error occured: {merge_metadata["merge_error"]}') - return False - else: - metadata_output = os.path.join(output_dir, f"{version}_metadata.json") - with open(metadata_output, 'w') as metadata_file: - metadata_file.write(json.dumps(merge_metadata, indent=4)) - - -if __name__ == '__main__': - - ap = argparse.ArgumentParser(description="Given a list of node files and/or a list of edge files " - "in kgx jsonl format, merge them into one node and one edge file.") - ap.add_argument( - '-n', '--nodes', - type=str, - nargs='*', - help='List of node file paths') - - ap.add_argument( - '-e', '--edges', - type=str, - nargs='*', - help='List of edge file paths') - - ap.add_argument( - '-o', '--output_dir', - type=str, - required=True, - help='The directory where the output should be saved') - - args = vars(ap.parse_args()) - if not (args["nodes"] or args["edges"]): - print(f'To merge kgx files you must provide at least one file to merge.') - sys.exit(1) - - merge_kgx_files(args["output_dir"], args["nodes"], args["edges"]) - diff --git a/docker-compose.yml b/docker-compose.yml index dd2e12c3..c8794725 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,7 +2,7 @@ services: orion: build: context: . - command: [python, /ORION/Common/build_manager.py, all] + command: [orion-build, all] environment: - ORION_STORAGE=/ORION_storage - ORION_GRAPHS=/ORION_graphs @@ -31,5 +31,5 @@ services: - "${ORION_GRAPHS}:/ORION_graphs" - "${ORION_LOGS}:/ORION_logs" user: 7474:7474 - + diff --git a/docs/ORION.ipynb b/docs/ORION.ipynb index 1a185e87..46c44dc7 100644 --- a/docs/ORION.ipynb +++ b/docs/ORION.ipynb @@ -93,13 +93,17 @@ { "cell_type": "markdown", "id": "2yw5k2k32cw", - "source": "### 3. Build the Example_Graph\n\nRun the build_manager to build the Example_Graph:", + "source": "### 3. Build the Example_Graph\n\nRun `orion-build` to build the Example_Graph:", "metadata": {} }, { "cell_type": "code", "id": "x5o3fezhybe", - "source": "%%bash\ncd ~/ORION_root/ORION/\ndocker compose run --rm orion python /ORION/Common/build_manager.py Example_Graph", + "source": [ + "%%bash\n", + "cd ~/ORION_root/ORION/\n", + "docker compose run --rm orion orion-build Example_Graph" + ], "metadata": {}, "execution_count": null, "outputs": [] @@ -126,4 +130,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} +} \ No newline at end of file diff --git a/graph_specs/litcoin-graph-spec.yaml b/graph_specs/litcoin-graph-spec.yaml index 84c6bdb5..4aa2d536 100644 --- a/graph_specs/litcoin-graph-spec.yaml +++ b/graph_specs/litcoin-graph-spec.yaml @@ -8,7 +8,7 @@ graphs: output_format: neo4j edge_merging_attributes: - abstract_id - edge_id_addition: True + add_edge_id: True sources: - source_id: LitCoin @@ -20,6 +20,6 @@ graphs: output_format: memgraph edge_merging_attributes: - abstract_id - edge_id_addition: True + add_edge_id: True sources: - source_id: LitCoinBagelService diff --git a/helm/orion/templates/graph-builder.yaml b/helm/orion/templates/graph-builder.yaml index c8f31b3b..ef2e9a8b 100644 --- a/helm/orion/templates/graph-builder.yaml +++ b/helm/orion/templates/graph-builder.yaml @@ -112,7 +112,7 @@ spec: - name: ds-graph-builder image: {{ .Values.orion.image.repository }}:{{ .Values.orion.image.tag }} imagePullPolicy: {{ .Values.orion.image.pullPolicy }} - args: ["python", "/ORION/Common/build_manager.py", {{ .Values.orion.graphID }} ] + args: ["orion-build", {{ .Values.orion.graphID }} ] volumeMounts: - mountPath: /ORION_storage name: ds-sources-volume diff --git a/orion/__init__.py b/orion/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/Common/answercoalesce_build.py b/orion/answercoalesce_build.py similarity index 98% rename from Common/answercoalesce_build.py rename to orion/answercoalesce_build.py index 0ca13b25..a7f9b05d 100644 --- a/Common/answercoalesce_build.py +++ b/orion/answercoalesce_build.py @@ -2,8 +2,8 @@ import json import requests from collections import defaultdict -from Common.utils import quick_jsonl_file_iterator -from Common.biolink_utils import BiolinkUtils +from orion.utils import quick_jsonl_file_iterator +from orion.biolink_utils import BiolinkUtils try: from tqdm import tqdm TQDM_AVAILABLE = True diff --git a/Common/biolink_constants.py b/orion/biolink_constants.py similarity index 98% rename from Common/biolink_constants.py rename to orion/biolink_constants.py index 3da4aa3f..369a814a 100644 --- a/Common/biolink_constants.py +++ b/orion/biolink_constants.py @@ -49,6 +49,10 @@ ORIGINAL_OBJECT = 'original_object' PREDICATE = 'predicate' ORIGINAL_PREDICATE = 'original_predicate' +RETRIEVAL_SOURCES = 'sources' +RETRIEVAL_SOURCE_ID = 'resource_id' +RETRIEVAL_SOURCE_ROLE = 'resource_role' + PRIMARY_KNOWLEDGE_SOURCE = 'primary_knowledge_source' AGGREGATOR_KNOWLEDGE_SOURCES = 'aggregator_knowledge_source' SUPPORTING_DATA_SOURCE = 'supporting_data_source' diff --git a/Common/biolink_utils.py b/orion/biolink_utils.py similarity index 100% rename from Common/biolink_utils.py rename to orion/biolink_utils.py diff --git a/Common/build_manager.py b/orion/build_manager.py similarity index 89% rename from Common/build_manager.py rename to orion/build_manager.py index ed3a1b45..6dc0f01a 100644 --- a/Common/build_manager.py +++ b/orion/build_manager.py @@ -8,23 +8,23 @@ from pathlib import Path from xxhash import xxh64_hexdigest -from Common.utils import LoggingUtil, GetDataPullError -from Common.data_sources import get_available_data_sources, get_data_source_metadata_path -from Common.exceptions import DataVersionError, GraphSpecError -from Common.load_manager import SourceDataManager -from Common.kgx_file_merger import KGXFileMerger, DONT_MERGE -from Common.kgx_validation import validate_graph -from Common.neo4j_tools import create_neo4j_dump -from Common.memgraph_tools import create_memgraph_dump -from Common.kgxmodel import GraphSpec, SubGraphSource, DataSource -from Common.normalization import NORMALIZATION_CODE_VERSION, NormalizationScheme -from Common.metadata import Metadata, GraphMetadata, SourceMetadata -from Common.supplementation import SequenceVariantSupplementation -from Common.meta_kg import MetaKnowledgeGraphBuilder, META_KG_FILENAME, TEST_DATA_FILENAME, EXAMPLE_DATA_FILENAME -from Common.redundant_kg import generate_redundant_kg -from Common.answercoalesce_build import generate_ac_files -from Common.collapse_qualifiers import generate_collapsed_qualifiers_kg -from Common.kgx_metadata import KGXGraphMetadata, KGXKnowledgeSource, generate_kgx_schema_file +from orion.utils import LoggingUtil, GetDataPullError +from orion.data_sources import get_available_data_sources, get_data_source_metadata_path +from orion.exceptions import DataVersionError, GraphSpecError +from orion.ingest_pipeline import IngestPipeline +from orion.kgx_file_merger import KGXFileMerger, DONT_MERGE +from orion.kgx_validation import validate_graph +from orion.neo4j_tools import create_neo4j_dump +from orion.kgxmodel import GraphSpec, SubGraphSource, DataSource +from orion.normalization import NORMALIZATION_CODE_VERSION, NormalizationScheme +from orion.metadata import Metadata, GraphMetadata, SourceMetadata +from orion.supplementation import SequenceVariantSupplementation +from orion.meta_kg import MetaKnowledgeGraphBuilder, META_KG_FILENAME, TEST_DATA_FILENAME, EXAMPLE_DATA_FILENAME +from orion.redundant_kg import generate_redundant_kg +from orion.answercoalesce_build import generate_ac_files +from orion.collapse_qualifiers import generate_collapsed_qualifiers_kg +from orion.kgx_metadata import KGXGraphMetadata, KGXKnowledgeSource, generate_kgx_schema_file + NODES_FILENAME = 'nodes.jsonl' EDGES_FILENAME = 'edges.jsonl' @@ -37,14 +37,15 @@ class GraphBuilder: def __init__(self, - graph_specs_dir=None): + graph_specs_dir=None, + graph_output_dir=None): - self.logger = LoggingUtil.init_logging("ORION.Common.GraphBuilder", + self.logger = LoggingUtil.init_logging("ORION.orion.GraphBuilder", line_format='medium', - log_file_path=os.environ['ORION_LOGS']) + log_file_path=os.getenv('ORION_LOGS')) - self.graphs_dir = self.get_graphs_dir() # path to the graphs output directory - self.source_data_manager = SourceDataManager() # access to the data sources and their metadata + self.graphs_dir = graph_output_dir if graph_output_dir else self.get_graph_output_dir() + self.ingest_pipeline = IngestPipeline() # access to the data sources and their metadata self.graph_specs = {} # graph_id -> GraphSpec all potential graphs that could be built, including sub-graphs self.load_graph_specs(graph_specs_dir=graph_specs_dir) self.build_results = {} @@ -263,7 +264,7 @@ def determine_graph_version(self, graph_spec: GraphSpec): # go out and find the latest version for any data source that doesn't have a version specified for source in graph_spec.sources: if not source.source_version: - source.source_version = self.source_data_manager.get_latest_source_version(source.id) + source.source_version = self.ingest_pipeline.get_latest_source_version(source.id) self.logger.info(f'Using {source.id} version: {source.version}') # for sub-graphs, if a graph version isn't specified, @@ -341,19 +342,19 @@ def build_dependencies(self, graph_spec: GraphSpec): for data_source in graph_spec.sources: source_id = data_source.id - source_metadata: SourceMetadata = self.source_data_manager.get_source_metadata(source_id, - data_source.source_version) + source_metadata: SourceMetadata = self.ingest_pipeline.get_source_metadata(source_id, + data_source.source_version) release_version = data_source.generate_version() release_metadata = source_metadata.get_release_info(release_version) if release_metadata is None: self.logger.info( f'Attempting to build graph {graph_id}, ' f'dependency {source_id} is not ready. Building now...') - pipeline_sucess = self.source_data_manager.run_pipeline(source_id, - source_version=data_source.source_version, - parsing_version=data_source.parsing_version, - normalization_scheme=data_source.normalization_scheme, - supplementation_version=data_source.supplementation_version) + pipeline_sucess = self.ingest_pipeline.run_pipeline(source_id, + source_version=data_source.source_version, + parsing_version=data_source.parsing_version, + normalization_scheme=data_source.normalization_scheme, + supplementation_version=data_source.supplementation_version) if not pipeline_sucess: self.logger.info(f'While attempting to build {graph_spec.graph_id}, ' f'data source pipeline failed for dependency {source_id}...') @@ -361,11 +362,11 @@ def build_dependencies(self, graph_spec: GraphSpec): release_metadata = source_metadata.get_release_info(release_version) data_source.release_info = release_metadata - data_source.file_paths = self.source_data_manager.get_final_file_paths(source_id, - data_source.source_version, - data_source.parsing_version, - data_source.normalization_scheme.get_composite_normalization_version(), - data_source.supplementation_version) + data_source.file_paths = self.ingest_pipeline.get_final_file_paths(source_id, + data_source.source_version, + data_source.parsing_version, + data_source.normalization_scheme.get_composite_normalization_version(), + data_source.supplementation_version) return True @staticmethod @@ -511,8 +512,8 @@ def generate_kgx_metadata_files(self, f.write(kgx_graph_metadata.to_json()) def load_graph_specs(self, graph_specs_dir=None): - graph_spec_file = os.environ.get('ORION_GRAPH_SPEC', None) - graph_spec_url = os.environ.get('ORION_GRAPH_SPEC_URL', None) + graph_spec_file = os.getenv('ORION_GRAPH_SPEC') + graph_spec_url = os.getenv('ORION_GRAPH_SPEC_URL') if graph_spec_file and graph_spec_url: raise GraphSpecError(f'Configuration Error - the environment variables ORION_GRAPH_SPEC and ' @@ -569,25 +570,35 @@ def parse_graph_spec(self, graph_spec_yaml): graph_wide_edge_norm_version = graph_yaml.get('edge_normalization_version', None) graph_wide_conflation = graph_yaml.get('conflation', None) graph_wide_strict_norm = graph_yaml.get('strict_normalization', None) + add_edge_id = graph_yaml.get('add_edge_id', None) + edge_id_type = graph_yaml.get('edge_id_type', None) edge_merging_attributes = graph_yaml.get('edge_merging_attributes', None) - edge_id_addition = graph_yaml.get('edge_id_addition', None) + if graph_wide_conflation is not None and type(graph_wide_conflation) != bool: + raise GraphSpecError(f'Invalid type (conflation: {graph_wide_conflation}), must be true or false.') + if graph_wide_strict_norm is not None and type(graph_wide_strict_norm) != bool: + raise GraphSpecError(f'Invalid type (strict_normalization: {graph_wide_strict_norm}), must be true or false.') + if add_edge_id is not None and type(add_edge_id) != bool: + raise GraphSpecError(f'Invalid type (add_edge_id: {add_edge_id}), must be true or false.') + if edge_id_type is not None and edge_id_type not in ('orion', 'uuid'): + raise GraphSpecError(f'Invalid edge_id_type: {edge_id_type}, must be "orion" or "uuid".') + if edge_id_type is not None and add_edge_id is None or add_edge_id is False: + add_edge_id = True if graph_wide_node_norm_version == 'latest': - graph_wide_node_norm_version = self.source_data_manager.get_latest_node_normalization_version() + graph_wide_node_norm_version = self.ingest_pipeline.get_latest_node_normalization_version() if graph_wide_edge_norm_version == 'latest': - graph_wide_edge_norm_version = self.source_data_manager.get_latest_edge_normalization_version() + graph_wide_edge_norm_version = self.ingest_pipeline.get_latest_edge_normalization_version() # apply them to all the data sources, this will overwrite anything defined at the source level for data_source in data_sources: + if data_source.merge_strategy == DONT_MERGE and add_edge_id is not None: + raise GraphSpecError(f'Graph {graph_id}, source {data_source.name} has merge_strategy:' + f' dont_merge, which is incompatible with add_edge_id.') if graph_wide_node_norm_version is not None: data_source.normalization_scheme.node_normalization_version = graph_wide_node_norm_version if graph_wide_edge_norm_version is not None: data_source.normalization_scheme.edge_normalization_version = graph_wide_edge_norm_version if graph_wide_conflation is not None: data_source.normalization_scheme.conflation = graph_wide_conflation - if edge_merging_attributes is not None and data_source.merge_strategy != DONT_MERGE: - data_source.edge_merging_attributes = edge_merging_attributes - if edge_id_addition is not None and data_source.merge_strategy != DONT_MERGE: - data_source.edge_id_addition = edge_id_addition if graph_wide_strict_norm is not None: data_source.normalization_scheme.strict = graph_wide_strict_norm @@ -598,6 +609,9 @@ def parse_graph_spec(self, graph_spec_yaml): graph_url=graph_url, graph_version=None, # this will get populated when a build is triggered graph_output_format=graph_output_format, + add_edge_id=add_edge_id, + edge_id_type=edge_id_type, + edge_merging_attributes=edge_merging_attributes, subgraphs=subgraph_sources, sources=data_sources) self.graph_specs[graph_id] = graph_spec @@ -643,11 +657,11 @@ def parse_data_source_spec(self, source_yml): # if normalization versions are not specified, set them to the current latest # source_version is intentionally not handled here because we want to do it lazily and avoid if not needed if not parsing_version or parsing_version == 'latest': - parsing_version = self.source_data_manager.get_latest_parsing_version(source_id) + parsing_version = self.ingest_pipeline.get_latest_parsing_version(source_id) if not node_normalization_version or node_normalization_version == 'latest': - node_normalization_version = self.source_data_manager.get_latest_node_normalization_version() + node_normalization_version = self.ingest_pipeline.get_latest_node_normalization_version() if not edge_normalization_version or edge_normalization_version == 'latest': - edge_normalization_version = self.source_data_manager.get_latest_edge_normalization_version() + edge_normalization_version = self.ingest_pipeline.get_latest_edge_normalization_version() # do some validation if type(strict_normalization) != bool: @@ -700,18 +714,18 @@ def get_graph_metadata(self, graph_id: str, graph_version: str): return GraphMetadata(graph_id, graph_output_dir) @staticmethod - def get_graphs_dir(): + def get_graph_output_dir(): # confirm the directory specified by the environment variable ORION_GRAPHS is valid - graphs_dir = os.environ.get('ORION_GRAPHS', None) + graphs_dir = os.getenv('ORION_GRAPHS') if graphs_dir and Path(graphs_dir).is_dir(): - return os.environ['ORION_GRAPHS'] + return graphs_dir # if invalid or not specified back out raise IOError('ORION graphs directory not configured properly. ' 'Specify a valid directory with environment variable ORION_GRAPHS.') -if __name__ == '__main__': +def main(): parser = argparse.ArgumentParser(description="Merge data sources into complete graphs.") parser.add_argument('graph_id', help='ID of the graph to build. Must match an ID from the configured Graph Spec.') @@ -732,3 +746,7 @@ def get_graphs_dir(): print(f'Invalid graph spec requested: {graph_id_arg}') for results_graph_id, results in graph_builder.build_results.items(): print(f'{results_graph_id}\t{results["version"]}') + + +if __name__ == '__main__': + main() diff --git a/orion/cli/__init__.py b/orion/cli/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/cli/generate_ac_files.py b/orion/cli/generate_ac_files.py similarity index 88% rename from cli/generate_ac_files.py rename to orion/cli/generate_ac_files.py index b777497b..6aed619d 100644 --- a/cli/generate_ac_files.py +++ b/orion/cli/generate_ac_files.py @@ -1,7 +1,7 @@ import argparse -from Common.answercoalesce_build import generate_ac_files +from orion.answercoalesce_build import generate_ac_files -if __name__ == '__main__': +def main(): ap = argparse.ArgumentParser( description='Generate node labels, names, links, backlinks, and other AnswerCoalesce files from KGX node/edge files.' ) @@ -16,3 +16,7 @@ input_edge_file=args['edges'], output_dir=args['outdir'] ) + + +if __name__ == '__main__': + main() diff --git a/cli/generate_meta_kg.py b/orion/cli/generate_meta_kg.py similarity index 91% rename from cli/generate_meta_kg.py rename to orion/cli/generate_meta_kg.py index a3f067b3..2a87999b 100644 --- a/cli/generate_meta_kg.py +++ b/orion/cli/generate_meta_kg.py @@ -1,8 +1,8 @@ import argparse import os -from Common.meta_kg import MetaKnowledgeGraphBuilder, META_KG_FILENAME, TEST_DATA_FILENAME +from orion.meta_kg import MetaKnowledgeGraphBuilder, META_KG_FILENAME, TEST_DATA_FILENAME -if __name__ == '__main__': +def main(): ap = argparse.ArgumentParser(description='Generate MetaKG and test data files ' 'from a pair of node and edge jsonl files.') ap.add_argument('nodes_filepath') @@ -31,4 +31,8 @@ mkgb.write_test_data_to_file(test_data_output_filepath) print(f'Test data complete ({test_data_output_filepath})') else: - print(f'Test data already exists! Did not overwrite. ({test_data_output_filepath})') \ No newline at end of file + print(f'Test data already exists! Did not overwrite. ({test_data_output_filepath})') + + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/cli/generate_redundant_kg.py b/orion/cli/generate_redundant_kg.py similarity index 87% rename from cli/generate_redundant_kg.py rename to orion/cli/generate_redundant_kg.py index 2ece03af..cc9efffc 100644 --- a/cli/generate_redundant_kg.py +++ b/orion/cli/generate_redundant_kg.py @@ -1,8 +1,8 @@ import argparse -from Common.redundant_kg import generate_redundant_kg +from orion.redundant_kg import generate_redundant_kg -if __name__ == '__main__': +def main(): ap = argparse.ArgumentParser(description='Generate redundant edge files. ' 'currently expanding from predicate and qualified_predicate.') ap.add_argument('-i', '--infile', help='Input edge file path', required=True) @@ -12,3 +12,7 @@ infile = args['infile'] edges_file_path = args['outfile'] generate_redundant_kg(infile, edges_file_path) + + +if __name__ == '__main__': + main() diff --git a/cli/memgraph_dump.py b/orion/cli/memgraph_dump.py similarity index 59% rename from cli/memgraph_dump.py rename to orion/cli/memgraph_dump.py index 59e4ccbc..51ebd109 100644 --- a/cli/memgraph_dump.py +++ b/orion/cli/memgraph_dump.py @@ -1,13 +1,13 @@ import argparse import os -from Common.utils import LoggingUtil -from Common.memgraph_tools import create_memgraph_dump +from orion.utils import LoggingUtil +from orion.memgraph_tools import create_memgraph_dump -logger = LoggingUtil.init_logging("ORION.cli.memgraph_dump", - line_format='medium', - log_file_path=os.environ['ORION_LOGS']) +def main(): + logger = LoggingUtil.init_logging("ORION.cli.memgraph_dump", + line_format='medium', + log_file_path=os.environ['ORION_LOGS']) -if __name__ == '__main__': ap = argparse.ArgumentParser(description='') ap.add_argument('nodes_filepath') ap.add_argument('edges_filepath') @@ -20,3 +20,7 @@ create_memgraph_dump(n_filepath, e_filepath, output_directory, logger=logger) + +if __name__ == '__main__': + main() + diff --git a/orion/cli/merge_kgs.py b/orion/cli/merge_kgs.py new file mode 100644 index 00000000..ae26f378 --- /dev/null +++ b/orion/cli/merge_kgs.py @@ -0,0 +1,38 @@ +import argparse +import sys + +from orion.kgx_file_merger import merge_kgx_files + + +def main(): + ap = argparse.ArgumentParser(description="Given a list of node files and/or a list of edge files " + "in kgx jsonl format, merge them into one node and one edge file.") + ap.add_argument( + '-n', '--nodes', + type=str, + nargs='*', + help='List of node file paths') + + ap.add_argument( + '-e', '--edges', + type=str, + nargs='*', + help='List of edge file paths') + + ap.add_argument( + '-o', '--output_dir', + type=str, + required=True, + help='The directory where the output should be saved') + + args = vars(ap.parse_args()) + if not (args["nodes"] or args["edges"]): + print(f'To merge kgx files you must provide at least one file to merge.') + sys.exit(1) + + merge_kgx_files(args["output_dir"], args["nodes"], args["edges"]) + + +if __name__ == '__main__': + main() + diff --git a/cli/neo4j_dump.py b/orion/cli/neo4j_dump.py similarity index 65% rename from cli/neo4j_dump.py rename to orion/cli/neo4j_dump.py index 1f7b50ee..6f64084f 100644 --- a/cli/neo4j_dump.py +++ b/orion/cli/neo4j_dump.py @@ -1,13 +1,13 @@ import argparse import os -from Common.utils import LoggingUtil -from Common.neo4j_tools import create_neo4j_dump +from orion.utils import LoggingUtil +from orion.neo4j_tools import create_neo4j_dump -logger = LoggingUtil.init_logging("ORION.cli.neo4j_dump", - line_format='medium', - log_file_path=os.environ['ORION_LOGS']) +def main(): + logger = LoggingUtil.init_logging("ORION.cli.neo4j_dump", + line_format='medium', + log_file_path=os.environ['ORION_LOGS']) -if __name__ == '__main__': ap = argparse.ArgumentParser(description='') ap.add_argument('nodes_filepath') ap.add_argument('edges_filepath') @@ -23,3 +23,7 @@ output_directory=output_directory, logger=logger) + +if __name__ == '__main__': + main() + diff --git a/Common/collapse_qualifiers.py b/orion/collapse_qualifiers.py similarity index 97% rename from Common/collapse_qualifiers.py rename to orion/collapse_qualifiers.py index f3563a0a..f054e81d 100644 --- a/Common/collapse_qualifiers.py +++ b/orion/collapse_qualifiers.py @@ -4,13 +4,13 @@ except ImportError: TQDM_AVAILABLE = False -from Common.biolink_constants import PREDICATE, QUALIFIED_PREDICATE, SUBJECT_DERIVATIVE_QUALIFIER, SUBJECT_FORM_OR_VARIANT_QUALIFIER, SUBJECT_PART_QUALIFIER, \ +from orion.biolink_constants import PREDICATE, QUALIFIED_PREDICATE, SUBJECT_DERIVATIVE_QUALIFIER, SUBJECT_FORM_OR_VARIANT_QUALIFIER, SUBJECT_PART_QUALIFIER, \ SUBJECT_DIRECTION_QUALIFIER, SUBJECT_ASPECT_QUALIFIER, OBJECT_DERIVATIVE_QUALIFIER, OBJECT_FORM_OR_VARIANT_QUALIFIER, \ OBJECT_PART_QUALIFIER, OBJECT_DIRECTION_QUALIFIER, OBJECT_ASPECT_QUALIFIER, CAUSAL_MECHANISM_QUALIFIER, \ ANATOMICAL_CONTEXT_QUALIFIER, SPECIES_CONTEXT_QUALIFIER -from Common.biolink_utils import get_biolink_model_toolkit -from Common.utils import quick_jsonl_file_iterator -from Common.kgx_file_writer import KGXFileWriter +from orion.biolink_utils import get_biolink_model_toolkit +from orion.utils import quick_jsonl_file_iterator +from orion.kgx_file_writer import KGXFileWriter ### The goal of this script is to collapse the qualifiers, which are in edge properties, into a single statement, then replace the ### existing predicate label with the collapsed qualifier statement. diff --git a/Common/config.py b/orion/config.py similarity index 100% rename from Common/config.py rename to orion/config.py diff --git a/Common/containers.py b/orion/containers.py similarity index 100% rename from Common/containers.py rename to orion/containers.py diff --git a/Common/data_sources.py b/orion/data_sources.py similarity index 100% rename from Common/data_sources.py rename to orion/data_sources.py diff --git a/Common/db_connectors.py b/orion/db_connectors.py similarity index 100% rename from Common/db_connectors.py rename to orion/db_connectors.py diff --git a/Common/exceptions.py b/orion/exceptions.py similarity index 100% rename from Common/exceptions.py rename to orion/exceptions.py diff --git a/Common/extractor.py b/orion/extractor.py similarity index 98% rename from Common/extractor.py rename to orion/extractor.py index e111f9a2..4b7091fa 100644 --- a/Common/extractor.py +++ b/orion/extractor.py @@ -1,7 +1,7 @@ import csv -from Common.kgxmodel import kgxnode, kgxedge -from Common.kgx_file_writer import KGXFileWriter -from Common.biolink_constants import PRIMARY_KNOWLEDGE_SOURCE, AGGREGATOR_KNOWLEDGE_SOURCES +from orion.kgxmodel import kgxnode, kgxedge +from orion.kgx_file_writer import KGXFileWriter +from orion.biolink_constants import PRIMARY_KNOWLEDGE_SOURCE, AGGREGATOR_KNOWLEDGE_SOURCES class Extractor: """ diff --git a/Common/hgvs_utils.py b/orion/hgvs_utils.py similarity index 100% rename from Common/hgvs_utils.py rename to orion/hgvs_utils.py diff --git a/Common/load_manager.py b/orion/ingest_pipeline.py similarity index 95% rename from Common/load_manager.py rename to orion/ingest_pipeline.py index 2bc1becc..208011ac 100644 --- a/Common/load_manager.py +++ b/orion/ingest_pipeline.py @@ -5,40 +5,41 @@ import time from collections import defaultdict -from Common.data_sources import SourceDataLoaderClassFactory, RESOURCE_HOGS, get_available_data_sources -from Common.exceptions import DataVersionError -from Common.utils import LoggingUtil, GetDataPullError -from Common.kgx_file_normalizer import KGXFileNormalizer -from Common.kgx_validation import validate_graph -from Common.normalization import NormalizationScheme, NodeNormalizer, EdgeNormalizer, NormalizationFailedError -from Common.metadata import SourceMetadata -from Common.loader_interface import SourceDataBrokenError, SourceDataFailedError -from Common.supplementation import SequenceVariantSupplementation, SupplementationFailedError +from orion.data_sources import SourceDataLoaderClassFactory, RESOURCE_HOGS, get_available_data_sources +from orion.exceptions import DataVersionError +from orion.utils import LoggingUtil, GetDataPullError +from orion.kgx_file_normalizer import KGXFileNormalizer +from orion.kgx_validation import validate_graph +from orion.normalization import NormalizationScheme, NodeNormalizer, EdgeNormalizer, NormalizationFailedError +from orion.metadata import SourceMetadata +from orion.loader_interface import SourceDataBrokenError, SourceDataFailedError +from orion.supplementation import SequenceVariantSupplementation, SupplementationFailedError SOURCE_DATA_LOADER_CLASSES = SourceDataLoaderClassFactory() -logger = LoggingUtil.init_logging("ORION.Common.SourceDataManager", +logger = LoggingUtil.init_logging("ORION.orion.IngestPipeline", line_format='medium', - log_file_path=os.environ['ORION_LOGS']) + log_file_path=os.getenv('ORION_LOGS')) -class SourceDataManager: +class IngestPipeline: def __init__(self, + storage_dir: str = None, test_mode: bool = False, fresh_start_mode: bool = False): self.test_mode = test_mode if test_mode: - logger.info(f'SourceDataManager running in test mode... test data sets will be used when possible.') + logger.info(f'IngestPipeline running in test mode... test data sets will be used when possible.') self.fresh_start_mode = fresh_start_mode if fresh_start_mode: - logger.info(f'SourceDataManager running in fresh start mode... previous state and files ignored.') + logger.info(f'IngestPipeline running in fresh start mode... previous state and files ignored.') - # locate and verify the main storage directory - self.storage_dir = self.init_storage_dir() + # lazy load the storage directory path + self.storage_dir = self.init_storage_dir(storage_dir) # dict of source_id -> latest source version (to prevent double lookups) self.latest_source_version_lookup = {} @@ -688,24 +689,31 @@ def get_final_file_paths(self, source_id: str, source_version: str, parsing_vers def get_source_version_path(self, source_id: str, source_version: str): return os.path.join(self.storage_dir, source_id, source_version) - def init_storage_dir(self): - # use the storage directory specified by the environment variable ORION_STORAGE + @staticmethod + def init_storage_dir(storage_dir: str=None): + # if a dir was provided programmatically try to use that + if storage_dir is not None: + if os.path.isdir(storage_dir): + return storage_dir + else: + raise IOError(f'Storage directory not valid: {storage_dir}') + # otherwise use the storage directory specified by the environment variable ORION_STORAGE # check to make sure it's set and valid, otherwise fail - if "ORION_STORAGE" not in os.environ: - raise Exception(f'You must use the environment variable ORION_STORAGE ' - f'to specify a storage directory.') - if os.path.isdir(os.environ["ORION_STORAGE"]): - return os.environ["ORION_STORAGE"] + storage_dir_from_env = os.getenv("ORION_STORAGE") + if storage_dir_from_env is None: + raise Exception(f'No storage directory was specified. You must either provide a path programmatically or ' + f'use the environment variable ORION_STORAGE to configure a storage directory.') + if os.path.isdir(storage_dir_from_env): + return storage_dir_from_env else: - raise IOError(f'Storage directory not valid: {os.environ["ORION_STORAGE"]}') + raise IOError(f'Storage directory not valid: {storage_dir_from_env}') def init_source_output_dir(self, source_id: str): source_dir_path = os.path.join(self.storage_dir, source_id) os.makedirs(source_dir_path, exist_ok=True) -if __name__ == '__main__': - +def main(): parser = argparse.ArgumentParser(description="Transform data sources into KGX files.") parser.add_argument('data_source', nargs="+", @@ -730,7 +738,7 @@ def init_source_output_dir(self, source_id: str): loader_test_mode = args.test_mode or test_mode_from_env loader_strict_normalization = (not args.lenient_normalization) - load_manager = SourceDataManager(test_mode=loader_test_mode, + ingest_pipeline = IngestPipeline(test_mode=loader_test_mode, fresh_start_mode=args.fresh_start_mode) for data_source in args.data_source: if data_source not in get_available_data_sources(): @@ -738,6 +746,10 @@ def init_source_output_dir(self, source_id: str): f'These are the available data sources: {", ".join(get_available_data_sources())}') else: cmd_line_normalization_scheme = NormalizationScheme(strict=loader_strict_normalization) - release_vers = load_manager.run_pipeline(data_source, normalization_scheme=cmd_line_normalization_scheme) + release_vers = ingest_pipeline.run_pipeline(data_source, normalization_scheme=cmd_line_normalization_scheme) if release_vers: print(f'Finished running data pipeline for {data_source} (release version {release_vers}).') + + +if __name__ == '__main__': + main() diff --git a/Common/kgx_file_converter.py b/orion/kgx_file_converter.py similarity index 99% rename from Common/kgx_file_converter.py rename to orion/kgx_file_converter.py index 301b212b..b7b69770 100644 --- a/Common/kgx_file_converter.py +++ b/orion/kgx_file_converter.py @@ -2,8 +2,8 @@ import json import argparse from collections import defaultdict -from Common.utils import quick_jsonl_file_iterator -from Common.biolink_constants import SUBJECT_ID, OBJECT_ID, PREDICATE +from orion.utils import quick_jsonl_file_iterator +from orion.biolink_constants import SUBJECT_ID, OBJECT_ID, PREDICATE def __normalize_value(v): diff --git a/Common/kgx_file_merger.py b/orion/kgx_file_merger.py similarity index 67% rename from Common/kgx_file_merger.py rename to orion/kgx_file_merger.py index 39b49ad0..6e0b423a 100644 --- a/Common/kgx_file_merger.py +++ b/orion/kgx_file_merger.py @@ -1,20 +1,17 @@ import os import jsonlines +import json +from datetime import datetime from itertools import chain -from Common.utils import LoggingUtil, quick_jsonl_file_iterator -from Common.kgxmodel import GraphSpec, SubGraphSource -from Common.biolink_constants import SUBJECT_ID, OBJECT_ID -from Common.merging import GraphMerger, DiskGraphMerger, MemoryGraphMerger -from Common.load_manager import RESOURCE_HOGS - -# import line_profiler -# import atexit -# profile = line_profiler.LineProfiler() -# atexit.register(profile.print_stats) - -logger = LoggingUtil.init_logging("ORION.Common.KGXFileMerger", +from orion.utils import LoggingUtil, quick_jsonl_file_iterator +from orion.kgxmodel import GraphSpec, GraphSource, SubGraphSource +from orion.biolink_constants import SUBJECT_ID, OBJECT_ID +from orion.merging import GraphMerger, DiskGraphMerger, MemoryGraphMerger +from orion.ingest_pipeline import RESOURCE_HOGS + +logger = LoggingUtil.init_logging("ORION.orion.KGXFileMerger", line_format='medium', - log_file_path=os.environ['ORION_LOGS']) + log_file_path=os.getenv('ORION_LOGS')) CONNECTED_EDGE_SUBSET = 'connected_edge_subset' DONT_MERGE = 'dont_merge_edges' @@ -27,14 +24,16 @@ def __init__(self, graph_spec: GraphSpec, output_directory: str = None, nodes_output_filename: str = None, - edges_output_filename: str = None): + edges_output_filename: str = None, + save_memory: bool = False): self.graph_spec = graph_spec self.output_directory = output_directory self.nodes_output_filename = nodes_output_filename self.edges_output_filename = edges_output_filename self.merge_metadata = self.init_merge_metadata() - self.edge_graph_merger: GraphMerger = self.init_edge_graph_merger() - self.node_graph_merger = MemoryGraphMerger() + self.edge_graph_merger: GraphMerger = self.init_edge_graph_merger(save_memory=save_memory) + self.node_graph_merger = MemoryGraphMerger() if not save_memory \ + else DiskGraphMerger(temp_directory=self.output_directory) # these will be edge files that have a dont_merge merge strategy self.unmerged_edge_files = {} @@ -50,6 +49,7 @@ def merge(self): secondary_sources = [] dont_merge_sources = [] for graph_source in chain(self.graph_spec.sources, self.graph_spec.subgraphs): + self.merge_metadata["sources"][graph_source.id] = {'release_version': graph_source.version} if not graph_source.merge_strategy: primary_sources.append(graph_source) elif graph_source.merge_strategy in SECONDARY_MERGE_STRATEGIES: @@ -78,24 +78,24 @@ def merge(self): self.merge_metadata["merge_error"] = error_message return - self.merge_metadata['merged_nodes'] = self.node_graph_merger.merged_node_counter - self.merge_metadata['merged_edges'] = self.edge_graph_merger.merged_edge_counter - - # NOTE about final counts, the implementation of DiskGraphMerger makes determining final counts impossible until - # the output files are written, because that's when the merging actually happens. + # NOTE about metadata counts: + # The implementation of DiskGraphMerger makes determining final and merging counts impossible until the output + # files are written because that's when the merging actually happens. So while you could use this without + # writing files, if using DiskGraphMerger the counts won't get updated. if self.nodes_output_filename and self.edges_output_filename: merged_nodes_written, merged_edges_written = self.__write_merged_graph_to_file() unmerged_edges_written = self.__write_unmerged_edges_to_file() self.merge_metadata['unmerged_edge_count'] = unmerged_edges_written self.merge_metadata['final_node_count'] += merged_nodes_written self.merge_metadata['final_edge_count'] += merged_edges_written + unmerged_edges_written + self.merge_metadata['merged_nodes'] += self.node_graph_merger.merged_node_counter + self.merge_metadata['merged_edges'] += self.edge_graph_merger.merged_edge_counter def merge_primary_sources(self, graph_sources: list): for i, graph_source in enumerate(graph_sources, start=1): logger.info(f"Processing {graph_source.id}. (primary source {i}/{len(graph_sources)})") - self.merge_metadata["sources"][graph_source.id] = {'release_version': graph_source.version} for file_path in graph_source.get_node_file_paths(): with jsonlines.open(file_path) as nodes: @@ -105,9 +105,7 @@ def merge_primary_sources(self, for file_path in graph_source.get_edge_file_paths(): with jsonlines.open(file_path) as edges: - edges_count = self.edge_graph_merger.merge_edges( - edges, additional_edge_attributes=graph_source.edge_merging_attributes, - add_edge_id=graph_source.edge_id_addition) + edges_count = self.edge_graph_merger.merge_edges(edges) source_filename = file_path.rsplit('/')[-1] self.merge_metadata["sources"][graph_source.id][source_filename] = {"edges": edges_count} return True @@ -120,27 +118,21 @@ def merge_secondary_sources(self, if graph_source.merge_strategy == CONNECTED_EDGE_SUBSET: logger.info(f"Merging {graph_source.id} using {CONNECTED_EDGE_SUBSET} merge strategy.") - self.merge_metadata["sources"][graph_source.id] = {'release_version': graph_source.version} - # For connected_edge_subset, only merge edges that connect to nodes in primary sources. # Here we establish that list once, before any connected_edge_subset sources are merged in, so we don't # include edges from one connected_edge_subset that are only connected to another connected_edge_subset. if not primary_node_ids: - primary_node_ids = set(self.node_graph_merger.nodes.keys()) + primary_node_ids = self.node_graph_merger.get_node_ids() nodes_to_add = set() for edge_file in graph_source.get_edge_file_paths(): edge_counter = 0 - additional_edge_attributes = graph_source.edge_merging_attributes - add_edge_id = graph_source.edge_id_addition for edge in quick_jsonl_file_iterator(edge_file): edge_subject_connected = edge[SUBJECT_ID] in primary_node_ids edge_object_connected = edge[OBJECT_ID] in primary_node_ids if edge_subject_connected or edge_object_connected: edge_counter += 1 - self.edge_graph_merger.merge_edge(edge, - additional_edge_attributes=additional_edge_attributes, - add_edge_id=add_edge_id) + self.edge_graph_merger.merge_edge(edge) if not edge_subject_connected: nodes_to_add.add(edge[SUBJECT_ID]) elif not edge_object_connected: @@ -176,7 +168,7 @@ def __write_merged_graph_to_file(self): merge_error_msg = f'Merge attempted for {self.graph_spec.graph_id} but merged files already existed!' logger.error(merge_error_msg) self.merge_metadata['merge_error'] = merge_error_msg - return + return 0, 0 logger.info(f'Writing merged nodes to file...') nodes_written = 0 @@ -206,33 +198,98 @@ def __write_unmerged_edges_to_file(self): edges_out.write(edge) edges_count += 1 edges_filename = edges_file.rsplit('/')[-1] - self.merge_metadata["sources"][graph_source_id][edges_filename]["edges"] = edges_count + self.merge_metadata["sources"][graph_source_id][edges_filename] = {"edges": edges_count} all_unmerged_edges_count += edges_count return all_unmerged_edges_count - def init_edge_graph_merger(self) -> GraphMerger: - needs_on_disk_merge = False - for graph_source in chain(self.graph_spec.sources, self.graph_spec.subgraphs): - if isinstance(graph_source, SubGraphSource): - for source_id in graph_source.graph_metadata.get_source_ids(): - if source_id in RESOURCE_HOGS: - needs_on_disk_merge = True - break - elif graph_source.id in RESOURCE_HOGS: - needs_on_disk_merge = True - break + def init_edge_graph_merger(self, save_memory: bool = False) -> GraphMerger: + needs_on_disk_merge = True + if not save_memory: + needs_on_disk_merge = False + for graph_source in chain(self.graph_spec.sources, self.graph_spec.subgraphs): + if isinstance(graph_source, SubGraphSource): + for source_id in graph_source.graph_metadata.get_source_ids(): + if source_id in RESOURCE_HOGS: + needs_on_disk_merge = True + break + elif graph_source.id in RESOURCE_HOGS: + needs_on_disk_merge = True + break + if needs_on_disk_merge: if self.output_directory is None: raise IOError(f'DiskGraphMerger attempted but no output directory was specified.') - return DiskGraphMerger(temp_directory=self.output_directory) + return DiskGraphMerger(temp_directory=self.output_directory, + edge_merging_attributes=self.graph_spec.edge_merging_attributes, + add_edge_id=self.graph_spec.add_edge_id, + edge_id_type=self.graph_spec.edge_id_type) else: - return MemoryGraphMerger() + return MemoryGraphMerger(edge_merging_attributes=self.graph_spec.edge_merging_attributes, + add_edge_id=self.graph_spec.add_edge_id, + edge_id_type=self.graph_spec.edge_id_type) @staticmethod def init_merge_metadata(): return {'sources': {}, + 'merged_nodes': 0, + 'merged_edges': 0, 'final_node_count': 0, 'final_edge_count': 0} def get_merge_metadata(self): return self.merge_metadata + + +# This was moved over from the cli implementation - it's a hacky way to merge files without a graph spec +# +# given a list of kgx jsonl node files and edge files, +# create a simple GraphSpec and use KGXFileMerge to merge the files into one node file and one edge file +def merge_kgx_files(output_dir: str, + nodes_files: list = None, + edges_files: list = None, + graph_id: str = "merged_graph"): + if not nodes_files: + nodes_files = [] + else: + for node_file in nodes_files: + if 'node' not in node_file: + print('All node files must contain the text "node" in their file name.') + return False + + if not edges_files: + edges_files = [] + else: + for edge_file in edges_files: + if 'edge' not in edge_file: + print(f'All edge files must contain the text "edge" in their file name. This file does not: {edge_file}') + return False + + current_time = datetime.now() + timestamp = current_time.strftime("%Y/%m/%d %H:%M:%S") + graph_source = GraphSource(id='cli_merge', + file_paths=nodes_files + edges_files) + graph_spec = GraphSpec( + graph_id='cli_merge', + graph_name='', + graph_description=f'Merged on {timestamp}', + graph_url='', + graph_version=graph_id, + graph_output_format='jsonl', + sources=[graph_source], + subgraphs=[] + ) + file_merger = KGXFileMerger(graph_spec=graph_spec, + output_directory=output_dir, + nodes_output_filename=f'{graph_id}_nodes.jsonl', + edges_output_filename=f'{graph_id}_edges.jsonl') + file_merger.merge() + + merge_metadata = file_merger.get_merge_metadata() + if "merge_error" in merge_metadata: + logger.error(f'Merge error occured: {merge_metadata["merge_error"]}') + return False + else: + metadata_output = os.path.join(output_dir, f"{graph_id}_metadata.json") + with open(metadata_output, 'w') as metadata_file: + metadata_file.write(json.dumps(merge_metadata, indent=4)) + return True diff --git a/Common/kgx_file_normalizer.py b/orion/kgx_file_normalizer.py similarity index 86% rename from Common/kgx_file_normalizer.py rename to orion/kgx_file_normalizer.py index 3ac25521..3e7628ce 100644 --- a/Common/kgx_file_normalizer.py +++ b/orion/kgx_file_normalizer.py @@ -2,13 +2,13 @@ import json import jsonlines import logging -from Common.biolink_utils import BiolinkInformationResources, INFORES_STATUS_INVALID, INFORES_STATUS_DEPRECATED -from Common.biolink_constants import SEQUENCE_VARIANT, PRIMARY_KNOWLEDGE_SOURCE, AGGREGATOR_KNOWLEDGE_SOURCES, \ - PUBLICATIONS, OBJECT_ID, SUBJECT_ID, PREDICATE, SUBCLASS_OF, ORIGINAL_OBJECT, ORIGINAL_SUBJECT -from Common.normalization import NormalizationScheme, NodeNormalizer, EdgeNormalizer, EdgeNormalizationResult, \ +from orion.biolink_constants import (SEQUENCE_VARIANT, RETRIEVAL_SOURCES, PRIMARY_KNOWLEDGE_SOURCE, + AGGREGATOR_KNOWLEDGE_SOURCES, PUBLICATIONS, OBJECT_ID, SUBJECT_ID, PREDICATE, + SUBCLASS_OF, ORIGINAL_OBJECT, ORIGINAL_SUBJECT) +from orion.normalization import NormalizationScheme, NodeNormalizer, EdgeNormalizer, EdgeNormalizationResult, \ NormalizationFailedError -from Common.utils import LoggingUtil, chunk_iterator -from Common.kgx_file_writer import KGXFileWriter +from orion.utils import LoggingUtil, chunk_iterator +from orion.kgx_file_writer import KGXFileWriter EDGE_PROPERTIES_THAT_SHOULD_BE_SETS = {AGGREGATOR_KNOWLEDGE_SOURCES, PUBLICATIONS} @@ -22,10 +22,10 @@ # class KGXFileNormalizer: - logger = LoggingUtil.init_logging("ORION.Common.KGXFileNormalizer", + logger = LoggingUtil.init_logging("ORION.orion.KGXFileNormalizer", line_format='medium', level=logging.INFO, - log_file_path=os.environ['ORION_LOGS']) + log_file_path=os.getenv('ORION_LOGS')) def __init__(self, source_nodes_file_path: str, @@ -65,8 +65,8 @@ def __init__(self, self.process_in_memory = process_in_memory self.preserve_unconnected_nodes = preserve_unconnected_nodes self.default_provenance = default_provenance - self.normalization_metadata = {'strict_normalization': normalization_scheme.strict, - 'sequence_variants_pre_normalized': sequence_variants_pre_normalized} + self.normalization_metadata = {'strict': normalization_scheme.strict, + 'conflation': normalization_scheme.conflation} # instances of the normalization service wrappers # strict normalization flag tells normalizer to throw away any nodes that don't normalize @@ -85,6 +85,7 @@ def normalize_kgx_files(self): if not self.preserve_unconnected_nodes: unconnected_nodes_removed = remove_unconnected_nodes(self.nodes_output_file_path, self.edges_output_file_path) self.normalization_metadata['unconnected_nodes_removed'] = unconnected_nodes_removed + self.normalization_metadata['final_normalized_nodes'] -= unconnected_nodes_removed else: self.normalization_metadata['unconnected_nodes_removed'] = 0 return self.normalization_metadata @@ -96,7 +97,7 @@ def normalize_node_file(self): # get the current node normalizer version node_norm_version = self.node_normalizer.get_current_node_norm_version() - self.normalization_metadata['regular_node_norm_version'] = node_norm_version + self.normalization_metadata['node_norm_version'] = node_norm_version regular_nodes_pre_norm = 0 regular_nodes_post_norm = 0 @@ -138,7 +139,7 @@ def normalize_node_file(self): actual_error=e) regular_nodes_post_norm += len(regular_nodes) if regular_nodes: - self.logger.info(f'Normalized {regular_nodes_pre_norm} regular nodes so far...') + self.logger.info(f'Normalized {regular_nodes_pre_norm} nodes so far...') variant_nodes_pre_norm += len(variant_nodes) if self.has_sequence_variants: @@ -167,7 +168,7 @@ def normalize_node_file(self): self.logger.info(f'Normalized {variant_nodes_pre_norm} variant nodes so far...') if regular_nodes: - self.logger.debug(f'Writing regular nodes to file...') + self.logger.debug(f'Writing nodes to file...') output_file_writer.write_normalized_nodes(regular_nodes) if variant_nodes: self.logger.debug(f'Writing sequence variant nodes to file...') @@ -176,7 +177,7 @@ def normalize_node_file(self): # grab the number of repeat writes from the file writer # assuming the input file contained all unique node IDs, # this is the number of nodes that started with different IDs but normalized to the same ID as another node - merged_node_count = output_file_writer.repeat_node_count + discarded_duplicate_node_count = output_file_writer.repeat_node_count except IOError as e: norm_error_msg = f'Error reading nodes file {self.source_nodes_file_path}' raise NormalizationFailedError(error_message=norm_error_msg, actual_error=e) @@ -203,21 +204,20 @@ def normalize_node_file(self): # update the metadata self.normalization_metadata.update({ - 'regular_nodes_pre_norm': regular_nodes_pre_norm, - 'regular_node_norm_failures': len(regular_node_norm_failures), - 'regular_nodes_post_norm': regular_nodes_post_norm, + 'node_count_pre_normalization': regular_nodes_pre_norm, + 'node_count_post_normalization': regular_nodes_post_norm, + 'node_normalization_failures': len(regular_node_norm_failures), }) if self.has_sequence_variants: self.normalization_metadata.update({ 'variant_nodes_pre_norm': variant_nodes_pre_norm, 'variant_node_norm_failures': len(variant_node_norm_failures), 'variant_nodes_split_count': variant_nodes_split_count, - 'variant_nodes_post_norm': variant_nodes_post_norm + 'variant_nodes_post_norm': variant_nodes_post_norm, }) self.normalization_metadata.update({ - 'all_nodes_post_norm': regular_nodes_post_norm + variant_nodes_post_norm, - 'merged_nodes_post_norm': merged_node_count, - 'final_normalized_nodes': regular_nodes_post_norm + variant_nodes_post_norm - merged_node_count + 'discarded_duplicate_node_count': discarded_duplicate_node_count, + 'final_normalized_nodes': regular_nodes_post_norm + variant_nodes_post_norm - discarded_duplicate_node_count }) # given file paths to the source data edge file and an output file, @@ -229,13 +229,11 @@ def normalize_edge_file(self): normalized_edge_count = 0 edge_splits = 0 edges_failed_due_to_nodes = 0 - edges_failed_due_to_predicates = 0 subclass_loops_removed = 0 node_norm_lookup = self.node_normalizer.node_normalization_lookup edge_norm_lookup = self.edge_normalizer.edge_normalization_lookup edge_norm_failures = set() - knowledge_sources = set() try: with jsonlines.open(self.source_edges_file_path) as source_json_reader, \ @@ -290,14 +288,8 @@ def normalize_edge_file(self): edge_count = 0 # ensure edge has a primary knowledge source - if PRIMARY_KNOWLEDGE_SOURCE not in edge: + if RETRIEVAL_SOURCES not in edge and PRIMARY_KNOWLEDGE_SOURCE not in edge: edge[PRIMARY_KNOWLEDGE_SOURCE] = self.default_provenance - knowledge_sources.add(self.default_provenance) - else: - knowledge_sources.add(edge[PRIMARY_KNOWLEDGE_SOURCE]) - if AGGREGATOR_KNOWLEDGE_SOURCES in edge: - for knowledge_source in edge[AGGREGATOR_KNOWLEDGE_SOURCES]: - knowledge_sources.add(knowledge_source) for norm_subject_id in normalized_subject_ids: for norm_object_id in normalized_object_ids: @@ -345,16 +337,6 @@ def normalize_edge_file(self): norm_error_msg = f'Error normalizing edges file {self.source_edges_file_path}' raise NormalizationFailedError(error_message=norm_error_msg, actual_error=e) - bl_inforesources = BiolinkInformationResources() - deprecated_infores_ids = [] - invalid_infores_ids = [] - for knowledge_source in knowledge_sources: - infores_status = bl_inforesources.get_infores_status(knowledge_source) - if infores_status == INFORES_STATUS_DEPRECATED: - deprecated_infores_ids.append(knowledge_source) - elif infores_status == INFORES_STATUS_INVALID: - invalid_infores_ids.append(knowledge_source) - try: self.logger.debug(f'Writing predicate map to file...') edge_norm_json = {} @@ -369,23 +351,15 @@ def normalize_edge_file(self): raise NormalizationFailedError(error_message=norm_error_msg, actual_error=e) self.normalization_metadata.update({ - 'edge_norm_version': self.edge_normalizer.edge_norm_version, + 'biolink_version': self.edge_normalizer.edge_norm_version, 'source_edges': number_of_source_edges, 'edges_failed_due_to_nodes': edges_failed_due_to_nodes, - 'edges_failed_due_to_predicates': edges_failed_due_to_predicates, # these keep track of how many edges merged into another, or split into multiple edges # this should be true: source_edges - failures - mergers + splits = edges post norm 'edge_splits': edge_splits, 'subclass_loops_removed': subclass_loops_removed, 'final_normalized_edges': normalized_edge_count }) - if deprecated_infores_ids: - self.normalization_metadata['deprecated_infores_ids'] = deprecated_infores_ids - self.logger.warning(f'Normalization found deprecated infores identifiers: {deprecated_infores_ids}') - if invalid_infores_ids: - self.normalization_metadata['invalid_infores_ids'] = invalid_infores_ids - self.logger.warning(f'Normalization found invalid infores identifiers: {invalid_infores_ids}') - def invert_edge(edge): inverted_edge = {} diff --git a/Common/kgx_file_writer.py b/orion/kgx_file_writer.py similarity index 94% rename from Common/kgx_file_writer.py rename to orion/kgx_file_writer.py index 54bf9e45..01f268ca 100644 --- a/Common/kgx_file_writer.py +++ b/orion/kgx_file_writer.py @@ -2,18 +2,18 @@ import jsonlines import logging -from Common.utils import LoggingUtil -from Common.kgxmodel import kgxnode, kgxedge -from Common.biolink_constants import PRIMARY_KNOWLEDGE_SOURCE, AGGREGATOR_KNOWLEDGE_SOURCES, \ +from orion.utils import LoggingUtil +from orion.kgxmodel import kgxnode, kgxedge +from orion.biolink_constants import PRIMARY_KNOWLEDGE_SOURCE, AGGREGATOR_KNOWLEDGE_SOURCES, \ SUBJECT_ID, OBJECT_ID, PREDICATE class KGXFileWriter: - logger = LoggingUtil.init_logging("ORION.Common.KGXFileWriter", + logger = LoggingUtil.init_logging("ORION.orion.KGXFileWriter", line_format='medium', level=logging.INFO, - log_file_path=os.environ.get('ORION_LOGS')) + log_file_path=os.getenv('ORION_LOGS')) """ constructor :param nodes_output_file_path: the file path for the nodes file diff --git a/Common/kgx_metadata.py b/orion/kgx_metadata.py similarity index 98% rename from Common/kgx_metadata.py rename to orion/kgx_metadata.py index dd6238b3..db5c3951 100644 --- a/Common/kgx_metadata.py +++ b/orion/kgx_metadata.py @@ -3,9 +3,9 @@ from collections import defaultdict from dataclasses import dataclass, field -from Common.utils import quick_jsonl_file_iterator -from Common.biolink_utils import BiolinkUtils -from Common.biolink_constants import * +from orion.utils import quick_jsonl_file_iterator +from orion.biolink_utils import BiolinkUtils +from orion.biolink_constants import * @dataclass @@ -57,12 +57,14 @@ def to_dict(self): output_dict["isBasedOn"] = [ks.to_dict() for ks in self.isBasedOn] return output_dict + @dataclass class KGXNodeType: categories: list = None id_prefixes: defaultdict = field(default_factory=lambda: defaultdict(int)) attributes: defaultdict = field(default_factory=lambda: defaultdict(int)) + @dataclass class KGXEdgeType: subject_categories: list = None diff --git a/Common/kgx_validation.py b/orion/kgx_validation.py similarity index 98% rename from Common/kgx_validation.py rename to orion/kgx_validation.py index 3aeb4262..27677870 100644 --- a/Common/kgx_validation.py +++ b/orion/kgx_validation.py @@ -1,9 +1,9 @@ from collections import defaultdict -from Common.utils import quick_jsonl_file_iterator -from Common.biolink_utils import BiolinkUtils, BiolinkInformationResources, \ +from orion.utils import quick_jsonl_file_iterator +from orion.biolink_utils import BiolinkUtils, BiolinkInformationResources, \ INFORES_STATUS_INVALID, INFORES_STATUS_DEPRECATED -from Common.biolink_constants import * +from orion.biolink_constants import * # this just sorts a dicts keys by its values diff --git a/Common/kgxmodel.py b/orion/kgxmodel.py similarity index 92% rename from Common/kgxmodel.py rename to orion/kgxmodel.py index b8dc7c79..ad5cbac0 100644 --- a/Common/kgxmodel.py +++ b/orion/kgxmodel.py @@ -1,7 +1,7 @@ from dataclasses import dataclass -from Common.biolink_constants import NAMED_THING -from Common.metadata import GraphMetadata, get_source_release_version -from Common.normalization import NormalizationScheme +from orion.biolink_constants import NAMED_THING +from orion.metadata import GraphMetadata, get_source_release_version +from orion.normalization import NormalizationScheme class kgxnode: @@ -43,6 +43,9 @@ class GraphSpec: graph_url: str graph_version: str graph_output_format: str + add_edge_id: bool = None + edge_id_type: str = None + edge_merging_attributes: list = None sources: list = None subgraphs: list = None @@ -53,6 +56,9 @@ def get_metadata_representation(self): 'graph_description': self.graph_description, 'graph_url': self.graph_url, 'graph_version': self.graph_version, + 'edge_merging_attributes': self.edge_merging_attributes, + 'add_edge_id': self.add_edge_id, + 'edge_id_type': self.edge_id_type, 'subgraphs': [subgraph.get_metadata_representation() for subgraph in self.subgraphs] if self.subgraphs else [], 'sources': [source.get_metadata_representation() for source in self.sources] if self.sources else [] } @@ -62,8 +68,6 @@ def get_metadata_representation(self): class GraphSource: id: str merge_strategy: str = None - edge_merging_attributes: list = None - edge_id_addition: bool = False normalization_scheme: NormalizationScheme = None file_paths: list = None @@ -119,8 +123,7 @@ def get_metadata_representation(self): 'normalization_scheme': self.normalization_scheme.get_metadata_representation(), 'release_version': self.generate_version(), 'merge_strategy': self.merge_strategy, - 'edge_merging_attributes': self.edge_merging_attributes, - 'edge_id_addition': self.edge_id_addition} + } if self.release_info: metadata.update(self.release_info) return metadata diff --git a/Common/loader_interface.py b/orion/loader_interface.py similarity index 98% rename from Common/loader_interface.py rename to orion/loader_interface.py index 5ca94b6f..3c771618 100644 --- a/Common/loader_interface.py +++ b/orion/loader_interface.py @@ -2,8 +2,8 @@ import os import json import inspect -from Common.kgx_file_writer import KGXFileWriter -from Common.utils import LoggingUtil +from orion.kgx_file_writer import KGXFileWriter +from orion.utils import LoggingUtil class SourceDataLoader: @@ -49,7 +49,7 @@ def __init__(self, test_mode: bool = False, source_data_dir: str = None): self.logger = LoggingUtil.init_logging(f"ORION.parsers.{self.get_name()}", level=logging.INFO, line_format='medium', - log_file_path=os.environ.get('ORION_LOGS')) + log_file_path=os.getenv('ORION_LOGS')) def get_latest_source_version(self): """Determine and return the latest source version ie. a unique identifier associated with the latest version.""" diff --git a/Common/memgraph_tools.py b/orion/memgraph_tools.py similarity index 96% rename from Common/memgraph_tools.py rename to orion/memgraph_tools.py index aa948f80..4580c696 100644 --- a/Common/memgraph_tools.py +++ b/orion/memgraph_tools.py @@ -1,5 +1,5 @@ import os -import Common.kgx_file_converter as kgx_file_converter +import orion.kgx_file_converter as kgx_file_converter def create_memgraph_dump(nodes_filepath: str, diff --git a/orion/merging.py b/orion/merging.py new file mode 100644 index 00000000..25bded66 --- /dev/null +++ b/orion/merging.py @@ -0,0 +1,398 @@ +import heapq +import os +import secrets +import uuid_utils as uuid +from xxhash import xxh64_hexdigest +from orion.biolink_utils import BiolinkUtils +from orion.biolink_constants import * +from orion.utils import quick_json_loads, quick_json_dumps, LoggingUtil + +ORION_UUID_NAMESPACE = uuid.UUID('e2a5b21f-4e4d-4a6e-b64a-1f3c78e2a9d0') + +NODE_ENTITY_TYPE = 'node' +EDGE_ENTITY_TYPE = 'edge' + +# TODO ideally we'd make the biolink model version configurable here +bmt = BiolinkUtils() + +logger = LoggingUtil.init_logging("ORION.orion.merging", + line_format='medium', + log_file_path=os.getenv('ORION_LOGS')) + +# Key functions for identifying duplicates during entity merging. +# Add entries to CUSTOM_KEY_FUNCTIONS to define custom matching logic for specific properties. + +# Default key function: dictionaries are duplicates if they have identical JSON representation +def default_dict_merge_key(entity): + return quick_json_dumps(entity) + +# Retrieval sources are duplicates if they have the same resource id and resource role +def retrieval_sources_key(retrieval_source): + return retrieval_source[RETRIEVAL_SOURCE_ID] + retrieval_source[RETRIEVAL_SOURCE_ROLE] + +# Map property names to their custom key functions +CUSTOM_KEY_FUNCTIONS = { + RETRIEVAL_SOURCES: retrieval_sources_key +} + +def node_key_function(node): + return node['id'] + + +def edge_key_function(edge, custom_key_attributes=None, edge_id_type=None): + qualifiers = sorted([f'{key}{value}' for key, value in edge.items() if bmt.is_qualifier(key)]) + primary_knowledge_source = edge.get(PRIMARY_KNOWLEDGE_SOURCE, "") + if not primary_knowledge_source: + for retrieval_source in edge.get(RETRIEVAL_SOURCES, []): + if retrieval_source[RETRIEVAL_SOURCE_ROLE] == PRIMARY_KNOWLEDGE_SOURCE: + primary_knowledge_source = retrieval_source[RETRIEVAL_SOURCE_ID] + break + standard_attributes = (f'{edge[SUBJECT_ID]}{edge[PREDICATE]}{edge[OBJECT_ID]}' + f'{primary_knowledge_source}{"".join(qualifiers)}') + if custom_key_attributes: + custom_attributes = [] + for attr in custom_key_attributes: + value = edge.get(attr, "") + if isinstance(value, dict): + raise ValueError(f'Edge merging attribute "{attr}" has a dictionary value. ' + f'Dictionaries are not currently supported as edge key attributes.') + if isinstance(value, list): + value = str(sorted(str(v) for v in value)) + else: + value = str(value) + custom_attributes.append(value) + key_input = f'{standard_attributes}{"".join(custom_attributes)}' + else: + key_input = standard_attributes + + if edge_id_type == 'uuid': + return str(uuid.uuid5(ORION_UUID_NAMESPACE, key_input)) + else: + return xxh64_hexdigest(key_input) + + +def entity_merging_function(entity_1, entity_2): + # for every property of entity 2 + for key, entity_2_value in entity_2.items(): + # if entity 1 also has the property and entity_2_value is not null/empty: + if (key in entity_1) and (entity_2_value is not None): + entity_1_value = entity_1[key] + + # check if one or both of them are lists so we can combine them + entity_1_is_list = isinstance(entity_1_value, list) + entity_2_is_list = isinstance(entity_2_value, list) + if entity_1_is_list and entity_2_is_list: + # if they're both lists just concat them + entity_1_value.extend(entity_2_value) + elif entity_1_is_list: + # if 1 is a list and 2 isn't, append the value of 2 to the list from 1 + entity_1_value.append(entity_2_value) + elif entity_2_is_list: + if entity_1_value is not None: + # if 2 is a list and 1 has a value, add the value of 1 to the list from 2 + entity_1[key] = [entity_1_value] + entity_2_value + else: + # if 2 is a list and 1 doesn't have a value, just use the list from 2 + entity_1[key] = entity_2_value + else: + # if neither is a list + if entity_1_value is None: + # if entity_1's value is None, use entity_2's value + entity_1[key] = entity_2_value + # else: keep the value from entity_1 + + # if either was a list remove duplicate values + if entity_1_is_list or entity_2_is_list: + # if the post-merge list is empty no need to deduplicate + if not entity_1[key]: + continue + # if the list is of dictionaries + if isinstance(entity_1[key][0], dict): + # Use a custom key function to determine equivalence if there is one + key_function = CUSTOM_KEY_FUNCTIONS.get(key, default_dict_merge_key) + # Merge dictionaries with matching keys + grouped = {} + for item in entity_1[key]: + item_key = key_function(item) + if item_key in grouped: + # Recursively merge equivalent-by-key dictionaries + grouped[item_key] = entity_merging_function(grouped[item_key], item) + else: + grouped[item_key] = item + entity_1[key] = list(grouped.values()) + else: + entity_1[key] = sorted(set(entity_1[key])) + else: + # if entity 1 doesn't have the property, add the property from entity 2 + entity_1[key] = entity_2_value + return entity_1 + + +class GraphMerger: + + def __init__(self, edge_merging_attributes=None, add_edge_id=False, edge_id_type=None): + self.merged_node_counter = 0 + self.merged_edge_counter = 0 + self.edge_merging_attributes = edge_merging_attributes + self.add_edge_id = add_edge_id + self.edge_id_type = edge_id_type + + def merge_nodes(self, nodes_iterable): + raise NotImplementedError + + def merge_edges(self, edges_iterable): + raise NotImplementedError + + def merge_node(self, node): + raise NotImplementedError + + def merge_edge(self, edge): + raise NotImplementedError + + def flush(self): + pass + + def get_node_ids(self): + raise NotImplementedError + + def get_merged_nodes_jsonl(self): + raise NotImplementedError + + def get_merged_edges_jsonl(self): + raise NotImplementedError + + +class DiskGraphMerger(GraphMerger): + + def __init__(self, temp_directory: str = None, chunk_size: int = 10_000_000, + edge_merging_attributes=None, add_edge_id=False, edge_id_type=None): + + super().__init__(edge_merging_attributes=edge_merging_attributes, + add_edge_id=add_edge_id, + edge_id_type=edge_id_type) + + self.chunk_size = chunk_size + self.temp_directory = temp_directory + self.node_ids = set() + self.temp_file_paths = { + NODE_ENTITY_TYPE: [], + EDGE_ENTITY_TYPE: [] + } + self.entity_buffers = { + NODE_ENTITY_TYPE: [], + EDGE_ENTITY_TYPE: [] + } + + def merge_node(self, node): + self.node_ids.add(node['id']) + key = node_key_function(node) + self.entity_buffers[NODE_ENTITY_TYPE].append((key, quick_json_dumps(node))) + if len(self.entity_buffers[NODE_ENTITY_TYPE]) >= self.chunk_size: + self.flush_node_buffer() + + def merge_nodes(self, nodes): + node_count = 0 + for node in nodes: + node_count += 1 + self.merge_node(node) + return node_count + + def merge_edge(self, edge): + key = edge_key_function(edge, custom_key_attributes=self.edge_merging_attributes, + edge_id_type=self.edge_id_type) + if self.add_edge_id: + edge[EDGE_ID] = key + self.entity_buffers[EDGE_ENTITY_TYPE].append((key, quick_json_dumps(edge))) + if len(self.entity_buffers[EDGE_ENTITY_TYPE]) >= self.chunk_size: + self.flush_edge_buffer() + + def merge_edges(self, edges): + edge_count = 0 + for edge in edges: + edge_count += 1 + self.merge_edge(edge) + return edge_count + + def sort_and_write_keyed_entities(self, keyed_entities, entity_type): + keyed_entities.sort(key=lambda x: x[0]) + temp_file_name = f'{entity_type}_{secrets.token_hex(6)}.temp' + temp_file_path = os.path.join(self.temp_directory, temp_file_name) + with open(temp_file_path, 'w') as temp_file: + for key, entity_json in keyed_entities: + temp_file.write(f'{key}{entity_json}\n') + self.temp_file_paths[entity_type].append(temp_file_path) + + def get_node_ids(self): + return self.node_ids + + def get_merged_nodes_jsonl(self): + self.flush_node_buffer() + for json_line in self.get_merged_entities(file_paths=self.temp_file_paths[NODE_ENTITY_TYPE], + merge_function=entity_merging_function, + entity_type=NODE_ENTITY_TYPE): + yield json_line + for file_path in self.temp_file_paths[NODE_ENTITY_TYPE]: + os.remove(file_path) + + def flush_node_buffer(self): + if not self.entity_buffers[NODE_ENTITY_TYPE]: + return + self.sort_and_write_keyed_entities(self.entity_buffers[NODE_ENTITY_TYPE], + NODE_ENTITY_TYPE) + self.entity_buffers[NODE_ENTITY_TYPE] = [] + + def get_merged_edges_jsonl(self): + self.flush_edge_buffer() + for json_line in self.get_merged_entities(file_paths=self.temp_file_paths[EDGE_ENTITY_TYPE], + merge_function=entity_merging_function, + entity_type=EDGE_ENTITY_TYPE): + yield json_line + for file_path in self.temp_file_paths[EDGE_ENTITY_TYPE]: + os.remove(file_path) + + def flush_edge_buffer(self): + if not self.entity_buffers[EDGE_ENTITY_TYPE]: + return + self.sort_and_write_keyed_entities(self.entity_buffers[EDGE_ENTITY_TYPE], + EDGE_ENTITY_TYPE) + self.entity_buffers[EDGE_ENTITY_TYPE] = [] + + @staticmethod + def parse_keyed_line(line): + # Split a keyed line into (key, raw_json). The key ends at the first '{'. + json_start = line.index('{') + return line[:json_start], line[json_start:].rstrip('\n') + + def get_merged_entities(self, + file_paths, + merge_function, + entity_type): + + # open all the files, which are chunk_size sized files of sorted and keyed entities + if not file_paths: + logger.error('get_merged_entities called but no file_paths were provided! Empty source?') + return + file_handlers = [open(file_path) for file_path in file_paths] + + # store a string that can be used to reference the counter for the appropriate entity type + merge_counter = 'merged_node_counter' if entity_type == NODE_ENTITY_TYPE else 'merged_edge_counter' + + # Here we use a min-heap to organize iterating through the entity files to compare their keys and merge entities + # with matching keys. Members of the heap are tuples representing each line from a file: + # (key, file_index, raw_json) where key is the previously calculated merging key for an entity, and raw_json + # is the raw json string for an entity. + + # First start the heap with the first line from each file. + heap = [] + for i, fh in enumerate(file_handlers): + line = fh.readline() + if line: + key, raw_json = self.parse_keyed_line(line) + heap.append((key, i, raw_json)) + else: + fh.close() + heapq.heapify(heap) + + # Then use the heap to iterate through all the files and merge matching entities + while heap: + # If we're here it means it's the first time encountering this key + min_key = heap[0][0] + merged_entity = None + merged_json = None + # Pop all entries with the current minimum key and merge them together + while heap and heap[0][0] == min_key: + key, i, raw_json = heapq.heappop(heap) + # If there's a merged entity it means we already merged entities with this key, use the same object + if merged_entity is not None: + merged_entity = merge_function(merged_entity, quick_json_loads(raw_json)) + setattr(self, merge_counter, getattr(self, merge_counter) + 1) + # Otherwise if there is merged_json it means we encountered a matching entity but didn't merge yet + elif merged_json is not None: + merged_entity = merge_function(quick_json_loads(merged_json), quick_json_loads(raw_json)) + setattr(self, merge_counter, getattr(self, merge_counter) + 1) + merged_json = None + # Otherwise this is the first time seeing this key + else: + merged_json = raw_json + + # read the next line from this file + line = file_handlers[i].readline() + if line: + next_key, next_raw_json = self.parse_keyed_line(line) + heapq.heappush(heap, (next_key, i, next_raw_json)) + else: + file_handlers[i].close() + + # if we did a merge we need to convert back to a json string for writing + if merged_entity is not None: + yield f'{quick_json_dumps(merged_entity)}\n' + # otherwise we can just write the raw json to file + else: + yield f'{merged_json}\n' + + def flush(self): + self.flush_node_buffer() + self.flush_edge_buffer() + + +class MemoryGraphMerger(GraphMerger): + + def __init__(self, edge_merging_attributes=None, add_edge_id=False, edge_id_type=None): + super().__init__(edge_merging_attributes=edge_merging_attributes, + add_edge_id=add_edge_id, + edge_id_type=edge_id_type) + self.nodes = {} + self.edges = {} + + # merge a list of nodes (dictionaries not kgxnode objects!) into the existing set + def merge_nodes(self, nodes): + node_count = 0 + for node in nodes: + node_count += 1 + self.merge_node(node) + return node_count + + def merge_node(self, node): + node_key = node['id'] + if node_key in self.nodes: + self.merged_node_counter += 1 + previous_node = self.nodes[node_key] + merged_node = entity_merging_function(previous_node, + node) + self.nodes[node_key] = merged_node + else: + self.nodes[node_key] = node + + # merge a list of edges (dictionaries not kgxedge objects!) into the existing list + def merge_edges(self, edges): + edge_count = 0 + for edge in edges: + edge_count += 1 + self.merge_edge(edge) + return edge_count + + def merge_edge(self, edge): + edge_key = edge_key_function(edge, custom_key_attributes=self.edge_merging_attributes, + edge_id_type=self.edge_id_type) + if edge_key in self.edges: + self.merged_edge_counter += 1 + merged_edge = entity_merging_function(quick_json_loads(self.edges[edge_key]), + edge) + if self.add_edge_id: + merged_edge[EDGE_ID] = edge_key + self.edges[edge_key] = quick_json_dumps(merged_edge) + else: + if self.add_edge_id: + edge[EDGE_ID] = edge_key + self.edges[edge_key] = quick_json_dumps(edge) + + def get_node_ids(self): + return set(self.nodes.keys()) + + def get_merged_nodes_jsonl(self): + for node in self.nodes.values(): + yield f'{quick_json_dumps(node)}\n' + + def get_merged_edges_jsonl(self): + for edge in self.edges.values(): + yield f'{edge}\n' diff --git a/Common/meta_kg.py b/orion/meta_kg.py similarity index 90% rename from Common/meta_kg.py rename to orion/meta_kg.py index 2886637d..77ccc223 100644 --- a/Common/meta_kg.py +++ b/orion/meta_kg.py @@ -2,9 +2,9 @@ import jsonlines from collections import defaultdict -from Common.biolink_constants import NODE_TYPES, SUBJECT_ID, OBJECT_ID, PREDICATE, PRIMARY_KNOWLEDGE_SOURCE, AGGREGATOR_KNOWLEDGE_SOURCES -from Common.utils import quick_jsonl_file_iterator -from Common.biolink_utils import BiolinkUtils +from orion.biolink_constants import NODE_TYPES, SUBJECT_ID, OBJECT_ID, PREDICATE, PRIMARY_KNOWLEDGE_SOURCE, AGGREGATOR_KNOWLEDGE_SOURCES +from orion.utils import quick_jsonl_file_iterator +from orion.biolink_utils import BiolinkUtils BL_ATTRIBUTE_MAP = { "equivalent_identifiers": "biolink:same_as", @@ -96,7 +96,7 @@ def analyze_edges(self, edges_file_path: str): edge_attribute_to_metadata = {} edge_type_key_to_attributes = defaultdict(set) edge_type_key_to_qualifiers = defaultdict(lambda: defaultdict(set)) - edge_type_key_to_example = {} + edge_type_key_to_test_data = {} edge_types = defaultdict(lambda: defaultdict(set)) # subject_id to object_id to set of predicates for edge in quick_jsonl_file_iterator(edges_file_path): @@ -125,6 +125,7 @@ def analyze_edges(self, edges_file_path: str): edge_attribute_to_metadata[key] = self.get_meta_attribute(key) predicate = edge[PREDICATE] + use_as_edge_example = False for subject_type in subject_types: for object_type in object_types: edge_types[subject_type][object_type].add(predicate) @@ -136,7 +137,10 @@ def analyze_edges(self, edges_file_path: str): edge_type_key_to_attributes[edge_type_key].update(edge_attributes) for qual, qual_val in edge_qualifiers.items(): try: - edge_type_key_to_qualifiers[edge_type_key][qual].add(qual_val) + if isinstance(qual_val, list): + edge_type_key_to_qualifiers[edge_type_key][qual].update(qual_val) + else: + edge_type_key_to_qualifiers[edge_type_key][qual].add(qual_val) except TypeError as e: error_message = f'Type of value for qualifier not expected: {qual}: {qual_val}, '\ f'ignoring for meta kg. Error: {e}' @@ -145,7 +149,8 @@ def analyze_edges(self, edges_file_path: str): else: print(error_message) - if edge_type_key not in edge_type_key_to_example: + if edge_type_key not in edge_type_key_to_test_data: + use_as_edge_example = True example_edge = { "subject_category": subject_type, "object_category": object_type, @@ -159,8 +164,9 @@ def analyze_edges(self, edges_file_path: str): "qualifier_value": qualifier_value} for qualifier, qualifier_value in edge_qualifiers.items() ] - edge_type_key_to_example[edge_type_key] = example_edge - self.example_edges.append(edge) + edge_type_key_to_test_data[edge_type_key] = example_edge + if use_as_edge_example: + self.example_edges.append(edge) for subject_node_type, object_types_to_predicates in edge_types.items(): for object_node_type, predicates in object_types_to_predicates.items(): @@ -177,8 +183,8 @@ def analyze_edges(self, edges_file_path: str): for qualifier, qual_vals in edge_type_key_to_qualifiers[edge_type_key].items()] } self.meta_kg['edges'].append(edge_metadata) - if edge_type_key in edge_type_key_to_example: - self.testing_data['edges'].append(edge_type_key_to_example[edge_type_key]) + if edge_type_key in edge_type_key_to_test_data: + self.testing_data['edges'].append(edge_type_key_to_test_data[edge_type_key]) def get_meta_attribute(self, attribute_name): original_attribute_name = attribute_name diff --git a/Common/metadata.py b/orion/metadata.py similarity index 99% rename from Common/metadata.py rename to orion/metadata.py index c50cdc9c..90bc608f 100644 --- a/Common/metadata.py +++ b/orion/metadata.py @@ -3,7 +3,7 @@ import json from xxhash import xxh64_hexdigest -from Common.normalization import NormalizationScheme +from orion.normalization import NormalizationScheme class Metadata: diff --git a/Common/neo4j_meta_kg.py b/orion/neo4j_meta_kg.py similarity index 98% rename from Common/neo4j_meta_kg.py rename to orion/neo4j_meta_kg.py index 39b37bd7..b0f91598 100644 --- a/Common/neo4j_meta_kg.py +++ b/orion/neo4j_meta_kg.py @@ -2,9 +2,9 @@ import json import os from collections import defaultdict -from Common.neo4j_tools import Neo4jTools -from Common.biolink_constants import NAMED_THING -from Common.biolink_utils import BiolinkUtils +from orion.neo4j_tools import Neo4jTools +from orion.biolink_constants import NAMED_THING +from orion.biolink_utils import BiolinkUtils class Neo4jMetaKGGenerator: diff --git a/Common/neo4j_tools.py b/orion/neo4j_tools.py similarity index 84% rename from Common/neo4j_tools.py rename to orion/neo4j_tools.py index fefce11d..38ffe7ca 100644 --- a/Common/neo4j_tools.py +++ b/orion/neo4j_tools.py @@ -1,10 +1,11 @@ +import signal import time import os import neo4j import subprocess -import Common.kgx_file_converter as kgx_file_converter -from Common.biolink_constants import NAMED_THING -from Common.utils import LoggingUtil +import orion.kgx_file_converter as kgx_file_converter +from orion.biolink_constants import NAMED_THING +from orion.utils import LoggingUtil class Neo4jTools: @@ -23,9 +24,9 @@ def __init__(self, self.graph_db_uri = f'bolt://{neo4j_host}:{bolt_port}' self.graph_db_auth = ("neo4j", self.password) self.neo4j_driver = neo4j.GraphDatabase.driver(self.graph_db_uri, auth=self.graph_db_auth) - self.logger = LoggingUtil.init_logging("ORION.Common.neo4j_tools", + self.logger = LoggingUtil.init_logging("ORION.orion.neo4j_tools", line_format='medium', - log_file_path=os.environ['ORION_LOGS']) + log_file_path=os.getenv('ORION_LOGS')) def import_csv_files(self, graph_directory: str, @@ -105,10 +106,52 @@ def start_neo4j(self): def stop_neo4j(self): self.logger.info(f'Stopping Neo4j DB...') - return self.__issue_neo4j_command('stop') + # We would prefer to stop the neo4j using "neo4j stop" it's not working with the current docker image. + # Instead, we can find and kill the neo4j process using the pid. + # + # We could attempt "stop neo4j" and only kill the process as a fallback as follows, but it takes 2 whole minutes + # to time out. Seeing as that always happens with the current docker image that's slow and not helpful. + # exit_code = self.__issue_neo4j_command('stop') + # if exit_code != 0: + # self.logger.warning(f'neo4j stop failed (exit code {exit_code}), falling back to process kill...') + exit_code = self.__kill_neo4j_process() + return exit_code + + def __kill_neo4j_process(self): + # See notes in the stop_neo4j() function. Using a custom docker image broke "stop neo4j", probably due to a + # mismatch in pids occurring the way we're running neo4j commands with subprocesses. This is pretty hacky and + # not guaranteed to work on setups that don't use the docker container, but it does seem to work so far. + try: + # Try the PID file first + pid_file = os.path.join(os.environ.get('NEO4J_HOME', '/var/lib/neo4j'), 'run', 'neo4j.pid') + pid = None + if os.path.exists(pid_file): + with open(pid_file, 'r') as f: + pid = int(f.read().strip()) + + if pid: + self.logger.info(f'Sending SIGTERM to Neo4j process (PID {pid})...') + os.kill(pid, 15) # SIGTERM + else: + self.logger.error(f'Neo4j PID was not found and Neo4j could not be stopped.') + return 1 + + # Wait for Neo4j to shut down gracefully + for i in range(30): + check = subprocess.run(['pgrep', '-f', 'org.neo4j'], capture_output=True) + if check.returncode != 0: # Process gone + self.logger.info('Neo4j process stopped successfully.') + return 0 + time.sleep(1) + + self.logger.error('Neo4j process did not stop within 30 seconds.') + return 1 + except Exception as e: + self.logger.error(f'Error killing Neo4j process: {e}') + return 1 def __issue_neo4j_command(self, command: str): - neo4j_cmd = ['neo4j', f'{command}'] + neo4j_cmd = ['neo4j', f'{command}', '--verbose'] neo4j_results: subprocess.CompletedProcess = subprocess.run(neo4j_cmd, capture_output=True) self.logger.info(neo4j_results.stdout) diff --git a/Common/normalization.py b/orion/normalization.py similarity index 98% rename from Common/normalization.py rename to orion/normalization.py index baa611e4..750c1175 100644 --- a/Common/normalization.py +++ b/orion/normalization.py @@ -7,8 +7,8 @@ from dataclasses import dataclass from robokop_genetics.genetics_normalization import GeneticsNormalizer -from Common.biolink_constants import * -from Common.utils import LoggingUtil +from orion.biolink_constants import * +from orion.utils import LoggingUtil NORMALIZATION_CODE_VERSION = '1.4' @@ -78,10 +78,10 @@ def __init__(self, :param node_normalization_version - not implemented yet """ # create a logger - self.logger = LoggingUtil.init_logging("ORION.Common.NodeNormalizer", + self.logger = LoggingUtil.init_logging("ORION.orion.NodeNormalizer", level=log_level, line_format='medium', - log_file_path=os.environ.get('ORION_LOGS')) + log_file_path=os.getenv('ORION_LOGS')) # storage for regular nodes that failed to normalize self.failed_to_normalize_ids = set() # storage for variant nodes that failed to normalize @@ -124,7 +124,7 @@ def hit_node_norm_service(self, curies, retries=0): self.logger.error(error_message) resp.raise_for_status() - def normalize_node_data(self, node_list: list, batch_size: int = 1000) -> list: + def normalize_node_data(self, node_list: list, batch_size: int = 5000) -> list: """ This method calls the NodeNormalization web service and normalizes a list of nodes. @@ -397,7 +397,7 @@ def __init__(self, :param log_level - overrides default log level """ # create a logger - self.logger = LoggingUtil.init_logging("ORION.Common.EdgeNormalizer", level=log_level, line_format='medium', log_file_path=os.environ.get('ORION_LOGS')) + self.logger = LoggingUtil.init_logging("ORION.orion.EdgeNormalizer", level=log_level, line_format='medium', log_file_path=os.getenv('ORION_LOGS')) # normalization map for future look up of all normalized predicates self.edge_normalization_lookup = {} self.cached_edge_norms = {} diff --git a/Common/predicates.py b/orion/predicates.py similarity index 98% rename from Common/predicates.py rename to orion/predicates.py index f46b7bd4..aa3f8f6e 100644 --- a/Common/predicates.py +++ b/orion/predicates.py @@ -1,6 +1,6 @@ import os import requests -from Common.prefixes import * +from orion.prefixes import * import time # these are predicates from DGIDB as well as drug and chemical activity types from drug central diff --git a/Common/prefixes.py b/orion/prefixes.py similarity index 100% rename from Common/prefixes.py rename to orion/prefixes.py diff --git a/Common/redundant_kg.py b/orion/redundant_kg.py similarity index 92% rename from Common/redundant_kg.py rename to orion/redundant_kg.py index 38eedc34..fe923dae 100644 --- a/Common/redundant_kg.py +++ b/orion/redundant_kg.py @@ -6,15 +6,15 @@ except ImportError: TQDM_AVAILABLE = False -from Common.biolink_utils import get_biolink_model_toolkit -from Common.biolink_constants import OBJECT_ASPECT_QUALIFIER, OBJECT_DIRECTION_QUALIFIER, SPECIES_CONTEXT_QUALIFIER, \ +from orion.biolink_utils import get_biolink_model_toolkit +from orion.biolink_constants import OBJECT_ASPECT_QUALIFIER, OBJECT_DIRECTION_QUALIFIER, SPECIES_CONTEXT_QUALIFIER, \ QUALIFIED_PREDICATE, PREDICATE -from Common.utils import quick_jsonl_file_iterator, snakify -from Common.kgx_file_writer import KGXFileWriter +from orion.utils import quick_jsonl_file_iterator, snakify +from orion.kgx_file_writer import KGXFileWriter bmt = get_biolink_model_toolkit() -# TODO - really we should get the full list of qualifiers from Common/biolink_constants.py, +# TODO - really we should get the full list of qualifiers from orion/biolink_constants.py, # but because we currently cannot deduce the association types of edges and/or permissible value enumerators, # we have to hard code qualifier handling anyway, we might as well check against a smaller list QUALIFIER_KEYS = [OBJECT_ASPECT_QUALIFIER, diff --git a/Common/supplementation.py b/orion/supplementation.py similarity index 96% rename from Common/supplementation.py rename to orion/supplementation.py index 5bf4d983..9f3a7ff9 100644 --- a/Common/supplementation.py +++ b/orion/supplementation.py @@ -7,11 +7,11 @@ from urllib.request import urlopen from zipfile import ZipFile from collections import defaultdict -from Common.biolink_constants import * -from Common.normalization import FALLBACK_EDGE_PREDICATE, NormalizationScheme -from Common.utils import LoggingUtil -from Common.kgx_file_writer import KGXFileWriter -from Common.kgx_file_normalizer import KGXFileNormalizer +from orion.biolink_constants import * +from orion.normalization import FALLBACK_EDGE_PREDICATE, NormalizationScheme +from orion.utils import LoggingUtil +from orion.kgx_file_writer import KGXFileWriter +from orion.kgx_file_normalizer import KGXFileNormalizer SNPEFF_PROVENANCE = "infores:robokop-snpeff" @@ -59,12 +59,12 @@ class SequenceVariantSupplementation: SUPPLEMENTATION_VERSION = "1.1" - def __init__(self): + def __init__(self, output_dir="."): - self.logger = LoggingUtil.init_logging("ORION.Common.SequenceVariantSupplementation", + self.logger = LoggingUtil.init_logging("ORION.orion.SequenceVariantSupplementation", line_format='medium', - log_file_path=environ['ORION_LOGS']) - workspace_dir = environ["ORION_STORAGE"] + log_file_path=os.getenv('ORION_LOGS')) + workspace_dir = os.getenv("ORION_STORAGE", output_dir) # if the snpEff dir exists, assume we already downloaded it self.snpeff_dir = path.join(workspace_dir, "snpEff") diff --git a/Common/utils.py b/orion/utils.py similarity index 98% rename from Common/utils.py rename to orion/utils.py index cdee5a72..c3a97f7f 100644 --- a/Common/utils.py +++ b/orion/utils.py @@ -46,7 +46,7 @@ def init_logging(name, level=logging.INFO, line_format='minimum', log_file_path= formatter = logging.Formatter(format_type) # set the logging level - if 'ORION_TEST_MODE' in os.environ and os.environ['ORION_TEST_MODE']: + if os.getenv('ORION_TEST_MODE'): level = logging.DEBUG logger.setLevel(level) @@ -108,7 +108,7 @@ def __init__(self, log_level=logging.INFO): :param log_level - overrides default log level """ # create a logger - self.logger = LoggingUtil.init_logging("ORION.Common.GetData", level=log_level, line_format='medium', log_file_path=os.environ.get('ORION_LOGS')) + self.logger = LoggingUtil.init_logging("ORION.orion.GetData", level=log_level, line_format='medium', log_file_path=os.getenv('ORION_LOGS')) @staticmethod def pull_via_ftp_binary(ftp_site, ftp_dir, ftp_file): diff --git a/parsers/BINDING/src/loadBINDINGDB.py b/parsers/BINDING/src/loadBINDINGDB.py index 96d8f06c..8fa85629 100644 --- a/parsers/BINDING/src/loadBINDINGDB.py +++ b/parsers/BINDING/src/loadBINDINGDB.py @@ -8,10 +8,10 @@ from requests.adapters import HTTPAdapter, Retry from parsers.BINDING.src.bindingdb_constraints import LOG_SCALE_AFFINITY_THRESHOLD #Change the binding affinity threshold here. Default is 10 uM Ki,Kd,EC50,orIC50 -from Common.utils import GetData -from Common.loader_interface import SourceDataLoader -from Common.extractor import Extractor -from Common.biolink_constants import PUBLICATIONS, AFFINITY, AFFINITY_PARAMETER, KNOWLEDGE_LEVEL, AGENT_TYPE, \ +from orion.utils import GetData +from orion.loader_interface import SourceDataLoader +from orion.extractor import Extractor +from orion.biolink_constants import PUBLICATIONS, AFFINITY, AFFINITY_PARAMETER, KNOWLEDGE_LEVEL, AGENT_TYPE, \ KNOWLEDGE_ASSERTION, MANUAL_AGENT # Full Binding Data. diff --git a/parsers/CCIDB/src/loadCCIDB.py b/parsers/CCIDB/src/loadCCIDB.py index 67346691..cb7dffb5 100644 --- a/parsers/CCIDB/src/loadCCIDB.py +++ b/parsers/CCIDB/src/loadCCIDB.py @@ -1,13 +1,13 @@ import os import enum -from Common.utils import GetData -from Common.biolink_constants import ANATOMICAL_CONTEXT_QUALIFIER, CAUSAL_MECHANISM_QUALIFIER, \ +from orion.utils import GetData +from orion.biolink_constants import ANATOMICAL_CONTEXT_QUALIFIER, CAUSAL_MECHANISM_QUALIFIER, \ FORM_OR_VARIANT_QUALIFIER, SPECIALIZATION_QUALIFIER, CONTEXT_QUALIFIER, KNOWLEDGE_LEVEL, AGENT_TYPE, \ SUBJECT_CONTEXT_QUALIFIER, SUBJECT_SPECIALIZATION_QUALIFIER, OBJECT_ASPECT_QUALIFIER, \ OBJECT_SPECIALIZATION_QUALIFIER, OBJECT_FORM_OR_VARIANT_QUALIFIER, DISEASE_CONTEXT_QUALIFIER, QUALIFIED_PREDICATE, \ MANUAL_AGENT, KNOWLEDGE_ASSERTION, PUBLICATIONS -from Common.loader_interface import SourceDataLoader +from orion.loader_interface import SourceDataLoader import pandas as pd diff --git a/parsers/CEBS/src/loadCEBS.py b/parsers/CEBS/src/loadCEBS.py index a8609e2e..e36fd101 100644 --- a/parsers/CEBS/src/loadCEBS.py +++ b/parsers/CEBS/src/loadCEBS.py @@ -4,13 +4,13 @@ import pandas as pd import curies -from Common.biolink_constants import PRIMARY_KNOWLEDGE_SOURCE, KNOWLEDGE_LEVEL, AGENT_TYPE, MANUAL_AGENT, \ +from orion.biolink_constants import PRIMARY_KNOWLEDGE_SOURCE, KNOWLEDGE_LEVEL, AGENT_TYPE, MANUAL_AGENT, \ KNOWLEDGE_ASSERTION, OBSERVATION, PUBLICATIONS, SPECIES_CONTEXT_QUALIFIER, POPULATION_CONTEXT_QUALIFIER, \ SEX_QUALIFIER, ANATOMICAL_CONTEXT_QUALIFIER -from Common.extractor import Extractor -from Common.loader_interface import SourceDataLoader -from Common.prefixes import INCHIKEY -from Common.utils import GetData +from orion.extractor import Extractor +from orion.loader_interface import SourceDataLoader +from orion.prefixes import INCHIKEY +from orion.utils import GetData class CEBSCOLUMNS: diff --git a/parsers/CTD/src/loadCTD.py b/parsers/CTD/src/loadCTD.py index 7f87e089..00e85ba8 100644 --- a/parsers/CTD/src/loadCTD.py +++ b/parsers/CTD/src/loadCTD.py @@ -7,11 +7,11 @@ from io import TextIOWrapper from bs4 import BeautifulSoup from operator import itemgetter -from Common.utils import GetData, GetDataPullError -from Common.loader_interface import SourceDataLoader, SourceDataFailedError -from Common.kgxmodel import kgxnode, kgxedge -from Common.prefixes import CTD, NCBITAXON, MESH -from Common.biolink_constants import * +from orion.utils import GetData, GetDataPullError +from orion.loader_interface import SourceDataLoader, SourceDataFailedError +from orion.kgxmodel import kgxnode, kgxedge +from orion.prefixes import CTD, NCBITAXON, MESH +from orion.biolink_constants import * ############## diff --git a/parsers/ClinGenDosageSensitivity/src/loadClinGenDosageSensitivity.py b/parsers/ClinGenDosageSensitivity/src/loadClinGenDosageSensitivity.py index c878eccf..695e0d23 100644 --- a/parsers/ClinGenDosageSensitivity/src/loadClinGenDosageSensitivity.py +++ b/parsers/ClinGenDosageSensitivity/src/loadClinGenDosageSensitivity.py @@ -1,12 +1,10 @@ import os -import csv +from orion.extractor import Extractor +from orion.loader_interface import SourceDataLoader +from orion.biolink_constants import PRIMARY_KNOWLEDGE_SOURCE, NODE_TYPES, SEQUENCE_VARIANT +from orion.utils import GetData from datetime import date -from collections.abc import Callable - -from Common.extractor import Extractor -from Common.loader_interface import SourceDataLoader -from Common.biolink_constants import PRIMARY_KNOWLEDGE_SOURCE -from Common.utils import GetData +import csv # Constants for data processing diff --git a/parsers/ClinGenGeneDiseaseValidity/src/loadClinGenGeneDiseaseValidity.py b/parsers/ClinGenGeneDiseaseValidity/src/loadClinGenGeneDiseaseValidity.py index a149dcf6..19796dec 100755 --- a/parsers/ClinGenGeneDiseaseValidity/src/loadClinGenGeneDiseaseValidity.py +++ b/parsers/ClinGenGeneDiseaseValidity/src/loadClinGenGeneDiseaseValidity.py @@ -1,8 +1,8 @@ import os import enum -from Common.extractor import Extractor -from Common.loader_interface import SourceDataLoader -from Common.utils import GetData +from orion.extractor import Extractor +from orion.loader_interface import SourceDataLoader +from orion.utils import GetData from datetime import date diff --git a/parsers/ClinGenVariantPathogenicity/src/loadClinGenVariantPathogenicity.py b/parsers/ClinGenVariantPathogenicity/src/loadClinGenVariantPathogenicity.py index 53326d7f..7a42b1bf 100644 --- a/parsers/ClinGenVariantPathogenicity/src/loadClinGenVariantPathogenicity.py +++ b/parsers/ClinGenVariantPathogenicity/src/loadClinGenVariantPathogenicity.py @@ -1,16 +1,16 @@ import os import csv -from Common.biolink_constants import ( +from orion.biolink_constants import ( PRIMARY_KNOWLEDGE_SOURCE, NODE_TYPES, SEQUENCE_VARIANT, PUBLICATIONS, NEGATED, ) -from Common.extractor import Extractor -from Common.loader_interface import SourceDataLoader -from Common.prefixes import CLINGEN_ALLELE_REGISTRY, PUBMED -from Common.utils import GetData +from orion.extractor import Extractor +from orion.loader_interface import SourceDataLoader +from orion.prefixes import CLINGEN_ALLELE_REGISTRY, PUBMED +from orion.utils import GetData from datetime import date diff --git a/parsers/FooDB/src/loadFDB.py b/parsers/FooDB/src/loadFDB.py index 1e1438ba..5d12c4a5 100644 --- a/parsers/FooDB/src/loadFDB.py +++ b/parsers/FooDB/src/loadFDB.py @@ -5,10 +5,10 @@ from bs4 import BeautifulSoup from parsers.FooDB.src.FoodSQL import FoodSQL -from Common.loader_interface import SourceDataLoader, SourceDataFailedError -from Common.utils import GetData -from Common.kgxmodel import kgxnode, kgxedge -from Common.prefixes import NCBITAXON +from orion.loader_interface import SourceDataLoader, SourceDataFailedError +from orion.utils import GetData +from orion.kgxmodel import kgxnode, kgxedge +from orion.prefixes import NCBITAXON ############## diff --git a/parsers/GOA/src/loadGOA.py b/parsers/GOA/src/loadGOA.py index 66282a4b..0d4e68fb 100644 --- a/parsers/GOA/src/loadGOA.py +++ b/parsers/GOA/src/loadGOA.py @@ -3,12 +3,12 @@ import enum import gzip -from Common.loader_interface import SourceDataLoader -from Common.extractor import Extractor +from orion.loader_interface import SourceDataLoader +from orion.extractor import Extractor from io import TextIOWrapper -from Common.utils import GetData -from Common.biolink_constants import * -from Common.prefixes import NCBITAXON +from orion.utils import GetData +from orion.biolink_constants import * +from orion.prefixes import NCBITAXON # the data header columns are: diff --git a/parsers/GTEx/src/loadGTEx.py b/parsers/GTEx/src/loadGTEx.py index 1c660544..be6956c7 100644 --- a/parsers/GTEx/src/loadGTEx.py +++ b/parsers/GTEx/src/loadGTEx.py @@ -3,11 +3,11 @@ import gzip import argparse from urllib import request -from Common.normalization import NodeNormalizer -from Common.loader_interface import SourceDataLoader -from Common.biolink_constants import * -from Common.prefixes import HGVS, UBERON -from Common.hgvs_utils import convert_variant_to_hgvs +from orion.normalization import NodeNormalizer +from orion.loader_interface import SourceDataLoader +from orion.biolink_constants import * +from orion.prefixes import HGVS, UBERON +from orion.hgvs_utils import convert_variant_to_hgvs class GTExLoader(SourceDataLoader): diff --git a/parsers/GWASCatalog/src/loadGWASCatalog.py b/parsers/GWASCatalog/src/loadGWASCatalog.py index 9acf8f00..58f47e66 100644 --- a/parsers/GWASCatalog/src/loadGWASCatalog.py +++ b/parsers/GWASCatalog/src/loadGWASCatalog.py @@ -7,11 +7,11 @@ from sys import float_info from collections import defaultdict -from Common.utils import GetData -from Common.loader_interface import SourceDataLoader -from Common.kgxmodel import kgxnode, kgxedge -from Common.biolink_constants import * -from Common.prefixes import DBSNP, EFO, ORPHANET, HP, NCIT, MONDO, GO +from orion.utils import GetData +from orion.loader_interface import SourceDataLoader +from orion.kgxmodel import kgxnode, kgxedge +from orion.biolink_constants import * +from orion.prefixes import DBSNP, EFO, ORPHANET, HP, NCIT, MONDO, GO # the data header columns are: diff --git a/parsers/GenomeAlliance/src/loadGenomeAlliance.py b/parsers/GenomeAlliance/src/loadGenomeAlliance.py index 559dc1e4..e03a650a 100644 --- a/parsers/GenomeAlliance/src/loadGenomeAlliance.py +++ b/parsers/GenomeAlliance/src/loadGenomeAlliance.py @@ -4,10 +4,10 @@ import gzip import requests -from Common.utils import GetData -from Common.loader_interface import SourceDataLoader -from Common.extractor import Extractor -from Common.biolink_constants import PRIMARY_KNOWLEDGE_SOURCE +from orion.utils import GetData +from orion.loader_interface import SourceDataLoader +from orion.extractor import Extractor +from orion.biolink_constants import PRIMARY_KNOWLEDGE_SOURCE # the data header columns for the orthologs tsv file: diff --git a/parsers/IntAct/src/loadIA.py b/parsers/IntAct/src/loadIA.py index 3da5c8f0..fee9dabf 100644 --- a/parsers/IntAct/src/loadIA.py +++ b/parsers/IntAct/src/loadIA.py @@ -6,11 +6,13 @@ from csv import reader from operator import itemgetter from zipfile import ZipFile -from Common.biolink_constants import * -from Common.utils import GetData -from Common.loader_interface import SourceDataLoader -from Common.prefixes import NCBITAXON, UNIPROTKB -from Common.kgxmodel import kgxnode, kgxedge + +from orion.biolink_constants import * +from orion.utils import GetData +from orion.loader_interface import SourceDataLoader +from orion.prefixes import NCBITAXON, UNIPROTKB +from orion.kgxmodel import kgxnode, kgxedge + # data column enumerators diff --git a/parsers/KinAce/src/loadKinAce.py b/parsers/KinAce/src/loadKinAce.py index 2e78b41e..b34ebd6c 100644 --- a/parsers/KinAce/src/loadKinAce.py +++ b/parsers/KinAce/src/loadKinAce.py @@ -2,10 +2,10 @@ import enum import requests -from Common.utils import GetData -from Common.loader_interface import SourceDataLoader -from Common.extractor import Extractor -from Common.biolink_constants import * +from orion.utils import GetData +from orion.loader_interface import SourceDataLoader +from orion.extractor import Extractor +from orion.biolink_constants import * ############## diff --git a/parsers/LINCS/src/loadLINCS.py b/parsers/LINCS/src/loadLINCS.py index 8924ea3b..2d6d994e 100644 --- a/parsers/LINCS/src/loadLINCS.py +++ b/parsers/LINCS/src/loadLINCS.py @@ -6,11 +6,11 @@ from yaml import SafeLoader from pathlib import Path -from Common.kgxmodel import kgxnode, kgxedge -from Common.loader_interface import SourceDataLoader -from Common.biolink_constants import * -from Common.prefixes import PUBCHEM_COMPOUND -from Common.utils import GetData +from orion.kgxmodel import kgxnode, kgxedge +from orion.loader_interface import SourceDataLoader +from orion.biolink_constants import * +from orion.prefixes import PUBCHEM_COMPOUND +from orion.utils import GetData class LINCSLoader(SourceDataLoader): diff --git a/parsers/LitCoin/src/bagel/bagel.py b/parsers/LitCoin/src/bagel/bagel.py index 11ad1ead..88ea96f3 100644 --- a/parsers/LitCoin/src/bagel/bagel.py +++ b/parsers/LitCoin/src/bagel/bagel.py @@ -5,7 +5,7 @@ from parsers.LitCoin.src.NER.nameres import NameResNEREngine from parsers.LitCoin.src.NER.sapbert import SAPBERTNEREngine from parsers.LitCoin.src.bagel.bagel_gpt import ask_classes_and_descriptions, LLM_RESULTS -from Common.normalization import NODE_NORMALIZATION_URL +from orion.normalization import NODE_NORMALIZATION_URL BAGEL_SUBJECT_SYN_TYPE = 'subject_bagel_syn_type' diff --git a/parsers/LitCoin/src/bagel/bagel_gpt.py b/parsers/LitCoin/src/bagel/bagel_gpt.py index 93ab0869..843ba4d3 100644 --- a/parsers/LitCoin/src/bagel/bagel_gpt.py +++ b/parsers/LitCoin/src/bagel/bagel_gpt.py @@ -2,17 +2,17 @@ import os from collections import defaultdict -from Common.config import CONFIG -from Common.utils import LoggingUtil +from orion.config import CONFIG +from orion.utils import LoggingUtil OPENAI_API_KEY = CONFIG.get("OPENAI_API_KEY") LLM_RESULTS = [] -logger = LoggingUtil.init_logging("ORION.Common.BagelGPT", +logger = LoggingUtil.init_logging("ORION.orion.BagelGPT", line_format='medium', - log_file_path=os.environ['ORION_LOGS']) + log_file_path=os.getenv('ORION_LOGS')) def ask_classes_and_descriptions(text, term, termlist, abstract_id, requests_session): """Get GPT results based only on the labels of the terms.""" diff --git a/parsers/LitCoin/src/bagel/bagel_service.py b/parsers/LitCoin/src/bagel/bagel_service.py index 0706d787..772691e1 100644 --- a/parsers/LitCoin/src/bagel/bagel_service.py +++ b/parsers/LitCoin/src/bagel/bagel_service.py @@ -1,6 +1,6 @@ import requests from requests.auth import HTTPBasicAuth -from Common.config import CONFIG +from orion.config import CONFIG BAGEL_ENDPOINT = 'https://bagel.apps.renci.org/' BAGEL_ENDPOINT += 'find_curies_openai' diff --git a/parsers/LitCoin/src/loadLitCoin.py b/parsers/LitCoin/src/loadLitCoin.py index 4755be23..292365bc 100644 --- a/parsers/LitCoin/src/loadLitCoin.py +++ b/parsers/LitCoin/src/loadLitCoin.py @@ -6,12 +6,13 @@ import requests.exceptions -from Common.biolink_utils import BiolinkUtils -from Common.loader_interface import SourceDataLoader -from Common.biolink_constants import PUBLICATIONS, NEGATED -from Common.utils import GetData, quick_jsonl_file_iterator -from Common.normalization import call_name_resolution, NAME_RESOLVER_API_ERROR -from Common.prefixes import PUBMED +from orion.biolink_utils import BiolinkUtils +from orion.loader_interface import SourceDataLoader +from orion.biolink_constants import PUBLICATIONS, NEGATED +from orion.utils import GetData, quick_jsonl_file_iterator +from orion.normalization import call_name_resolution, NAME_RESOLVER_API_ERROR +from orion.prefixes import PUBMED + from parsers.LitCoin.src.bagel.bagel_service import call_bagel_service from parsers.LitCoin.src.bagel.bagel import get_orion_bagel_results, extract_best_match, \ diff --git a/parsers/MONDOProperties/src/loadMP.py b/parsers/MONDOProperties/src/loadMP.py index a2483d25..7588b8d3 100644 --- a/parsers/MONDOProperties/src/loadMP.py +++ b/parsers/MONDOProperties/src/loadMP.py @@ -4,9 +4,9 @@ from collections import defaultdict from gzip import GzipFile -from Common.utils import GetData -from Common.loader_interface import SourceDataLoader -from Common.kgxmodel import kgxnode +from orion.utils import GetData +from orion.loader_interface import SourceDataLoader +from orion.kgxmodel import kgxnode #This cutoff defines which MONDO classes will be turned into properties. To find it, I used yasgui to run diff --git a/parsers/MetabolomicsWorkbench/src/loadMetabolomicsWorkbench.py b/parsers/MetabolomicsWorkbench/src/loadMetabolomicsWorkbench.py index f5c9921d..bf41244a 100644 --- a/parsers/MetabolomicsWorkbench/src/loadMetabolomicsWorkbench.py +++ b/parsers/MetabolomicsWorkbench/src/loadMetabolomicsWorkbench.py @@ -6,11 +6,11 @@ from yaml import SafeLoader from pathlib import Path -from Common.kgxmodel import kgxnode, kgxedge -from Common.loader_interface import SourceDataLoader -from Common.biolink_constants import * -from Common.prefixes import PUBCHEM_COMPOUND -from Common.utils import GetData +from orion.kgxmodel import kgxnode, kgxedge +from orion.loader_interface import SourceDataLoader +from orion.biolink_constants import * +from orion.prefixes import PUBCHEM_COMPOUND +from orion.utils import GetData class MetabolomicsWorkbenchLoader(SourceDataLoader): diff --git a/parsers/PHAROS/src/legacy_pharos_mysql.py b/parsers/PHAROS/src/legacy_pharos_mysql.py index 12346a6a..fc354bef 100644 --- a/parsers/PHAROS/src/legacy_pharos_mysql.py +++ b/parsers/PHAROS/src/legacy_pharos_mysql.py @@ -1,7 +1,7 @@ import os import mysql.connector import logging -from Common.utils import LoggingUtil, GetData, NodeNormUtils, EdgeNormUtils +from orion.utils import LoggingUtil, GetData, NodeNormUtils, EdgeNormUtils from pathlib import Path diff --git a/parsers/PHAROS/src/loadPHAROS.py b/parsers/PHAROS/src/loadPHAROS.py index 516da25c..44a95f17 100644 --- a/parsers/PHAROS/src/loadPHAROS.py +++ b/parsers/PHAROS/src/loadPHAROS.py @@ -2,12 +2,12 @@ import argparse import re -from Common.loader_interface import SourceDataLoader, SourceDataBrokenError, SourceDataFailedError -from Common.kgxmodel import kgxnode, kgxedge -from Common.biolink_constants import * -from Common.utils import GetData, snakify -from Common.db_connectors import MySQLConnector -from Common.predicates import DGIDB_PREDICATE_MAPPING +from orion.loader_interface import SourceDataLoader, SourceDataBrokenError, SourceDataFailedError +from orion.kgxmodel import kgxnode, kgxedge +from orion.biolink_constants import * +from orion.utils import GetData, snakify +from orion.db_connectors import MySQLConnector +from orion.predicates import DGIDB_PREDICATE_MAPPING class PHAROSLoader(SourceDataLoader): diff --git a/parsers/Reactome/src/loadReactome.py b/parsers/Reactome/src/loadReactome.py index fb65488a..5a227b9a 100755 --- a/parsers/Reactome/src/loadReactome.py +++ b/parsers/Reactome/src/loadReactome.py @@ -6,12 +6,12 @@ import neo4j from bs4 import BeautifulSoup -from Common.loader_interface import SourceDataLoader -from Common.kgxmodel import kgxnode, kgxedge -from Common.neo4j_tools import Neo4jTools -from Common.biolink_constants import * -from Common.prefixes import REACTOME, NCBITAXON, GTOPDB, UNIPROTKB, CHEBI, KEGG_COMPOUND, KEGG_GLYCAN, PUBCHEM_COMPOUND, NCBIGENE, CLINVAR -from Common.utils import GetData +from orion.loader_interface import SourceDataLoader +from orion.kgxmodel import kgxnode, kgxedge +from orion.neo4j_tools import Neo4jTools +from orion.biolink_constants import * +from orion.prefixes import REACTOME, NCBITAXON, GTOPDB, UNIPROTKB, CHEBI, KEGG_COMPOUND, KEGG_GLYCAN, PUBCHEM_COMPOUND, NCBIGENE, CLINVAR +from orion.utils import GetData SUBJECT_COLUMN = 0 diff --git a/parsers/SGD/src/loadSGD.py b/parsers/SGD/src/loadSGD.py index 2c82c9f4..1eeb8f37 100644 --- a/parsers/SGD/src/loadSGD.py +++ b/parsers/SGD/src/loadSGD.py @@ -3,10 +3,10 @@ import requests from parsers.SGD.src.sgd_source_retriever import retrieve_sgd_files -from Common.loader_interface import SourceDataLoader -from Common.extractor import Extractor -from Common.prefixes import PUBMED -from Common.biolink_constants import PRIMARY_KNOWLEDGE_SOURCE, NODE_TYPES, PUBLICATIONS +from orion.loader_interface import SourceDataLoader +from orion.extractor import Extractor +from orion.prefixes import PUBMED +from orion.biolink_constants import PRIMARY_KNOWLEDGE_SOURCE, NODE_TYPES, PUBLICATIONS from parsers.yeast.src.yeast_constants import SGD_ALL_GENES_FILE # Maps Genes to GO Terms. diff --git a/parsers/SIGNOR/src/loadSIGNOR.py b/parsers/SIGNOR/src/loadSIGNOR.py index 24116a6a..cb270a92 100644 --- a/parsers/SIGNOR/src/loadSIGNOR.py +++ b/parsers/SIGNOR/src/loadSIGNOR.py @@ -8,10 +8,10 @@ from requests_toolbelt.multipart.encoder import MultipartEncoder -from Common.biolink_constants import * -from Common.prefixes import * -from Common.utils import GetData -from Common.loader_interface import SourceDataLoader +from orion.biolink_constants import * +from orion.prefixes import * +from orion.utils import GetData +from orion.loader_interface import SourceDataLoader from parsers.SIGNOR.src.signor_mechanism_predicate_mapping import ptm_dict, mechanism_map, effect_mapping diff --git a/parsers/SIGNOR/src/signor_mechanism_predicate_mapping.py b/parsers/SIGNOR/src/signor_mechanism_predicate_mapping.py index 2f0aa3f3..6f61a643 100644 --- a/parsers/SIGNOR/src/signor_mechanism_predicate_mapping.py +++ b/parsers/SIGNOR/src/signor_mechanism_predicate_mapping.py @@ -1,4 +1,4 @@ -from Common.biolink_constants import * +from orion.biolink_constants import * ptm_dict = { "acetylation": "increased", diff --git a/parsers/STRING/src/loadSTRINGDB.py b/parsers/STRING/src/loadSTRINGDB.py index 5fed9498..67c1e71d 100644 --- a/parsers/STRING/src/loadSTRINGDB.py +++ b/parsers/STRING/src/loadSTRINGDB.py @@ -3,11 +3,11 @@ import gzip import requests as rq -from Common.utils import GetData -from Common.loader_interface import SourceDataLoader -from Common.extractor import Extractor -from Common.prefixes import ENSEMBL, NCBITAXON -from Common.biolink_constants import * +from orion.utils import GetData +from orion.loader_interface import SourceDataLoader +from orion.extractor import Extractor +from orion.prefixes import ENSEMBL, NCBITAXON +from orion.biolink_constants import * # Full PPI Data. diff --git a/parsers/UberGraph/src/loadUG.py b/parsers/UberGraph/src/loadUG.py index 925ac3b9..40384af6 100644 --- a/parsers/UberGraph/src/loadUG.py +++ b/parsers/UberGraph/src/loadUG.py @@ -2,9 +2,9 @@ import tarfile from io import TextIOWrapper -from Common.utils import GetData -from Common.loader_interface import SourceDataLoader -from Common.biolink_constants import KNOWLEDGE_LEVEL, AGENT_TYPE, KNOWLEDGE_ASSERTION, MANUAL_AGENT +from orion.utils import GetData +from orion.loader_interface import SourceDataLoader +from orion.biolink_constants import KNOWLEDGE_LEVEL, AGENT_TYPE, KNOWLEDGE_ASSERTION, MANUAL_AGENT from parsers.UberGraph.src.ubergraph import UberGraphTools diff --git a/parsers/UberGraph/src/ubergraph.py b/parsers/UberGraph/src/ubergraph.py index 4f8a7361..37c29083 100644 --- a/parsers/UberGraph/src/ubergraph.py +++ b/parsers/UberGraph/src/ubergraph.py @@ -2,7 +2,7 @@ import curies import tarfile from io import TextIOWrapper -from Common.biolink_utils import get_biolink_prefix_map +from orion.biolink_utils import get_biolink_prefix_map OBO_MISSING_MAPPINGS = { 'NCBIGene': 'http://purl.obolibrary.org/obo/NCBIGene_', diff --git a/parsers/ViralProteome/src/get_uniref_taxon_indexes.py b/parsers/ViralProteome/src/get_uniref_taxon_indexes.py index 94c235d3..aa681b58 100644 --- a/parsers/ViralProteome/src/get_uniref_taxon_indexes.py +++ b/parsers/ViralProteome/src/get_uniref_taxon_indexes.py @@ -1,7 +1,7 @@ import os import argparse # from parsers.ViralProteome.src.loadUniRef import UniRefSimLoader -from Common.utils import LoggingUtil, GetData +from orion.utils import LoggingUtil, GetData from pathlib import Path # create a logger diff --git a/parsers/ViralProteome/src/loadUniRef.py b/parsers/ViralProteome/src/loadUniRef.py index 7c1725ae..be5181bf 100644 --- a/parsers/ViralProteome/src/loadUniRef.py +++ b/parsers/ViralProteome/src/loadUniRef.py @@ -5,9 +5,9 @@ import datetime from xml.etree import ElementTree as ETree -from Common.utils import LoggingUtil, GetData -from Common.kgx_file_writer import KGXFileWriter -from Common.loader_interface import SourceDataLoader +from orion.utils import LoggingUtil, GetData +from orion.kgx_file_writer import KGXFileWriter +from orion.loader_interface import SourceDataLoader ############## diff --git a/parsers/ViralProteome/src/loadVP.py b/parsers/ViralProteome/src/loadVP.py index cc0f7215..8a26a337 100644 --- a/parsers/ViralProteome/src/loadVP.py +++ b/parsers/ViralProteome/src/loadVP.py @@ -4,9 +4,9 @@ import tarfile from csv import reader -from Common.utils import GetData -from Common.extractor import Extractor -from Common.loader_interface import SourceDataLoader +from orion.utils import GetData +from orion.extractor import Extractor +from orion.loader_interface import SourceDataLoader from parsers.GOA.src.loadGOA import get_goa_predicate, get_goa_edge_properties, get_goa_subject_props, DATACOLS diff --git a/parsers/_parser_template/src/parser.py b/parsers/_parser_template/src/parser.py index 61aec9c4..62068dfd 100644 --- a/parsers/_parser_template/src/parser.py +++ b/parsers/_parser_template/src/parser.py @@ -3,11 +3,11 @@ import enum import gzip -from Common.extractor import Extractor -from Common.loader_interface import SourceDataLoader -from Common.biolink_constants import PRIMARY_KNOWLEDGE_SOURCE -from Common.prefixes import HGNC # only an example, use existing curie prefixes or add your own to the prefixes file -from Common.utils import GetData +from orion.extractor import Extractor +from orion.loader_interface import SourceDataLoader +from orion.biolink_constants import PRIMARY_KNOWLEDGE_SOURCE +from orion.prefixes import HGNC # only an example, use existing curie prefixes or add your own to the prefixes file +from orion.utils import GetData # if parsing a tsv or csv type file with columns, use a enum to represent each field diff --git a/parsers/camkp/src/loadCAMKP.py b/parsers/camkp/src/loadCAMKP.py index a75f63d8..704a0488 100644 --- a/parsers/camkp/src/loadCAMKP.py +++ b/parsers/camkp/src/loadCAMKP.py @@ -5,10 +5,10 @@ import yaml import json -from Common.utils import GetData -from Common.kgxmodel import kgxnode, kgxedge -from Common.biolink_constants import XREFS, KNOWLEDGE_LEVEL, KNOWLEDGE_ASSERTION, AGENT_TYPE, MANUAL_AGENT -from Common.loader_interface import SourceDataLoader +from orion.utils import GetData +from orion.kgxmodel import kgxnode, kgxedge +from orion.biolink_constants import XREFS, KNOWLEDGE_LEVEL, KNOWLEDGE_ASSERTION, AGENT_TYPE, MANUAL_AGENT +from orion.loader_interface import SourceDataLoader from gzip import GzipFile diff --git a/parsers/chebi/src/loadChebiProperties.py b/parsers/chebi/src/loadChebiProperties.py index 4fef6f47..bc2fec34 100644 --- a/parsers/chebi/src/loadChebiProperties.py +++ b/parsers/chebi/src/loadChebiProperties.py @@ -3,10 +3,10 @@ import gzip from collections import defaultdict -from Common.utils import GetData -from Common.loader_interface import SourceDataLoader -from Common.kgxmodel import kgxnode -from Common.prefixes import CHEBI +from orion.utils import GetData +from orion.loader_interface import SourceDataLoader +from orion.kgxmodel import kgxnode +from orion.prefixes import CHEBI RELATION_TYPE_ID_COLUMN = 1 RELATION_INIT_ID_COLUMN = 3 # This mistake stemmed from the relation.tsv column swap diff --git a/parsers/clinicaltrials/src/loadCTKP.py b/parsers/clinicaltrials/src/loadCTKP.py index bf2ece94..032e1972 100644 --- a/parsers/clinicaltrials/src/loadCTKP.py +++ b/parsers/clinicaltrials/src/loadCTKP.py @@ -3,11 +3,11 @@ import requests import json -from Common.biolink_constants import * -from Common.extractor import Extractor -from Common.utils import GetData -from Common.loader_interface import SourceDataLoader -from Common.utils import GetDataPullError +from orion.biolink_constants import * +from orion.extractor import Extractor +from orion.utils import GetData +from orion.loader_interface import SourceDataLoader +from orion.utils import GetDataPullError # the data header columns the nodes files are: diff --git a/parsers/cohd/src/loadCOHD.py b/parsers/cohd/src/loadCOHD.py index 9c6bb04b..9c745688 100644 --- a/parsers/cohd/src/loadCOHD.py +++ b/parsers/cohd/src/loadCOHD.py @@ -3,8 +3,8 @@ import requests import yaml -from Common.loader_interface import SourceDataLoader -from Common.utils import GetData, quick_jsonl_file_iterator +from orion.loader_interface import SourceDataLoader +from orion.utils import GetData, quick_jsonl_file_iterator ############## diff --git a/parsers/cord19/src/loadCord19.py b/parsers/cord19/src/loadCord19.py index b300f63e..83f689f7 100644 --- a/parsers/cord19/src/loadCord19.py +++ b/parsers/cord19/src/loadCord19.py @@ -2,10 +2,10 @@ import enum from copy import deepcopy -from Common.utils import GetData -from Common.loader_interface import SourceDataLoader -from Common.extractor import Extractor -from Common.biolink_constants import AGGREGATOR_KNOWLEDGE_SOURCES, PRIMARY_KNOWLEDGE_SOURCE +from orion.utils import GetData +from orion.loader_interface import SourceDataLoader +from orion.extractor import Extractor +from orion.biolink_constants import AGGREGATOR_KNOWLEDGE_SOURCES, PRIMARY_KNOWLEDGE_SOURCE # the data header columns for both nodes files are: diff --git a/parsers/drugcentral/src/loaddrugcentral.py b/parsers/drugcentral/src/loaddrugcentral.py index c2223b3d..0b55ec9b 100644 --- a/parsers/drugcentral/src/loaddrugcentral.py +++ b/parsers/drugcentral/src/loaddrugcentral.py @@ -4,14 +4,14 @@ import gzip import os -from Common.extractor import Extractor -from Common.loader_interface import SourceDataLoader, SourceDataFailedError, SourceDataBrokenError -from Common.utils import GetData, snakify -from Common.biolink_constants import PRIMARY_KNOWLEDGE_SOURCE, AGGREGATOR_KNOWLEDGE_SOURCES, PUBLICATIONS, \ +from orion.extractor import Extractor +from orion.loader_interface import SourceDataLoader, SourceDataFailedError, SourceDataBrokenError +from orion.utils import GetData, snakify +from orion.biolink_constants import PRIMARY_KNOWLEDGE_SOURCE, AGGREGATOR_KNOWLEDGE_SOURCES, PUBLICATIONS, \ KNOWLEDGE_LEVEL, KNOWLEDGE_ASSERTION, AGENT_TYPE, MANUAL_AGENT, AFFINITY, AFFINITY_PARAMETER -from Common.prefixes import DRUGCENTRAL, MEDDRA, UMLS, UNIPROTKB, PUBMED -from Common.predicates import DGIDB_PREDICATE_MAPPING -from Common.db_connectors import PostgresConnector +from orion.prefixes import DRUGCENTRAL, MEDDRA, UMLS, UNIPROTKB, PUBMED +from orion.predicates import DGIDB_PREDICATE_MAPPING +from orion.db_connectors import PostgresConnector class DrugCentralLoader(SourceDataLoader): diff --git a/parsers/drugmechdb/src/loadDrugMechDB.py b/parsers/drugmechdb/src/loadDrugMechDB.py index b4775457..06502ae5 100644 --- a/parsers/drugmechdb/src/loadDrugMechDB.py +++ b/parsers/drugmechdb/src/loadDrugMechDB.py @@ -4,11 +4,11 @@ import pandas as pd import ast -from Common.utils import GetData -from Common.loader_interface import SourceDataLoader -from Common.kgxmodel import kgxnode, kgxedge -from Common.extractor import Extractor -from Common.biolink_constants import KNOWLEDGE_LEVEL, KNOWLEDGE_ASSERTION, AGENT_TYPE, MANUAL_AGENT, \ +from orion.utils import GetData +from orion.loader_interface import SourceDataLoader +from orion.kgxmodel import kgxnode, kgxedge +from orion.extractor import Extractor +from orion.biolink_constants import KNOWLEDGE_LEVEL, KNOWLEDGE_ASSERTION, AGENT_TYPE, MANUAL_AGENT, \ QUALIFIED_PREDICATE, OBJECT_ASPECT_QUALIFIER, OBJECT_DIRECTION_QUALIFIER def load_json(json_data): diff --git a/parsers/ehr/src/loadEHR.py b/parsers/ehr/src/loadEHR.py index 217b85ff..65836370 100644 --- a/parsers/ehr/src/loadEHR.py +++ b/parsers/ehr/src/loadEHR.py @@ -4,8 +4,8 @@ import csv import json -from Common.utils import GetData -from Common.loader_interface import SourceDataLoader +from orion.utils import GetData +from orion.loader_interface import SourceDataLoader class EHRMayTreatLoader(SourceDataLoader): diff --git a/parsers/gtopdb/src/loadGtoPdb.py b/parsers/gtopdb/src/loadGtoPdb.py index 2435b616..0988fd5d 100644 --- a/parsers/gtopdb/src/loadGtoPdb.py +++ b/parsers/gtopdb/src/loadGtoPdb.py @@ -6,12 +6,12 @@ import enum from bs4 import BeautifulSoup -from Common.utils import GetData, snakify -from Common.loader_interface import SourceDataLoader, SourceDataFailedError, SourceDataBrokenError -from Common.prefixes import GTOPDB, HGNC, ENSEMBL, PUBMED -from Common.kgxmodel import kgxnode, kgxedge -from Common.predicates import DGIDB_PREDICATE_MAPPING -from Common.biolink_constants import * +from orion.utils import GetData, snakify +from orion.loader_interface import SourceDataLoader, SourceDataFailedError, SourceDataBrokenError +from orion.prefixes import GTOPDB, HGNC, ENSEMBL, PUBMED +from orion.kgxmodel import kgxnode, kgxedge +from orion.predicates import DGIDB_PREDICATE_MAPPING +from orion.biolink_constants import * class INTERACTIONS_COLS(enum.Enum): @@ -348,7 +348,7 @@ def get_ligands_diffs(self, file_path): ligands = norm_ligands.copy() - from Common.normalization import NodeNormalizer + from orion.normalization import NodeNormalizer gd = NodeNormalizer() fails = gd.normalize_node_data(norm_ligands) diff --git a/parsers/hetio/src/loadHetio.py b/parsers/hetio/src/loadHetio.py index 1c76fc35..6746577d 100644 --- a/parsers/hetio/src/loadHetio.py +++ b/parsers/hetio/src/loadHetio.py @@ -4,11 +4,11 @@ import argparse import bz2 -from Common.utils import GetDataPullError -from Common.loader_interface import SourceDataLoader -from Common.extractor import Extractor -from Common.prefixes import NCBIGENE, DRUGBANK, UBERON, DOID, MESH, UMLS -from Common.biolink_constants import * +from orion.utils import GetDataPullError +from orion.loader_interface import SourceDataLoader +from orion.extractor import Extractor +from orion.prefixes import NCBIGENE, DRUGBANK, UBERON, DOID, MESH, UMLS +from orion.biolink_constants import * class HetioLoader(SourceDataLoader): diff --git a/parsers/hgnc/src/loadHGNC.py b/parsers/hgnc/src/loadHGNC.py index 9f892bae..5ee3a812 100644 --- a/parsers/hgnc/src/loadHGNC.py +++ b/parsers/hgnc/src/loadHGNC.py @@ -3,11 +3,12 @@ from pathlib import Path -from Common.utils import GetData -from Common.loader_interface import SourceDataLoader -from Common.kgxmodel import kgxnode, kgxedge -from Common.prefixes import HGNC, HGNC_FAMILY -from Common.biolink_constants import AGENT_TYPE, MANUAL_AGENT, KNOWLEDGE_ASSERTION, KNOWLEDGE_LEVEL, PUBLICATIONS +from orion.utils import GetData +from orion.loader_interface import SourceDataLoader +from orion.kgxmodel import kgxnode, kgxedge +from orion.prefixes import HGNC, HGNC_FAMILY +from orion.biolink_constants import AGENT_TYPE, MANUAL_AGENT, KNOWLEDGE_ASSERTION, KNOWLEDGE_LEVEL, PUBLICATIONS + ############## # Class: HGNC loader diff --git a/parsers/hmdb/src/loadHMDB.py b/parsers/hmdb/src/loadHMDB.py index 17ea0dfa..29945b04 100644 --- a/parsers/hmdb/src/loadHMDB.py +++ b/parsers/hmdb/src/loadHMDB.py @@ -6,11 +6,11 @@ from bs4 import BeautifulSoup from zipfile import ZipFile -from Common.biolink_constants import * -from Common.utils import GetData -from Common.loader_interface import SourceDataLoader -from Common.prefixes import CTD, HMDB, OMIM, UNIPROTKB -from Common.kgxmodel import kgxnode, kgxedge +from orion.biolink_constants import * +from orion.utils import GetData +from orion.loader_interface import SourceDataLoader +from orion.prefixes import CTD, HMDB, OMIM, UNIPROTKB +from orion.kgxmodel import kgxnode, kgxedge ############## diff --git a/parsers/molepro/src/loadMolePro.py b/parsers/molepro/src/loadMolePro.py index d9ccb247..d9bee49e 100644 --- a/parsers/molepro/src/loadMolePro.py +++ b/parsers/molepro/src/loadMolePro.py @@ -1,8 +1,8 @@ import os -from Common.biolink_constants import * -from Common.utils import GetData -from Common.loader_interface import SourceDataLoader +from orion.biolink_constants import * +from orion.utils import GetData +from orion.loader_interface import SourceDataLoader """ diff --git a/parsers/monarchkg/src/loadMonarchKG.py b/parsers/monarchkg/src/loadMonarchKG.py index e9e46a2d..098885b6 100644 --- a/parsers/monarchkg/src/loadMonarchKG.py +++ b/parsers/monarchkg/src/loadMonarchKG.py @@ -4,10 +4,10 @@ import orjson import requests -from Common.loader_interface import SourceDataLoader -from Common.kgxmodel import kgxedge -from Common.biolink_constants import * -from Common.utils import GetData, GetDataPullError +from orion.loader_interface import SourceDataLoader +from orion.kgxmodel import kgxedge +from orion.biolink_constants import * +from orion.utils import GetData, GetDataPullError ############## diff --git a/parsers/ohd_carolina/src/loadOHD.py b/parsers/ohd_carolina/src/loadOHD.py index 6132246d..8954872c 100644 --- a/parsers/ohd_carolina/src/loadOHD.py +++ b/parsers/ohd_carolina/src/loadOHD.py @@ -7,10 +7,10 @@ from io import TextIOWrapper from zipfile import ZipFile -from Common.extractor import Extractor -from Common.loader_interface import SourceDataLoader -from Common.biolink_constants import * -from Common.utils import GetData +from orion.extractor import Extractor +from orion.loader_interface import SourceDataLoader +from orion.biolink_constants import * +from orion.utils import GetData class EDGESDATACOLS(enum.IntEnum): diff --git a/parsers/panther/src/loadPanther.py b/parsers/panther/src/loadPanther.py index 8ab7424a..94b3e908 100644 --- a/parsers/panther/src/loadPanther.py +++ b/parsers/panther/src/loadPanther.py @@ -4,13 +4,14 @@ import requests -from bs4 import BeautifulSoup -from Common.biolink_constants import * -from Common.utils import GetData, GetDataPullError -from Common.loader_interface import SourceDataLoader -from Common.kgxmodel import kgxnode, kgxedge from functools import partial from typing import NamedTuple +from bs4 import BeautifulSoup + +from orion.biolink_constants import * +from orion.utils import GetData, GetDataPullError +from orion.loader_interface import SourceDataLoader +from orion.kgxmodel import kgxnode, kgxedge class LabeledID(NamedTuple): diff --git a/parsers/scent/src/loadScent.py b/parsers/scent/src/loadScent.py index b3df7ce4..6844965a 100644 --- a/parsers/scent/src/loadScent.py +++ b/parsers/scent/src/loadScent.py @@ -1,10 +1,10 @@ import os import enum -from Common.utils import GetData -from Common.loader_interface import SourceDataLoader -from Common.extractor import Extractor -from Common.biolink_constants import PRIMARY_KNOWLEDGE_SOURCE, AGGREGATOR_KNOWLEDGE_SOURCES +from orion.utils import GetData +from orion.loader_interface import SourceDataLoader +from orion.extractor import Extractor +from orion.biolink_constants import PRIMARY_KNOWLEDGE_SOURCE, AGGREGATOR_KNOWLEDGE_SOURCES # the scent odorant edge header columns: diff --git a/parsers/textminingkp/src/loadTMKP.py b/parsers/textminingkp/src/loadTMKP.py index b4f74e75..ffec1354 100644 --- a/parsers/textminingkp/src/loadTMKP.py +++ b/parsers/textminingkp/src/loadTMKP.py @@ -3,10 +3,10 @@ import argparse import enum -from Common.biolink_constants import * -from Common.utils import GetData -from Common.kgxmodel import kgxedge -from Common.loader_interface import SourceDataLoader +from orion.biolink_constants import * +from orion.utils import GetData +from orion.kgxmodel import kgxedge +from orion.loader_interface import SourceDataLoader from gzip import GzipFile diff --git a/parsers/yeast/src/loadCostanza2016.py b/parsers/yeast/src/loadCostanza2016.py index 64bee95d..4400550f 100644 --- a/parsers/yeast/src/loadCostanza2016.py +++ b/parsers/yeast/src/loadCostanza2016.py @@ -2,10 +2,10 @@ import enum import csv import requests -from Common.loader_interface import SourceDataLoader -from Common.extractor import Extractor -from Common.biolink_constants import PRIMARY_KNOWLEDGE_SOURCE, PUBLICATIONS -from Common.prefixes import PUBMED +from orion.loader_interface import SourceDataLoader +from orion.extractor import Extractor +from orion.biolink_constants import PRIMARY_KNOWLEDGE_SOURCE, PUBLICATIONS +from orion.prefixes import PUBMED from intermine.webservice import Service diff --git a/parsers/yeast/src/loadHistoneMap.py b/parsers/yeast/src/loadHistoneMap.py index 84859adc..af7783b9 100644 --- a/parsers/yeast/src/loadHistoneMap.py +++ b/parsers/yeast/src/loadHistoneMap.py @@ -5,9 +5,9 @@ from parsers.SGD.src.sgd_source_retriever import SGDAllGenes from parsers.yeast.src.yeast_constants import YEAST_GENOME_RESOLUTION, SGD_ALL_GENES_FILE, HISTONE_LOCI_FILE -from Common.loader_interface import SourceDataLoader -from Common.extractor import Extractor -from Common.biolink_constants import PRIMARY_KNOWLEDGE_SOURCE +from orion.loader_interface import SourceDataLoader +from orion.extractor import Extractor +from orion.biolink_constants import PRIMARY_KNOWLEDGE_SOURCE #List of Binned Histone Modifications class HISTONEMODBINS_EDGEUMAN(enum.IntEnum): diff --git a/parsers/yeast/src/loadYeastGeneExpressionGasch.py b/parsers/yeast/src/loadYeastGeneExpressionGasch.py index 968f10ca..0df4605d 100644 --- a/parsers/yeast/src/loadYeastGeneExpressionGasch.py +++ b/parsers/yeast/src/loadYeastGeneExpressionGasch.py @@ -3,9 +3,9 @@ import pandas as pd from parsers.SGD.src.sgd_source_retriever import SGDAllGenes -from Common.loader_interface import SourceDataLoader -from Common.extractor import Extractor -from Common.biolink_constants import PRIMARY_KNOWLEDGE_SOURCE +from orion.loader_interface import SourceDataLoader +from orion.extractor import Extractor +from orion.biolink_constants import PRIMARY_KNOWLEDGE_SOURCE # Maps Experimental Condition affects Nucleosome edge. diff --git a/parsers/yeast/src/loadYeastNucleosomesGSE61888.py b/parsers/yeast/src/loadYeastNucleosomesGSE61888.py index b094bd89..ad1876c5 100644 --- a/parsers/yeast/src/loadYeastNucleosomesGSE61888.py +++ b/parsers/yeast/src/loadYeastNucleosomesGSE61888.py @@ -2,10 +2,10 @@ import enum import pandas as pd -from Common.utils import GetData, int_to_roman_numeral -from Common.loader_interface import SourceDataLoader -from Common.extractor import Extractor -from Common.biolink_constants import PRIMARY_KNOWLEDGE_SOURCE +from orion.utils import GetData, int_to_roman_numeral +from orion.loader_interface import SourceDataLoader +from orion.extractor import Extractor +from orion.biolink_constants import PRIMARY_KNOWLEDGE_SOURCE from parsers.yeast.src.yeast_constants import HISTONE_LOCI_FILE, YEAST_GENOME_RESOLUTION from parsers.yeast.src.loadHistoneMap import YeastHistoneMapLoader diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..030198e3 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,64 @@ +[build-system] +requires = ["uv_build >= 0.8.0"] +build-backend = "uv_build" + +[project] +name = "robokop-orion" +version = "0.2.0" +description = "ORION ingests data from knowledge bases and converts it into interoperable Biolink Model knowledge graphs." +license = "MIT" +license-files = ["LICEN[CS]E*"] +readme = "README.md" +requires-python = ">=3.12" +dependencies = [ + "bmt>=1.4.6", + "jsonlines>=4.0.0", + "orjson>=3.11.7", + "python-dotenv>=1.0.1", + "pyyaml>=6.0.1", + "requests>=2.32.5", + "requests-toolbelt>=1.0.0", + "robokop-genetics>=0.7.0", + "uuid-utils>=0.14.1", + "xxhash>=3.6.0", +] + +[project.optional-dependencies] +robokop = [ + "beautifulsoup4>=4.12.3", + "celery>=5.4.0", + "curies>=0.7.9", + "intermine", + "mysql-connector-python>=9.4.0", + "neo4j>=5.28.0", + "openpyxl>=3.1.5", + "pandas>=2.3.3", + "polars>=1.19.0", + "prefixmaps>=0.2.6", + "psycopg2-binary>=2.9.9", + "pyoxigraph>=0.3.22", + "redis>=5.2.1", +] + +[project.scripts] +orion-build = "orion.build_manager:main" +orion-ingest = "orion.ingest_pipeline:main" +orion-merge = "orion.cli.merge_kgs:main" +orion-meta-kg = "orion.cli.generate_meta_kg:main" +orion-redundant-kg = "orion.cli.generate_redundant_kg:main" +orion-ac = "orion.cli.generate_ac_files:main" +orion-neo4j-dump = "orion.cli.neo4j_dump:main" +orion-memgraph-dump = "orion.cli.memgraph_dump:main" + +[dependency-groups] +dev = [ + "pytest>=8.4.2", +] + +[tool.uv.build-backend] +module-root = "" +module-name = "orion" + +[tool.uv.sources] +intermine = { git = "https://github.com/EvanDietzMorris/intermine-ws-python.git" } + diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 10ca6b31..00000000 --- a/requirements.txt +++ /dev/null @@ -1,24 +0,0 @@ -pandas>=2.3.3 -openpyxl>=3.1.5 -requests>=2.32.5 -requests-toolbelt>=1.0.0 -pytest>=8.4.2 -robokop-genetics>=0.7.0 -# intermine is on pypi but as of 6/23 it's broken for python 3.10+, this fork fixes the issue -git+https://github.com/EvanDietzMorris/intermine-ws-python.git -jsonlines>=4.0.0 -pyyaml>=6.0.1 -beautifulsoup4>=4.12.3 -psycopg2-binary>=2.9.9 -orjson>=3.11.3 -xxhash>=3.6.0 -mysql-connector-python>=9.4.0 -neo4j>=5.28.0 -pyoxigraph>=0.5.2 -curies>=0.7.9 -prefixmaps>=0.2.6 -bmt>=1.4.6 -python-dotenv>=1.0.1 -polars>=1.19.0 -celery>=5.4.0 -redis>=5.2.1 diff --git a/tests/test_file_writer.py b/tests/test_file_writer.py index f037602c..1f410d05 100644 --- a/tests/test_file_writer.py +++ b/tests/test_file_writer.py @@ -1,9 +1,9 @@ import os -from Common.utils import quick_jsonl_file_iterator -from Common.kgx_file_writer import KGXFileWriter -from Common.kgxmodel import kgxnode, kgxedge -from Common.biolink_constants import * +from orion.utils import quick_jsonl_file_iterator +from orion.kgx_file_writer import KGXFileWriter +from orion.kgxmodel import kgxnode, kgxedge +from orion.biolink_constants import * test_workspace_dir = os.path.dirname(os.path.abspath(__file__)) + '/workspace/' # TODO this is hacky and should be done with better design in pytest or somewhere else diff --git a/tests/test_graph_spec.py b/tests/test_graph_spec.py index f82931cc..91030e90 100644 --- a/tests/test_graph_spec.py +++ b/tests/test_graph_spec.py @@ -3,7 +3,7 @@ import requests.exceptions from unittest.mock import MagicMock -from Common.build_manager import GraphBuilder, GraphSpecError +from orion.build_manager import GraphBuilder, GraphSpecError def clear_graph_spec_config(): @@ -16,43 +16,52 @@ def reset_graph_spec_config(): os.environ['ORION_GRAPH_SPEC_URL'] = '' -def get_testing_graph_spec_dir(): +@pytest.fixture(scope='module') +def test_graph_spec_dir(): # this is ORION/tests/graph_specs not ORION/graph_specs testing_specs_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'graph_specs') return testing_specs_dir +@pytest.fixture(scope='module') +def test_graph_output_dir(): + testing_output_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'workspace') + return testing_output_dir -def get_source_data_manager_mock(): + +def get_ingest_pipeline_mock(): s_d_mock = MagicMock() s_d_mock.get_latest_source_version = MagicMock() s_d_mock.get_latest_source_version.side_effect = lambda arg: arg + '_v1' return s_d_mock -def test_empty_graph_spec_config(): +def test_empty_graph_spec_config(test_graph_spec_dir, test_graph_output_dir): clear_graph_spec_config() with pytest.raises(GraphSpecError): - graph_builder = GraphBuilder(graph_specs_dir=get_testing_graph_spec_dir()) + graph_builder = GraphBuilder(graph_specs_dir=test_graph_spec_dir, + graph_output_dir=test_graph_output_dir) -def test_invalid_graph_spec_config(): +def test_invalid_graph_spec_config(test_graph_spec_dir, test_graph_output_dir): clear_graph_spec_config() os.environ['ORION_GRAPH_SPEC'] = 'invalid-spec.yaml' with pytest.raises(GraphSpecError): - graph_builder = GraphBuilder(graph_specs_dir=get_testing_graph_spec_dir()) + graph_builder = GraphBuilder(graph_specs_dir=test_graph_spec_dir, + graph_output_dir=test_graph_output_dir) -def test_invalid_graph_spec_url_config(): +def test_invalid_graph_spec_url_config(test_graph_output_dir): clear_graph_spec_config() os.environ['ORION_GRAPH_SPEC_URL'] = 'http://localhost/invalid_graph_spec_url' with pytest.raises(requests.exceptions.ConnectionError): - graph_builder = GraphBuilder() + graph_builder = GraphBuilder(graph_output_dir=test_graph_output_dir) # the graph spec is loaded up properly but doesn't attempt to determine versions when unspecified -def test_valid_graph_spec_config(): +def test_valid_graph_spec_config(test_graph_spec_dir, test_graph_output_dir): reset_graph_spec_config() - graph_builder = GraphBuilder(graph_specs_dir=get_testing_graph_spec_dir()) + graph_builder = GraphBuilder(graph_specs_dir=test_graph_spec_dir, + graph_output_dir=test_graph_output_dir) assert len(graph_builder.graph_specs) testing_graph_spec = graph_builder.graph_specs.get('Testing_Graph', None) assert testing_graph_spec is not None @@ -63,9 +72,10 @@ def test_valid_graph_spec_config(): # graph spec sources are able to return versions once source_version(s) are set -def test_graph_spec_lazy_versions(): +def test_graph_spec_lazy_versions(test_graph_spec_dir, test_graph_output_dir): reset_graph_spec_config() - graph_builder = GraphBuilder(graph_specs_dir=get_testing_graph_spec_dir()) + graph_builder = GraphBuilder(graph_specs_dir=test_graph_spec_dir, + graph_output_dir=test_graph_output_dir) testing_graph_spec = graph_builder.graph_specs.get('Testing_Graph', None) for source in testing_graph_spec.sources: assert source.version is None @@ -77,10 +87,11 @@ def test_graph_spec_lazy_versions(): # mock the source_data_manager to return deterministic source_versions # then see if a graph with a subgraph can properly determine graph versions -def test_graph_spec_subgraph_version(): +def test_graph_spec_subgraph_version(test_graph_spec_dir, test_graph_output_dir): reset_graph_spec_config() - graph_builder = GraphBuilder(graph_specs_dir=get_testing_graph_spec_dir()) - graph_builder.source_data_manager = get_source_data_manager_mock() + graph_builder = GraphBuilder(graph_specs_dir=test_graph_spec_dir, + graph_output_dir=test_graph_output_dir) + graph_builder.ingest_pipeline = get_ingest_pipeline_mock() testing_graph_spec = graph_builder.graph_specs.get('Testing_Graph_2', None) assert testing_graph_spec.graph_version is None @@ -98,10 +109,11 @@ def test_graph_spec_subgraph_version(): # make sure a graph spec with an invalid subgraph fails with the appropriate exception -def test_graph_spec_invalid_subgraph(): +def test_graph_spec_invalid_subgraph(test_graph_spec_dir, test_graph_output_dir): reset_graph_spec_config() - graph_builder = GraphBuilder(graph_specs_dir=get_testing_graph_spec_dir()) - graph_builder.source_data_manager = get_source_data_manager_mock() + graph_builder = GraphBuilder(graph_specs_dir=test_graph_spec_dir, + graph_output_dir=test_graph_output_dir) + graph_builder.ingest_pipeline = get_ingest_pipeline_mock() testing_graph_spec = graph_builder.graph_specs.get('Testing_Graph_3', None) assert testing_graph_spec.graph_version is None with pytest.raises(GraphSpecError): @@ -109,10 +121,11 @@ def test_graph_spec_invalid_subgraph(): # make sure a graph spec with an invalid subgraph version (which is otherwise valid) fails to build -def test_graph_spec_invalid_subgraph_version(): +def test_graph_spec_invalid_subgraph_version(test_graph_spec_dir, test_graph_output_dir): reset_graph_spec_config() - graph_builder = GraphBuilder(graph_specs_dir=get_testing_graph_spec_dir()) - graph_builder.source_data_manager = get_source_data_manager_mock() + graph_builder = GraphBuilder(graph_specs_dir=test_graph_spec_dir, + graph_output_dir=test_graph_output_dir) + graph_builder.ingest_pipeline = get_ingest_pipeline_mock() testing_graph_spec = graph_builder.graph_specs.get('Testing_Graph_4', None) graph_builder.determine_graph_version(testing_graph_spec) assert graph_builder.build_graph(testing_graph_spec) is False diff --git a/tests/test_loaders.py b/tests/test_loaders.py index 06bb1d95..d42047f9 100644 --- a/tests/test_loaders.py +++ b/tests/test_loaders.py @@ -9,7 +9,7 @@ from parsers.GOA.src.loadGOA import GOALoader from parsers.UberGraph.src.loadUG import UGLoader from parsers.FooDB.src.loadFDB import FDBLoader -from Common.utils import GetData +from orion.utils import GetData def test_vp_load(): diff --git a/tests/test_merging.py b/tests/test_merging.py index 36ec3a98..863e673b 100644 --- a/tests/test_merging.py +++ b/tests/test_merging.py @@ -1,18 +1,25 @@ -from Common.merging import GraphMerger, MemoryGraphMerger, DiskGraphMerger -from Common.biolink_constants import * +from orion.merging import GraphMerger, MemoryGraphMerger, DiskGraphMerger, NODE_ENTITY_TYPE, EDGE_ENTITY_TYPE +from orion.biolink_constants import * import os import json +import uuid -TEMP_DIRECTORY = os.path.dirname(os.path.abspath(__file__)) + '/workspace' + +def make_node(node_num, **properties): + node = {'id': f'NODE:{node_num}', 'name': f'Node {node_num}', NODE_TYPES: [NAMED_THING]} + node.update(properties) + return node + + +def make_edge(subj_num, obj_num, predicate='testing:predicate', **properties): + edge = {SUBJECT_ID: f'NODE:{subj_num}', PREDICATE: predicate, OBJECT_ID: f'NODE:{obj_num}'} + edge.update(properties) + return edge def node_property_merging_test(graph_merger: GraphMerger): - test_nodes = [{'id': 'NODE:1', - 'name': 'Node 1', - NODE_TYPES: [NAMED_THING], - SYNONYMS: ['SYN_X', f'SYN_{i}'], - 'testing_prop': [i]} + test_nodes = [make_node(1, **{SYNONYMS: ['SYN_X', f'SYN_{i}'], 'testing_prop': [i]}) for i in range(1, 11)] graph_merger.merge_nodes(test_nodes) @@ -30,21 +37,14 @@ def test_node_property_merging_in_memory(): node_property_merging_test(MemoryGraphMerger()) -def test_node_property_merging_on_disk(): - node_property_merging_test(DiskGraphMerger(temp_directory=TEMP_DIRECTORY, chunk_size=8)) +def test_node_property_merging_on_disk(tmp_path): + node_property_merging_test(DiskGraphMerger(temp_directory=str(tmp_path), chunk_size=8)) def node_merging_counts_test(graph_merger: GraphMerger): - nodes_1_20 = [] - for i in range(1, 21): - node = {'id': f'NODE:{i}', 'name': f'Node {i}', NODE_TYPES: [NAMED_THING]} - nodes_1_20.append(node) - - nodes_6_25 = [] - for i in range(6, 26): - node = {'id': f'NODE:{i}', 'name': f'Node {i}', NODE_TYPES: [NAMED_THING]} - nodes_6_25.append(node) + nodes_1_20 = [make_node(i) for i in range(1, 21)] + nodes_6_25 = [make_node(i) for i in range(6, 26)] input_node_counter = 0 input_node_counter += graph_merger.merge_nodes(nodes_1_20) @@ -57,20 +57,17 @@ def node_merging_counts_test(graph_merger: GraphMerger): def test_node_merging_counts_in_memory(): - node_property_merging_test(MemoryGraphMerger()) + node_merging_counts_test(MemoryGraphMerger()) -def test_node_merging_counts_on_disk(): - node_property_merging_test(DiskGraphMerger(temp_directory=TEMP_DIRECTORY, chunk_size=8)) +def test_node_merging_counts_on_disk(tmp_path): + node_merging_counts_test(DiskGraphMerger(temp_directory=str(tmp_path), chunk_size=8)) def edge_property_merging_test(graph_merger: GraphMerger): - test_edges = [{SUBJECT_ID: f'NODE:1', - PREDICATE: 'testing:predicate', - OBJECT_ID: f'NODE:2', - 'testing_property': [i], - AGGREGATOR_KNOWLEDGE_SOURCES: [f'source_{i}', 'source_X'], - 'abstract_id': f'test_abstract_id'} + test_edges = [make_edge(1, 2, + **{'testing_property': [i], + PUBLICATIONS: [f'PMID:{i}', 'PMID:12345']}) for i in range(1, 11)] graph_merger.merge_edges(test_edges) @@ -78,92 +75,162 @@ def edge_property_merging_test(graph_merger: GraphMerger): assert len(merged_edges) == 1 assert len(merged_edges[0]['testing_property']) == 10 - # AGGREGATOR_KNOWLEDGE_SOURCES is one of the properties - # for which merging should collapse into a set of distinct values instead of appending blindly - # in this case we should've ended up with source_1 through source_10 and source_X = 11 total - assert len(merged_edges[0][AGGREGATOR_KNOWLEDGE_SOURCES]) == 11 + # 'PMID:12345' was included on every edge but the duplicates should've been removed resulting in 11 unique pubs + assert len(merged_edges[0][PUBLICATIONS]) == 11 assert 'id' not in merged_edges[0] -def edge_property_merging_test_edge_id_with_merging(graph_merger: GraphMerger): - # test custom edge merging attributes with edge id addition - test_edges = [{SUBJECT_ID: f'NODE:1', - PREDICATE: 'testing:predicate', - OBJECT_ID: f'NODE:2', - 'testing_property': [i], - AGGREGATOR_KNOWLEDGE_SOURCES: [f'source_{i}', 'source_X'], - 'abstract_id': f'test_abstract_id'} +def edge_merging_attributes_same_value_test(graph_merger: GraphMerger): + # edges with the same edge_merging_attributes(s) value should merge into one + test_edges = [make_edge(1, 2, + **{'testing_property': [i], + 'abstract_id': 'test_abstract_id'}) for i in range(1, 11)] - graph_merger.merge_edges(test_edges, additional_edge_attributes=['abstract_id'], add_edge_id=True) + graph_merger.merge_edges(test_edges) merged_edges = [json.loads(edge) for edge in graph_merger.get_merged_edges_jsonl()] - print(merged_edges) assert len(merged_edges) == 1 assert len(merged_edges[0]['testing_property']) == 10 - assert 'id' in merged_edges[0] - assert merged_edges[0]['id'] is not None - -def edge_property_merging_test_edge_id_without_merging(graph_merger: GraphMerger): - # test custom edge merging attributes with edge id addition - test_edges = [{SUBJECT_ID: f'NODE:1', - PREDICATE: 'testing:predicate', - OBJECT_ID: f'NODE:2', - 'testing_property': [i], - AGGREGATOR_KNOWLEDGE_SOURCES: [f'source_{i}', 'source_X'], - 'abstract_id': f'test_abstract_id_{i}'} + assert merged_edges[0]['abstract_id'] == 'test_abstract_id' + + +def edge_merging_attributes_different_values_test(graph_merger: GraphMerger): + # edges with different edge_merging_attributes values should stay separate + test_edges = [make_edge(1, 2, + **{'testing_property': [i], + 'abstract_id': f'test_abstract_id_{i}'}) for i in range(1, 11)] - graph_merger.merge_edges(test_edges, additional_edge_attributes=['abstract_id'], add_edge_id=True) + graph_merger.merge_edges(test_edges) merged_edges = [json.loads(edge) for edge in graph_merger.get_merged_edges_jsonl()] assert len(merged_edges) == 10 - assert 'id' in merged_edges[0] - assert merged_edges[0]['id'] is not None + assert len(merged_edges[0]['testing_property']) == 1 + + +def add_edge_id_test(graph_merger: GraphMerger): + # edges should get an 'id' field when add_edge_id=True + test_edges = [make_edge(1, 2, **{'testing_property': [i]}) + for i in range(1, 6)] + + graph_merger.merge_edges(test_edges) + merged_edges = [json.loads(edge) for edge in graph_merger.get_merged_edges_jsonl()] + assert len(merged_edges) == 1 + assert merged_edges[0].get('id') is not None + + +def edge_id_unique_for_distinct_edges_test(graph_merger: GraphMerger): + # distinct edges should each get a different id + test_edges = [make_edge(i, i+1, **{'testing_property': [i]}) + for i in range(1, 6)] + + graph_merger.merge_edges(test_edges) + merged_edges = [json.loads(edge) for edge in graph_merger.get_merged_edges_jsonl()] + assert len(merged_edges) == 5 + edge_ids = [edge['id'] for edge in merged_edges] + assert all(eid is not None for eid in edge_ids) + assert len(set(edge_ids)) == 5 + def test_edge_property_merging_in_memory(): edge_property_merging_test(MemoryGraphMerger()) -def test_edge_property_merging_on_disk(): - edge_property_merging_test(DiskGraphMerger(temp_directory=TEMP_DIRECTORY, chunk_size=8)) +def test_edge_property_merging_on_disk(tmp_path): + edge_property_merging_test(DiskGraphMerger(temp_directory=str(tmp_path), chunk_size=8)) + +def test_edge_merging_attributes_same_value_in_memory(): + edge_merging_attributes_same_value_test(MemoryGraphMerger(edge_merging_attributes=['abstract_id'])) + +def test_edge_merging_attributes_same_value_on_disk(tmp_path): + edge_merging_attributes_same_value_test(DiskGraphMerger(temp_directory=str(tmp_path), chunk_size=8, + edge_merging_attributes=['abstract_id'])) + +def test_edge_merging_attributes_different_values_in_memory(): + edge_merging_attributes_different_values_test(MemoryGraphMerger(edge_merging_attributes=['abstract_id'])) -def test_edge_property_merging_in_memory_edge_id_with_merging(): - edge_property_merging_test_edge_id_with_merging(MemoryGraphMerger()) +def test_edge_merging_attributes_different_values_on_disk(tmp_path): + edge_merging_attributes_different_values_test(DiskGraphMerger(temp_directory=str(tmp_path), chunk_size=8, + edge_merging_attributes=['abstract_id'])) -def test_edge_property_merging_in_memory_edge_id_without_merging(): - edge_property_merging_test_edge_id_without_merging(MemoryGraphMerger()) +def test_add_edge_id_in_memory(): + add_edge_id_test(MemoryGraphMerger(add_edge_id=True)) -def test_edge_property_merging_on_disk_edge_id_with_merging(): - edge_property_merging_test_edge_id_with_merging(DiskGraphMerger(temp_directory=TEMP_DIRECTORY, chunk_size=8)) +def test_add_edge_id_on_disk(tmp_path): + add_edge_id_test(DiskGraphMerger(temp_directory=str(tmp_path), chunk_size=8, add_edge_id=True)) + +def test_edge_id_unique_for_distinct_edges_in_memory(): + edge_id_unique_for_distinct_edges_test(MemoryGraphMerger(add_edge_id=True)) + +def test_edge_id_unique_for_distinct_edges_on_disk(tmp_path): + edge_id_unique_for_distinct_edges_test(DiskGraphMerger(temp_directory=str(tmp_path), + chunk_size=8, + add_edge_id=True)) + +def primary_knowledge_source_merging_test(graph_merger: GraphMerger): + # edges with same subject/predicate/object but different primary_knowledge_source should NOT merge + edge_source_a = make_edge(1, 2, **{PRIMARY_KNOWLEDGE_SOURCE: 'source_A', 'prop': [1]}) + edge_source_b = make_edge(1, 2, **{PRIMARY_KNOWLEDGE_SOURCE: 'source_B', 'prop': [2]}) + # edges with same primary_knowledge_source SHOULD merge + edge_source_a_dup = make_edge(1, 2, **{PRIMARY_KNOWLEDGE_SOURCE: 'source_A', 'prop': [3]}) + + graph_merger.merge_edges([edge_source_a, edge_source_b, edge_source_a_dup]) + merged_edges = [json.loads(edge) for edge in graph_merger.get_merged_edges_jsonl()] + assert len(merged_edges) == 2 + + for edge in merged_edges: + if edge[PRIMARY_KNOWLEDGE_SOURCE] == 'source_A': + assert edge['prop'] == [1, 3] + elif edge[PRIMARY_KNOWLEDGE_SOURCE] == 'source_B': + assert edge['prop'] == [2] + + +def primary_knowledge_source_from_retrieval_sources_test(graph_merger: GraphMerger): + # when PRIMARY_KNOWLEDGE_SOURCE is not a top-level property, + # the primary source should be extracted from RETRIEVAL_SOURCES for the merge key + edge_1 = make_edge(1, 2, **{'prop': [1], + RETRIEVAL_SOURCES: [{RETRIEVAL_SOURCE_ID: 'source_A', + RETRIEVAL_SOURCE_ROLE: PRIMARY_KNOWLEDGE_SOURCE}]}) + edge_2 = make_edge(1, 2, **{'prop': [2], + RETRIEVAL_SOURCES: [{RETRIEVAL_SOURCE_ID: 'source_B', + RETRIEVAL_SOURCE_ROLE: PRIMARY_KNOWLEDGE_SOURCE}]}) + edge_3 = make_edge(1, 2, **{'prop': [3], + RETRIEVAL_SOURCES: [{RETRIEVAL_SOURCE_ID: 'source_A', + RETRIEVAL_SOURCE_ROLE: PRIMARY_KNOWLEDGE_SOURCE}]}) + + graph_merger.merge_edges([edge_1, edge_2, edge_3]) + merged_edges = [json.loads(edge) for edge in graph_merger.get_merged_edges_jsonl()] + assert len(merged_edges) == 2 + + +def test_primary_knowledge_source_merging_in_memory(): + primary_knowledge_source_merging_test(MemoryGraphMerger()) + +def test_primary_knowledge_source_merging_on_disk(tmp_path): + primary_knowledge_source_merging_test(DiskGraphMerger(temp_directory=str(tmp_path), chunk_size=8)) + +def test_primary_knowledge_source_from_retrieval_sources_in_memory(): + primary_knowledge_source_from_retrieval_sources_test(MemoryGraphMerger()) + +def test_primary_knowledge_source_from_retrieval_sources_on_disk(tmp_path): + primary_knowledge_source_from_retrieval_sources_test(DiskGraphMerger(temp_directory=str(tmp_path), chunk_size=8)) -def test_edge_property_merging_on_dist_edge_id_without_merging(): - edge_property_merging_test_edge_id_without_merging(DiskGraphMerger(temp_directory=TEMP_DIRECTORY, chunk_size=8)) def edge_merging_counts_test(graph_merger: GraphMerger): - edges_1_10_pred_1 = [] - edges_1_10_pred_2 = [] - for i in range(1, 11): - edge = {SUBJECT_ID: f'NODE:{i}', - PREDICATE: 'testing:predicate_1', - OBJECT_ID: f'NODE:{i+1}', - 'testing_property': [f'{i}']} - edges_1_10_pred_1.append(edge) - edge = {SUBJECT_ID: f'NODE:{i}', - PREDICATE: 'testing:predicate_2', - OBJECT_ID: f'NODE:{i+1}', - 'testing_property': [f'{i}']} - edges_1_10_pred_2.append(edge) - edges_6_20_pred_1 = [] - edges_6_20_pred_2 = [] - for i in range(6, 21): - edge = {SUBJECT_ID: f'NODE:{i}', - PREDICATE: 'testing:predicate_1', - OBJECT_ID: f'NODE:{i+1}', - 'testing_property': [f'{i}']} - edges_6_20_pred_1.append(edge) - edge = {SUBJECT_ID: f'NODE:{i}', - PREDICATE: 'testing:predicate_2', - OBJECT_ID: f'NODE:{i+1}', - 'testing_property': [f'{i}']} - edges_6_20_pred_2.append(edge) + edges_1_10_pred_1 = [make_edge(i, i+1, predicate='testing:predicate_1', + **{'string_property': f'{i}', 'string_list_property': [f'edges_1_10_{i}'], + 'int_property': i}) + for i in range(1, 11)] + edges_1_10_pred_2 = [make_edge(i, i+1, predicate='testing:predicate_2', + **{'string_property': f'{i}', 'string_list_property': [f'edges_1_10_{i}'], + 'int_property': i}) + for i in range(1, 11)] + edges_6_20_pred_1 = [make_edge(i, i+1, predicate='testing:predicate_1', + **{'string_property': f'{i}', 'string_list_property': [f'edges_6_21_{i}'], + 'int_property': 999999}) + for i in range(6, 21)] + edges_6_20_pred_2 = [make_edge(i, i+1, predicate='testing:predicate_2', + **{'string_property': f'{i}', 'string_list_property': [f'edges_6_21_{i}'], + 'int_property': 999999}) + for i in range(6, 21)] input_edge_counter = 0 input_edge_counter += graph_merger.merge_edges(edges_1_10_pred_1) @@ -176,11 +243,12 @@ def edge_merging_counts_test(graph_merger: GraphMerger): assert len(merged_edges) == 40 for edge in merged_edges: if int(edge[SUBJECT_ID].split(':')[1]) < 6: - assert len(edge['testing_property']) == 1 + assert len(edge['string_list_property']) == 1 elif int(edge[SUBJECT_ID].split(':')[1]) < 11: - assert len(edge['testing_property']) == 2 + assert len(edge['string_list_property']) == 2 else: - assert len(edge['testing_property']) == 1 + assert len(edge['string_list_property']) == 1 + assert edge['int_property'] == int(edge[SUBJECT_ID].split(':')[1]) or edge['int_property'] == 999999 assert graph_merger.merged_edge_counter == 10 @@ -189,36 +257,28 @@ def test_edge_merging_counts_in_memory(): edge_merging_counts_test(MemoryGraphMerger()) -def test_edge_merging_counts_on_disk(): - edge_merging_counts_test(DiskGraphMerger(temp_directory=TEMP_DIRECTORY, chunk_size=8)) +def test_edge_merging_counts_on_disk(tmp_path): + edge_merging_counts_test(DiskGraphMerger(temp_directory=str(tmp_path), chunk_size=8)) def test_qualifier_edge_merging(): - test_edges_up = [{SUBJECT_ID: f'NODE:1', - PREDICATE: 'testing:predicate', - OBJECT_ID: f'NODE:2', - SUBJECT_ASPECT_QUALIFIER: f'test_aspect', - SUBJECT_DIRECTION_QUALIFIER: 'up', - 'testing_prop': [i]} + test_edges_up = [make_edge(1, 2, **{SUBJECT_ASPECT_QUALIFIER: 'test_aspect', + SUBJECT_DIRECTION_QUALIFIER: 'up', + 'testing_prop': [i]}) for i in range(1, 16)] - test_edges_down = [{SUBJECT_ID: f'NODE:1', - PREDICATE: 'testing:predicate', - OBJECT_ID: f'NODE:2', - SUBJECT_ASPECT_QUALIFIER: f'test_aspect', - SUBJECT_DIRECTION_QUALIFIER: 'down', - 'testing_prop': [i]} + test_edges_down = [make_edge(1, 2, **{SUBJECT_ASPECT_QUALIFIER: 'test_aspect', + SUBJECT_DIRECTION_QUALIFIER: 'down', + 'testing_prop': [i]}) for i in range(1, 11)] - test_edges_other = [{SUBJECT_ID: f'NODE:1', - PREDICATE: 'testing:predicate', - OBJECT_ID: f'NODE:2', - SUBJECT_ASPECT_QUALIFIER: f'test_aspect', - SUBJECT_DIRECTION_QUALIFIER: 'down', - SPECIES_CONTEXT_QUALIFIER: 'test_species', - 'testing_prop': [i]} + test_edges_other = [make_edge(1, 2, **{SUBJECT_ASPECT_QUALIFIER: 'test_aspect', + SUBJECT_DIRECTION_QUALIFIER: 'down', + SPECIES_CONTEXT_QUALIFIER: 'test_species', + 'testing_prop': [i]}) for i in range(1, 6)] + graph_merger = MemoryGraphMerger() graph_merger.merge_edges(test_edges_up) graph_merger.merge_edges(test_edges_down) @@ -226,6 +286,7 @@ def test_qualifier_edge_merging(): merged_edges = [json.loads(edge) for edge in graph_merger.get_merged_edges_jsonl()] assert len(merged_edges) == 3 + assert graph_merger.merged_edge_counter == 27 passed_tests = 0 for edge in merged_edges: @@ -239,7 +300,196 @@ def test_qualifier_edge_merging(): elif edge[SUBJECT_DIRECTION_QUALIFIER] == 'down' and SPECIES_CONTEXT_QUALIFIER in edge: assert len(edge['testing_prop']) == 5 passed_tests += 1 - assert passed_tests == 3 +def retrieval_source_merging(graph_merger: GraphMerger): + + test_edges = [ + make_edge(1, 2, + **{"int_property": [1, 2, 3, 4, 5], + "string_property": ["a", "b", "c", "d"], + RETRIEVAL_SOURCES: [ + {"id": "rs1", + RETRIEVAL_SOURCE_ID: "source_A", + RETRIEVAL_SOURCE_ROLE: "primary", + "upstream_resource_ids": ["upstream_1", "upstream_2"]}, + {"id": "rs2", + RETRIEVAL_SOURCE_ID: "source_B", + RETRIEVAL_SOURCE_ROLE: "supporting", + "upstream_resource_ids": ["upstream_3"]}, + {"id": "rs3", + RETRIEVAL_SOURCE_ID: "source_C", + RETRIEVAL_SOURCE_ROLE: "aggregator", + "upstream_resource_ids": ["upstream_4", "upstream_5"]} + ]}), + make_edge(1, 2, + **{"int_property": [4, 5, 6, 1], + "string_property": ["c", "d", "e", "f"], + RETRIEVAL_SOURCES: [ + {"id": "rs4", # duplicate, should merge + RETRIEVAL_SOURCE_ID: "source_A", + RETRIEVAL_SOURCE_ROLE: "primary", + "upstream_resource_ids": ["upstream_2", "upstream_6"]}, + {"id": "rs5", + RETRIEVAL_SOURCE_ID: "source_D", + RETRIEVAL_SOURCE_ROLE: "aggregator", + "upstream_resource_ids": ["upstream_7"]}, + {"id": "rs6", # different role, not a duplicate + RETRIEVAL_SOURCE_ID: "source_B", + RETRIEVAL_SOURCE_ROLE: "aggregator", + "upstream_resource_ids": ["upstream_8"]} + ]}), + ] + graph_merger.merge_edges(test_edges) + + merged_edges = [json.loads(edge) for edge in graph_merger.get_merged_edges_jsonl()] + assert len(merged_edges) == 1 + assert merged_edges[0]['int_property'] == [1, 2, 3, 4, 5, 6] + assert merged_edges[0]['string_property'] == ["a", "b", "c", "d", "e", "f"] + # Should have 5 retrieval sources after merging (source_A+primary merged, rest unique) + assert len(merged_edges[0][RETRIEVAL_SOURCES]) == 5 + + # Find the merged source_A+primary retrieval source and verify upstream_resource_ids were merged + source_a_primary = [rs for rs in merged_edges[0][RETRIEVAL_SOURCES] + if rs[RETRIEVAL_SOURCE_ID] == "source_A" + and rs[RETRIEVAL_SOURCE_ROLE] == "primary"][0] + # Should have upstream_1, upstream_2, upstream_6 (upstream_2 deduplicated) + assert len(source_a_primary["upstream_resource_ids"]) == 3 + assert set(source_a_primary["upstream_resource_ids"]) == {"upstream_1", "upstream_2", "upstream_6"} + +def test_retrieval_source_merging_in_memory(): + retrieval_source_merging(MemoryGraphMerger()) + + +def test_retrieval_source_merging_on_disk(tmp_path): + retrieval_source_merging(DiskGraphMerger(temp_directory=str(tmp_path), chunk_size=8)) + + +def single_entity_merge_test(graph_merger: GraphMerger): + for i in range(1, 11): + graph_merger.merge_node(make_node(1, **{'prop': [i]})) + for i in range(1, 6): + graph_merger.merge_edge(make_edge(1, 2, **{'prop': [i]})) + + merged_nodes = [json.loads(n) for n in graph_merger.get_merged_nodes_jsonl()] + assert len(merged_nodes) == 1 + assert len(merged_nodes[0]['prop']) == 10 + + merged_edges = [json.loads(e) for e in graph_merger.get_merged_edges_jsonl()] + assert len(merged_edges) == 1 + assert len(merged_edges[0]['prop']) == 5 + + +def test_single_entity_merge_in_memory(): + single_entity_merge_test(MemoryGraphMerger()) + + +def test_single_entity_merge_on_disk(tmp_path): + single_entity_merge_test(DiskGraphMerger(temp_directory=str(tmp_path), chunk_size=4)) + + +def empty_merge_test(graph_merger: GraphMerger): + graph_merger.merge_nodes([]) + graph_merger.merge_edges([]) + merged_nodes = list(graph_merger.get_merged_nodes_jsonl()) + merged_edges = list(graph_merger.get_merged_edges_jsonl()) + assert len(merged_nodes) == 0 + assert len(merged_edges) == 0 + + +def test_empty_merge_in_memory(): + empty_merge_test(MemoryGraphMerger()) + + +def test_empty_merge_on_disk(tmp_path): + empty_merge_test(DiskGraphMerger(temp_directory=str(tmp_path), chunk_size=8)) + + +def test_disk_merger_temp_file_cleanup(tmp_path): + graph_merger = DiskGraphMerger(temp_directory=str(tmp_path), chunk_size=4) + nodes = [make_node(i) for i in range(1, 11)] + edges = [make_edge(i, i+1) for i in range(1, 11)] + graph_merger.merge_nodes(nodes) + graph_merger.merge_edges(edges) + + # temp files should exist before reading merged results + assert len(graph_merger.temp_file_paths[NODE_ENTITY_TYPE]) > 0 + assert len(graph_merger.temp_file_paths[EDGE_ENTITY_TYPE]) > 0 + temp_node_paths = list(graph_merger.temp_file_paths[NODE_ENTITY_TYPE]) + temp_edge_paths = list(graph_merger.temp_file_paths[EDGE_ENTITY_TYPE]) + for path in temp_node_paths + temp_edge_paths: + assert os.path.exists(path) + + # consume the merged results (this triggers cleanup) + list(graph_merger.get_merged_nodes_jsonl()) + list(graph_merger.get_merged_edges_jsonl()) + + # temp files should be cleaned up + for path in temp_node_paths + temp_edge_paths: + assert not os.path.exists(path) + + +def uuid_edge_id_test(graph_merger: GraphMerger): + # edges should get a valid uuid5 'id' field when edge_id_type='uuid' + test_edges = [make_edge(1, 2, **{'testing_property': [i]}) + for i in range(1, 6)] + + graph_merger.merge_edges(test_edges) + merged_edges = [json.loads(edge) for edge in graph_merger.get_merged_edges_jsonl()] + assert len(merged_edges) == 1 + edge_id = merged_edges[0].get('id') + assert edge_id is not None + # validate it's a proper uuid + parsed = uuid.UUID(edge_id) + assert parsed.version == 5 + + +def uuid_edge_id_unique_for_distinct_edges_test(graph_merger: GraphMerger): + # distinct edges should each get a different uuid + test_edges = [make_edge(i, i+1, **{'testing_property': [i]}) + for i in range(1, 6)] + + graph_merger.merge_edges(test_edges) + merged_edges = [json.loads(edge) for edge in graph_merger.get_merged_edges_jsonl()] + assert len(merged_edges) == 5 + edge_ids = [edge['id'] for edge in merged_edges] + assert all(uuid.UUID(eid).version == 5 for eid in edge_ids) + assert len(set(edge_ids)) == 5 + + +def uuid_edge_id_deterministic_test(graph_merger_factory): + # same edge attributes should produce the same uuid across separate mergers + edge = make_edge(1, 2, **{'testing_property': [1]}) + + merger_1 = graph_merger_factory() + merger_1.merge_edges([edge]) + edges_1 = [json.loads(e) for e in merger_1.get_merged_edges_jsonl()] + + merger_2 = graph_merger_factory() + merger_2.merge_edges([make_edge(1, 2, **{'testing_property': [1]})]) + edges_2 = [json.loads(e) for e in merger_2.get_merged_edges_jsonl()] + + assert edges_1[0]['id'] == edges_2[0]['id'] + + +def test_uuid_edge_id_in_memory(): + uuid_edge_id_test(MemoryGraphMerger(add_edge_id=True, edge_id_type='uuid')) + +def test_uuid_edge_id_on_disk(tmp_path): + uuid_edge_id_test(DiskGraphMerger(temp_directory=str(tmp_path), chunk_size=8, + add_edge_id=True, edge_id_type='uuid')) + +def test_uuid_edge_id_unique_for_distinct_edges_in_memory(): + uuid_edge_id_unique_for_distinct_edges_test(MemoryGraphMerger(add_edge_id=True, edge_id_type='uuid')) + +def test_uuid_edge_id_unique_for_distinct_edges_on_disk(tmp_path): + uuid_edge_id_unique_for_distinct_edges_test(DiskGraphMerger(temp_directory=str(tmp_path), chunk_size=8, + add_edge_id=True, edge_id_type='uuid')) + +def test_uuid_edge_id_deterministic_in_memory(): + uuid_edge_id_deterministic_test(lambda: MemoryGraphMerger(add_edge_id=True, edge_id_type='uuid')) + +def test_uuid_edge_id_deterministic_on_disk(tmp_path): + uuid_edge_id_deterministic_test(lambda: DiskGraphMerger(temp_directory=str(tmp_path), chunk_size=8, + add_edge_id=True, edge_id_type='uuid')) \ No newline at end of file diff --git a/tests/test_metadata_manager.py b/tests/test_metadata_manager.py index 8f726a0a..ab837a5d 100644 --- a/tests/test_metadata_manager.py +++ b/tests/test_metadata_manager.py @@ -1,6 +1,6 @@ import os import pytest -from Common.metadata import SourceMetadata +from orion.metadata import SourceMetadata test_storage_dir = os.path.dirname(os.path.abspath(__file__)) + '/storage' diff --git a/tests/test_normalization.py b/tests/test_normalization.py index ce0bde69..f3e347bd 100644 --- a/tests/test_normalization.py +++ b/tests/test_normalization.py @@ -1,8 +1,8 @@ import pytest -from Common.biolink_constants import * -from Common.normalization import NodeNormalizer, EdgeNormalizer, EdgeNormalizationResult, \ +from orion.biolink_constants import * +from orion.normalization import NodeNormalizer, EdgeNormalizer, EdgeNormalizationResult, \ FALLBACK_EDGE_PREDICATE, CUSTOM_NODE_TYPES -from Common.kgx_file_normalizer import invert_edge +from orion.kgx_file_normalizer import invert_edge INVALID_NODE_TYPE = "testing:Type1" diff --git a/tests/test_source_metadata.py b/tests/test_source_metadata.py index 5455899d..e2949c50 100644 --- a/tests/test_source_metadata.py +++ b/tests/test_source_metadata.py @@ -3,8 +3,8 @@ import pytest -from Common.data_sources import SOURCE_DATA_LOADER_CLASS_IMPORTS, get_data_source_metadata_path -from Common.kgx_metadata import KGXKnowledgeSource +from orion.data_sources import SOURCE_DATA_LOADER_CLASS_IMPORTS, get_data_source_metadata_path +from orion.kgx_metadata import KGXKnowledgeSource ORION_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) diff --git a/uv.lock b/uv.lock new file mode 100644 index 00000000..f2ffe320 --- /dev/null +++ b/uv.lock @@ -0,0 +1,1325 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" + +[[package]] +name = "amqp" +version = "5.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "vine" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/79/fc/ec94a357dfc6683d8c86f8b4cfa5416a4c36b28052ec8260c77aca96a443/amqp-5.3.1.tar.gz", hash = "sha256:cddc00c725449522023bad949f70fff7b48f0b1ade74d170a6f10ab044739432", size = 129013, upload-time = "2024-11-12T19:55:44.051Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/99/fc813cd978842c26c82534010ea849eee9ab3a13ea2b74e95cb9c99e747b/amqp-5.3.1-py3-none-any.whl", hash = "sha256:43b3319e1b4e7d1251833a93d672b4af1e40f3d632d479b98661a95f117880a2", size = 50944, upload-time = "2024-11-12T19:55:41.782Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "attrs" +version = "25.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032, upload-time = "2025-03-13T11:10:22.779Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815, upload-time = "2025-03-13T11:10:21.14Z" }, +] + +[[package]] +name = "beautifulsoup4" +version = "4.12.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "soupsieve" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/ca/824b1195773ce6166d388573fc106ce56d4a805bd7427b624e063596ec58/beautifulsoup4-4.12.3.tar.gz", hash = "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051", size = 581181, upload-time = "2024-01-17T16:53:17.902Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/fe/e8c672695b37eecc5cbf43e1d0638d88d66ba3a44c4d321c796f4e59167f/beautifulsoup4-4.12.3-py3-none-any.whl", hash = "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed", size = 147925, upload-time = "2024-01-17T16:53:12.779Z" }, +] + +[[package]] +name = "billiard" +version = "4.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7c/58/1546c970afcd2a2428b1bfafecf2371d8951cc34b46701bea73f4280989e/billiard-4.2.1.tar.gz", hash = "sha256:12b641b0c539073fc8d3f5b8b7be998956665c4233c7c1fcd66a7e677c4fb36f", size = 155031, upload-time = "2024-09-21T13:40:22.491Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/da/43b15f28fe5f9e027b41c539abc5469052e9d48fd75f8ff094ba2a0ae767/billiard-4.2.1-py3-none-any.whl", hash = "sha256:40b59a4ac8806ba2c2369ea98d876bc6108b051c227baffd928c644d15d8f3cb", size = 86766, upload-time = "2024-09-21T13:40:20.188Z" }, +] + +[[package]] +name = "bmt" +version = "1.4.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "deprecation" }, + { name = "linkml-runtime" }, + { name = "stringcase" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/08/6b/46e2aed81a8fda96cbf811e2c869db7100c8308127f1bd3d63d3f549a67f/bmt-1.4.6.tar.gz", hash = "sha256:ea0dfce825e6f5f6ef4e0bc81bad0696d36b6f4292540db57e730afea69bd7b9", size = 15847, upload-time = "2025-10-06T23:31:34.761Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/4a/548942d033add1b1ace0384605190769c72181bff9a8e70effaa9a8211d3/bmt-1.4.6-py3-none-any.whl", hash = "sha256:f7b9d096c554aeec49996cf9c142d9c3a1dc78cee33d72734e2032fb2aa033b0", size = 16303, upload-time = "2025-10-06T23:31:33.486Z" }, +] + +[[package]] +name = "celery" +version = "5.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "billiard" }, + { name = "click" }, + { name = "click-didyoumean" }, + { name = "click-plugins" }, + { name = "click-repl" }, + { name = "kombu" }, + { name = "python-dateutil" }, + { name = "tzdata" }, + { name = "vine" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8a/9c/cf0bce2cc1c8971bf56629d8f180e4ca35612c7e79e6e432e785261a8be4/celery-5.4.0.tar.gz", hash = "sha256:504a19140e8d3029d5acad88330c541d4c3f64c789d85f94756762d8bca7e706", size = 1575692, upload-time = "2024-04-17T20:29:43.675Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/c4/6a4d3772e5407622feb93dd25c86ce3c0fee746fa822a777a627d56b4f2a/celery-5.4.0-py3-none-any.whl", hash = "sha256:369631eb580cf8c51a82721ec538684994f8277637edde2dfc0dacd73ed97f64", size = 425983, upload-time = "2024-04-17T20:29:39.406Z" }, +] + +[[package]] +name = "certifi" +version = "2025.8.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386, upload-time = "2025-08-03T03:07:47.08Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216, upload-time = "2025-08-03T03:07:45.777Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/83/2d/5fd176ceb9b2fc619e63405525573493ca23441330fcdaee6bef9460e924/charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14", size = 122371, upload-time = "2025-08-09T07:57:28.46Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/5e/14c94999e418d9b87682734589404a25854d5f5d0408df68bc15b6ff54bb/charset_normalizer-3.4.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1", size = 205655, upload-time = "2025-08-09T07:56:08.475Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a8/c6ec5d389672521f644505a257f50544c074cf5fc292d5390331cd6fc9c3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884", size = 146223, upload-time = "2025-08-09T07:56:09.708Z" }, + { url = "https://files.pythonhosted.org/packages/fc/eb/a2ffb08547f4e1e5415fb69eb7db25932c52a52bed371429648db4d84fb1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018", size = 159366, upload-time = "2025-08-09T07:56:11.326Z" }, + { url = "https://files.pythonhosted.org/packages/82/10/0fd19f20c624b278dddaf83b8464dcddc2456cb4b02bb902a6da126b87a1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392", size = 157104, upload-time = "2025-08-09T07:56:13.014Z" }, + { url = "https://files.pythonhosted.org/packages/16/ab/0233c3231af734f5dfcf0844aa9582d5a1466c985bbed6cedab85af9bfe3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f", size = 151830, upload-time = "2025-08-09T07:56:14.428Z" }, + { url = "https://files.pythonhosted.org/packages/ae/02/e29e22b4e02839a0e4a06557b1999d0a47db3567e82989b5bb21f3fbbd9f/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154", size = 148854, upload-time = "2025-08-09T07:56:16.051Z" }, + { url = "https://files.pythonhosted.org/packages/05/6b/e2539a0a4be302b481e8cafb5af8792da8093b486885a1ae4d15d452bcec/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491", size = 160670, upload-time = "2025-08-09T07:56:17.314Z" }, + { url = "https://files.pythonhosted.org/packages/31/e7/883ee5676a2ef217a40ce0bffcc3d0dfbf9e64cbcfbdf822c52981c3304b/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93", size = 158501, upload-time = "2025-08-09T07:56:18.641Z" }, + { url = "https://files.pythonhosted.org/packages/c1/35/6525b21aa0db614cf8b5792d232021dca3df7f90a1944db934efa5d20bb1/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f", size = 153173, upload-time = "2025-08-09T07:56:20.289Z" }, + { url = "https://files.pythonhosted.org/packages/50/ee/f4704bad8201de513fdc8aac1cabc87e38c5818c93857140e06e772b5892/charset_normalizer-3.4.3-cp312-cp312-win32.whl", hash = "sha256:fb6fecfd65564f208cbf0fba07f107fb661bcd1a7c389edbced3f7a493f70e37", size = 99822, upload-time = "2025-08-09T07:56:21.551Z" }, + { url = "https://files.pythonhosted.org/packages/39/f5/3b3836ca6064d0992c58c7561c6b6eee1b3892e9665d650c803bd5614522/charset_normalizer-3.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:86df271bf921c2ee3818f0522e9a5b8092ca2ad8b065ece5d7d9d0e9f4849bcc", size = 107543, upload-time = "2025-08-09T07:56:23.115Z" }, + { url = "https://files.pythonhosted.org/packages/65/ca/2135ac97709b400c7654b4b764daf5c5567c2da45a30cdd20f9eefe2d658/charset_normalizer-3.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe", size = 205326, upload-time = "2025-08-09T07:56:24.721Z" }, + { url = "https://files.pythonhosted.org/packages/71/11/98a04c3c97dd34e49c7d247083af03645ca3730809a5509443f3c37f7c99/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8", size = 146008, upload-time = "2025-08-09T07:56:26.004Z" }, + { url = "https://files.pythonhosted.org/packages/60/f5/4659a4cb3c4ec146bec80c32d8bb16033752574c20b1252ee842a95d1a1e/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9", size = 159196, upload-time = "2025-08-09T07:56:27.25Z" }, + { url = "https://files.pythonhosted.org/packages/86/9e/f552f7a00611f168b9a5865a1414179b2c6de8235a4fa40189f6f79a1753/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31", size = 156819, upload-time = "2025-08-09T07:56:28.515Z" }, + { url = "https://files.pythonhosted.org/packages/7e/95/42aa2156235cbc8fa61208aded06ef46111c4d3f0de233107b3f38631803/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f", size = 151350, upload-time = "2025-08-09T07:56:29.716Z" }, + { url = "https://files.pythonhosted.org/packages/c2/a9/3865b02c56f300a6f94fc631ef54f0a8a29da74fb45a773dfd3dcd380af7/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927", size = 148644, upload-time = "2025-08-09T07:56:30.984Z" }, + { url = "https://files.pythonhosted.org/packages/77/d9/cbcf1a2a5c7d7856f11e7ac2d782aec12bdfea60d104e60e0aa1c97849dc/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9", size = 160468, upload-time = "2025-08-09T07:56:32.252Z" }, + { url = "https://files.pythonhosted.org/packages/f6/42/6f45efee8697b89fda4d50580f292b8f7f9306cb2971d4b53f8914e4d890/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5", size = 158187, upload-time = "2025-08-09T07:56:33.481Z" }, + { url = "https://files.pythonhosted.org/packages/70/99/f1c3bdcfaa9c45b3ce96f70b14f070411366fa19549c1d4832c935d8e2c3/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc", size = 152699, upload-time = "2025-08-09T07:56:34.739Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ad/b0081f2f99a4b194bcbb1934ef3b12aa4d9702ced80a37026b7607c72e58/charset_normalizer-3.4.3-cp313-cp313-win32.whl", hash = "sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce", size = 99580, upload-time = "2025-08-09T07:56:35.981Z" }, + { url = "https://files.pythonhosted.org/packages/9a/8f/ae790790c7b64f925e5c953b924aaa42a243fb778fed9e41f147b2a5715a/charset_normalizer-3.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef", size = 107366, upload-time = "2025-08-09T07:56:37.339Z" }, + { url = "https://files.pythonhosted.org/packages/8e/91/b5a06ad970ddc7a0e513112d40113e834638f4ca1120eb727a249fb2715e/charset_normalizer-3.4.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15", size = 204342, upload-time = "2025-08-09T07:56:38.687Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ec/1edc30a377f0a02689342f214455c3f6c2fbedd896a1d2f856c002fc3062/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db", size = 145995, upload-time = "2025-08-09T07:56:40.048Z" }, + { url = "https://files.pythonhosted.org/packages/17/e5/5e67ab85e6d22b04641acb5399c8684f4d37caf7558a53859f0283a650e9/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d", size = 158640, upload-time = "2025-08-09T07:56:41.311Z" }, + { url = "https://files.pythonhosted.org/packages/f1/e5/38421987f6c697ee3722981289d554957c4be652f963d71c5e46a262e135/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096", size = 156636, upload-time = "2025-08-09T07:56:43.195Z" }, + { url = "https://files.pythonhosted.org/packages/a0/e4/5a075de8daa3ec0745a9a3b54467e0c2967daaaf2cec04c845f73493e9a1/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa", size = 150939, upload-time = "2025-08-09T07:56:44.819Z" }, + { url = "https://files.pythonhosted.org/packages/02/f7/3611b32318b30974131db62b4043f335861d4d9b49adc6d57c1149cc49d4/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049", size = 148580, upload-time = "2025-08-09T07:56:46.684Z" }, + { url = "https://files.pythonhosted.org/packages/7e/61/19b36f4bd67f2793ab6a99b979b4e4f3d8fc754cbdffb805335df4337126/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0", size = 159870, upload-time = "2025-08-09T07:56:47.941Z" }, + { url = "https://files.pythonhosted.org/packages/06/57/84722eefdd338c04cf3030ada66889298eaedf3e7a30a624201e0cbe424a/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92", size = 157797, upload-time = "2025-08-09T07:56:49.756Z" }, + { url = "https://files.pythonhosted.org/packages/72/2a/aff5dd112b2f14bcc3462c312dce5445806bfc8ab3a7328555da95330e4b/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16", size = 152224, upload-time = "2025-08-09T07:56:51.369Z" }, + { url = "https://files.pythonhosted.org/packages/b7/8c/9839225320046ed279c6e839d51f028342eb77c91c89b8ef2549f951f3ec/charset_normalizer-3.4.3-cp314-cp314-win32.whl", hash = "sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce", size = 100086, upload-time = "2025-08-09T07:56:52.722Z" }, + { url = "https://files.pythonhosted.org/packages/ee/7a/36fbcf646e41f710ce0a563c1c9a343c6edf9be80786edeb15b6f62e17db/charset_normalizer-3.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c", size = 107400, upload-time = "2025-08-09T07:56:55.172Z" }, + { url = "https://files.pythonhosted.org/packages/8a/1f/f041989e93b001bc4e44bb1669ccdcf54d3f00e628229a85b08d330615c5/charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", size = 53175, upload-time = "2025-08-09T07:57:26.864Z" }, +] + +[[package]] +name = "click" +version = "8.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" }, +] + +[[package]] +name = "click-didyoumean" +version = "0.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/30/ce/217289b77c590ea1e7c24242d9ddd6e249e52c795ff10fac2c50062c48cb/click_didyoumean-0.3.1.tar.gz", hash = "sha256:4f82fdff0dbe64ef8ab2279bd6aa3f6a99c3b28c05aa09cbfc07c9d7fbb5a463", size = 3089, upload-time = "2024-03-24T08:22:07.499Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/5b/974430b5ffdb7a4f1941d13d83c64a0395114503cc357c6b9ae4ce5047ed/click_didyoumean-0.3.1-py3-none-any.whl", hash = "sha256:5c4bb6007cfea5f2fd6583a2fb6701a22a41eb98957e63d0fac41c10e7c3117c", size = 3631, upload-time = "2024-03-24T08:22:06.356Z" }, +] + +[[package]] +name = "click-plugins" +version = "1.1.1.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c3/a4/34847b59150da33690a36da3681d6bbc2ec14ee9a846bc30a6746e5984e4/click_plugins-1.1.1.2.tar.gz", hash = "sha256:d7af3984a99d243c131aa1a828331e7630f4a88a9741fd05c927b204bcf92261", size = 8343, upload-time = "2025-06-25T00:47:37.555Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/9a/2abecb28ae875e39c8cad711eb1186d8d14eab564705325e77e4e6ab9ae5/click_plugins-1.1.1.2-py2.py3-none-any.whl", hash = "sha256:008d65743833ffc1f5417bf0e78e8d2c23aab04d9745ba817bd3e71b0feb6aa6", size = 11051, upload-time = "2025-06-25T00:47:36.731Z" }, +] + +[[package]] +name = "click-repl" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "prompt-toolkit" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cb/a2/57f4ac79838cfae6912f997b4d1a64a858fb0c86d7fcaae6f7b58d267fca/click-repl-0.3.0.tar.gz", hash = "sha256:17849c23dba3d667247dc4defe1757fff98694e90fe37474f3feebb69ced26a9", size = 10449, upload-time = "2023-06-15T12:43:51.141Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/40/9d857001228658f0d59e97ebd4c346fe73e138c6de1bce61dc568a57c7f8/click_repl-0.3.0-py3-none-any.whl", hash = "sha256:fb7e06deb8da8de86180a33a9da97ac316751c094c6899382da7feeeeb51b812", size = 10289, upload-time = "2023-06-15T12:43:48.626Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "curies" +version = "0.7.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "pytrie" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/65/d0/143fa90df55e5e5b1a0408f43a2e84c7f76293083a7ba63546b33cff31f6/curies-0.7.9.tar.gz", hash = "sha256:3b63c5fea7b0e967629a3a384b1a8c59b56c503487c1dcbacddeab59e25db4d8", size = 241312, upload-time = "2024-04-03T08:13:59.96Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/ce/464bad6035f6d681485dc3534a5e5c5cd69615770a02bbd2572bc7cb362b/curies-0.7.9-py3-none-any.whl", hash = "sha256:e4c5beb91642376953c94db0ee2fb5d2b011c3b16749516436114ba61442f260", size = 45810, upload-time = "2024-04-03T08:13:58.141Z" }, +] + +[[package]] +name = "deprecated" +version = "1.2.18" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/97/06afe62762c9a8a86af0cfb7bfdab22a43ad17138b07af5b1a58442690a2/deprecated-1.2.18.tar.gz", hash = "sha256:422b6f6d859da6f2ef57857761bfb392480502a64c3028ca9bbe86085d72115d", size = 2928744, upload-time = "2025-01-27T10:46:25.7Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/c6/ac0b6c1e2d138f1002bcf799d330bd6d85084fece321e662a14223794041/Deprecated-1.2.18-py2.py3-none-any.whl", hash = "sha256:bd5011788200372a32418f888e326a09ff80d0214bd961147cfed01b5c018eec", size = 9998, upload-time = "2025-01-27T10:46:09.186Z" }, +] + +[[package]] +name = "deprecation" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/d3/8ae2869247df154b64c1884d7346d412fed0c49df84db635aab2d1c40e62/deprecation-2.1.0.tar.gz", hash = "sha256:72b3bde64e5d778694b0cf68178aed03d15e15477116add3fb773e581f9518ff", size = 173788, upload-time = "2020-04-20T14:23:38.738Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/c3/253a89ee03fc9b9682f1541728eb66db7db22148cd94f89ab22528cd1e1b/deprecation-2.1.0-py2.py3-none-any.whl", hash = "sha256:a10811591210e1fb0e768a8c25517cabeabcba6f0bf96564f8ff45189f90b14a", size = 11178, upload-time = "2020-04-20T14:23:36.581Z" }, +] + +[[package]] +name = "et-xmlfile" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/38/af70d7ab1ae9d4da450eeec1fa3918940a5fafb9055e934af8d6eb0c2313/et_xmlfile-2.0.0.tar.gz", hash = "sha256:dab3f4764309081ce75662649be815c4c9081e88f0837825f90fd28317d4da54", size = 17234, upload-time = "2024-10-25T17:25:40.039Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/8b/5fe2cc11fee489817272089c4203e679c63b570a5aaeb18d852ae3cbba6a/et_xmlfile-2.0.0-py3-none-any.whl", hash = "sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa", size = 18059, upload-time = "2024-10-25T17:25:39.051Z" }, +] + +[[package]] +name = "hbreader" +version = "0.9.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/66/3a649ce125e03d1d43727a8b833cd211f0b9fe54a7e5be326f50d6f1d951/hbreader-0.9.1.tar.gz", hash = "sha256:d2c132f8ba6276d794c66224c3297cec25c8079d0a4cf019c061611e0a3b94fa", size = 19016, upload-time = "2021-02-25T19:22:32.799Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/24/61844afbf38acf419e01ca2639f7bd079584523d34471acbc4152ee991c5/hbreader-0.9.1-py3-none-any.whl", hash = "sha256:9a6e76c9d1afc1b977374a5dc430a1ebb0ea0488205546d4678d6e31cc5f6801", size = 7595, upload-time = "2021-02-25T19:22:31.944Z" }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, +] + +[[package]] +name = "intermine" +version = "1.13.0" +source = { git = "https://github.com/EvanDietzMorris/intermine-ws-python.git#c993b9ec3ce76b29601ef82744ebb3fc73500c3e" } + +[[package]] +name = "json-flattener" +version = "0.1.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/77/b00e46d904818826275661a690532d3a3a43a4ded0264b2d7fcdb5c0feea/json_flattener-0.1.9.tar.gz", hash = "sha256:84cf8523045ffb124301a602602201665fcb003a171ece87e6f46ed02f7f0c15", size = 11479, upload-time = "2022-02-26T01:36:04.545Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/cc/7fbd75d3362e939eb98bcf9bd22f3f7df8c237a85148899ed3d38e5614e5/json_flattener-0.1.9-py3-none-any.whl", hash = "sha256:6b027746f08bf37a75270f30c6690c7149d5f704d8af1740c346a3a1236bc941", size = 10799, upload-time = "2022-02-26T01:36:03.06Z" }, +] + +[[package]] +name = "jsonasobj2" +version = "1.0.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "hbreader" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/67/3a/feb245b755f7a47a0df4f30be645e8485d15ff13d0c95e018e4505a8811f/jsonasobj2-1.0.4.tar.gz", hash = "sha256:f50b1668ef478004aa487b2d2d094c304e5cb6b79337809f4a1f2975cc7fbb4e", size = 95522, upload-time = "2021-06-02T17:43:28.39Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/90/0d93963711f811efe528e3cead2f2bfb78c196df74d8a24fe8d655288e50/jsonasobj2-1.0.4-py3-none-any.whl", hash = "sha256:12e86f86324d54fcf60632db94ea74488d5314e3da554c994fe1e2c6f29acb79", size = 6324, upload-time = "2021-06-02T17:43:27.126Z" }, +] + +[[package]] +name = "jsonlines" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/35/87/bcda8e46c88d0e34cad2f09ee2d0c7f5957bccdb9791b0b934ec84d84be4/jsonlines-4.0.0.tar.gz", hash = "sha256:0c6d2c09117550c089995247f605ae4cf77dd1533041d366351f6f298822ea74", size = 11359, upload-time = "2023-09-01T12:34:44.187Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/62/d9ba6323b9202dd2fe166beab8a86d29465c41a0288cbe229fac60c1ab8d/jsonlines-4.0.0-py3-none-any.whl", hash = "sha256:185b334ff2ca5a91362993f42e83588a360cf95ce4b71a73548502bda52a7c55", size = 8701, upload-time = "2023-09-01T12:34:42.563Z" }, +] + +[[package]] +name = "jsonschema" +version = "4.25.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/69/f7185de793a29082a9f3c7728268ffb31cb5095131a9c139a74078e27336/jsonschema-4.25.1.tar.gz", hash = "sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85", size = 357342, upload-time = "2025-08-18T17:03:50.038Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl", hash = "sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63", size = 90040, upload-time = "2025-08-18T17:03:48.373Z" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bf/ce/46fbd9c8119cfc3581ee5643ea49464d168028cfb5caff5fc0596d0cf914/jsonschema_specifications-2025.4.1.tar.gz", hash = "sha256:630159c9f4dbea161a6a2205c3011cc4f18ff381b189fff48bb39b9bf26ae608", size = 15513, upload-time = "2025-04-23T12:34:07.418Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/0e/b27cdbaccf30b890c40ed1da9fd4a3593a5cf94dae54fb34f8a4b74fcd3f/jsonschema_specifications-2025.4.1-py3-none-any.whl", hash = "sha256:4653bffbd6584f7de83a67e0d620ef16900b390ddc7939d56684d6c81e33f1af", size = 18437, upload-time = "2025-04-23T12:34:05.422Z" }, +] + +[[package]] +name = "kombu" +version = "5.5.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "amqp" }, + { name = "packaging" }, + { name = "tzdata" }, + { name = "vine" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0f/d3/5ff936d8319ac86b9c409f1501b07c426e6ad41966fedace9ef1b966e23f/kombu-5.5.4.tar.gz", hash = "sha256:886600168275ebeada93b888e831352fe578168342f0d1d5833d88ba0d847363", size = 461992, upload-time = "2025-06-01T10:19:22.281Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/70/a07dcf4f62598c8ad579df241af55ced65bed76e42e45d3c368a6d82dbc1/kombu-5.5.4-py3-none-any.whl", hash = "sha256:a12ed0557c238897d8e518f1d1fdf84bd1516c5e305af2dacd85c2015115feb8", size = 210034, upload-time = "2025-06-01T10:19:20.436Z" }, +] + +[[package]] +name = "linkml-runtime" +version = "1.9.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "curies" }, + { name = "deprecated" }, + { name = "hbreader" }, + { name = "json-flattener" }, + { name = "jsonasobj2" }, + { name = "jsonschema" }, + { name = "prefixcommons" }, + { name = "prefixmaps" }, + { name = "pydantic" }, + { name = "pyyaml" }, + { name = "rdflib" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e0/38/38ac19a5b81982f03709cda0a008327dc775d11f008d0cbdc0f2a5389e24/linkml_runtime-1.9.5.tar.gz", hash = "sha256:78dc1383adf11ad5f20bb11b6adde56ed566fbd2429a292d57699ad4596c738a", size = 480288, upload-time = "2025-08-15T22:22:51.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/28/cdcbe1f0521a98b891dd30249513eef1ddcc7bb406be953b4a8d7825e68f/linkml_runtime-1.9.5-py3-none-any.whl", hash = "sha256:fece3e8aa25a4246165c6528b6a7fe83a929b985d2ce1951cc8a0f4da1a30b90", size = 576405, upload-time = "2025-08-15T22:22:49.264Z" }, +] + +[[package]] +name = "mysql-connector-python" +version = "9.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/02/77/2b45e6460d05b1f1b7a4c8eb79a50440b4417971973bb78c9ef6cad630a6/mysql_connector_python-9.4.0.tar.gz", hash = "sha256:d111360332ae78933daf3d48ff497b70739aa292ab0017791a33e826234e743b", size = 12185532, upload-time = "2025-07-22T08:02:05.788Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/7c/a543fb17c2dfa6be8548dfdc5879a0c7924cd5d1c79056c48472bb8fe858/mysql_connector_python-9.4.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:4efa3898a24aba6a4bfdbf7c1f5023c78acca3150d72cc91199cca2ccd22f76f", size = 17503693, upload-time = "2025-07-22T07:58:08.96Z" }, + { url = "https://files.pythonhosted.org/packages/cb/6e/c22fbee05f5cfd6ba76155b6d45f6261d8d4c1e36e23de04e7f25fbd01a4/mysql_connector_python-9.4.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:665c13e7402235162e5b7a2bfdee5895192121b64ea455c90a81edac6a48ede5", size = 18371987, upload-time = "2025-07-22T07:58:13.273Z" }, + { url = "https://files.pythonhosted.org/packages/b4/fd/f426f5f35a3d3180c7f84d1f96b4631be2574df94ca1156adab8618b236c/mysql_connector_python-9.4.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:815aa6cad0f351c1223ef345781a538f2e5e44ef405fdb3851eb322bd9c4ca2b", size = 33516214, upload-time = "2025-07-22T07:58:18.967Z" }, + { url = "https://files.pythonhosted.org/packages/45/5a/1b053ae80b43cd3ccebc4bb99a98826969b3b0f8adebdcc2530750ad76ed/mysql_connector_python-9.4.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:b3436a2c8c0ec7052932213e8d01882e6eb069dbab33402e685409084b133a1c", size = 33918565, upload-time = "2025-07-22T07:58:25.28Z" }, + { url = "https://files.pythonhosted.org/packages/cb/69/36b989de675d98ba8ff7d45c96c30c699865c657046f2e32db14e78f13d9/mysql_connector_python-9.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:57b0c224676946b70548c56798d5023f65afa1ba5b8ac9f04a143d27976c7029", size = 16392563, upload-time = "2025-07-22T07:58:29.623Z" }, + { url = "https://files.pythonhosted.org/packages/79/e2/13036479cd1070d1080cee747de6c96bd6fbb021b736dd3ccef2b19016c8/mysql_connector_python-9.4.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:fde3bbffb5270a4b02077029914e6a9d2ec08f67d8375b4111432a2778e7540b", size = 17503749, upload-time = "2025-07-22T07:58:33.649Z" }, + { url = "https://files.pythonhosted.org/packages/31/df/b89e6551b91332716d384dcc3223e1f8065902209dcd9e477a3df80154f7/mysql_connector_python-9.4.0-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:25f77ad7d845df3b5a5a3a6a8d1fed68248dc418a6938a371d1ddaaab6b9a8e3", size = 18372145, upload-time = "2025-07-22T07:58:37.384Z" }, + { url = "https://files.pythonhosted.org/packages/07/bd/af0de40a01d5cb4df19318cc018e64666f2b7fa89bffa1ab5b35337aae2c/mysql_connector_python-9.4.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:227dd420c71e6d4788d52d98f298e563f16b6853577e5ade4bd82d644257c812", size = 33516503, upload-time = "2025-07-22T07:58:41.987Z" }, + { url = "https://files.pythonhosted.org/packages/d1/9b/712053216fcbe695e519ecb1035ffd767c2de9f51ccba15078537c99d6fa/mysql_connector_python-9.4.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:5163381a312d38122eded2197eb5cd7ccf1a5c5881d4e7a6de10d6ea314d088e", size = 33918904, upload-time = "2025-07-22T07:58:46.796Z" }, + { url = "https://files.pythonhosted.org/packages/64/15/cbd996d425c59811849f3c1d1b1dae089a1ae18c4acd4d8de2b847b772df/mysql_connector_python-9.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:c727cb1f82b40c9aaa7a15ab5cf0a7f87c5d8dce32eab5ff2530a4aa6054e7df", size = 16392566, upload-time = "2025-07-22T07:58:50.223Z" }, + { url = "https://files.pythonhosted.org/packages/36/34/b6165e15fd45a8deb00932d8e7d823de7650270873b4044c4db6688e1d8f/mysql_connector_python-9.4.0-py2.py3-none-any.whl", hash = "sha256:56e679169c704dab279b176fab2a9ee32d2c632a866c0f7cd48a8a1e2cf802c4", size = 406574, upload-time = "2025-07-22T07:59:08.394Z" }, +] + +[[package]] +name = "neo4j" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytz" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/34/485ab7c0252bd5d9c9ff0672f61153a8007490af2069f664d8766709c7ba/neo4j-6.0.2.tar.gz", hash = "sha256:c98734c855b457e7a976424dc04446d652838d00907d250d6e9a595e88892378", size = 240139, upload-time = "2025-10-02T11:31:06.724Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/4e/11813da186859070b0512e8071dac4796624ac4dc28e25e7c530df730d23/neo4j-6.0.2-py3-none-any.whl", hash = "sha256:dc3fc1c99f6da2293d9deefead1e31dd7429bbb513eccf96e4134b7dbf770243", size = 325761, upload-time = "2025-10-02T11:31:04.855Z" }, +] + +[[package]] +name = "numpy" +version = "2.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/37/7d/3fec4199c5ffb892bed55cff901e4f39a58c81df9c44c280499e92cad264/numpy-2.3.2.tar.gz", hash = "sha256:e0486a11ec30cdecb53f184d496d1c6a20786c81e55e41640270130056f8ee48", size = 20489306, upload-time = "2025-07-24T21:32:07.553Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/6d/745dd1c1c5c284d17725e5c802ca4d45cfc6803519d777f087b71c9f4069/numpy-2.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bc3186bea41fae9d8e90c2b4fb5f0a1f5a690682da79b92574d63f56b529080b", size = 20956420, upload-time = "2025-07-24T20:28:18.002Z" }, + { url = "https://files.pythonhosted.org/packages/bc/96/e7b533ea5740641dd62b07a790af5d9d8fec36000b8e2d0472bd7574105f/numpy-2.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2f4f0215edb189048a3c03bd5b19345bdfa7b45a7a6f72ae5945d2a28272727f", size = 14184660, upload-time = "2025-07-24T20:28:39.522Z" }, + { url = "https://files.pythonhosted.org/packages/2b/53/102c6122db45a62aa20d1b18c9986f67e6b97e0d6fbc1ae13e3e4c84430c/numpy-2.3.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:8b1224a734cd509f70816455c3cffe13a4f599b1bf7130f913ba0e2c0b2006c0", size = 5113382, upload-time = "2025-07-24T20:28:48.544Z" }, + { url = "https://files.pythonhosted.org/packages/2b/21/376257efcbf63e624250717e82b4fae93d60178f09eb03ed766dbb48ec9c/numpy-2.3.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:3dcf02866b977a38ba3ec10215220609ab9667378a9e2150615673f3ffd6c73b", size = 6647258, upload-time = "2025-07-24T20:28:59.104Z" }, + { url = "https://files.pythonhosted.org/packages/91/ba/f4ebf257f08affa464fe6036e13f2bf9d4642a40228781dc1235da81be9f/numpy-2.3.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:572d5512df5470f50ada8d1972c5f1082d9a0b7aa5944db8084077570cf98370", size = 14281409, upload-time = "2025-07-24T20:40:30.298Z" }, + { url = "https://files.pythonhosted.org/packages/59/ef/f96536f1df42c668cbacb727a8c6da7afc9c05ece6d558927fb1722693e1/numpy-2.3.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8145dd6d10df13c559d1e4314df29695613575183fa2e2d11fac4c208c8a1f73", size = 16641317, upload-time = "2025-07-24T20:40:56.625Z" }, + { url = "https://files.pythonhosted.org/packages/f6/a7/af813a7b4f9a42f498dde8a4c6fcbff8100eed00182cc91dbaf095645f38/numpy-2.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:103ea7063fa624af04a791c39f97070bf93b96d7af7eb23530cd087dc8dbe9dc", size = 16056262, upload-time = "2025-07-24T20:41:20.797Z" }, + { url = "https://files.pythonhosted.org/packages/8b/5d/41c4ef8404caaa7f05ed1cfb06afe16a25895260eacbd29b4d84dff2920b/numpy-2.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc927d7f289d14f5e037be917539620603294454130b6de200091e23d27dc9be", size = 18579342, upload-time = "2025-07-24T20:41:50.753Z" }, + { url = "https://files.pythonhosted.org/packages/a1/4f/9950e44c5a11636f4a3af6e825ec23003475cc9a466edb7a759ed3ea63bd/numpy-2.3.2-cp312-cp312-win32.whl", hash = "sha256:d95f59afe7f808c103be692175008bab926b59309ade3e6d25009e9a171f7036", size = 6320610, upload-time = "2025-07-24T20:42:01.551Z" }, + { url = "https://files.pythonhosted.org/packages/7c/2f/244643a5ce54a94f0a9a2ab578189c061e4a87c002e037b0829dd77293b6/numpy-2.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:9e196ade2400c0c737d93465327d1ae7c06c7cb8a1756121ebf54b06ca183c7f", size = 12786292, upload-time = "2025-07-24T20:42:20.738Z" }, + { url = "https://files.pythonhosted.org/packages/54/cd/7b5f49d5d78db7badab22d8323c1b6ae458fbf86c4fdfa194ab3cd4eb39b/numpy-2.3.2-cp312-cp312-win_arm64.whl", hash = "sha256:ee807923782faaf60d0d7331f5e86da7d5e3079e28b291973c545476c2b00d07", size = 10194071, upload-time = "2025-07-24T20:42:36.657Z" }, + { url = "https://files.pythonhosted.org/packages/1c/c0/c6bb172c916b00700ed3bf71cb56175fd1f7dbecebf8353545d0b5519f6c/numpy-2.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c8d9727f5316a256425892b043736d63e89ed15bbfe6556c5ff4d9d4448ff3b3", size = 20949074, upload-time = "2025-07-24T20:43:07.813Z" }, + { url = "https://files.pythonhosted.org/packages/20/4e/c116466d22acaf4573e58421c956c6076dc526e24a6be0903219775d862e/numpy-2.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:efc81393f25f14d11c9d161e46e6ee348637c0a1e8a54bf9dedc472a3fae993b", size = 14177311, upload-time = "2025-07-24T20:43:29.335Z" }, + { url = "https://files.pythonhosted.org/packages/78/45/d4698c182895af189c463fc91d70805d455a227261d950e4e0f1310c2550/numpy-2.3.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:dd937f088a2df683cbb79dda9a772b62a3e5a8a7e76690612c2737f38c6ef1b6", size = 5106022, upload-time = "2025-07-24T20:43:37.999Z" }, + { url = "https://files.pythonhosted.org/packages/9f/76/3e6880fef4420179309dba72a8c11f6166c431cf6dee54c577af8906f914/numpy-2.3.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:11e58218c0c46c80509186e460d79fbdc9ca1eb8d8aee39d8f2dc768eb781089", size = 6640135, upload-time = "2025-07-24T20:43:49.28Z" }, + { url = "https://files.pythonhosted.org/packages/34/fa/87ff7f25b3c4ce9085a62554460b7db686fef1e0207e8977795c7b7d7ba1/numpy-2.3.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5ad4ebcb683a1f99f4f392cc522ee20a18b2bb12a2c1c42c3d48d5a1adc9d3d2", size = 14278147, upload-time = "2025-07-24T20:44:10.328Z" }, + { url = "https://files.pythonhosted.org/packages/1d/0f/571b2c7a3833ae419fe69ff7b479a78d313581785203cc70a8db90121b9a/numpy-2.3.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:938065908d1d869c7d75d8ec45f735a034771c6ea07088867f713d1cd3bbbe4f", size = 16635989, upload-time = "2025-07-24T20:44:34.88Z" }, + { url = "https://files.pythonhosted.org/packages/24/5a/84ae8dca9c9a4c592fe11340b36a86ffa9fd3e40513198daf8a97839345c/numpy-2.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:66459dccc65d8ec98cc7df61307b64bf9e08101f9598755d42d8ae65d9a7a6ee", size = 16053052, upload-time = "2025-07-24T20:44:58.872Z" }, + { url = "https://files.pythonhosted.org/packages/57/7c/e5725d99a9133b9813fcf148d3f858df98511686e853169dbaf63aec6097/numpy-2.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a7af9ed2aa9ec5950daf05bb11abc4076a108bd3c7db9aa7251d5f107079b6a6", size = 18577955, upload-time = "2025-07-24T20:45:26.714Z" }, + { url = "https://files.pythonhosted.org/packages/ae/11/7c546fcf42145f29b71e4d6f429e96d8d68e5a7ba1830b2e68d7418f0bbd/numpy-2.3.2-cp313-cp313-win32.whl", hash = "sha256:906a30249315f9c8e17b085cc5f87d3f369b35fedd0051d4a84686967bdbbd0b", size = 6311843, upload-time = "2025-07-24T20:49:24.444Z" }, + { url = "https://files.pythonhosted.org/packages/aa/6f/a428fd1cb7ed39b4280d057720fed5121b0d7754fd2a9768640160f5517b/numpy-2.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:c63d95dc9d67b676e9108fe0d2182987ccb0f11933c1e8959f42fa0da8d4fa56", size = 12782876, upload-time = "2025-07-24T20:49:43.227Z" }, + { url = "https://files.pythonhosted.org/packages/65/85/4ea455c9040a12595fb6c43f2c217257c7b52dd0ba332c6a6c1d28b289fe/numpy-2.3.2-cp313-cp313-win_arm64.whl", hash = "sha256:b05a89f2fb84d21235f93de47129dd4f11c16f64c87c33f5e284e6a3a54e43f2", size = 10192786, upload-time = "2025-07-24T20:49:59.443Z" }, + { url = "https://files.pythonhosted.org/packages/80/23/8278f40282d10c3f258ec3ff1b103d4994bcad78b0cba9208317f6bb73da/numpy-2.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4e6ecfeddfa83b02318f4d84acf15fbdbf9ded18e46989a15a8b6995dfbf85ab", size = 21047395, upload-time = "2025-07-24T20:45:58.821Z" }, + { url = "https://files.pythonhosted.org/packages/1f/2d/624f2ce4a5df52628b4ccd16a4f9437b37c35f4f8a50d00e962aae6efd7a/numpy-2.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:508b0eada3eded10a3b55725b40806a4b855961040180028f52580c4729916a2", size = 14300374, upload-time = "2025-07-24T20:46:20.207Z" }, + { url = "https://files.pythonhosted.org/packages/f6/62/ff1e512cdbb829b80a6bd08318a58698867bca0ca2499d101b4af063ee97/numpy-2.3.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:754d6755d9a7588bdc6ac47dc4ee97867271b17cee39cb87aef079574366db0a", size = 5228864, upload-time = "2025-07-24T20:46:30.58Z" }, + { url = "https://files.pythonhosted.org/packages/7d/8e/74bc18078fff03192d4032cfa99d5a5ca937807136d6f5790ce07ca53515/numpy-2.3.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:a9f66e7d2b2d7712410d3bc5684149040ef5f19856f20277cd17ea83e5006286", size = 6737533, upload-time = "2025-07-24T20:46:46.111Z" }, + { url = "https://files.pythonhosted.org/packages/19/ea/0731efe2c9073ccca5698ef6a8c3667c4cf4eea53fcdcd0b50140aba03bc/numpy-2.3.2-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de6ea4e5a65d5a90c7d286ddff2b87f3f4ad61faa3db8dabe936b34c2275b6f8", size = 14352007, upload-time = "2025-07-24T20:47:07.1Z" }, + { url = "https://files.pythonhosted.org/packages/cf/90/36be0865f16dfed20f4bc7f75235b963d5939707d4b591f086777412ff7b/numpy-2.3.2-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3ef07ec8cbc8fc9e369c8dcd52019510c12da4de81367d8b20bc692aa07573a", size = 16701914, upload-time = "2025-07-24T20:47:32.459Z" }, + { url = "https://files.pythonhosted.org/packages/94/30/06cd055e24cb6c38e5989a9e747042b4e723535758e6153f11afea88c01b/numpy-2.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:27c9f90e7481275c7800dc9c24b7cc40ace3fdb970ae4d21eaff983a32f70c91", size = 16132708, upload-time = "2025-07-24T20:47:58.129Z" }, + { url = "https://files.pythonhosted.org/packages/9a/14/ecede608ea73e58267fd7cb78f42341b3b37ba576e778a1a06baffbe585c/numpy-2.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:07b62978075b67eee4065b166d000d457c82a1efe726cce608b9db9dd66a73a5", size = 18651678, upload-time = "2025-07-24T20:48:25.402Z" }, + { url = "https://files.pythonhosted.org/packages/40/f3/2fe6066b8d07c3685509bc24d56386534c008b462a488b7f503ba82b8923/numpy-2.3.2-cp313-cp313t-win32.whl", hash = "sha256:c771cfac34a4f2c0de8e8c97312d07d64fd8f8ed45bc9f5726a7e947270152b5", size = 6441832, upload-time = "2025-07-24T20:48:37.181Z" }, + { url = "https://files.pythonhosted.org/packages/0b/ba/0937d66d05204d8f28630c9c60bc3eda68824abde4cf756c4d6aad03b0c6/numpy-2.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:72dbebb2dcc8305c431b2836bcc66af967df91be793d63a24e3d9b741374c450", size = 12927049, upload-time = "2025-07-24T20:48:56.24Z" }, + { url = "https://files.pythonhosted.org/packages/e9/ed/13542dd59c104d5e654dfa2ac282c199ba64846a74c2c4bcdbc3a0f75df1/numpy-2.3.2-cp313-cp313t-win_arm64.whl", hash = "sha256:72c6df2267e926a6d5286b0a6d556ebe49eae261062059317837fda12ddf0c1a", size = 10262935, upload-time = "2025-07-24T20:49:13.136Z" }, + { url = "https://files.pythonhosted.org/packages/c9/7c/7659048aaf498f7611b783e000c7268fcc4dcf0ce21cd10aad7b2e8f9591/numpy-2.3.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:448a66d052d0cf14ce9865d159bfc403282c9bc7bb2a31b03cc18b651eca8b1a", size = 20950906, upload-time = "2025-07-24T20:50:30.346Z" }, + { url = "https://files.pythonhosted.org/packages/80/db/984bea9d4ddf7112a04cfdfb22b1050af5757864cfffe8e09e44b7f11a10/numpy-2.3.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:546aaf78e81b4081b2eba1d105c3b34064783027a06b3ab20b6eba21fb64132b", size = 14185607, upload-time = "2025-07-24T20:50:51.923Z" }, + { url = "https://files.pythonhosted.org/packages/e4/76/b3d6f414f4eca568f469ac112a3b510938d892bc5a6c190cb883af080b77/numpy-2.3.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:87c930d52f45df092f7578889711a0768094debf73cfcde105e2d66954358125", size = 5114110, upload-time = "2025-07-24T20:51:01.041Z" }, + { url = "https://files.pythonhosted.org/packages/9e/d2/6f5e6826abd6bca52392ed88fe44a4b52aacb60567ac3bc86c67834c3a56/numpy-2.3.2-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:8dc082ea901a62edb8f59713c6a7e28a85daddcb67454c839de57656478f5b19", size = 6642050, upload-time = "2025-07-24T20:51:11.64Z" }, + { url = "https://files.pythonhosted.org/packages/c4/43/f12b2ade99199e39c73ad182f103f9d9791f48d885c600c8e05927865baf/numpy-2.3.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:af58de8745f7fa9ca1c0c7c943616c6fe28e75d0c81f5c295810e3c83b5be92f", size = 14296292, upload-time = "2025-07-24T20:51:33.488Z" }, + { url = "https://files.pythonhosted.org/packages/5d/f9/77c07d94bf110a916b17210fac38680ed8734c236bfed9982fd8524a7b47/numpy-2.3.2-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed5527c4cf10f16c6d0b6bee1f89958bccb0ad2522c8cadc2efd318bcd545f5", size = 16638913, upload-time = "2025-07-24T20:51:58.517Z" }, + { url = "https://files.pythonhosted.org/packages/9b/d1/9d9f2c8ea399cc05cfff8a7437453bd4e7d894373a93cdc46361bbb49a7d/numpy-2.3.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:095737ed986e00393ec18ec0b21b47c22889ae4b0cd2d5e88342e08b01141f58", size = 16071180, upload-time = "2025-07-24T20:52:22.827Z" }, + { url = "https://files.pythonhosted.org/packages/4c/41/82e2c68aff2a0c9bf315e47d61951099fed65d8cb2c8d9dc388cb87e947e/numpy-2.3.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5e40e80299607f597e1a8a247ff8d71d79c5b52baa11cc1cce30aa92d2da6e0", size = 18576809, upload-time = "2025-07-24T20:52:51.015Z" }, + { url = "https://files.pythonhosted.org/packages/14/14/4b4fd3efb0837ed252d0f583c5c35a75121038a8c4e065f2c259be06d2d8/numpy-2.3.2-cp314-cp314-win32.whl", hash = "sha256:7d6e390423cc1f76e1b8108c9b6889d20a7a1f59d9a60cac4a050fa734d6c1e2", size = 6366410, upload-time = "2025-07-24T20:56:44.949Z" }, + { url = "https://files.pythonhosted.org/packages/11/9e/b4c24a6b8467b61aced5c8dc7dcfce23621baa2e17f661edb2444a418040/numpy-2.3.2-cp314-cp314-win_amd64.whl", hash = "sha256:b9d0878b21e3918d76d2209c924ebb272340da1fb51abc00f986c258cd5e957b", size = 12918821, upload-time = "2025-07-24T20:57:06.479Z" }, + { url = "https://files.pythonhosted.org/packages/0e/0f/0dc44007c70b1007c1cef86b06986a3812dd7106d8f946c09cfa75782556/numpy-2.3.2-cp314-cp314-win_arm64.whl", hash = "sha256:2738534837c6a1d0c39340a190177d7d66fdf432894f469728da901f8f6dc910", size = 10477303, upload-time = "2025-07-24T20:57:22.879Z" }, + { url = "https://files.pythonhosted.org/packages/8b/3e/075752b79140b78ddfc9c0a1634d234cfdbc6f9bbbfa6b7504e445ad7d19/numpy-2.3.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:4d002ecf7c9b53240be3bb69d80f86ddbd34078bae04d87be81c1f58466f264e", size = 21047524, upload-time = "2025-07-24T20:53:22.086Z" }, + { url = "https://files.pythonhosted.org/packages/fe/6d/60e8247564a72426570d0e0ea1151b95ce5bd2f1597bb878a18d32aec855/numpy-2.3.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:293b2192c6bcce487dbc6326de5853787f870aeb6c43f8f9c6496db5b1781e45", size = 14300519, upload-time = "2025-07-24T20:53:44.053Z" }, + { url = "https://files.pythonhosted.org/packages/4d/73/d8326c442cd428d47a067070c3ac6cc3b651a6e53613a1668342a12d4479/numpy-2.3.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:0a4f2021a6da53a0d580d6ef5db29947025ae8b35b3250141805ea9a32bbe86b", size = 5228972, upload-time = "2025-07-24T20:53:53.81Z" }, + { url = "https://files.pythonhosted.org/packages/34/2e/e71b2d6dad075271e7079db776196829019b90ce3ece5c69639e4f6fdc44/numpy-2.3.2-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:9c144440db4bf3bb6372d2c3e49834cc0ff7bb4c24975ab33e01199e645416f2", size = 6737439, upload-time = "2025-07-24T20:54:04.742Z" }, + { url = "https://files.pythonhosted.org/packages/15/b0/d004bcd56c2c5e0500ffc65385eb6d569ffd3363cb5e593ae742749b2daa/numpy-2.3.2-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f92d6c2a8535dc4fe4419562294ff957f83a16ebdec66df0805e473ffaad8bd0", size = 14352479, upload-time = "2025-07-24T20:54:25.819Z" }, + { url = "https://files.pythonhosted.org/packages/11/e3/285142fcff8721e0c99b51686426165059874c150ea9ab898e12a492e291/numpy-2.3.2-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cefc2219baa48e468e3db7e706305fcd0c095534a192a08f31e98d83a7d45fb0", size = 16702805, upload-time = "2025-07-24T20:54:50.814Z" }, + { url = "https://files.pythonhosted.org/packages/33/c3/33b56b0e47e604af2c7cd065edca892d180f5899599b76830652875249a3/numpy-2.3.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:76c3e9501ceb50b2ff3824c3589d5d1ab4ac857b0ee3f8f49629d0de55ecf7c2", size = 16133830, upload-time = "2025-07-24T20:55:17.306Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ae/7b1476a1f4d6a48bc669b8deb09939c56dd2a439db1ab03017844374fb67/numpy-2.3.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:122bf5ed9a0221b3419672493878ba4967121514b1d7d4656a7580cd11dddcbf", size = 18652665, upload-time = "2025-07-24T20:55:46.665Z" }, + { url = "https://files.pythonhosted.org/packages/14/ba/5b5c9978c4bb161034148ade2de9db44ec316fab89ce8c400db0e0c81f86/numpy-2.3.2-cp314-cp314t-win32.whl", hash = "sha256:6f1ae3dcb840edccc45af496f312528c15b1f79ac318169d094e85e4bb35fdf1", size = 6514777, upload-time = "2025-07-24T20:55:57.66Z" }, + { url = "https://files.pythonhosted.org/packages/eb/46/3dbaf0ae7c17cdc46b9f662c56da2054887b8d9e737c1476f335c83d33db/numpy-2.3.2-cp314-cp314t-win_amd64.whl", hash = "sha256:087ffc25890d89a43536f75c5fe8770922008758e8eeeef61733957041ed2f9b", size = 13111856, upload-time = "2025-07-24T20:56:17.318Z" }, + { url = "https://files.pythonhosted.org/packages/c1/9e/1652778bce745a67b5fe05adde60ed362d38eb17d919a540e813d30f6874/numpy-2.3.2-cp314-cp314t-win_arm64.whl", hash = "sha256:092aeb3449833ea9c0bf0089d70c29ae480685dd2377ec9cdbbb620257f84631", size = 10544226, upload-time = "2025-07-24T20:56:34.509Z" }, +] + +[[package]] +name = "openpyxl" +version = "3.1.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "et-xmlfile" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/f9/88d94a75de065ea32619465d2f77b29a0469500e99012523b91cc4141cd1/openpyxl-3.1.5.tar.gz", hash = "sha256:cf0e3cf56142039133628b5acffe8ef0c12bc902d2aadd3e0fe5878dc08d1050", size = 186464, upload-time = "2024-06-28T14:03:44.161Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/da/977ded879c29cbd04de313843e76868e6e13408a94ed6b987245dc7c8506/openpyxl-3.1.5-py2.py3-none-any.whl", hash = "sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2", size = 250910, upload-time = "2024-06-28T14:03:41.161Z" }, +] + +[[package]] +name = "orjson" +version = "3.11.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/45/b268004f745ede84e5798b48ee12b05129d19235d0e15267aa57dcdb400b/orjson-3.11.7.tar.gz", hash = "sha256:9b1a67243945819ce55d24a30b59d6a168e86220452d2c96f4d1f093e71c0c49", size = 6144992, upload-time = "2026-02-02T15:38:49.29Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/80/bf/76f4f1665f6983385938f0e2a5d7efa12a58171b8456c252f3bae8a4cf75/orjson-3.11.7-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:bd03ea7606833655048dab1a00734a2875e3e86c276e1d772b2a02556f0d895f", size = 228545, upload-time = "2026-02-02T15:37:46.376Z" }, + { url = "https://files.pythonhosted.org/packages/79/53/6c72c002cb13b5a978a068add59b25a8bdf2800ac1c9c8ecdb26d6d97064/orjson-3.11.7-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:89e440ebc74ce8ab5c7bc4ce6757b4a6b1041becb127df818f6997b5c71aa60b", size = 125224, upload-time = "2026-02-02T15:37:47.697Z" }, + { url = "https://files.pythonhosted.org/packages/2c/83/10e48852865e5dd151bdfe652c06f7da484578ed02c5fca938e3632cb0b8/orjson-3.11.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ede977b5fe5ac91b1dffc0a517ca4542d2ec8a6a4ff7b2652d94f640796342a", size = 128154, upload-time = "2026-02-02T15:37:48.954Z" }, + { url = "https://files.pythonhosted.org/packages/6e/52/a66e22a2b9abaa374b4a081d410edab6d1e30024707b87eab7c734afe28d/orjson-3.11.7-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b7b1dae39230a393df353827c855a5f176271c23434cfd2db74e0e424e693e10", size = 123548, upload-time = "2026-02-02T15:37:50.187Z" }, + { url = "https://files.pythonhosted.org/packages/de/38/605d371417021359f4910c496f764c48ceb8997605f8c25bf1dfe58c0ebe/orjson-3.11.7-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed46f17096e28fb28d2975834836a639af7278aa87c84f68ab08fbe5b8bd75fa", size = 129000, upload-time = "2026-02-02T15:37:51.426Z" }, + { url = "https://files.pythonhosted.org/packages/44/98/af32e842b0ffd2335c89714d48ca4e3917b42f5d6ee5537832e069a4b3ac/orjson-3.11.7-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3726be79e36e526e3d9c1aceaadbfb4a04ee80a72ab47b3f3c17fefb9812e7b8", size = 141686, upload-time = "2026-02-02T15:37:52.607Z" }, + { url = "https://files.pythonhosted.org/packages/96/0b/fc793858dfa54be6feee940c1463370ece34b3c39c1ca0aa3845f5ba9892/orjson-3.11.7-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0724e265bc548af1dedebd9cb3d24b4e1c1e685a343be43e87ba922a5c5fff2f", size = 130812, upload-time = "2026-02-02T15:37:53.944Z" }, + { url = "https://files.pythonhosted.org/packages/dc/91/98a52415059db3f374757d0b7f0f16e3b5cd5976c90d1c2b56acaea039e6/orjson-3.11.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7745312efa9e11c17fbd3cb3097262d079da26930ae9ae7ba28fb738367cbad", size = 133440, upload-time = "2026-02-02T15:37:55.615Z" }, + { url = "https://files.pythonhosted.org/packages/dc/b6/cb540117bda61791f46381f8c26c8f93e802892830a6055748d3bb1925ab/orjson-3.11.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f904c24bdeabd4298f7a977ef14ca2a022ca921ed670b92ecd16ab6f3d01f867", size = 138386, upload-time = "2026-02-02T15:37:56.814Z" }, + { url = "https://files.pythonhosted.org/packages/63/1a/50a3201c334a7f17c231eee5f841342190723794e3b06293f26e7cf87d31/orjson-3.11.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b9fc4d0f81f394689e0814617aadc4f2ea0e8025f38c226cbf22d3b5ddbf025d", size = 408853, upload-time = "2026-02-02T15:37:58.291Z" }, + { url = "https://files.pythonhosted.org/packages/87/cd/8de1c67d0be44fdc22701e5989c0d015a2adf391498ad42c4dc589cd3013/orjson-3.11.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:849e38203e5be40b776ed2718e587faf204d184fc9a008ae441f9442320c0cab", size = 144130, upload-time = "2026-02-02T15:38:00.163Z" }, + { url = "https://files.pythonhosted.org/packages/0f/fe/d605d700c35dd55f51710d159fc54516a280923cd1b7e47508982fbb387d/orjson-3.11.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4682d1db3bcebd2b64757e0ddf9e87ae5f00d29d16c5cdf3a62f561d08cc3dd2", size = 134818, upload-time = "2026-02-02T15:38:01.507Z" }, + { url = "https://files.pythonhosted.org/packages/e4/e4/15ecc67edb3ddb3e2f46ae04475f2d294e8b60c1825fbe28a428b93b3fbd/orjson-3.11.7-cp312-cp312-win32.whl", hash = "sha256:f4f7c956b5215d949a1f65334cf9d7612dde38f20a95f2315deef167def91a6f", size = 127923, upload-time = "2026-02-02T15:38:02.75Z" }, + { url = "https://files.pythonhosted.org/packages/34/70/2e0855361f76198a3965273048c8e50a9695d88cd75811a5b46444895845/orjson-3.11.7-cp312-cp312-win_amd64.whl", hash = "sha256:bf742e149121dc5648ba0a08ea0871e87b660467ef168a3a5e53bc1fbd64bb74", size = 125007, upload-time = "2026-02-02T15:38:04.032Z" }, + { url = "https://files.pythonhosted.org/packages/68/40/c2051bd19fc467610fed469dc29e43ac65891571138f476834ca192bc290/orjson-3.11.7-cp312-cp312-win_arm64.whl", hash = "sha256:26c3b9132f783b7d7903bf1efb095fed8d4a3a85ec0d334ee8beff3d7a4749d5", size = 126089, upload-time = "2026-02-02T15:38:05.297Z" }, + { url = "https://files.pythonhosted.org/packages/89/25/6e0e52cac5aab51d7b6dcd257e855e1dec1c2060f6b28566c509b4665f62/orjson-3.11.7-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:1d98b30cc1313d52d4af17d9c3d307b08389752ec5f2e5febdfada70b0f8c733", size = 228390, upload-time = "2026-02-02T15:38:06.8Z" }, + { url = "https://files.pythonhosted.org/packages/a5/29/a77f48d2fc8a05bbc529e5ff481fb43d914f9e383ea2469d4f3d51df3d00/orjson-3.11.7-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:d897e81f8d0cbd2abb82226d1860ad2e1ab3ff16d7b08c96ca00df9d45409ef4", size = 125189, upload-time = "2026-02-02T15:38:08.181Z" }, + { url = "https://files.pythonhosted.org/packages/89/25/0a16e0729a0e6a1504f9d1a13cdd365f030068aab64cec6958396b9969d7/orjson-3.11.7-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:814be4b49b228cfc0b3c565acf642dd7d13538f966e3ccde61f4f55be3e20785", size = 128106, upload-time = "2026-02-02T15:38:09.41Z" }, + { url = "https://files.pythonhosted.org/packages/66/da/a2e505469d60666a05ab373f1a6322eb671cb2ba3a0ccfc7d4bc97196787/orjson-3.11.7-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d06e5c5fed5caedd2e540d62e5b1c25e8c82431b9e577c33537e5fa4aa909539", size = 123363, upload-time = "2026-02-02T15:38:10.73Z" }, + { url = "https://files.pythonhosted.org/packages/23/bf/ed73f88396ea35c71b38961734ea4a4746f7ca0768bf28fd551d37e48dd0/orjson-3.11.7-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:31c80ce534ac4ea3739c5ee751270646cbc46e45aea7576a38ffec040b4029a1", size = 129007, upload-time = "2026-02-02T15:38:12.138Z" }, + { url = "https://files.pythonhosted.org/packages/73/3c/b05d80716f0225fc9008fbf8ab22841dcc268a626aa550561743714ce3bf/orjson-3.11.7-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f50979824bde13d32b4320eedd513431c921102796d86be3eee0b58e58a3ecd1", size = 141667, upload-time = "2026-02-02T15:38:13.398Z" }, + { url = "https://files.pythonhosted.org/packages/61/e8/0be9b0addd9bf86abfc938e97441dcd0375d494594b1c8ad10fe57479617/orjson-3.11.7-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e54f3808e2b6b945078c41aa8d9b5834b28c50843846e97807e5adb75fa9705", size = 130832, upload-time = "2026-02-02T15:38:14.698Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ec/c68e3b9021a31d9ec15a94931db1410136af862955854ed5dd7e7e4f5bff/orjson-3.11.7-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a12b80df61aab7b98b490fe9e4879925ba666fccdfcd175252ce4d9035865ace", size = 133373, upload-time = "2026-02-02T15:38:16.109Z" }, + { url = "https://files.pythonhosted.org/packages/d2/45/f3466739aaafa570cc8e77c6dbb853c48bf56e3b43738020e2661e08b0ac/orjson-3.11.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:996b65230271f1a97026fd0e6a753f51fbc0c335d2ad0c6201f711b0da32693b", size = 138307, upload-time = "2026-02-02T15:38:17.453Z" }, + { url = "https://files.pythonhosted.org/packages/e1/84/9f7f02288da1ffb31405c1be07657afd1eecbcb4b64ee2817b6fe0f785fa/orjson-3.11.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ab49d4b2a6a1d415ddb9f37a21e02e0d5dbfe10b7870b21bf779fc21e9156157", size = 408695, upload-time = "2026-02-02T15:38:18.831Z" }, + { url = "https://files.pythonhosted.org/packages/18/07/9dd2f0c0104f1a0295ffbe912bc8d63307a539b900dd9e2c48ef7810d971/orjson-3.11.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:390a1dce0c055ddf8adb6aa94a73b45a4a7d7177b5c584b8d1c1947f2ba60fb3", size = 144099, upload-time = "2026-02-02T15:38:20.28Z" }, + { url = "https://files.pythonhosted.org/packages/a5/66/857a8e4a3292e1f7b1b202883bcdeb43a91566cf59a93f97c53b44bd6801/orjson-3.11.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1eb80451a9c351a71dfaf5b7ccc13ad065405217726b59fdbeadbcc544f9d223", size = 134806, upload-time = "2026-02-02T15:38:22.186Z" }, + { url = "https://files.pythonhosted.org/packages/0a/5b/6ebcf3defc1aab3a338ca777214966851e92efb1f30dc7fc8285216e6d1b/orjson-3.11.7-cp313-cp313-win32.whl", hash = "sha256:7477aa6a6ec6139c5cb1cc7b214643592169a5494d200397c7fc95d740d5fcf3", size = 127914, upload-time = "2026-02-02T15:38:23.511Z" }, + { url = "https://files.pythonhosted.org/packages/00/04/c6f72daca5092e3117840a1b1e88dfc809cc1470cf0734890d0366b684a1/orjson-3.11.7-cp313-cp313-win_amd64.whl", hash = "sha256:b9f95dcdea9d4f805daa9ddf02617a89e484c6985fa03055459f90e87d7a0757", size = 124986, upload-time = "2026-02-02T15:38:24.836Z" }, + { url = "https://files.pythonhosted.org/packages/03/ba/077a0f6f1085d6b806937246860fafbd5b17f3919c70ee3f3d8d9c713f38/orjson-3.11.7-cp313-cp313-win_arm64.whl", hash = "sha256:800988273a014a0541483dc81021247d7eacb0c845a9d1a34a422bc718f41539", size = 126045, upload-time = "2026-02-02T15:38:26.216Z" }, + { url = "https://files.pythonhosted.org/packages/e9/1e/745565dca749813db9a093c5ebc4bac1a9475c64d54b95654336ac3ed961/orjson-3.11.7-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:de0a37f21d0d364954ad5de1970491d7fbd0fb1ef7417d4d56a36dc01ba0c0a0", size = 228391, upload-time = "2026-02-02T15:38:27.757Z" }, + { url = "https://files.pythonhosted.org/packages/46/19/e40f6225da4d3aa0c8dc6e5219c5e87c2063a560fe0d72a88deb59776794/orjson-3.11.7-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:c2428d358d85e8da9d37cba18b8c4047c55222007a84f97156a5b22028dfbfc0", size = 125188, upload-time = "2026-02-02T15:38:29.241Z" }, + { url = "https://files.pythonhosted.org/packages/9d/7e/c4de2babef2c0817fd1f048fd176aa48c37bec8aef53d2fa932983032cce/orjson-3.11.7-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c4bc6c6ac52cdaa267552544c73e486fecbd710b7ac09bc024d5a78555a22f6", size = 128097, upload-time = "2026-02-02T15:38:30.618Z" }, + { url = "https://files.pythonhosted.org/packages/eb/74/233d360632bafd2197f217eee7fb9c9d0229eac0c18128aee5b35b0014fe/orjson-3.11.7-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd0d68edd7dfca1b2eca9361a44ac9f24b078de3481003159929a0573f21a6bf", size = 123364, upload-time = "2026-02-02T15:38:32.363Z" }, + { url = "https://files.pythonhosted.org/packages/79/51/af79504981dd31efe20a9e360eb49c15f06df2b40e7f25a0a52d9ae888e8/orjson-3.11.7-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:623ad1b9548ef63886319c16fa317848e465a21513b31a6ad7b57443c3e0dcf5", size = 129076, upload-time = "2026-02-02T15:38:33.68Z" }, + { url = "https://files.pythonhosted.org/packages/67/e2/da898eb68b72304f8de05ca6715870d09d603ee98d30a27e8a9629abc64b/orjson-3.11.7-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6e776b998ac37c0396093d10290e60283f59cfe0fc3fccbd0ccc4bd04dd19892", size = 141705, upload-time = "2026-02-02T15:38:34.989Z" }, + { url = "https://files.pythonhosted.org/packages/c5/89/15364d92acb3d903b029e28d834edb8780c2b97404cbf7929aa6b9abdb24/orjson-3.11.7-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:652c6c3af76716f4a9c290371ba2e390ede06f6603edb277b481daf37f6f464e", size = 130855, upload-time = "2026-02-02T15:38:36.379Z" }, + { url = "https://files.pythonhosted.org/packages/c2/8b/ecdad52d0b38d4b8f514be603e69ccd5eacf4e7241f972e37e79792212ec/orjson-3.11.7-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a56df3239294ea5964adf074c54bcc4f0ccd21636049a2cf3ca9cf03b5d03cf1", size = 133386, upload-time = "2026-02-02T15:38:37.704Z" }, + { url = "https://files.pythonhosted.org/packages/b9/0e/45e1dcf10e17d0924b7c9162f87ec7b4ca79e28a0548acf6a71788d3e108/orjson-3.11.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bda117c4148e81f746655d5a3239ae9bd00cb7bc3ca178b5fc5a5997e9744183", size = 138295, upload-time = "2026-02-02T15:38:39.096Z" }, + { url = "https://files.pythonhosted.org/packages/63/d7/4d2e8b03561257af0450f2845b91fbd111d7e526ccdf737267108075e0ba/orjson-3.11.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:23d6c20517a97a9daf1d48b580fcdc6f0516c6f4b5038823426033690b4d2650", size = 408720, upload-time = "2026-02-02T15:38:40.634Z" }, + { url = "https://files.pythonhosted.org/packages/78/cf/d45343518282108b29c12a65892445fc51f9319dc3c552ceb51bb5905ed2/orjson-3.11.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:8ff206156006da5b847c9304b6308a01e8cdbc8cce824e2779a5ba71c3def141", size = 144152, upload-time = "2026-02-02T15:38:42.262Z" }, + { url = "https://files.pythonhosted.org/packages/a9/3a/d6001f51a7275aacd342e77b735c71fa04125a3f93c36fee4526bc8c654e/orjson-3.11.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:962d046ee1765f74a1da723f4b33e3b228fe3a48bd307acce5021dfefe0e29b2", size = 134814, upload-time = "2026-02-02T15:38:43.627Z" }, + { url = "https://files.pythonhosted.org/packages/1d/d3/f19b47ce16820cc2c480f7f1723e17f6d411b3a295c60c8ad3aa9ff1c96a/orjson-3.11.7-cp314-cp314-win32.whl", hash = "sha256:89e13dd3f89f1c38a9c9eba5fbf7cdc2d1feca82f5f290864b4b7a6aac704576", size = 127997, upload-time = "2026-02-02T15:38:45.06Z" }, + { url = "https://files.pythonhosted.org/packages/12/df/172771902943af54bf661a8d102bdf2e7f932127968080632bda6054b62c/orjson-3.11.7-cp314-cp314-win_amd64.whl", hash = "sha256:845c3e0d8ded9c9271cd79596b9b552448b885b97110f628fb687aee2eed11c1", size = 124985, upload-time = "2026-02-02T15:38:46.388Z" }, + { url = "https://files.pythonhosted.org/packages/6f/1c/f2a8d8a1b17514660a614ce5f7aac74b934e69f5abc2700cc7ced882a009/orjson-3.11.7-cp314-cp314-win_arm64.whl", hash = "sha256:4a2e9c5be347b937a2e0203866f12bba36082e89b402ddb9e927d5822e43088d", size = 126038, upload-time = "2026-02-02T15:38:47.703Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pandas" +version = "2.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "python-dateutil" }, + { name = "pytz" }, + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223, upload-time = "2025-09-29T23:34:51.853Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/fb/231d89e8637c808b997d172b18e9d4a4bc7bf31296196c260526055d1ea0/pandas-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d21f6d74eb1725c2efaa71a2bfc661a0689579b58e9c0ca58a739ff0b002b53", size = 11597846, upload-time = "2025-09-29T23:19:48.856Z" }, + { url = "https://files.pythonhosted.org/packages/5c/bd/bf8064d9cfa214294356c2d6702b716d3cf3bb24be59287a6a21e24cae6b/pandas-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3fd2f887589c7aa868e02632612ba39acb0b8948faf5cc58f0850e165bd46f35", size = 10729618, upload-time = "2025-09-29T23:39:08.659Z" }, + { url = "https://files.pythonhosted.org/packages/57/56/cf2dbe1a3f5271370669475ead12ce77c61726ffd19a35546e31aa8edf4e/pandas-2.3.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecaf1e12bdc03c86ad4a7ea848d66c685cb6851d807a26aa245ca3d2017a1908", size = 11737212, upload-time = "2025-09-29T23:19:59.765Z" }, + { url = "https://files.pythonhosted.org/packages/e5/63/cd7d615331b328e287d8233ba9fdf191a9c2d11b6af0c7a59cfcec23de68/pandas-2.3.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b3d11d2fda7eb164ef27ffc14b4fcab16a80e1ce67e9f57e19ec0afaf715ba89", size = 12362693, upload-time = "2025-09-29T23:20:14.098Z" }, + { url = "https://files.pythonhosted.org/packages/a6/de/8b1895b107277d52f2b42d3a6806e69cfef0d5cf1d0ba343470b9d8e0a04/pandas-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a68e15f780eddf2b07d242e17a04aa187a7ee12b40b930bfdd78070556550e98", size = 12771002, upload-time = "2025-09-29T23:20:26.76Z" }, + { url = "https://files.pythonhosted.org/packages/87/21/84072af3187a677c5893b170ba2c8fbe450a6ff911234916da889b698220/pandas-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:371a4ab48e950033bcf52b6527eccb564f52dc826c02afd9a1bc0ab731bba084", size = 13450971, upload-time = "2025-09-29T23:20:41.344Z" }, + { url = "https://files.pythonhosted.org/packages/86/41/585a168330ff063014880a80d744219dbf1dd7a1c706e75ab3425a987384/pandas-2.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:a16dcec078a01eeef8ee61bf64074b4e524a2a3f4b3be9326420cabe59c4778b", size = 10992722, upload-time = "2025-09-29T23:20:54.139Z" }, + { url = "https://files.pythonhosted.org/packages/cd/4b/18b035ee18f97c1040d94debd8f2e737000ad70ccc8f5513f4eefad75f4b/pandas-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:56851a737e3470de7fa88e6131f41281ed440d29a9268dcbf0002da5ac366713", size = 11544671, upload-time = "2025-09-29T23:21:05.024Z" }, + { url = "https://files.pythonhosted.org/packages/31/94/72fac03573102779920099bcac1c3b05975c2cb5f01eac609faf34bed1ca/pandas-2.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdcd9d1167f4885211e401b3036c0c8d9e274eee67ea8d0758a256d60704cfe8", size = 10680807, upload-time = "2025-09-29T23:21:15.979Z" }, + { url = "https://files.pythonhosted.org/packages/16/87/9472cf4a487d848476865321de18cc8c920b8cab98453ab79dbbc98db63a/pandas-2.3.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e32e7cc9af0f1cc15548288a51a3b681cc2a219faa838e995f7dc53dbab1062d", size = 11709872, upload-time = "2025-09-29T23:21:27.165Z" }, + { url = "https://files.pythonhosted.org/packages/15/07/284f757f63f8a8d69ed4472bfd85122bd086e637bf4ed09de572d575a693/pandas-2.3.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318d77e0e42a628c04dc56bcef4b40de67918f7041c2b061af1da41dcff670ac", size = 12306371, upload-time = "2025-09-29T23:21:40.532Z" }, + { url = "https://files.pythonhosted.org/packages/33/81/a3afc88fca4aa925804a27d2676d22dcd2031c2ebe08aabd0ae55b9ff282/pandas-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e0a175408804d566144e170d0476b15d78458795bb18f1304fb94160cabf40c", size = 12765333, upload-time = "2025-09-29T23:21:55.77Z" }, + { url = "https://files.pythonhosted.org/packages/8d/0f/b4d4ae743a83742f1153464cf1a8ecfafc3ac59722a0b5c8602310cb7158/pandas-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2d9ab0fc11822b5eece72ec9587e172f63cff87c00b062f6e37448ced4493", size = 13418120, upload-time = "2025-09-29T23:22:10.109Z" }, + { url = "https://files.pythonhosted.org/packages/4f/c7/e54682c96a895d0c808453269e0b5928a07a127a15704fedb643e9b0a4c8/pandas-2.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:f8bfc0e12dc78f777f323f55c58649591b2cd0c43534e8355c51d3fede5f4dee", size = 10993991, upload-time = "2025-09-29T23:25:04.889Z" }, + { url = "https://files.pythonhosted.org/packages/f9/ca/3f8d4f49740799189e1395812f3bf23b5e8fc7c190827d55a610da72ce55/pandas-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:75ea25f9529fdec2d2e93a42c523962261e567d250b0013b16210e1d40d7c2e5", size = 12048227, upload-time = "2025-09-29T23:22:24.343Z" }, + { url = "https://files.pythonhosted.org/packages/0e/5a/f43efec3e8c0cc92c4663ccad372dbdff72b60bdb56b2749f04aa1d07d7e/pandas-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74ecdf1d301e812db96a465a525952f4dde225fdb6d8e5a521d47e1f42041e21", size = 11411056, upload-time = "2025-09-29T23:22:37.762Z" }, + { url = "https://files.pythonhosted.org/packages/46/b1/85331edfc591208c9d1a63a06baa67b21d332e63b7a591a5ba42a10bb507/pandas-2.3.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6435cb949cb34ec11cc9860246ccb2fdc9ecd742c12d3304989017d53f039a78", size = 11645189, upload-time = "2025-09-29T23:22:51.688Z" }, + { url = "https://files.pythonhosted.org/packages/44/23/78d645adc35d94d1ac4f2a3c4112ab6f5b8999f4898b8cdf01252f8df4a9/pandas-2.3.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:900f47d8f20860de523a1ac881c4c36d65efcb2eb850e6948140fa781736e110", size = 12121912, upload-time = "2025-09-29T23:23:05.042Z" }, + { url = "https://files.pythonhosted.org/packages/53/da/d10013df5e6aaef6b425aa0c32e1fc1f3e431e4bcabd420517dceadce354/pandas-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a45c765238e2ed7d7c608fc5bc4a6f88b642f2f01e70c0c23d2224dd21829d86", size = 12712160, upload-time = "2025-09-29T23:23:28.57Z" }, + { url = "https://files.pythonhosted.org/packages/bd/17/e756653095a083d8a37cbd816cb87148debcfcd920129b25f99dd8d04271/pandas-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c4fc4c21971a1a9f4bdb4c73978c7f7256caa3e62b323f70d6cb80db583350bc", size = 13199233, upload-time = "2025-09-29T23:24:24.876Z" }, + { url = "https://files.pythonhosted.org/packages/04/fd/74903979833db8390b73b3a8a7d30d146d710bd32703724dd9083950386f/pandas-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ee15f284898e7b246df8087fc82b87b01686f98ee67d85a17b7ab44143a3a9a0", size = 11540635, upload-time = "2025-09-29T23:25:52.486Z" }, + { url = "https://files.pythonhosted.org/packages/21/00/266d6b357ad5e6d3ad55093a7e8efc7dd245f5a842b584db9f30b0f0a287/pandas-2.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1611aedd912e1ff81ff41c745822980c49ce4a7907537be8692c8dbc31924593", size = 10759079, upload-time = "2025-09-29T23:26:33.204Z" }, + { url = "https://files.pythonhosted.org/packages/ca/05/d01ef80a7a3a12b2f8bbf16daba1e17c98a2f039cbc8e2f77a2c5a63d382/pandas-2.3.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d2cefc361461662ac48810cb14365a365ce864afe85ef1f447ff5a1e99ea81c", size = 11814049, upload-time = "2025-09-29T23:27:15.384Z" }, + { url = "https://files.pythonhosted.org/packages/15/b2/0e62f78c0c5ba7e3d2c5945a82456f4fac76c480940f805e0b97fcbc2f65/pandas-2.3.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ee67acbbf05014ea6c763beb097e03cd629961c8a632075eeb34247120abcb4b", size = 12332638, upload-time = "2025-09-29T23:27:51.625Z" }, + { url = "https://files.pythonhosted.org/packages/c5/33/dd70400631b62b9b29c3c93d2feee1d0964dc2bae2e5ad7a6c73a7f25325/pandas-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c46467899aaa4da076d5abc11084634e2d197e9460643dd455ac3db5856b24d6", size = 12886834, upload-time = "2025-09-29T23:28:21.289Z" }, + { url = "https://files.pythonhosted.org/packages/d3/18/b5d48f55821228d0d2692b34fd5034bb185e854bdb592e9c640f6290e012/pandas-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6253c72c6a1d990a410bc7de641d34053364ef8bcd3126f7e7450125887dffe3", size = 13409925, upload-time = "2025-09-29T23:28:58.261Z" }, + { url = "https://files.pythonhosted.org/packages/a6/3d/124ac75fcd0ecc09b8fdccb0246ef65e35b012030defb0e0eba2cbbbe948/pandas-2.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:1b07204a219b3b7350abaae088f451860223a52cfb8a6c53358e7948735158e5", size = 11109071, upload-time = "2025-09-29T23:32:27.484Z" }, + { url = "https://files.pythonhosted.org/packages/89/9c/0e21c895c38a157e0faa1fb64587a9226d6dd46452cac4532d80c3c4a244/pandas-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2462b1a365b6109d275250baaae7b760fd25c726aaca0054649286bcfbb3e8ec", size = 12048504, upload-time = "2025-09-29T23:29:31.47Z" }, + { url = "https://files.pythonhosted.org/packages/d7/82/b69a1c95df796858777b68fbe6a81d37443a33319761d7c652ce77797475/pandas-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0242fe9a49aa8b4d78a4fa03acb397a58833ef6199e9aa40a95f027bb3a1b6e7", size = 11410702, upload-time = "2025-09-29T23:29:54.591Z" }, + { url = "https://files.pythonhosted.org/packages/f9/88/702bde3ba0a94b8c73a0181e05144b10f13f29ebfc2150c3a79062a8195d/pandas-2.3.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a21d830e78df0a515db2b3d2f5570610f5e6bd2e27749770e8bb7b524b89b450", size = 11634535, upload-time = "2025-09-29T23:30:21.003Z" }, + { url = "https://files.pythonhosted.org/packages/a4/1e/1bac1a839d12e6a82ec6cb40cda2edde64a2013a66963293696bbf31fbbb/pandas-2.3.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e3ebdb170b5ef78f19bfb71b0dc5dc58775032361fa188e814959b74d726dd5", size = 12121582, upload-time = "2025-09-29T23:30:43.391Z" }, + { url = "https://files.pythonhosted.org/packages/44/91/483de934193e12a3b1d6ae7c8645d083ff88dec75f46e827562f1e4b4da6/pandas-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d051c0e065b94b7a3cea50eb1ec32e912cd96dba41647eb24104b6c6c14c5788", size = 12699963, upload-time = "2025-09-29T23:31:10.009Z" }, + { url = "https://files.pythonhosted.org/packages/70/44/5191d2e4026f86a2a109053e194d3ba7a31a2d10a9c2348368c63ed4e85a/pandas-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3869faf4bd07b3b66a9f462417d0ca3a9df29a9f6abd5d0d0dbab15dac7abe87", size = 13202175, upload-time = "2025-09-29T23:31:59.173Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "polars" +version = "1.19.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/26/d9/66ada2204483c4c4d83898ade77eacd5fbef26ae4975a0d7d5de134ca46a/polars-1.19.0.tar.gz", hash = "sha256:b52ada5c43fcdadf64f282522198c5549ee4e46ea57d236a4d7e572643070d9d", size = 4267947, upload-time = "2025-01-03T21:11:20.621Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/7d/e8645281281d44d96752443366ceef2df76c9c1e17dce040111abb6a4a12/polars-1.19.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:51c01837268a1aa41785e60ed7d3363d4b52f652ab0eef4981f887bdfa2e9ca7", size = 29472039, upload-time = "2025-01-03T21:09:22.955Z" }, + { url = "https://files.pythonhosted.org/packages/d7/fb/7e5054598d6bb7a47e4ca086797bae61270f7d570350cf779dd97384d913/polars-1.19.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:20f8235e810f6ee795d7a215a3560945e6a1b57d017f87ba0c8542dced1fc665", size = 26150541, upload-time = "2025-01-03T21:09:31.561Z" }, + { url = "https://files.pythonhosted.org/packages/ba/ba/6d715730c28b035abd308fc2cf0fcbae0cedea6216797e83ce4a9a96c6d4/polars-1.19.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be0ea51f7b3553652bf0d53f3b925e969a898d4feb9980acecf8e3037d696903", size = 32751173, upload-time = "2025-01-03T21:09:37.112Z" }, + { url = "https://files.pythonhosted.org/packages/ea/9a/bee8ab37ab82b8eea75170afa3b37ea7e1df74e4c4da8f6c93b3009977fd/polars-1.19.0-cp39-abi3-manylinux_2_24_aarch64.whl", hash = "sha256:30305ef4e1b634c67a5d985832296fade9908482c5b1abb0100800808b2d090e", size = 29704437, upload-time = "2025-01-03T21:09:42.657Z" }, + { url = "https://files.pythonhosted.org/packages/57/ec/74afa5699e37e03e3acc7f241f4e2c3e8c91847524005424d9cf038b3034/polars-1.19.0-cp39-abi3-win_amd64.whl", hash = "sha256:de4aa45e24f8f94a1da9cc6031a7db6fa65ac7de8246fac0bc581ebb427d0643", size = 32846039, upload-time = "2025-01-03T21:09:48.524Z" }, + { url = "https://files.pythonhosted.org/packages/cf/5b/c6f6c70ddc9d3070dee65f4640437cb84ccb4cca04f7a81b01db15329ae3/polars-1.19.0-cp39-abi3-win_arm64.whl", hash = "sha256:d7ca7aeb63fa22c0a00f6cfa95dd5252c249e83dd4d1b954583a59f97a8e407b", size = 29029208, upload-time = "2025-01-03T21:09:54.824Z" }, +] + +[[package]] +name = "prefixcommons" +version = "0.1.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "pytest-logging" }, + { name = "pyyaml" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/b5/c5b63a4bf5dedb36567181fdb98dbcc7aaa025faebabaaffa2f5eb4b8feb/prefixcommons-0.1.12.tar.gz", hash = "sha256:22c4e2d37b63487b3ab48f0495b70f14564cb346a15220f23919eb0c1851f69f", size = 24063, upload-time = "2022-07-19T00:06:12.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/e8/715b09df3dab02b07809d812042dc47a46236b5603d9d3a2572dbd1d8a97/prefixcommons-0.1.12-py3-none-any.whl", hash = "sha256:16dbc0a1f775e003c724f19a694fcfa3174608f5c8b0e893d494cf8098ac7f8b", size = 29482, upload-time = "2022-07-19T00:06:08.709Z" }, +] + +[[package]] +name = "prefixmaps" +version = "0.2.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "curies" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4d/cf/f588bcdfd2c841839b9d59ce219a46695da56aa2805faff937bbafb9ee2b/prefixmaps-0.2.6.tar.gz", hash = "sha256:7421e1244eea610217fa1ba96c9aebd64e8162a930dc0626207cd8bf62ecf4b9", size = 709899, upload-time = "2024-10-17T16:30:57.738Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/b2/2b2153173f2819e3d7d1949918612981bc6bd895b75ffa392d63d115f327/prefixmaps-0.2.6-py3-none-any.whl", hash = "sha256:f6cef28a7320fc6337cf411be212948ce570333a0ce958940ef684c7fb192a62", size = 754732, upload-time = "2024-10-17T16:30:55.731Z" }, +] + +[[package]] +name = "prompt-toolkit" +version = "3.0.52" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" }, +] + +[[package]] +name = "psycopg2-binary" +version = "2.9.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/07/e720e53bfab016ebcc34241695ccc06a9e3d91ba19b40ca81317afbdc440/psycopg2-binary-2.9.9.tar.gz", hash = "sha256:7f01846810177d829c7692f1f5ada8096762d9172af1b1a28d4ab5b77c923c1c", size = 384973, upload-time = "2023-10-03T12:48:55.128Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/d0/5f2db14e7b53552276ab613399a83f83f85b173a862d3f20580bc7231139/psycopg2_binary-2.9.9-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:8532fd6e6e2dc57bcb3bc90b079c60de896d2128c5d9d6f24a63875a95a088cf", size = 2823784, upload-time = "2023-10-03T12:47:00.404Z" }, + { url = "https://files.pythonhosted.org/packages/18/ca/da384fd47233e300e3e485c90e7aab5d7def896d1281239f75901faf87d4/psycopg2_binary-2.9.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0605eaed3eb239e87df0d5e3c6489daae3f7388d455d0c0b4df899519c6a38d", size = 2553308, upload-time = "2023-11-01T10:40:33.984Z" }, + { url = "https://files.pythonhosted.org/packages/50/66/fa53d2d3d92f6e1ef469d92afc6a4fe3f6e8a9a04b687aa28fb1f1d954ee/psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f8544b092a29a6ddd72f3556a9fcf249ec412e10ad28be6a0c0d948924f2212", size = 2851283, upload-time = "2023-10-03T12:47:02.736Z" }, + { url = "https://files.pythonhosted.org/packages/04/37/2429360ac5547378202db14eec0dde76edbe1f6627df5a43c7e164922859/psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2d423c8d8a3c82d08fe8af900ad5b613ce3632a1249fd6a223941d0735fce493", size = 3081839, upload-time = "2023-10-03T12:47:05.027Z" }, + { url = "https://files.pythonhosted.org/packages/62/2a/c0530b59d7e0d09824bc2102ecdcec0456b8ca4d47c0caa82e86fce3ed4c/psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e5afae772c00980525f6d6ecf7cbca55676296b580c0e6abb407f15f3706996", size = 3264488, upload-time = "2023-10-03T12:47:08.962Z" }, + { url = "https://files.pythonhosted.org/packages/19/57/9f172b900795ea37246c78b5f52e00f4779984370855b3e161600156906d/psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e6f98446430fdf41bd36d4faa6cb409f5140c1c2cf58ce0bbdaf16af7d3f119", size = 3020700, upload-time = "2023-10-03T12:47:12.23Z" }, + { url = "https://files.pythonhosted.org/packages/94/68/1176fc14ea76861b7b8360be5176e87fb20d5091b137c76570eb4e237324/psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c77e3d1862452565875eb31bdb45ac62502feabbd53429fdc39a1cc341d681ba", size = 2355968, upload-time = "2023-10-03T12:47:14.817Z" }, + { url = "https://files.pythonhosted.org/packages/70/bb/aec2646a705a09079d008ce88073401cd61fc9b04f92af3eb282caa3a2ec/psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:cb16c65dcb648d0a43a2521f2f0a2300f40639f6f8c1ecbc662141e4e3e1ee07", size = 2536101, upload-time = "2023-10-03T12:47:17.454Z" }, + { url = "https://files.pythonhosted.org/packages/14/33/12818c157e333cb9d9e6753d1b2463b6f60dbc1fade115f8e4dc5c52cac4/psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:911dda9c487075abd54e644ccdf5e5c16773470a6a5d3826fda76699410066fb", size = 2487064, upload-time = "2023-10-03T12:47:20.717Z" }, + { url = "https://files.pythonhosted.org/packages/56/a2/7851c68fe8768f3c9c246198b6356ee3e4a8a7f6820cc798443faada3400/psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:57fede879f08d23c85140a360c6a77709113efd1c993923c59fde17aa27599fe", size = 2456257, upload-time = "2023-10-03T12:47:23.004Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ee/3ba07c6dc7c3294e717e94720da1597aedc82a10b1b180203ce183d4631a/psycopg2_binary-2.9.9-cp312-cp312-win32.whl", hash = "sha256:64cf30263844fa208851ebb13b0732ce674d8ec6a0c86a4e160495d299ba3c93", size = 1024709, upload-time = "2023-10-28T09:37:24.991Z" }, + { url = "https://files.pythonhosted.org/packages/7b/08/9c66c269b0d417a0af9fb969535f0371b8c538633535a7a6a5ca3f9231e2/psycopg2_binary-2.9.9-cp312-cp312-win_amd64.whl", hash = "sha256:81ff62668af011f9a48787564ab7eded4e9fb17a4a6a74af5ffa6a457400d2ab", size = 1163864, upload-time = "2023-10-28T09:37:28.155Z" }, +] + +[[package]] +name = "pydantic" +version = "2.11.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/00/dd/4325abf92c39ba8623b5af936ddb36ffcfe0beae70405d456ab1fb2f5b8c/pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db", size = 788350, upload-time = "2025-06-14T08:33:17.137Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/c0/ec2b1c8712ca690e5d61979dee872603e92b8a32f94cc1b72d53beab008a/pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b", size = 444782, upload-time = "2025-06-14T08:33:14.905Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.33.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/8a/2b41c97f554ec8c71f2a8a5f85cb56a8b0956addfe8b0efb5b3d77e8bdc3/pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc", size = 2009000, upload-time = "2025-04-23T18:31:25.863Z" }, + { url = "https://files.pythonhosted.org/packages/a1/02/6224312aacb3c8ecbaa959897af57181fb6cf3a3d7917fd44d0f2917e6f2/pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7", size = 1847996, upload-time = "2025-04-23T18:31:27.341Z" }, + { url = "https://files.pythonhosted.org/packages/d6/46/6dcdf084a523dbe0a0be59d054734b86a981726f221f4562aed313dbcb49/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025", size = 1880957, upload-time = "2025-04-23T18:31:28.956Z" }, + { url = "https://files.pythonhosted.org/packages/ec/6b/1ec2c03837ac00886ba8160ce041ce4e325b41d06a034adbef11339ae422/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011", size = 1964199, upload-time = "2025-04-23T18:31:31.025Z" }, + { url = "https://files.pythonhosted.org/packages/2d/1d/6bf34d6adb9debd9136bd197ca72642203ce9aaaa85cfcbfcf20f9696e83/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f", size = 2120296, upload-time = "2025-04-23T18:31:32.514Z" }, + { url = "https://files.pythonhosted.org/packages/e0/94/2bd0aaf5a591e974b32a9f7123f16637776c304471a0ab33cf263cf5591a/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88", size = 2676109, upload-time = "2025-04-23T18:31:33.958Z" }, + { url = "https://files.pythonhosted.org/packages/f9/41/4b043778cf9c4285d59742281a769eac371b9e47e35f98ad321349cc5d61/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1", size = 2002028, upload-time = "2025-04-23T18:31:39.095Z" }, + { url = "https://files.pythonhosted.org/packages/cb/d5/7bb781bf2748ce3d03af04d5c969fa1308880e1dca35a9bd94e1a96a922e/pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b", size = 2100044, upload-time = "2025-04-23T18:31:41.034Z" }, + { url = "https://files.pythonhosted.org/packages/fe/36/def5e53e1eb0ad896785702a5bbfd25eed546cdcf4087ad285021a90ed53/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1", size = 2058881, upload-time = "2025-04-23T18:31:42.757Z" }, + { url = "https://files.pythonhosted.org/packages/01/6c/57f8d70b2ee57fc3dc8b9610315949837fa8c11d86927b9bb044f8705419/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6", size = 2227034, upload-time = "2025-04-23T18:31:44.304Z" }, + { url = "https://files.pythonhosted.org/packages/27/b9/9c17f0396a82b3d5cbea4c24d742083422639e7bb1d5bf600e12cb176a13/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea", size = 2234187, upload-time = "2025-04-23T18:31:45.891Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6a/adf5734ffd52bf86d865093ad70b2ce543415e0e356f6cacabbc0d9ad910/pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290", size = 1892628, upload-time = "2025-04-23T18:31:47.819Z" }, + { url = "https://files.pythonhosted.org/packages/43/e4/5479fecb3606c1368d496a825d8411e126133c41224c1e7238be58b87d7e/pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2", size = 1955866, upload-time = "2025-04-23T18:31:49.635Z" }, + { url = "https://files.pythonhosted.org/packages/0d/24/8b11e8b3e2be9dd82df4b11408a67c61bb4dc4f8e11b5b0fc888b38118b5/pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab", size = 1888894, upload-time = "2025-04-23T18:31:51.609Z" }, + { url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688, upload-time = "2025-04-23T18:31:53.175Z" }, + { url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808, upload-time = "2025-04-23T18:31:54.79Z" }, + { url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580, upload-time = "2025-04-23T18:31:57.393Z" }, + { url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859, upload-time = "2025-04-23T18:31:59.065Z" }, + { url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810, upload-time = "2025-04-23T18:32:00.78Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498, upload-time = "2025-04-23T18:32:02.418Z" }, + { url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611, upload-time = "2025-04-23T18:32:04.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924, upload-time = "2025-04-23T18:32:06.129Z" }, + { url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196, upload-time = "2025-04-23T18:32:08.178Z" }, + { url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389, upload-time = "2025-04-23T18:32:10.242Z" }, + { url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223, upload-time = "2025-04-23T18:32:12.382Z" }, + { url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473, upload-time = "2025-04-23T18:32:14.034Z" }, + { url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269, upload-time = "2025-04-23T18:32:15.783Z" }, + { url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921, upload-time = "2025-04-23T18:32:18.473Z" }, + { url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162, upload-time = "2025-04-23T18:32:20.188Z" }, + { url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560, upload-time = "2025-04-23T18:32:22.354Z" }, + { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pyoxigraph" +version = "0.3.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/68/14/33446bc01926701595b0890811f96ab941fd61e89f257ae262368a3b90c5/pyoxigraph-0.3.22.tar.gz", hash = "sha256:430b18cb3cec37b8c71cee0f70ea10601b9e479f1b8c364861660ae9f8629fd9", size = 4306498, upload-time = "2023-12-01T14:37:34.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/54/bf0e750bcc9cc29813454a7e56b18cd611dcacfb6533e615c44af5f9d1d1/pyoxigraph-0.3.22-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2baadd8dba65ff91bdcdf85e57d928806d94612b85da58d64526f0f1d5cd4df", size = 6495789, upload-time = "2023-12-01T17:54:10.045Z" }, + { url = "https://files.pythonhosted.org/packages/b7/bc/339893be78df7ed09fc2a4c59028cfcdc6335d96eacf58a206ded042da35/pyoxigraph-0.3.22-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f7e217e82e541f7df4697705c7cbfbd62e019c50786669647cb261445d75215", size = 6821696, upload-time = "2023-12-01T14:48:55.074Z" }, + { url = "https://files.pythonhosted.org/packages/bb/be/2466aaf6b705952e5f87a758fff1dfd1fc213650fb0b0c812f6b1968b81a/pyoxigraph-0.3.22-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:963bc825e34d7238bffb942572ac0e59a6512e7d33ec8f898f495964a8dac1de", size = 6941181, upload-time = "2023-12-04T11:52:00.908Z" }, + { url = "https://files.pythonhosted.org/packages/81/2d/6847756fc2b0d21f1a674268ac476c86de961c83dca0ab04b0b46a045f06/pyoxigraph-0.3.22-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:c99cd7d305a5f154d6fa7eca3a93b153ac94ad2a4aff6c404ec56db38d538ea4", size = 7312588, upload-time = "2023-12-01T15:05:49.69Z" }, + { url = "https://files.pythonhosted.org/packages/01/6b/7bcebf6e590db0384cb56b0ac76385cea3b51ea37170da849214d1d9b1cf/pyoxigraph-0.3.22-cp37-abi3-macosx_10_14_x86_64.macosx_11_0_arm64.macosx_10_14_universal2.whl", hash = "sha256:32d5630c9fb3d7b819a25401b3afdbd01dbfc9624b1519d41216622fe3af52e6", size = 10561197, upload-time = "2023-12-01T15:17:42.567Z" }, + { url = "https://files.pythonhosted.org/packages/4d/05/141746d5b1a8e6210613317d6cb772ab5bfe931a3ebdb98a92fa6ed59c39/pyoxigraph-0.3.22-cp37-abi3-macosx_10_14_x86_64.whl", hash = "sha256:6368f24bc236a6055171f4a80cb63b9ad76fcbdbcb4a3ef981eb6d86d8975c11", size = 5478410, upload-time = "2023-12-01T15:17:45.497Z" }, + { url = "https://files.pythonhosted.org/packages/15/ef/85854115c9a8a08eea3d9e46317ca9774566dacd33964b41acd30a8156cd/pyoxigraph-0.3.22-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:821e1103cf1e8f12d0738cf1b2625c8374758e33075ca67161ead3669f53e4cb", size = 5092580, upload-time = "2023-12-01T15:17:48.102Z" }, + { url = "https://files.pythonhosted.org/packages/7f/96/ad8b73eb8c649a089d07cbe323034e8d09cdabcdab44194786c4c2faee28/pyoxigraph-0.3.22-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:630f1090d67d1199c86f358094289816e0c00a21000164cfe06499c8689f8b9e", size = 6493664, upload-time = "2023-12-01T17:54:12.064Z" }, + { url = "https://files.pythonhosted.org/packages/a9/08/a185a89bf7832e7377b0d9c961073731c0e5e13beaf2b91d93adaec13baa/pyoxigraph-0.3.22-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1aca511243209005da32470bbfec9e023ac31095bbeaa8cedabe0a652adce38c", size = 6816344, upload-time = "2023-12-01T14:48:57.322Z" }, + { url = "https://files.pythonhosted.org/packages/45/b4/37f7114c4a21b1ee210505e953fdda533b6ff9a458570905ebfe53588cf2/pyoxigraph-0.3.22-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:ab329df388865afa9a934f1eac2e75264b220962a21bbcded6cb7ead96d1f1dd", size = 6940281, upload-time = "2023-12-04T11:52:02.933Z" }, + { url = "https://files.pythonhosted.org/packages/91/d3/7bff379c8075bb46301c29027e355f8a6f6388252e8626636619c2dbca19/pyoxigraph-0.3.22-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:60b7f13331b91827e2edfa8633ffb7e3bfc8630b708578fb0bc8d43c76754f20", size = 7308086, upload-time = "2023-12-01T15:05:51.979Z" }, + { url = "https://files.pythonhosted.org/packages/9d/7d/dd2d8c606633c2da157e7dd5a7c92b7bfdef1866bd4f382763ff17454431/pyoxigraph-0.3.22-cp37-abi3-win_amd64.whl", hash = "sha256:9a4ffd8ce28c3e8ce888662e0d9e9155e5226ecd8cd967f3c46391cf266c4c1d", size = 4716868, upload-time = "2023-12-01T14:51:37.776Z" }, +] + +[[package]] +name = "pyparsing" +version = "3.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bb/22/f1129e69d94ffff626bdb5c835506b3a5b4f3d070f17ea295e12c2c6f60f/pyparsing-3.2.3.tar.gz", hash = "sha256:b9c13f1ab8b3b542f72e28f634bad4de758ab3ce4546e4301970ad6fa77c38be", size = 1088608, upload-time = "2025-03-25T05:01:28.114Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/e7/df2285f3d08fee213f2d041540fa4fc9ca6c2d44cf36d3a035bf2a8d2bcc/pyparsing-3.2.3-py3-none-any.whl", hash = "sha256:a749938e02d6fd0b59b356ca504a24982314bb090c383e3cf201c95ef7e2bfcf", size = 111120, upload-time = "2025-03-25T05:01:24.908Z" }, +] + +[[package]] +name = "pytest" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, +] + +[[package]] +name = "pytest-logging" +version = "2015.11.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dc/1e/fb11174c9eaebcec27d36e9e994b90ffa168bc3226925900b9dbbf16c9da/pytest-logging-2015.11.4.tar.gz", hash = "sha256:cec5c85ecf18aab7b2ead5498a31b9f758680ef5a902b9054ab3f2bdbb77c896", size = 3916, upload-time = "2015-11-04T12:15:54.122Z" } + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bc/57/e84d88dfe0aec03b7a2d4327012c1627ab5f03652216c63d49846d7a6c58/python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", size = 39115, upload-time = "2024-01-23T06:33:00.505Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863, upload-time = "2024-01-23T06:32:58.246Z" }, +] + +[[package]] +name = "pytrie" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sortedcontainers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d3/19/15ec77ab9c85f7c36eb590d6ab7dd529f8c8516c0e2219f1a77a99d7ee77/PyTrie-0.4.0.tar.gz", hash = "sha256:8f4488f402d3465993fb6b6efa09866849ed8cda7903b50647b7d0342b805379", size = 95139, upload-time = "2020-10-21T15:39:30.334Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/fd/499b261a34e9c6e39b9f5711c4b3093bca980b8db4b49de3009d808f41c9/PyTrie-0.4.0-py3-none-any.whl", hash = "sha256:f687c224ee8c66cda8e8628a903011b692635ffbb08d4b39c5f92b18eb78c950", size = 6061, upload-time = "2024-03-09T16:59:46.768Z" }, +] + +[[package]] +name = "pytz" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/e5/af35f7ea75cf72f2cd079c95ee16797de7cd71f29ea7c68ae5ce7be1eda0/PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43", size = 125201, upload-time = "2023-07-18T00:00:23.308Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/06/1b305bf6aa704343be85444c9d011f626c763abb40c0edc1cad13bfd7f86/PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28", size = 178692, upload-time = "2023-08-28T18:43:24.924Z" }, + { url = "https://files.pythonhosted.org/packages/84/02/404de95ced348b73dd84f70e15a41843d817ff8c1744516bf78358f2ffd2/PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9", size = 165622, upload-time = "2023-08-28T18:43:26.54Z" }, + { url = "https://files.pythonhosted.org/packages/c7/4c/4a2908632fc980da6d918b9de9c1d9d7d7e70b2672b1ad5166ed27841ef7/PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef", size = 696937, upload-time = "2024-01-18T20:40:22.92Z" }, + { url = "https://files.pythonhosted.org/packages/b4/33/720548182ffa8344418126017aa1d4ab4aeec9a2275f04ce3f3573d8ace8/PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0", size = 724969, upload-time = "2023-08-28T18:43:28.56Z" }, + { url = "https://files.pythonhosted.org/packages/4f/78/77b40157b6cb5f2d3d31a3d9b2efd1ba3505371f76730d267e8b32cf4b7f/PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4", size = 712604, upload-time = "2023-08-28T18:43:30.206Z" }, + { url = "https://files.pythonhosted.org/packages/2e/97/3e0e089ee85e840f4b15bfa00e4e63d84a3691ababbfea92d6f820ea6f21/PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54", size = 126098, upload-time = "2023-08-28T18:43:31.835Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/fbade56564ad486809c27b322d0f7e6a89c01f6b4fe208402e90d4443a99/PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df", size = 138675, upload-time = "2023-08-28T18:43:33.613Z" }, +] + +[[package]] +name = "rdflib" +version = "7.1.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyparsing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e8/7e/cb2d74466bd8495051ebe2d241b1cb1d4acf9740d481126aef19ef2697f5/rdflib-7.1.4.tar.gz", hash = "sha256:fed46e24f26a788e2ab8e445f7077f00edcf95abb73bcef4b86cefa8b62dd174", size = 4692745, upload-time = "2025-03-29T02:23:02.386Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/31/e9b6f04288dcd3fa60cb3179260d6dad81b92aef3063d679ac7d80a827ea/rdflib-7.1.4-py3-none-any.whl", hash = "sha256:72f4adb1990fa5241abd22ddaf36d7cafa5d91d9ff2ba13f3086d339b213d997", size = 565051, upload-time = "2025-03-29T02:22:44.987Z" }, +] + +[[package]] +name = "redis" +version = "5.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/47/da/d283a37303a995cd36f8b92db85135153dc4f7a8e4441aa827721b442cfb/redis-5.2.1.tar.gz", hash = "sha256:16f2e22dff21d5125e8481515e386711a34cbec50f0e44413dd7d9c060a54e0f", size = 4608355, upload-time = "2024-12-06T09:50:41.956Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/5f/fa26b9b2672cbe30e07d9a5bdf39cf16e3b80b42916757c5f92bca88e4ba/redis-5.2.1-py3-none-any.whl", hash = "sha256:ee7e1056b9aea0f04c6c2ed59452947f34c4940ee025f5dd83e6a6418b6989e4", size = 261502, upload-time = "2024-12-06T09:50:39.656Z" }, +] + +[[package]] +name = "referencing" +version = "0.36.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2f/db/98b5c277be99dd18bfd91dd04e1b759cad18d1a338188c936e92f921c7e2/referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa", size = 74744, upload-time = "2025-01-25T08:48:16.138Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0", size = 26775, upload-time = "2025-01-25T08:48:14.241Z" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + +[[package]] +name = "requests-toolbelt" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", size = 206888, upload-time = "2023-05-01T04:11:33.229Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481, upload-time = "2023-05-01T04:11:28.427Z" }, +] + +[[package]] +name = "robokop-genetics" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "bmt" }, + { name = "redis" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/41/a6/75b9edf1186d3dbfb485910b40570ea9c7452ffce195934de2a40d17167f/robokop_genetics-0.7.0.tar.gz", hash = "sha256:87eb12250867c18f7e149d869fe9173f664f83e90d6c7b910303fd9ba9efc931", size = 18837, upload-time = "2025-10-07T07:18:29.144Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/96/7e941b2ad392429aac56b0826965534e79edff20784eb767c702cab8fbef/robokop_genetics-0.7.0-py3-none-any.whl", hash = "sha256:fe33f004138f5feb5c43157411b146411bf249b97a1a6900348607f874dd8494", size = 18695, upload-time = "2025-10-07T07:18:27.895Z" }, +] + +[[package]] +name = "robokop-orion" +version = "0.2.0" +source = { editable = "." } +dependencies = [ + { name = "bmt" }, + { name = "jsonlines" }, + { name = "orjson" }, + { name = "python-dotenv" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "requests-toolbelt" }, + { name = "robokop-genetics" }, + { name = "uuid-utils" }, + { name = "xxhash" }, +] + +[package.optional-dependencies] +robokop = [ + { name = "beautifulsoup4" }, + { name = "celery" }, + { name = "curies" }, + { name = "intermine" }, + { name = "mysql-connector-python" }, + { name = "neo4j" }, + { name = "openpyxl" }, + { name = "pandas" }, + { name = "polars" }, + { name = "prefixmaps" }, + { name = "psycopg2-binary" }, + { name = "pyoxigraph" }, + { name = "redis" }, +] + +[package.dev-dependencies] +dev = [ + { name = "pytest" }, +] + +[package.metadata] +requires-dist = [ + { name = "beautifulsoup4", marker = "extra == 'robokop'", specifier = ">=4.12.3" }, + { name = "bmt", specifier = ">=1.4.6" }, + { name = "celery", marker = "extra == 'robokop'", specifier = ">=5.4.0" }, + { name = "curies", marker = "extra == 'robokop'", specifier = ">=0.7.9" }, + { name = "intermine", marker = "extra == 'robokop'", git = "https://github.com/EvanDietzMorris/intermine-ws-python.git" }, + { name = "jsonlines", specifier = ">=4.0.0" }, + { name = "mysql-connector-python", marker = "extra == 'robokop'", specifier = ">=9.4.0" }, + { name = "neo4j", marker = "extra == 'robokop'", specifier = ">=5.28.0" }, + { name = "openpyxl", marker = "extra == 'robokop'", specifier = ">=3.1.5" }, + { name = "orjson", specifier = ">=3.11.7" }, + { name = "pandas", marker = "extra == 'robokop'", specifier = ">=2.3.3" }, + { name = "polars", marker = "extra == 'robokop'", specifier = ">=1.19.0" }, + { name = "prefixmaps", marker = "extra == 'robokop'", specifier = ">=0.2.6" }, + { name = "psycopg2-binary", marker = "extra == 'robokop'", specifier = ">=2.9.9" }, + { name = "pyoxigraph", marker = "extra == 'robokop'", specifier = ">=0.3.22" }, + { name = "python-dotenv", specifier = ">=1.0.1" }, + { name = "pyyaml", specifier = ">=6.0.1" }, + { name = "redis", marker = "extra == 'robokop'", specifier = ">=5.2.1" }, + { name = "requests", specifier = ">=2.32.5" }, + { name = "requests-toolbelt", specifier = ">=1.0.0" }, + { name = "robokop-genetics", specifier = ">=0.7.0" }, + { name = "uuid-utils", specifier = ">=0.14.1" }, + { name = "xxhash", specifier = ">=3.6.0" }, +] +provides-extras = ["robokop"] + +[package.metadata.requires-dev] +dev = [{ name = "pytest", specifier = ">=8.4.2" }] + +[[package]] +name = "rpds-py" +version = "0.27.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e9/dd/2c0cbe774744272b0ae725f44032c77bdcab6e8bcf544bffa3b6e70c8dba/rpds_py-0.27.1.tar.gz", hash = "sha256:26a1c73171d10b7acccbded82bf6a586ab8203601e565badc74bbbf8bc5a10f8", size = 27479, upload-time = "2025-08-27T12:16:36.024Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/fe/38de28dee5df58b8198c743fe2bea0c785c6d40941b9950bac4cdb71a014/rpds_py-0.27.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:ae2775c1973e3c30316892737b91f9283f9908e3cc7625b9331271eaaed7dc90", size = 361887, upload-time = "2025-08-27T12:13:10.233Z" }, + { url = "https://files.pythonhosted.org/packages/7c/9a/4b6c7eedc7dd90986bf0fab6ea2a091ec11c01b15f8ba0a14d3f80450468/rpds_py-0.27.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2643400120f55c8a96f7c9d858f7be0c88d383cd4653ae2cf0d0c88f668073e5", size = 345795, upload-time = "2025-08-27T12:13:11.65Z" }, + { url = "https://files.pythonhosted.org/packages/6f/0e/e650e1b81922847a09cca820237b0edee69416a01268b7754d506ade11ad/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16323f674c089b0360674a4abd28d5042947d54ba620f72514d69be4ff64845e", size = 385121, upload-time = "2025-08-27T12:13:13.008Z" }, + { url = "https://files.pythonhosted.org/packages/1b/ea/b306067a712988e2bff00dcc7c8f31d26c29b6d5931b461aa4b60a013e33/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9a1f4814b65eacac94a00fc9a526e3fdafd78e439469644032032d0d63de4881", size = 398976, upload-time = "2025-08-27T12:13:14.368Z" }, + { url = "https://files.pythonhosted.org/packages/2c/0a/26dc43c8840cb8fe239fe12dbc8d8de40f2365e838f3d395835dde72f0e5/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ba32c16b064267b22f1850a34051121d423b6f7338a12b9459550eb2096e7ec", size = 525953, upload-time = "2025-08-27T12:13:15.774Z" }, + { url = "https://files.pythonhosted.org/packages/22/14/c85e8127b573aaf3a0cbd7fbb8c9c99e735a4a02180c84da2a463b766e9e/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5c20f33fd10485b80f65e800bbe5f6785af510b9f4056c5a3c612ebc83ba6cb", size = 407915, upload-time = "2025-08-27T12:13:17.379Z" }, + { url = "https://files.pythonhosted.org/packages/ed/7b/8f4fee9ba1fb5ec856eb22d725a4efa3deb47f769597c809e03578b0f9d9/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:466bfe65bd932da36ff279ddd92de56b042f2266d752719beb97b08526268ec5", size = 386883, upload-time = "2025-08-27T12:13:18.704Z" }, + { url = "https://files.pythonhosted.org/packages/86/47/28fa6d60f8b74fcdceba81b272f8d9836ac0340570f68f5df6b41838547b/rpds_py-0.27.1-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:41e532bbdcb57c92ba3be62c42e9f096431b4cf478da9bc3bc6ce5c38ab7ba7a", size = 405699, upload-time = "2025-08-27T12:13:20.089Z" }, + { url = "https://files.pythonhosted.org/packages/d0/fd/c5987b5e054548df56953a21fe2ebed51fc1ec7c8f24fd41c067b68c4a0a/rpds_py-0.27.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f149826d742b406579466283769a8ea448eed82a789af0ed17b0cd5770433444", size = 423713, upload-time = "2025-08-27T12:13:21.436Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ba/3c4978b54a73ed19a7d74531be37a8bcc542d917c770e14d372b8daea186/rpds_py-0.27.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:80c60cfb5310677bd67cb1e85a1e8eb52e12529545441b43e6f14d90b878775a", size = 562324, upload-time = "2025-08-27T12:13:22.789Z" }, + { url = "https://files.pythonhosted.org/packages/b5/6c/6943a91768fec16db09a42b08644b960cff540c66aab89b74be6d4a144ba/rpds_py-0.27.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:7ee6521b9baf06085f62ba9c7a3e5becffbc32480d2f1b351559c001c38ce4c1", size = 593646, upload-time = "2025-08-27T12:13:24.122Z" }, + { url = "https://files.pythonhosted.org/packages/11/73/9d7a8f4be5f4396f011a6bb7a19fe26303a0dac9064462f5651ced2f572f/rpds_py-0.27.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a512c8263249a9d68cac08b05dd59d2b3f2061d99b322813cbcc14c3c7421998", size = 558137, upload-time = "2025-08-27T12:13:25.557Z" }, + { url = "https://files.pythonhosted.org/packages/6e/96/6772cbfa0e2485bcceef8071de7821f81aeac8bb45fbfd5542a3e8108165/rpds_py-0.27.1-cp312-cp312-win32.whl", hash = "sha256:819064fa048ba01b6dadc5116f3ac48610435ac9a0058bbde98e569f9e785c39", size = 221343, upload-time = "2025-08-27T12:13:26.967Z" }, + { url = "https://files.pythonhosted.org/packages/67/b6/c82f0faa9af1c6a64669f73a17ee0eeef25aff30bb9a1c318509efe45d84/rpds_py-0.27.1-cp312-cp312-win_amd64.whl", hash = "sha256:d9199717881f13c32c4046a15f024971a3b78ad4ea029e8da6b86e5aa9cf4594", size = 232497, upload-time = "2025-08-27T12:13:28.326Z" }, + { url = "https://files.pythonhosted.org/packages/e1/96/2817b44bd2ed11aebacc9251da03689d56109b9aba5e311297b6902136e2/rpds_py-0.27.1-cp312-cp312-win_arm64.whl", hash = "sha256:33aa65b97826a0e885ef6e278fbd934e98cdcfed80b63946025f01e2f5b29502", size = 222790, upload-time = "2025-08-27T12:13:29.71Z" }, + { url = "https://files.pythonhosted.org/packages/cc/77/610aeee8d41e39080c7e14afa5387138e3c9fa9756ab893d09d99e7d8e98/rpds_py-0.27.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:e4b9fcfbc021633863a37e92571d6f91851fa656f0180246e84cbd8b3f6b329b", size = 361741, upload-time = "2025-08-27T12:13:31.039Z" }, + { url = "https://files.pythonhosted.org/packages/3a/fc/c43765f201c6a1c60be2043cbdb664013def52460a4c7adace89d6682bf4/rpds_py-0.27.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1441811a96eadca93c517d08df75de45e5ffe68aa3089924f963c782c4b898cf", size = 345574, upload-time = "2025-08-27T12:13:32.902Z" }, + { url = "https://files.pythonhosted.org/packages/20/42/ee2b2ca114294cd9847d0ef9c26d2b0851b2e7e00bf14cc4c0b581df0fc3/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55266dafa22e672f5a4f65019015f90336ed31c6383bd53f5e7826d21a0e0b83", size = 385051, upload-time = "2025-08-27T12:13:34.228Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e8/1e430fe311e4799e02e2d1af7c765f024e95e17d651612425b226705f910/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d78827d7ac08627ea2c8e02c9e5b41180ea5ea1f747e9db0915e3adf36b62dcf", size = 398395, upload-time = "2025-08-27T12:13:36.132Z" }, + { url = "https://files.pythonhosted.org/packages/82/95/9dc227d441ff2670651c27a739acb2535ccaf8b351a88d78c088965e5996/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae92443798a40a92dc5f0b01d8a7c93adde0c4dc965310a29ae7c64d72b9fad2", size = 524334, upload-time = "2025-08-27T12:13:37.562Z" }, + { url = "https://files.pythonhosted.org/packages/87/01/a670c232f401d9ad461d9a332aa4080cd3cb1d1df18213dbd0d2a6a7ab51/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c46c9dd2403b66a2a3b9720ec4b74d4ab49d4fabf9f03dfdce2d42af913fe8d0", size = 407691, upload-time = "2025-08-27T12:13:38.94Z" }, + { url = "https://files.pythonhosted.org/packages/03/36/0a14aebbaa26fe7fab4780c76f2239e76cc95a0090bdb25e31d95c492fcd/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2efe4eb1d01b7f5f1939f4ef30ecea6c6b3521eec451fb93191bf84b2a522418", size = 386868, upload-time = "2025-08-27T12:13:40.192Z" }, + { url = "https://files.pythonhosted.org/packages/3b/03/8c897fb8b5347ff6c1cc31239b9611c5bf79d78c984430887a353e1409a1/rpds_py-0.27.1-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:15d3b4d83582d10c601f481eca29c3f138d44c92187d197aff663a269197c02d", size = 405469, upload-time = "2025-08-27T12:13:41.496Z" }, + { url = "https://files.pythonhosted.org/packages/da/07/88c60edc2df74850d496d78a1fdcdc7b54360a7f610a4d50008309d41b94/rpds_py-0.27.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4ed2e16abbc982a169d30d1a420274a709949e2cbdef119fe2ec9d870b42f274", size = 422125, upload-time = "2025-08-27T12:13:42.802Z" }, + { url = "https://files.pythonhosted.org/packages/6b/86/5f4c707603e41b05f191a749984f390dabcbc467cf833769b47bf14ba04f/rpds_py-0.27.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a75f305c9b013289121ec0f1181931975df78738cdf650093e6b86d74aa7d8dd", size = 562341, upload-time = "2025-08-27T12:13:44.472Z" }, + { url = "https://files.pythonhosted.org/packages/b2/92/3c0cb2492094e3cd9baf9e49bbb7befeceb584ea0c1a8b5939dca4da12e5/rpds_py-0.27.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:67ce7620704745881a3d4b0ada80ab4d99df390838839921f99e63c474f82cf2", size = 592511, upload-time = "2025-08-27T12:13:45.898Z" }, + { url = "https://files.pythonhosted.org/packages/10/bb/82e64fbb0047c46a168faa28d0d45a7851cd0582f850b966811d30f67ad8/rpds_py-0.27.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9d992ac10eb86d9b6f369647b6a3f412fc0075cfd5d799530e84d335e440a002", size = 557736, upload-time = "2025-08-27T12:13:47.408Z" }, + { url = "https://files.pythonhosted.org/packages/00/95/3c863973d409210da7fb41958172c6b7dbe7fc34e04d3cc1f10bb85e979f/rpds_py-0.27.1-cp313-cp313-win32.whl", hash = "sha256:4f75e4bd8ab8db624e02c8e2fc4063021b58becdbe6df793a8111d9343aec1e3", size = 221462, upload-time = "2025-08-27T12:13:48.742Z" }, + { url = "https://files.pythonhosted.org/packages/ce/2c/5867b14a81dc217b56d95a9f2a40fdbc56a1ab0181b80132beeecbd4b2d6/rpds_py-0.27.1-cp313-cp313-win_amd64.whl", hash = "sha256:f9025faafc62ed0b75a53e541895ca272815bec18abe2249ff6501c8f2e12b83", size = 232034, upload-time = "2025-08-27T12:13:50.11Z" }, + { url = "https://files.pythonhosted.org/packages/c7/78/3958f3f018c01923823f1e47f1cc338e398814b92d83cd278364446fac66/rpds_py-0.27.1-cp313-cp313-win_arm64.whl", hash = "sha256:ed10dc32829e7d222b7d3b93136d25a406ba9788f6a7ebf6809092da1f4d279d", size = 222392, upload-time = "2025-08-27T12:13:52.587Z" }, + { url = "https://files.pythonhosted.org/packages/01/76/1cdf1f91aed5c3a7bf2eba1f1c4e4d6f57832d73003919a20118870ea659/rpds_py-0.27.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:92022bbbad0d4426e616815b16bc4127f83c9a74940e1ccf3cfe0b387aba0228", size = 358355, upload-time = "2025-08-27T12:13:54.012Z" }, + { url = "https://files.pythonhosted.org/packages/c3/6f/bf142541229374287604caf3bb2a4ae17f0a580798fd72d3b009b532db4e/rpds_py-0.27.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:47162fdab9407ec3f160805ac3e154df042e577dd53341745fc7fb3f625e6d92", size = 342138, upload-time = "2025-08-27T12:13:55.791Z" }, + { url = "https://files.pythonhosted.org/packages/1a/77/355b1c041d6be40886c44ff5e798b4e2769e497b790f0f7fd1e78d17e9a8/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb89bec23fddc489e5d78b550a7b773557c9ab58b7946154a10a6f7a214a48b2", size = 380247, upload-time = "2025-08-27T12:13:57.683Z" }, + { url = "https://files.pythonhosted.org/packages/d6/a4/d9cef5c3946ea271ce2243c51481971cd6e34f21925af2783dd17b26e815/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e48af21883ded2b3e9eb48cb7880ad8598b31ab752ff3be6457001d78f416723", size = 390699, upload-time = "2025-08-27T12:13:59.137Z" }, + { url = "https://files.pythonhosted.org/packages/3a/06/005106a7b8c6c1a7e91b73169e49870f4af5256119d34a361ae5240a0c1d/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6f5b7bd8e219ed50299e58551a410b64daafb5017d54bbe822e003856f06a802", size = 521852, upload-time = "2025-08-27T12:14:00.583Z" }, + { url = "https://files.pythonhosted.org/packages/e5/3e/50fb1dac0948e17a02eb05c24510a8fe12d5ce8561c6b7b7d1339ab7ab9c/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08f1e20bccf73b08d12d804d6e1c22ca5530e71659e6673bce31a6bb71c1e73f", size = 402582, upload-time = "2025-08-27T12:14:02.034Z" }, + { url = "https://files.pythonhosted.org/packages/cb/b0/f4e224090dc5b0ec15f31a02d746ab24101dd430847c4d99123798661bfc/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0dc5dceeaefcc96dc192e3a80bbe1d6c410c469e97bdd47494a7d930987f18b2", size = 384126, upload-time = "2025-08-27T12:14:03.437Z" }, + { url = "https://files.pythonhosted.org/packages/54/77/ac339d5f82b6afff1df8f0fe0d2145cc827992cb5f8eeb90fc9f31ef7a63/rpds_py-0.27.1-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:d76f9cc8665acdc0c9177043746775aa7babbf479b5520b78ae4002d889f5c21", size = 399486, upload-time = "2025-08-27T12:14:05.443Z" }, + { url = "https://files.pythonhosted.org/packages/d6/29/3e1c255eee6ac358c056a57d6d6869baa00a62fa32eea5ee0632039c50a3/rpds_py-0.27.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:134fae0e36022edad8290a6661edf40c023562964efea0cc0ec7f5d392d2aaef", size = 414832, upload-time = "2025-08-27T12:14:06.902Z" }, + { url = "https://files.pythonhosted.org/packages/3f/db/6d498b844342deb3fa1d030598db93937a9964fcf5cb4da4feb5f17be34b/rpds_py-0.27.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb11a4f1b2b63337cfd3b4d110af778a59aae51c81d195768e353d8b52f88081", size = 557249, upload-time = "2025-08-27T12:14:08.37Z" }, + { url = "https://files.pythonhosted.org/packages/60/f3/690dd38e2310b6f68858a331399b4d6dbb9132c3e8ef8b4333b96caf403d/rpds_py-0.27.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:13e608ac9f50a0ed4faec0e90ece76ae33b34c0e8656e3dceb9a7db994c692cd", size = 587356, upload-time = "2025-08-27T12:14:10.034Z" }, + { url = "https://files.pythonhosted.org/packages/86/e3/84507781cccd0145f35b1dc32c72675200c5ce8d5b30f813e49424ef68fc/rpds_py-0.27.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dd2135527aa40f061350c3f8f89da2644de26cd73e4de458e79606384f4f68e7", size = 555300, upload-time = "2025-08-27T12:14:11.783Z" }, + { url = "https://files.pythonhosted.org/packages/e5/ee/375469849e6b429b3516206b4580a79e9ef3eb12920ddbd4492b56eaacbe/rpds_py-0.27.1-cp313-cp313t-win32.whl", hash = "sha256:3020724ade63fe320a972e2ffd93b5623227e684315adce194941167fee02688", size = 216714, upload-time = "2025-08-27T12:14:13.629Z" }, + { url = "https://files.pythonhosted.org/packages/21/87/3fc94e47c9bd0742660e84706c311a860dcae4374cf4a03c477e23ce605a/rpds_py-0.27.1-cp313-cp313t-win_amd64.whl", hash = "sha256:8ee50c3e41739886606388ba3ab3ee2aae9f35fb23f833091833255a31740797", size = 228943, upload-time = "2025-08-27T12:14:14.937Z" }, + { url = "https://files.pythonhosted.org/packages/70/36/b6e6066520a07cf029d385de869729a895917b411e777ab1cde878100a1d/rpds_py-0.27.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:acb9aafccaae278f449d9c713b64a9e68662e7799dbd5859e2c6b3c67b56d334", size = 362472, upload-time = "2025-08-27T12:14:16.333Z" }, + { url = "https://files.pythonhosted.org/packages/af/07/b4646032e0dcec0df9c73a3bd52f63bc6c5f9cda992f06bd0e73fe3fbebd/rpds_py-0.27.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b7fb801aa7f845ddf601c49630deeeccde7ce10065561d92729bfe81bd21fb33", size = 345676, upload-time = "2025-08-27T12:14:17.764Z" }, + { url = "https://files.pythonhosted.org/packages/b0/16/2f1003ee5d0af4bcb13c0cf894957984c32a6751ed7206db2aee7379a55e/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fe0dd05afb46597b9a2e11c351e5e4283c741237e7f617ffb3252780cca9336a", size = 385313, upload-time = "2025-08-27T12:14:19.829Z" }, + { url = "https://files.pythonhosted.org/packages/05/cd/7eb6dd7b232e7f2654d03fa07f1414d7dfc980e82ba71e40a7c46fd95484/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b6dfb0e058adb12d8b1d1b25f686e94ffa65d9995a5157afe99743bf7369d62b", size = 399080, upload-time = "2025-08-27T12:14:21.531Z" }, + { url = "https://files.pythonhosted.org/packages/20/51/5829afd5000ec1cb60f304711f02572d619040aa3ec033d8226817d1e571/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed090ccd235f6fa8bb5861684567f0a83e04f52dfc2e5c05f2e4b1309fcf85e7", size = 523868, upload-time = "2025-08-27T12:14:23.485Z" }, + { url = "https://files.pythonhosted.org/packages/05/2c/30eebca20d5db95720ab4d2faec1b5e4c1025c473f703738c371241476a2/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bf876e79763eecf3e7356f157540d6a093cef395b65514f17a356f62af6cc136", size = 408750, upload-time = "2025-08-27T12:14:24.924Z" }, + { url = "https://files.pythonhosted.org/packages/90/1a/cdb5083f043597c4d4276eae4e4c70c55ab5accec078da8611f24575a367/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:12ed005216a51b1d6e2b02a7bd31885fe317e45897de81d86dcce7d74618ffff", size = 387688, upload-time = "2025-08-27T12:14:27.537Z" }, + { url = "https://files.pythonhosted.org/packages/7c/92/cf786a15320e173f945d205ab31585cc43969743bb1a48b6888f7a2b0a2d/rpds_py-0.27.1-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:ee4308f409a40e50593c7e3bb8cbe0b4d4c66d1674a316324f0c2f5383b486f9", size = 407225, upload-time = "2025-08-27T12:14:28.981Z" }, + { url = "https://files.pythonhosted.org/packages/33/5c/85ee16df5b65063ef26017bef33096557a4c83fbe56218ac7cd8c235f16d/rpds_py-0.27.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0b08d152555acf1f455154d498ca855618c1378ec810646fcd7c76416ac6dc60", size = 423361, upload-time = "2025-08-27T12:14:30.469Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8e/1c2741307fcabd1a334ecf008e92c4f47bb6f848712cf15c923becfe82bb/rpds_py-0.27.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:dce51c828941973a5684d458214d3a36fcd28da3e1875d659388f4f9f12cc33e", size = 562493, upload-time = "2025-08-27T12:14:31.987Z" }, + { url = "https://files.pythonhosted.org/packages/04/03/5159321baae9b2222442a70c1f988cbbd66b9be0675dd3936461269be360/rpds_py-0.27.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:c1476d6f29eb81aa4151c9a31219b03f1f798dc43d8af1250a870735516a1212", size = 592623, upload-time = "2025-08-27T12:14:33.543Z" }, + { url = "https://files.pythonhosted.org/packages/ff/39/c09fd1ad28b85bc1d4554a8710233c9f4cefd03d7717a1b8fbfd171d1167/rpds_py-0.27.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3ce0cac322b0d69b63c9cdb895ee1b65805ec9ffad37639f291dd79467bee675", size = 558800, upload-time = "2025-08-27T12:14:35.436Z" }, + { url = "https://files.pythonhosted.org/packages/c5/d6/99228e6bbcf4baa764b18258f519a9035131d91b538d4e0e294313462a98/rpds_py-0.27.1-cp314-cp314-win32.whl", hash = "sha256:dfbfac137d2a3d0725758cd141f878bf4329ba25e34979797c89474a89a8a3a3", size = 221943, upload-time = "2025-08-27T12:14:36.898Z" }, + { url = "https://files.pythonhosted.org/packages/be/07/c802bc6b8e95be83b79bdf23d1aa61d68324cb1006e245d6c58e959e314d/rpds_py-0.27.1-cp314-cp314-win_amd64.whl", hash = "sha256:a6e57b0abfe7cc513450fcf529eb486b6e4d3f8aee83e92eb5f1ef848218d456", size = 233739, upload-time = "2025-08-27T12:14:38.386Z" }, + { url = "https://files.pythonhosted.org/packages/c8/89/3e1b1c16d4c2d547c5717377a8df99aee8099ff050f87c45cb4d5fa70891/rpds_py-0.27.1-cp314-cp314-win_arm64.whl", hash = "sha256:faf8d146f3d476abfee026c4ae3bdd9ca14236ae4e4c310cbd1cf75ba33d24a3", size = 223120, upload-time = "2025-08-27T12:14:39.82Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/dc7931dc2fa4a6e46b2a4fa744a9fe5c548efd70e0ba74f40b39fa4a8c10/rpds_py-0.27.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:ba81d2b56b6d4911ce735aad0a1d4495e808b8ee4dc58715998741a26874e7c2", size = 358944, upload-time = "2025-08-27T12:14:41.199Z" }, + { url = "https://files.pythonhosted.org/packages/e6/22/4af76ac4e9f336bfb1a5f240d18a33c6b2fcaadb7472ac7680576512b49a/rpds_py-0.27.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:84f7d509870098de0e864cad0102711c1e24e9b1a50ee713b65928adb22269e4", size = 342283, upload-time = "2025-08-27T12:14:42.699Z" }, + { url = "https://files.pythonhosted.org/packages/1c/15/2a7c619b3c2272ea9feb9ade67a45c40b3eeb500d503ad4c28c395dc51b4/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9e960fc78fecd1100539f14132425e1d5fe44ecb9239f8f27f079962021523e", size = 380320, upload-time = "2025-08-27T12:14:44.157Z" }, + { url = "https://files.pythonhosted.org/packages/a2/7d/4c6d243ba4a3057e994bb5bedd01b5c963c12fe38dde707a52acdb3849e7/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:62f85b665cedab1a503747617393573995dac4600ff51869d69ad2f39eb5e817", size = 391760, upload-time = "2025-08-27T12:14:45.845Z" }, + { url = "https://files.pythonhosted.org/packages/b4/71/b19401a909b83bcd67f90221330bc1ef11bc486fe4e04c24388d28a618ae/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fed467af29776f6556250c9ed85ea5a4dd121ab56a5f8b206e3e7a4c551e48ec", size = 522476, upload-time = "2025-08-27T12:14:47.364Z" }, + { url = "https://files.pythonhosted.org/packages/e4/44/1a3b9715c0455d2e2f0f6df5ee6d6f5afdc423d0773a8a682ed2b43c566c/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2729615f9d430af0ae6b36cf042cb55c0936408d543fb691e1a9e36648fd35a", size = 403418, upload-time = "2025-08-27T12:14:49.991Z" }, + { url = "https://files.pythonhosted.org/packages/1c/4b/fb6c4f14984eb56673bc868a66536f53417ddb13ed44b391998100a06a96/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1b207d881a9aef7ba753d69c123a35d96ca7cb808056998f6b9e8747321f03b8", size = 384771, upload-time = "2025-08-27T12:14:52.159Z" }, + { url = "https://files.pythonhosted.org/packages/c0/56/d5265d2d28b7420d7b4d4d85cad8ef891760f5135102e60d5c970b976e41/rpds_py-0.27.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:639fd5efec029f99b79ae47e5d7e00ad8a773da899b6309f6786ecaf22948c48", size = 400022, upload-time = "2025-08-27T12:14:53.859Z" }, + { url = "https://files.pythonhosted.org/packages/8f/e9/9f5fc70164a569bdd6ed9046486c3568d6926e3a49bdefeeccfb18655875/rpds_py-0.27.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fecc80cb2a90e28af8a9b366edacf33d7a91cbfe4c2c4544ea1246e949cfebeb", size = 416787, upload-time = "2025-08-27T12:14:55.673Z" }, + { url = "https://files.pythonhosted.org/packages/d4/64/56dd03430ba491db943a81dcdef115a985aac5f44f565cd39a00c766d45c/rpds_py-0.27.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:42a89282d711711d0a62d6f57d81aa43a1368686c45bc1c46b7f079d55692734", size = 557538, upload-time = "2025-08-27T12:14:57.245Z" }, + { url = "https://files.pythonhosted.org/packages/3f/36/92cc885a3129993b1d963a2a42ecf64e6a8e129d2c7cc980dbeba84e55fb/rpds_py-0.27.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:cf9931f14223de59551ab9d38ed18d92f14f055a5f78c1d8ad6493f735021bbb", size = 588512, upload-time = "2025-08-27T12:14:58.728Z" }, + { url = "https://files.pythonhosted.org/packages/dd/10/6b283707780a81919f71625351182b4f98932ac89a09023cb61865136244/rpds_py-0.27.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f39f58a27cc6e59f432b568ed8429c7e1641324fbe38131de852cd77b2d534b0", size = 555813, upload-time = "2025-08-27T12:15:00.334Z" }, + { url = "https://files.pythonhosted.org/packages/04/2e/30b5ea18c01379da6272a92825dd7e53dc9d15c88a19e97932d35d430ef7/rpds_py-0.27.1-cp314-cp314t-win32.whl", hash = "sha256:d5fa0ee122dc09e23607a28e6d7b150da16c662e66409bbe85230e4c85bb528a", size = 217385, upload-time = "2025-08-27T12:15:01.937Z" }, + { url = "https://files.pythonhosted.org/packages/32/7d/97119da51cb1dd3f2f3c0805f155a3aa4a95fa44fe7d78ae15e69edf4f34/rpds_py-0.27.1-cp314-cp314t-win_amd64.whl", hash = "sha256:6567d2bb951e21232c2f660c24cf3470bb96de56cdcb3f071a83feeaff8a2772", size = 230097, upload-time = "2025-08-27T12:15:03.961Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "sortedcontainers" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" }, +] + +[[package]] +name = "soupsieve" +version = "2.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6d/e6/21ccce3262dd4889aa3332e5a119a3491a95e8f60939870a3a035aabac0d/soupsieve-2.8.tar.gz", hash = "sha256:e2dd4a40a628cb5f28f6d4b0db8800b8f581b65bb380b97de22ba5ca8d72572f", size = 103472, upload-time = "2025-08-27T15:39:51.78Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/a0/bb38d3b76b8cae341dad93a2dd83ab7462e6dbcdd84d43f54ee60a8dc167/soupsieve-2.8-py3-none-any.whl", hash = "sha256:0cc76456a30e20f5d7f2e14a98a4ae2ee4e5abdc7c5ea0aafe795f344bc7984c", size = 36679, upload-time = "2025-08-27T15:39:50.179Z" }, +] + +[[package]] +name = "stringcase" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/1f/1241aa3d66e8dc1612427b17885f5fcd9c9ee3079fc0d28e9a3aeeb36fa3/stringcase-1.2.0.tar.gz", hash = "sha256:48a06980661908efe8d9d34eab2b6c13aefa2163b3ced26972902e3bdfd87008", size = 2958, upload-time = "2017-08-06T01:40:57.021Z" } + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726, upload-time = "2025-05-21T18:55:23.885Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload-time = "2025-05-21T18:55:22.152Z" }, +] + +[[package]] +name = "tzdata" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380, upload-time = "2025-03-23T13:54:43.652Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" }, +] + +[[package]] +name = "urllib3" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, +] + +[[package]] +name = "uuid-utils" +version = "0.14.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/d1/38a573f0c631c062cf42fa1f5d021d4dd3c31fb23e4376e4b56b0c9fbbed/uuid_utils-0.14.1.tar.gz", hash = "sha256:9bfc95f64af80ccf129c604fb6b8ca66c6f256451e32bc4570f760e4309c9b69", size = 22195, upload-time = "2026-02-20T22:50:38.833Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/b7/add4363039a34506a58457d96d4aa2126061df3a143eb4d042aedd6a2e76/uuid_utils-0.14.1-cp39-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:93a3b5dc798a54a1feb693f2d1cb4cf08258c32ff05ae4929b5f0a2ca624a4f0", size = 604679, upload-time = "2026-02-20T22:50:27.469Z" }, + { url = "https://files.pythonhosted.org/packages/dd/84/d1d0bef50d9e66d31b2019997c741b42274d53dde2e001b7a83e9511c339/uuid_utils-0.14.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:ccd65a4b8e83af23eae5e56d88034b2fe7264f465d3e830845f10d1591b81741", size = 309346, upload-time = "2026-02-20T22:50:31.857Z" }, + { url = "https://files.pythonhosted.org/packages/ef/ed/b6d6fd52a6636d7c3eddf97d68da50910bf17cd5ac221992506fb56cf12e/uuid_utils-0.14.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b56b0cacd81583834820588378e432b0696186683b813058b707aedc1e16c4b1", size = 344714, upload-time = "2026-02-20T22:50:42.642Z" }, + { url = "https://files.pythonhosted.org/packages/a8/a7/a19a1719fb626fe0b31882db36056d44fe904dc0cf15b06fdf56b2679cf7/uuid_utils-0.14.1-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb3cf14de789097320a3c56bfdfdd51b1225d11d67298afbedee7e84e3837c96", size = 350914, upload-time = "2026-02-20T22:50:36.487Z" }, + { url = "https://files.pythonhosted.org/packages/1d/fc/f6690e667fdc3bb1a73f57951f97497771c56fe23e3d302d7404be394d4f/uuid_utils-0.14.1-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60e0854a90d67f4b0cc6e54773deb8be618f4c9bad98d3326f081423b5d14fae", size = 482609, upload-time = "2026-02-20T22:50:37.511Z" }, + { url = "https://files.pythonhosted.org/packages/54/6e/dcd3fa031320921a12ec7b4672dea3bd1dd90ddffa363a91831ba834d559/uuid_utils-0.14.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce6743ba194de3910b5feb1a62590cd2587e33a73ab6af8a01b642ceb5055862", size = 345699, upload-time = "2026-02-20T22:50:46.87Z" }, + { url = "https://files.pythonhosted.org/packages/04/28/e5220204b58b44ac0047226a9d016a113fde039280cc8732d9e6da43b39f/uuid_utils-0.14.1-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:043fb58fde6cf1620a6c066382f04f87a8e74feb0f95a585e4ed46f5d44af57b", size = 372205, upload-time = "2026-02-20T22:50:28.438Z" }, + { url = "https://files.pythonhosted.org/packages/c7/d9/3d2eb98af94b8dfffc82b6a33b4dfc87b0a5de2c68a28f6dde0db1f8681b/uuid_utils-0.14.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c915d53f22945e55fe0d3d3b0b87fd965a57f5fd15666fd92d6593a73b1dd297", size = 521836, upload-time = "2026-02-20T22:50:23.057Z" }, + { url = "https://files.pythonhosted.org/packages/a8/15/0eb106cc6fe182f7577bc0ab6e2f0a40be247f35c5e297dbf7bbc460bd02/uuid_utils-0.14.1-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:0972488e3f9b449e83f006ead5a0e0a33ad4a13e4462e865b7c286ab7d7566a3", size = 625260, upload-time = "2026-02-20T22:50:25.949Z" }, + { url = "https://files.pythonhosted.org/packages/3c/17/f539507091334b109e7496830af2f093d9fc8082411eafd3ece58af1f8ba/uuid_utils-0.14.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:1c238812ae0c8ffe77d8d447a32c6dfd058ea4631246b08b5a71df586ff08531", size = 587824, upload-time = "2026-02-20T22:50:35.225Z" }, + { url = "https://files.pythonhosted.org/packages/2e/c2/d37a7b2e41f153519367d4db01f0526e0d4b06f1a4a87f1c5dfca5d70a8b/uuid_utils-0.14.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:bec8f8ef627af86abf8298e7ec50926627e29b34fa907fcfbedb45aaa72bca43", size = 551407, upload-time = "2026-02-20T22:50:44.915Z" }, + { url = "https://files.pythonhosted.org/packages/65/36/2d24b2cbe78547c6532da33fb8613debd3126eccc33a6374ab788f5e46e9/uuid_utils-0.14.1-cp39-abi3-win32.whl", hash = "sha256:b54d6aa6252d96bac1fdbc80d26ba71bad9f220b2724d692ad2f2310c22ef523", size = 183476, upload-time = "2026-02-20T22:50:32.745Z" }, + { url = "https://files.pythonhosted.org/packages/83/92/2d7e90df8b1a69ec4cff33243ce02b7a62f926ef9e2f0eca5a026889cd73/uuid_utils-0.14.1-cp39-abi3-win_amd64.whl", hash = "sha256:fc27638c2ce267a0ce3e06828aff786f91367f093c80625ee21dad0208e0f5ba", size = 187147, upload-time = "2026-02-20T22:50:45.807Z" }, + { url = "https://files.pythonhosted.org/packages/d9/26/529f4beee17e5248e37e0bc17a2761d34c0fa3b1e5729c88adb2065bae6e/uuid_utils-0.14.1-cp39-abi3-win_arm64.whl", hash = "sha256:b04cb49b42afbc4ff8dbc60cf054930afc479d6f4dd7f1ec3bbe5dbfdde06b7a", size = 188132, upload-time = "2026-02-20T22:50:41.718Z" }, +] + +[[package]] +name = "vine" +version = "5.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/e4/d07b5f29d283596b9727dd5275ccbceb63c44a1a82aa9e4bfd20426762ac/vine-5.1.0.tar.gz", hash = "sha256:8b62e981d35c41049211cf62a0a1242d8c1ee9bd15bb196ce38aefd6799e61e0", size = 48980, upload-time = "2023-11-05T08:46:53.857Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/ff/7c0c86c43b3cbb927e0ccc0255cb4057ceba4799cd44ae95174ce8e8b5b2/vine-5.1.0-py3-none-any.whl", hash = "sha256:40fdf3c48b2cfe1c38a49e9ae2da6fda88e4794c810050a728bd7413811fb1dc", size = 9636, upload-time = "2023-11-05T08:46:51.205Z" }, +] + +[[package]] +name = "wcwidth" +version = "0.2.13" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/63/53559446a878410fc5a5974feb13d31d78d752eb18aeba59c7fef1af7598/wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5", size = 101301, upload-time = "2024-01-06T02:10:57.829Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166, upload-time = "2024-01-06T02:10:55.763Z" }, +] + +[[package]] +name = "wrapt" +version = "1.17.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/8f/aeb76c5b46e273670962298c23e7ddde79916cb74db802131d49a85e4b7d/wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0", size = 55547, upload-time = "2025-08-12T05:53:21.714Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/41/cad1aba93e752f1f9268c77270da3c469883d56e2798e7df6240dcb2287b/wrapt-1.17.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ab232e7fdb44cdfbf55fc3afa31bcdb0d8980b9b95c38b6405df2acb672af0e0", size = 53998, upload-time = "2025-08-12T05:51:47.138Z" }, + { url = "https://files.pythonhosted.org/packages/60/f8/096a7cc13097a1869fe44efe68dace40d2a16ecb853141394047f0780b96/wrapt-1.17.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9baa544e6acc91130e926e8c802a17f3b16fbea0fd441b5a60f5cf2cc5c3deba", size = 39020, upload-time = "2025-08-12T05:51:35.906Z" }, + { url = "https://files.pythonhosted.org/packages/33/df/bdf864b8997aab4febb96a9ae5c124f700a5abd9b5e13d2a3214ec4be705/wrapt-1.17.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6b538e31eca1a7ea4605e44f81a48aa24c4632a277431a6ed3f328835901f4fd", size = 39098, upload-time = "2025-08-12T05:51:57.474Z" }, + { url = "https://files.pythonhosted.org/packages/9f/81/5d931d78d0eb732b95dc3ddaeeb71c8bb572fb01356e9133916cd729ecdd/wrapt-1.17.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:042ec3bb8f319c147b1301f2393bc19dba6e176b7da446853406d041c36c7828", size = 88036, upload-time = "2025-08-12T05:52:34.784Z" }, + { url = "https://files.pythonhosted.org/packages/ca/38/2e1785df03b3d72d34fc6252d91d9d12dc27a5c89caef3335a1bbb8908ca/wrapt-1.17.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3af60380ba0b7b5aeb329bc4e402acd25bd877e98b3727b0135cb5c2efdaefe9", size = 88156, upload-time = "2025-08-12T05:52:13.599Z" }, + { url = "https://files.pythonhosted.org/packages/b3/8b/48cdb60fe0603e34e05cffda0b2a4adab81fd43718e11111a4b0100fd7c1/wrapt-1.17.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b02e424deef65c9f7326d8c19220a2c9040c51dc165cddb732f16198c168396", size = 87102, upload-time = "2025-08-12T05:52:14.56Z" }, + { url = "https://files.pythonhosted.org/packages/3c/51/d81abca783b58f40a154f1b2c56db1d2d9e0d04fa2d4224e357529f57a57/wrapt-1.17.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:74afa28374a3c3a11b3b5e5fca0ae03bef8450d6aa3ab3a1e2c30e3a75d023dc", size = 87732, upload-time = "2025-08-12T05:52:36.165Z" }, + { url = "https://files.pythonhosted.org/packages/9e/b1/43b286ca1392a006d5336412d41663eeef1ad57485f3e52c767376ba7e5a/wrapt-1.17.3-cp312-cp312-win32.whl", hash = "sha256:4da9f45279fff3543c371d5ababc57a0384f70be244de7759c85a7f989cb4ebe", size = 36705, upload-time = "2025-08-12T05:53:07.123Z" }, + { url = "https://files.pythonhosted.org/packages/28/de/49493f962bd3c586ab4b88066e967aa2e0703d6ef2c43aa28cb83bf7b507/wrapt-1.17.3-cp312-cp312-win_amd64.whl", hash = "sha256:e71d5c6ebac14875668a1e90baf2ea0ef5b7ac7918355850c0908ae82bcb297c", size = 38877, upload-time = "2025-08-12T05:53:05.436Z" }, + { url = "https://files.pythonhosted.org/packages/f1/48/0f7102fe9cb1e8a5a77f80d4f0956d62d97034bbe88d33e94699f99d181d/wrapt-1.17.3-cp312-cp312-win_arm64.whl", hash = "sha256:604d076c55e2fdd4c1c03d06dc1a31b95130010517b5019db15365ec4a405fc6", size = 36885, upload-time = "2025-08-12T05:52:54.367Z" }, + { url = "https://files.pythonhosted.org/packages/fc/f6/759ece88472157acb55fc195e5b116e06730f1b651b5b314c66291729193/wrapt-1.17.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a47681378a0439215912ef542c45a783484d4dd82bac412b71e59cf9c0e1cea0", size = 54003, upload-time = "2025-08-12T05:51:48.627Z" }, + { url = "https://files.pythonhosted.org/packages/4f/a9/49940b9dc6d47027dc850c116d79b4155f15c08547d04db0f07121499347/wrapt-1.17.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a30837587c6ee3cd1a4d1c2ec5d24e77984d44e2f34547e2323ddb4e22eb77", size = 39025, upload-time = "2025-08-12T05:51:37.156Z" }, + { url = "https://files.pythonhosted.org/packages/45/35/6a08de0f2c96dcdd7fe464d7420ddb9a7655a6561150e5fc4da9356aeaab/wrapt-1.17.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:16ecf15d6af39246fe33e507105d67e4b81d8f8d2c6598ff7e3ca1b8a37213f7", size = 39108, upload-time = "2025-08-12T05:51:58.425Z" }, + { url = "https://files.pythonhosted.org/packages/0c/37/6faf15cfa41bf1f3dba80cd3f5ccc6622dfccb660ab26ed79f0178c7497f/wrapt-1.17.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6fd1ad24dc235e4ab88cda009e19bf347aabb975e44fd5c2fb22a3f6e4141277", size = 88072, upload-time = "2025-08-12T05:52:37.53Z" }, + { url = "https://files.pythonhosted.org/packages/78/f2/efe19ada4a38e4e15b6dff39c3e3f3f73f5decf901f66e6f72fe79623a06/wrapt-1.17.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ed61b7c2d49cee3c027372df5809a59d60cf1b6c2f81ee980a091f3afed6a2d", size = 88214, upload-time = "2025-08-12T05:52:15.886Z" }, + { url = "https://files.pythonhosted.org/packages/40/90/ca86701e9de1622b16e09689fc24b76f69b06bb0150990f6f4e8b0eeb576/wrapt-1.17.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:423ed5420ad5f5529db9ce89eac09c8a2f97da18eb1c870237e84c5a5c2d60aa", size = 87105, upload-time = "2025-08-12T05:52:17.914Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e0/d10bd257c9a3e15cbf5523025252cc14d77468e8ed644aafb2d6f54cb95d/wrapt-1.17.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e01375f275f010fcbf7f643b4279896d04e571889b8a5b3f848423d91bf07050", size = 87766, upload-time = "2025-08-12T05:52:39.243Z" }, + { url = "https://files.pythonhosted.org/packages/e8/cf/7d848740203c7b4b27eb55dbfede11aca974a51c3d894f6cc4b865f42f58/wrapt-1.17.3-cp313-cp313-win32.whl", hash = "sha256:53e5e39ff71b3fc484df8a522c933ea2b7cdd0d5d15ae82e5b23fde87d44cbd8", size = 36711, upload-time = "2025-08-12T05:53:10.074Z" }, + { url = "https://files.pythonhosted.org/packages/57/54/35a84d0a4d23ea675994104e667ceff49227ce473ba6a59ba2c84f250b74/wrapt-1.17.3-cp313-cp313-win_amd64.whl", hash = "sha256:1f0b2f40cf341ee8cc1a97d51ff50dddb9fcc73241b9143ec74b30fc4f44f6cb", size = 38885, upload-time = "2025-08-12T05:53:08.695Z" }, + { url = "https://files.pythonhosted.org/packages/01/77/66e54407c59d7b02a3c4e0af3783168fff8e5d61def52cda8728439d86bc/wrapt-1.17.3-cp313-cp313-win_arm64.whl", hash = "sha256:7425ac3c54430f5fc5e7b6f41d41e704db073309acfc09305816bc6a0b26bb16", size = 36896, upload-time = "2025-08-12T05:52:55.34Z" }, + { url = "https://files.pythonhosted.org/packages/02/a2/cd864b2a14f20d14f4c496fab97802001560f9f41554eef6df201cd7f76c/wrapt-1.17.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cf30f6e3c077c8e6a9a7809c94551203c8843e74ba0c960f4a98cd80d4665d39", size = 54132, upload-time = "2025-08-12T05:51:49.864Z" }, + { url = "https://files.pythonhosted.org/packages/d5/46/d011725b0c89e853dc44cceb738a307cde5d240d023d6d40a82d1b4e1182/wrapt-1.17.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e228514a06843cae89621384cfe3a80418f3c04aadf8a3b14e46a7be704e4235", size = 39091, upload-time = "2025-08-12T05:51:38.935Z" }, + { url = "https://files.pythonhosted.org/packages/2e/9e/3ad852d77c35aae7ddebdbc3b6d35ec8013af7d7dddad0ad911f3d891dae/wrapt-1.17.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5ea5eb3c0c071862997d6f3e02af1d055f381b1d25b286b9d6644b79db77657c", size = 39172, upload-time = "2025-08-12T05:51:59.365Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f7/c983d2762bcce2326c317c26a6a1e7016f7eb039c27cdf5c4e30f4160f31/wrapt-1.17.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:281262213373b6d5e4bb4353bc36d1ba4084e6d6b5d242863721ef2bf2c2930b", size = 87163, upload-time = "2025-08-12T05:52:40.965Z" }, + { url = "https://files.pythonhosted.org/packages/e4/0f/f673f75d489c7f22d17fe0193e84b41540d962f75fce579cf6873167c29b/wrapt-1.17.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4a8d2b25efb6681ecacad42fca8859f88092d8732b170de6a5dddd80a1c8fa", size = 87963, upload-time = "2025-08-12T05:52:20.326Z" }, + { url = "https://files.pythonhosted.org/packages/df/61/515ad6caca68995da2fac7a6af97faab8f78ebe3bf4f761e1b77efbc47b5/wrapt-1.17.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:373342dd05b1d07d752cecbec0c41817231f29f3a89aa8b8843f7b95992ed0c7", size = 86945, upload-time = "2025-08-12T05:52:21.581Z" }, + { url = "https://files.pythonhosted.org/packages/d3/bd/4e70162ce398462a467bc09e768bee112f1412e563620adc353de9055d33/wrapt-1.17.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d40770d7c0fd5cbed9d84b2c3f2e156431a12c9a37dc6284060fb4bec0b7ffd4", size = 86857, upload-time = "2025-08-12T05:52:43.043Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b8/da8560695e9284810b8d3df8a19396a6e40e7518059584a1a394a2b35e0a/wrapt-1.17.3-cp314-cp314-win32.whl", hash = "sha256:fbd3c8319de8e1dc79d346929cd71d523622da527cca14e0c1d257e31c2b8b10", size = 37178, upload-time = "2025-08-12T05:53:12.605Z" }, + { url = "https://files.pythonhosted.org/packages/db/c8/b71eeb192c440d67a5a0449aaee2310a1a1e8eca41676046f99ed2487e9f/wrapt-1.17.3-cp314-cp314-win_amd64.whl", hash = "sha256:e1a4120ae5705f673727d3253de3ed0e016f7cd78dc463db1b31e2463e1f3cf6", size = 39310, upload-time = "2025-08-12T05:53:11.106Z" }, + { url = "https://files.pythonhosted.org/packages/45/20/2cda20fd4865fa40f86f6c46ed37a2a8356a7a2fde0773269311f2af56c7/wrapt-1.17.3-cp314-cp314-win_arm64.whl", hash = "sha256:507553480670cab08a800b9463bdb881b2edeed77dc677b0a5915e6106e91a58", size = 37266, upload-time = "2025-08-12T05:52:56.531Z" }, + { url = "https://files.pythonhosted.org/packages/77/ed/dd5cf21aec36c80443c6f900449260b80e2a65cf963668eaef3b9accce36/wrapt-1.17.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ed7c635ae45cfbc1a7371f708727bf74690daedc49b4dba310590ca0bd28aa8a", size = 56544, upload-time = "2025-08-12T05:51:51.109Z" }, + { url = "https://files.pythonhosted.org/packages/8d/96/450c651cc753877ad100c7949ab4d2e2ecc4d97157e00fa8f45df682456a/wrapt-1.17.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:249f88ed15503f6492a71f01442abddd73856a0032ae860de6d75ca62eed8067", size = 40283, upload-time = "2025-08-12T05:51:39.912Z" }, + { url = "https://files.pythonhosted.org/packages/d1/86/2fcad95994d9b572db57632acb6f900695a648c3e063f2cd344b3f5c5a37/wrapt-1.17.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a03a38adec8066d5a37bea22f2ba6bbf39fcdefbe2d91419ab864c3fb515454", size = 40366, upload-time = "2025-08-12T05:52:00.693Z" }, + { url = "https://files.pythonhosted.org/packages/64/0e/f4472f2fdde2d4617975144311f8800ef73677a159be7fe61fa50997d6c0/wrapt-1.17.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5d4478d72eb61c36e5b446e375bbc49ed002430d17cdec3cecb36993398e1a9e", size = 108571, upload-time = "2025-08-12T05:52:44.521Z" }, + { url = "https://files.pythonhosted.org/packages/cc/01/9b85a99996b0a97c8a17484684f206cbb6ba73c1ce6890ac668bcf3838fb/wrapt-1.17.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223db574bb38637e8230eb14b185565023ab624474df94d2af18f1cdb625216f", size = 113094, upload-time = "2025-08-12T05:52:22.618Z" }, + { url = "https://files.pythonhosted.org/packages/25/02/78926c1efddcc7b3aa0bc3d6b33a822f7d898059f7cd9ace8c8318e559ef/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e405adefb53a435f01efa7ccdec012c016b5a1d3f35459990afc39b6be4d5056", size = 110659, upload-time = "2025-08-12T05:52:24.057Z" }, + { url = "https://files.pythonhosted.org/packages/dc/ee/c414501ad518ac3e6fe184753632fe5e5ecacdcf0effc23f31c1e4f7bfcf/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:88547535b787a6c9ce4086917b6e1d291aa8ed914fdd3a838b3539dc95c12804", size = 106946, upload-time = "2025-08-12T05:52:45.976Z" }, + { url = "https://files.pythonhosted.org/packages/be/44/a1bd64b723d13bb151d6cc91b986146a1952385e0392a78567e12149c7b4/wrapt-1.17.3-cp314-cp314t-win32.whl", hash = "sha256:41b1d2bc74c2cac6f9074df52b2efbef2b30bdfe5f40cb78f8ca22963bc62977", size = 38717, upload-time = "2025-08-12T05:53:15.214Z" }, + { url = "https://files.pythonhosted.org/packages/79/d9/7cfd5a312760ac4dd8bf0184a6ee9e43c33e47f3dadc303032ce012b8fa3/wrapt-1.17.3-cp314-cp314t-win_amd64.whl", hash = "sha256:73d496de46cd2cdbdbcce4ae4bcdb4afb6a11234a1df9c085249d55166b95116", size = 41334, upload-time = "2025-08-12T05:53:14.178Z" }, + { url = "https://files.pythonhosted.org/packages/46/78/10ad9781128ed2f99dbc474f43283b13fea8ba58723e98844367531c18e9/wrapt-1.17.3-cp314-cp314t-win_arm64.whl", hash = "sha256:f38e60678850c42461d4202739f9bf1e3a737c7ad283638251e79cc49effb6b6", size = 38471, upload-time = "2025-08-12T05:52:57.784Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22", size = 23591, upload-time = "2025-08-12T05:53:20.674Z" }, +] + +[[package]] +name = "xxhash" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/02/84/30869e01909fb37a6cc7e18688ee8bf1e42d57e7e0777636bd47524c43c7/xxhash-3.6.0.tar.gz", hash = "sha256:f0162a78b13a0d7617b2845b90c763339d1f1d82bb04a4b07f4ab535cc5e05d6", size = 85160, upload-time = "2025-10-02T14:37:08.097Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/07/d9412f3d7d462347e4511181dea65e47e0d0e16e26fbee2ea86a2aefb657/xxhash-3.6.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:01362c4331775398e7bb34e3ab403bc9ee9f7c497bc7dee6272114055277dd3c", size = 32744, upload-time = "2025-10-02T14:34:34.622Z" }, + { url = "https://files.pythonhosted.org/packages/79/35/0429ee11d035fc33abe32dca1b2b69e8c18d236547b9a9b72c1929189b9a/xxhash-3.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b7b2df81a23f8cb99656378e72501b2cb41b1827c0f5a86f87d6b06b69f9f204", size = 30816, upload-time = "2025-10-02T14:34:36.043Z" }, + { url = "https://files.pythonhosted.org/packages/b7/f2/57eb99aa0f7d98624c0932c5b9a170e1806406cdbcdb510546634a1359e0/xxhash-3.6.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:dc94790144e66b14f67b10ac8ed75b39ca47536bf8800eb7c24b50271ea0c490", size = 194035, upload-time = "2025-10-02T14:34:37.354Z" }, + { url = "https://files.pythonhosted.org/packages/4c/ed/6224ba353690d73af7a3f1c7cdb1fc1b002e38f783cb991ae338e1eb3d79/xxhash-3.6.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:93f107c673bccf0d592cdba077dedaf52fe7f42dcd7676eba1f6d6f0c3efffd2", size = 212914, upload-time = "2025-10-02T14:34:38.6Z" }, + { url = "https://files.pythonhosted.org/packages/38/86/fb6b6130d8dd6b8942cc17ab4d90e223653a89aa32ad2776f8af7064ed13/xxhash-3.6.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2aa5ee3444c25b69813663c9f8067dcfaa2e126dc55e8dddf40f4d1c25d7effa", size = 212163, upload-time = "2025-10-02T14:34:39.872Z" }, + { url = "https://files.pythonhosted.org/packages/ee/dc/e84875682b0593e884ad73b2d40767b5790d417bde603cceb6878901d647/xxhash-3.6.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7f99123f0e1194fa59cc69ad46dbae2e07becec5df50a0509a808f90a0f03f0", size = 445411, upload-time = "2025-10-02T14:34:41.569Z" }, + { url = "https://files.pythonhosted.org/packages/11/4f/426f91b96701ec2f37bb2b8cec664eff4f658a11f3fa9d94f0a887ea6d2b/xxhash-3.6.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:49e03e6fe2cac4a1bc64952dd250cf0dbc5ef4ebb7b8d96bce82e2de163c82a2", size = 193883, upload-time = "2025-10-02T14:34:43.249Z" }, + { url = "https://files.pythonhosted.org/packages/53/5a/ddbb83eee8e28b778eacfc5a85c969673e4023cdeedcfcef61f36731610b/xxhash-3.6.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bd17fede52a17a4f9a7bc4472a5867cb0b160deeb431795c0e4abe158bc784e9", size = 210392, upload-time = "2025-10-02T14:34:45.042Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c2/ff69efd07c8c074ccdf0a4f36fcdd3d27363665bcdf4ba399abebe643465/xxhash-3.6.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:6fb5f5476bef678f69db04f2bd1efbed3030d2aba305b0fc1773645f187d6a4e", size = 197898, upload-time = "2025-10-02T14:34:46.302Z" }, + { url = "https://files.pythonhosted.org/packages/58/ca/faa05ac19b3b622c7c9317ac3e23954187516298a091eb02c976d0d3dd45/xxhash-3.6.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:843b52f6d88071f87eba1631b684fcb4b2068cd2180a0224122fe4ef011a9374", size = 210655, upload-time = "2025-10-02T14:34:47.571Z" }, + { url = "https://files.pythonhosted.org/packages/d4/7a/06aa7482345480cc0cb597f5c875b11a82c3953f534394f620b0be2f700c/xxhash-3.6.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7d14a6cfaf03b1b6f5f9790f76880601ccc7896aff7ab9cd8978a939c1eb7e0d", size = 414001, upload-time = "2025-10-02T14:34:49.273Z" }, + { url = "https://files.pythonhosted.org/packages/23/07/63ffb386cd47029aa2916b3d2f454e6cc5b9f5c5ada3790377d5430084e7/xxhash-3.6.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:418daf3db71e1413cfe211c2f9a528456936645c17f46b5204705581a45390ae", size = 191431, upload-time = "2025-10-02T14:34:50.798Z" }, + { url = "https://files.pythonhosted.org/packages/0f/93/14fde614cadb4ddf5e7cebf8918b7e8fac5ae7861c1875964f17e678205c/xxhash-3.6.0-cp312-cp312-win32.whl", hash = "sha256:50fc255f39428a27299c20e280d6193d8b63b8ef8028995323bf834a026b4fbb", size = 30617, upload-time = "2025-10-02T14:34:51.954Z" }, + { url = "https://files.pythonhosted.org/packages/13/5d/0d125536cbe7565a83d06e43783389ecae0c0f2ed037b48ede185de477c0/xxhash-3.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:c0f2ab8c715630565ab8991b536ecded9416d615538be8ecddce43ccf26cbc7c", size = 31534, upload-time = "2025-10-02T14:34:53.276Z" }, + { url = "https://files.pythonhosted.org/packages/54/85/6ec269b0952ec7e36ba019125982cf11d91256a778c7c3f98a4c5043d283/xxhash-3.6.0-cp312-cp312-win_arm64.whl", hash = "sha256:eae5c13f3bc455a3bbb68bdc513912dc7356de7e2280363ea235f71f54064829", size = 27876, upload-time = "2025-10-02T14:34:54.371Z" }, + { url = "https://files.pythonhosted.org/packages/33/76/35d05267ac82f53ae9b0e554da7c5e281ee61f3cad44c743f0fcd354f211/xxhash-3.6.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:599e64ba7f67472481ceb6ee80fa3bd828fd61ba59fb11475572cc5ee52b89ec", size = 32738, upload-time = "2025-10-02T14:34:55.839Z" }, + { url = "https://files.pythonhosted.org/packages/31/a8/3fbce1cd96534a95e35d5120637bf29b0d7f5d8fa2f6374e31b4156dd419/xxhash-3.6.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7d8b8aaa30fca4f16f0c84a5c8d7ddee0e25250ec2796c973775373257dde8f1", size = 30821, upload-time = "2025-10-02T14:34:57.219Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ea/d387530ca7ecfa183cb358027f1833297c6ac6098223fd14f9782cd0015c/xxhash-3.6.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d597acf8506d6e7101a4a44a5e428977a51c0fadbbfd3c39650cca9253f6e5a6", size = 194127, upload-time = "2025-10-02T14:34:59.21Z" }, + { url = "https://files.pythonhosted.org/packages/ba/0c/71435dcb99874b09a43b8d7c54071e600a7481e42b3e3ce1eb5226a5711a/xxhash-3.6.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:858dc935963a33bc33490128edc1c12b0c14d9c7ebaa4e387a7869ecc4f3e263", size = 212975, upload-time = "2025-10-02T14:35:00.816Z" }, + { url = "https://files.pythonhosted.org/packages/84/7a/c2b3d071e4bb4a90b7057228a99b10d51744878f4a8a6dd643c8bd897620/xxhash-3.6.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba284920194615cb8edf73bf52236ce2e1664ccd4a38fdb543506413529cc546", size = 212241, upload-time = "2025-10-02T14:35:02.207Z" }, + { url = "https://files.pythonhosted.org/packages/81/5f/640b6eac0128e215f177df99eadcd0f1b7c42c274ab6a394a05059694c5a/xxhash-3.6.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4b54219177f6c6674d5378bd862c6aedf64725f70dd29c472eaae154df1a2e89", size = 445471, upload-time = "2025-10-02T14:35:03.61Z" }, + { url = "https://files.pythonhosted.org/packages/5e/1e/3c3d3ef071b051cc3abbe3721ffb8365033a172613c04af2da89d5548a87/xxhash-3.6.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:42c36dd7dbad2f5238950c377fcbf6811b1cdb1c444fab447960030cea60504d", size = 193936, upload-time = "2025-10-02T14:35:05.013Z" }, + { url = "https://files.pythonhosted.org/packages/2c/bd/4a5f68381939219abfe1c22a9e3a5854a4f6f6f3c4983a87d255f21f2e5d/xxhash-3.6.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f22927652cba98c44639ffdc7aaf35828dccf679b10b31c4ad72a5b530a18eb7", size = 210440, upload-time = "2025-10-02T14:35:06.239Z" }, + { url = "https://files.pythonhosted.org/packages/eb/37/b80fe3d5cfb9faff01a02121a0f4d565eb7237e9e5fc66e73017e74dcd36/xxhash-3.6.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b45fad44d9c5c119e9c6fbf2e1c656a46dc68e280275007bbfd3d572b21426db", size = 197990, upload-time = "2025-10-02T14:35:07.735Z" }, + { url = "https://files.pythonhosted.org/packages/d7/fd/2c0a00c97b9e18f72e1f240ad4e8f8a90fd9d408289ba9c7c495ed7dc05c/xxhash-3.6.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:6f2580ffab1a8b68ef2b901cde7e55fa8da5e4be0977c68f78fc80f3c143de42", size = 210689, upload-time = "2025-10-02T14:35:09.438Z" }, + { url = "https://files.pythonhosted.org/packages/93/86/5dd8076a926b9a95db3206aba20d89a7fc14dd5aac16e5c4de4b56033140/xxhash-3.6.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:40c391dd3cd041ebc3ffe6f2c862f402e306eb571422e0aa918d8070ba31da11", size = 414068, upload-time = "2025-10-02T14:35:11.162Z" }, + { url = "https://files.pythonhosted.org/packages/af/3c/0bb129170ee8f3650f08e993baee550a09593462a5cddd8e44d0011102b1/xxhash-3.6.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f205badabde7aafd1a31e8ca2a3e5a763107a71c397c4481d6a804eb5063d8bd", size = 191495, upload-time = "2025-10-02T14:35:12.971Z" }, + { url = "https://files.pythonhosted.org/packages/e9/3a/6797e0114c21d1725e2577508e24006fd7ff1d8c0c502d3b52e45c1771d8/xxhash-3.6.0-cp313-cp313-win32.whl", hash = "sha256:2577b276e060b73b73a53042ea5bd5203d3e6347ce0d09f98500f418a9fcf799", size = 30620, upload-time = "2025-10-02T14:35:14.129Z" }, + { url = "https://files.pythonhosted.org/packages/86/15/9bc32671e9a38b413a76d24722a2bf8784a132c043063a8f5152d390b0f9/xxhash-3.6.0-cp313-cp313-win_amd64.whl", hash = "sha256:757320d45d2fbcce8f30c42a6b2f47862967aea7bf458b9625b4bbe7ee390392", size = 31542, upload-time = "2025-10-02T14:35:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/39/c5/cc01e4f6188656e56112d6a8e0dfe298a16934b8c47a247236549a3f7695/xxhash-3.6.0-cp313-cp313-win_arm64.whl", hash = "sha256:457b8f85dec5825eed7b69c11ae86834a018b8e3df5e77783c999663da2f96d6", size = 27880, upload-time = "2025-10-02T14:35:16.315Z" }, + { url = "https://files.pythonhosted.org/packages/f3/30/25e5321c8732759e930c555176d37e24ab84365482d257c3b16362235212/xxhash-3.6.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a42e633d75cdad6d625434e3468126c73f13f7584545a9cf34e883aa1710e702", size = 32956, upload-time = "2025-10-02T14:35:17.413Z" }, + { url = "https://files.pythonhosted.org/packages/9f/3c/0573299560d7d9f8ab1838f1efc021a280b5ae5ae2e849034ef3dee18810/xxhash-3.6.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:568a6d743219e717b07b4e03b0a828ce593833e498c3b64752e0f5df6bfe84db", size = 31072, upload-time = "2025-10-02T14:35:18.844Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1c/52d83a06e417cd9d4137722693424885cc9878249beb3a7c829e74bf7ce9/xxhash-3.6.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bec91b562d8012dae276af8025a55811b875baace6af510412a5e58e3121bc54", size = 196409, upload-time = "2025-10-02T14:35:20.31Z" }, + { url = "https://files.pythonhosted.org/packages/e3/8e/c6d158d12a79bbd0b878f8355432075fc82759e356ab5a111463422a239b/xxhash-3.6.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78e7f2f4c521c30ad5e786fdd6bae89d47a32672a80195467b5de0480aa97b1f", size = 215736, upload-time = "2025-10-02T14:35:21.616Z" }, + { url = "https://files.pythonhosted.org/packages/bc/68/c4c80614716345d55071a396cf03d06e34b5f4917a467faf43083c995155/xxhash-3.6.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3ed0df1b11a79856df5ffcab572cbd6b9627034c1c748c5566fa79df9048a7c5", size = 214833, upload-time = "2025-10-02T14:35:23.32Z" }, + { url = "https://files.pythonhosted.org/packages/7e/e9/ae27c8ffec8b953efa84c7c4a6c6802c263d587b9fc0d6e7cea64e08c3af/xxhash-3.6.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0e4edbfc7d420925b0dd5e792478ed393d6e75ff8fc219a6546fb446b6a417b1", size = 448348, upload-time = "2025-10-02T14:35:25.111Z" }, + { url = "https://files.pythonhosted.org/packages/d7/6b/33e21afb1b5b3f46b74b6bd1913639066af218d704cc0941404ca717fc57/xxhash-3.6.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fba27a198363a7ef87f8c0f6b171ec36b674fe9053742c58dd7e3201c1ab30ee", size = 196070, upload-time = "2025-10-02T14:35:26.586Z" }, + { url = "https://files.pythonhosted.org/packages/96/b6/fcabd337bc5fa624e7203aa0fa7d0c49eed22f72e93229431752bddc83d9/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:794fe9145fe60191c6532fa95063765529770edcdd67b3d537793e8004cabbfd", size = 212907, upload-time = "2025-10-02T14:35:28.087Z" }, + { url = "https://files.pythonhosted.org/packages/4b/d3/9ee6160e644d660fcf176c5825e61411c7f62648728f69c79ba237250143/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:6105ef7e62b5ac73a837778efc331a591d8442f8ef5c7e102376506cb4ae2729", size = 200839, upload-time = "2025-10-02T14:35:29.857Z" }, + { url = "https://files.pythonhosted.org/packages/0d/98/e8de5baa5109394baf5118f5e72ab21a86387c4f89b0e77ef3e2f6b0327b/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:f01375c0e55395b814a679b3eea205db7919ac2af213f4a6682e01220e5fe292", size = 213304, upload-time = "2025-10-02T14:35:31.222Z" }, + { url = "https://files.pythonhosted.org/packages/7b/1d/71056535dec5c3177eeb53e38e3d367dd1d16e024e63b1cee208d572a033/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:d706dca2d24d834a4661619dcacf51a75c16d65985718d6a7d73c1eeeb903ddf", size = 416930, upload-time = "2025-10-02T14:35:32.517Z" }, + { url = "https://files.pythonhosted.org/packages/dc/6c/5cbde9de2cd967c322e651c65c543700b19e7ae3e0aae8ece3469bf9683d/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5f059d9faeacd49c0215d66f4056e1326c80503f51a1532ca336a385edadd033", size = 193787, upload-time = "2025-10-02T14:35:33.827Z" }, + { url = "https://files.pythonhosted.org/packages/19/fa/0172e350361d61febcea941b0cc541d6e6c8d65d153e85f850a7b256ff8a/xxhash-3.6.0-cp313-cp313t-win32.whl", hash = "sha256:1244460adc3a9be84731d72b8e80625788e5815b68da3da8b83f78115a40a7ec", size = 30916, upload-time = "2025-10-02T14:35:35.107Z" }, + { url = "https://files.pythonhosted.org/packages/ad/e6/e8cf858a2b19d6d45820f072eff1bea413910592ff17157cabc5f1227a16/xxhash-3.6.0-cp313-cp313t-win_amd64.whl", hash = "sha256:b1e420ef35c503869c4064f4a2f2b08ad6431ab7b229a05cce39d74268bca6b8", size = 31799, upload-time = "2025-10-02T14:35:36.165Z" }, + { url = "https://files.pythonhosted.org/packages/56/15/064b197e855bfb7b343210e82490ae672f8bc7cdf3ddb02e92f64304ee8a/xxhash-3.6.0-cp313-cp313t-win_arm64.whl", hash = "sha256:ec44b73a4220623235f67a996c862049f375df3b1052d9899f40a6382c32d746", size = 28044, upload-time = "2025-10-02T14:35:37.195Z" }, + { url = "https://files.pythonhosted.org/packages/7e/5e/0138bc4484ea9b897864d59fce9be9086030825bc778b76cb5a33a906d37/xxhash-3.6.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a40a3d35b204b7cc7643cbcf8c9976d818cb47befcfac8bbefec8038ac363f3e", size = 32754, upload-time = "2025-10-02T14:35:38.245Z" }, + { url = "https://files.pythonhosted.org/packages/18/d7/5dac2eb2ec75fd771957a13e5dda560efb2176d5203f39502a5fc571f899/xxhash-3.6.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a54844be970d3fc22630b32d515e79a90d0a3ddb2644d8d7402e3c4c8da61405", size = 30846, upload-time = "2025-10-02T14:35:39.6Z" }, + { url = "https://files.pythonhosted.org/packages/fe/71/8bc5be2bb00deb5682e92e8da955ebe5fa982da13a69da5a40a4c8db12fb/xxhash-3.6.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:016e9190af8f0a4e3741343777710e3d5717427f175adfdc3e72508f59e2a7f3", size = 194343, upload-time = "2025-10-02T14:35:40.69Z" }, + { url = "https://files.pythonhosted.org/packages/e7/3b/52badfb2aecec2c377ddf1ae75f55db3ba2d321c5e164f14461c90837ef3/xxhash-3.6.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4f6f72232f849eb9d0141e2ebe2677ece15adfd0fa599bc058aad83c714bb2c6", size = 213074, upload-time = "2025-10-02T14:35:42.29Z" }, + { url = "https://files.pythonhosted.org/packages/a2/2b/ae46b4e9b92e537fa30d03dbc19cdae57ed407e9c26d163895e968e3de85/xxhash-3.6.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:63275a8aba7865e44b1813d2177e0f5ea7eadad3dd063a21f7cf9afdc7054063", size = 212388, upload-time = "2025-10-02T14:35:43.929Z" }, + { url = "https://files.pythonhosted.org/packages/f5/80/49f88d3afc724b4ac7fbd664c8452d6db51b49915be48c6982659e0e7942/xxhash-3.6.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cd01fa2aa00d8b017c97eb46b9a794fbdca53fc14f845f5a328c71254b0abb7", size = 445614, upload-time = "2025-10-02T14:35:45.216Z" }, + { url = "https://files.pythonhosted.org/packages/ed/ba/603ce3961e339413543d8cd44f21f2c80e2a7c5cfe692a7b1f2cccf58f3c/xxhash-3.6.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0226aa89035b62b6a86d3c68df4d7c1f47a342b8683da2b60cedcddb46c4d95b", size = 194024, upload-time = "2025-10-02T14:35:46.959Z" }, + { url = "https://files.pythonhosted.org/packages/78/d1/8e225ff7113bf81545cfdcd79eef124a7b7064a0bba53605ff39590b95c2/xxhash-3.6.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c6e193e9f56e4ca4923c61238cdaced324f0feac782544eb4c6d55ad5cc99ddd", size = 210541, upload-time = "2025-10-02T14:35:48.301Z" }, + { url = "https://files.pythonhosted.org/packages/6f/58/0f89d149f0bad89def1a8dd38feb50ccdeb643d9797ec84707091d4cb494/xxhash-3.6.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9176dcaddf4ca963d4deb93866d739a343c01c969231dbe21680e13a5d1a5bf0", size = 198305, upload-time = "2025-10-02T14:35:49.584Z" }, + { url = "https://files.pythonhosted.org/packages/11/38/5eab81580703c4df93feb5f32ff8fa7fe1e2c51c1f183ee4e48d4bb9d3d7/xxhash-3.6.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c1ce4009c97a752e682b897aa99aef84191077a9433eb237774689f14f8ec152", size = 210848, upload-time = "2025-10-02T14:35:50.877Z" }, + { url = "https://files.pythonhosted.org/packages/5e/6b/953dc4b05c3ce678abca756416e4c130d2382f877a9c30a20d08ee6a77c0/xxhash-3.6.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:8cb2f4f679b01513b7adbb9b1b2f0f9cdc31b70007eaf9d59d0878809f385b11", size = 414142, upload-time = "2025-10-02T14:35:52.15Z" }, + { url = "https://files.pythonhosted.org/packages/08/a9/238ec0d4e81a10eb5026d4a6972677cbc898ba6c8b9dbaec12ae001b1b35/xxhash-3.6.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:653a91d7c2ab54a92c19ccf43508b6a555440b9be1bc8be553376778be7f20b5", size = 191547, upload-time = "2025-10-02T14:35:53.547Z" }, + { url = "https://files.pythonhosted.org/packages/f1/ee/3cf8589e06c2164ac77c3bf0aa127012801128f1feebf2a079272da5737c/xxhash-3.6.0-cp314-cp314-win32.whl", hash = "sha256:a756fe893389483ee8c394d06b5ab765d96e68fbbfe6fde7aa17e11f5720559f", size = 31214, upload-time = "2025-10-02T14:35:54.746Z" }, + { url = "https://files.pythonhosted.org/packages/02/5d/a19552fbc6ad4cb54ff953c3908bbc095f4a921bc569433d791f755186f1/xxhash-3.6.0-cp314-cp314-win_amd64.whl", hash = "sha256:39be8e4e142550ef69629c9cd71b88c90e9a5db703fecbcf265546d9536ca4ad", size = 32290, upload-time = "2025-10-02T14:35:55.791Z" }, + { url = "https://files.pythonhosted.org/packages/b1/11/dafa0643bc30442c887b55baf8e73353a344ee89c1901b5a5c54a6c17d39/xxhash-3.6.0-cp314-cp314-win_arm64.whl", hash = "sha256:25915e6000338999236f1eb68a02a32c3275ac338628a7eaa5a269c401995679", size = 28795, upload-time = "2025-10-02T14:35:57.162Z" }, + { url = "https://files.pythonhosted.org/packages/2c/db/0e99732ed7f64182aef4a6fb145e1a295558deec2a746265dcdec12d191e/xxhash-3.6.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c5294f596a9017ca5a3e3f8884c00b91ab2ad2933cf288f4923c3fd4346cf3d4", size = 32955, upload-time = "2025-10-02T14:35:58.267Z" }, + { url = "https://files.pythonhosted.org/packages/55/f4/2a7c3c68e564a099becfa44bb3d398810cc0ff6749b0d3cb8ccb93f23c14/xxhash-3.6.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1cf9dcc4ab9cff01dfbba78544297a3a01dafd60f3bde4e2bfd016cf7e4ddc67", size = 31072, upload-time = "2025-10-02T14:35:59.382Z" }, + { url = "https://files.pythonhosted.org/packages/c6/d9/72a29cddc7250e8a5819dad5d466facb5dc4c802ce120645630149127e73/xxhash-3.6.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:01262da8798422d0685f7cef03b2bd3f4f46511b02830861df548d7def4402ad", size = 196579, upload-time = "2025-10-02T14:36:00.838Z" }, + { url = "https://files.pythonhosted.org/packages/63/93/b21590e1e381040e2ca305a884d89e1c345b347404f7780f07f2cdd47ef4/xxhash-3.6.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51a73fb7cb3a3ead9f7a8b583ffd9b8038e277cdb8cb87cf890e88b3456afa0b", size = 215854, upload-time = "2025-10-02T14:36:02.207Z" }, + { url = "https://files.pythonhosted.org/packages/ce/b8/edab8a7d4fa14e924b29be877d54155dcbd8b80be85ea00d2be3413a9ed4/xxhash-3.6.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b9c6df83594f7df8f7f708ce5ebeacfc69f72c9fbaaababf6cf4758eaada0c9b", size = 214965, upload-time = "2025-10-02T14:36:03.507Z" }, + { url = "https://files.pythonhosted.org/packages/27/67/dfa980ac7f0d509d54ea0d5a486d2bb4b80c3f1bb22b66e6a05d3efaf6c0/xxhash-3.6.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:627f0af069b0ea56f312fd5189001c24578868643203bca1abbc2c52d3a6f3ca", size = 448484, upload-time = "2025-10-02T14:36:04.828Z" }, + { url = "https://files.pythonhosted.org/packages/8c/63/8ffc2cc97e811c0ca5d00ab36604b3ea6f4254f20b7bc658ca825ce6c954/xxhash-3.6.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aa912c62f842dfd013c5f21a642c9c10cd9f4c4e943e0af83618b4a404d9091a", size = 196162, upload-time = "2025-10-02T14:36:06.182Z" }, + { url = "https://files.pythonhosted.org/packages/4b/77/07f0e7a3edd11a6097e990f6e5b815b6592459cb16dae990d967693e6ea9/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b465afd7909db30168ab62afe40b2fcf79eedc0b89a6c0ab3123515dc0df8b99", size = 213007, upload-time = "2025-10-02T14:36:07.733Z" }, + { url = "https://files.pythonhosted.org/packages/ae/d8/bc5fa0d152837117eb0bef6f83f956c509332ce133c91c63ce07ee7c4873/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:a881851cf38b0a70e7c4d3ce81fc7afd86fbc2a024f4cfb2a97cf49ce04b75d3", size = 200956, upload-time = "2025-10-02T14:36:09.106Z" }, + { url = "https://files.pythonhosted.org/packages/26/a5/d749334130de9411783873e9b98ecc46688dad5db64ca6e04b02acc8b473/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:9b3222c686a919a0f3253cfc12bb118b8b103506612253b5baeaac10d8027cf6", size = 213401, upload-time = "2025-10-02T14:36:10.585Z" }, + { url = "https://files.pythonhosted.org/packages/89/72/abed959c956a4bfc72b58c0384bb7940663c678127538634d896b1195c10/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:c5aa639bc113e9286137cec8fadc20e9cd732b2cc385c0b7fa673b84fc1f2a93", size = 417083, upload-time = "2025-10-02T14:36:12.276Z" }, + { url = "https://files.pythonhosted.org/packages/0c/b3/62fd2b586283b7d7d665fb98e266decadf31f058f1cf6c478741f68af0cb/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5c1343d49ac102799905e115aee590183c3921d475356cb24b4de29a4bc56518", size = 193913, upload-time = "2025-10-02T14:36:14.025Z" }, + { url = "https://files.pythonhosted.org/packages/9a/9a/c19c42c5b3f5a4aad748a6d5b4f23df3bed7ee5445accc65a0fb3ff03953/xxhash-3.6.0-cp314-cp314t-win32.whl", hash = "sha256:5851f033c3030dd95c086b4a36a2683c2ff4a799b23af60977188b057e467119", size = 31586, upload-time = "2025-10-02T14:36:15.603Z" }, + { url = "https://files.pythonhosted.org/packages/03/d6/4cc450345be9924fd5dc8c590ceda1db5b43a0a889587b0ae81a95511360/xxhash-3.6.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0444e7967dac37569052d2409b00a8860c2135cff05502df4da80267d384849f", size = 32526, upload-time = "2025-10-02T14:36:16.708Z" }, + { url = "https://files.pythonhosted.org/packages/0f/c9/7243eb3f9eaabd1a88a5a5acadf06df2d83b100c62684b7425c6a11bcaa8/xxhash-3.6.0-cp314-cp314t-win_arm64.whl", hash = "sha256:bb79b1e63f6fd84ec778a4b1916dfe0a7c3fdb986c06addd5db3a0d413819d95", size = 28898, upload-time = "2025-10-02T14:36:17.843Z" }, +]