mapkgsutils 0.0.2__tar.gz → 0.0.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mapkgsutils-0.0.4/PKG-INFO +114 -0
- mapkgsutils-0.0.4/README.md +65 -0
- {mapkgsutils-0.0.2 → mapkgsutils-0.0.4}/pyproject.toml +2 -2
- {mapkgsutils-0.0.2 → mapkgsutils-0.0.4}/src/mapkgsutils/config/schema.py +5 -6
- mapkgsutils-0.0.4/src/mapkgsutils/consolidate.py +382 -0
- {mapkgsutils-0.0.2 → mapkgsutils-0.0.4}/src/mapkgsutils/context.py +9 -15
- {mapkgsutils-0.0.2 → mapkgsutils-0.0.4}/src/mapkgsutils/parsers/base.py +13 -83
- {mapkgsutils-0.0.2 → mapkgsutils-0.0.4}/src/mapkgsutils/version.py +1 -1
- mapkgsutils-0.0.2/PKG-INFO +0 -385
- mapkgsutils-0.0.2/README.md +0 -336
- {mapkgsutils-0.0.2 → mapkgsutils-0.0.4}/LICENSE +0 -0
- {mapkgsutils-0.0.2 → mapkgsutils-0.0.4}/src/mapkgsutils/__init__.py +0 -0
- {mapkgsutils-0.0.2 → mapkgsutils-0.0.4}/src/mapkgsutils/config/__init__.py +0 -0
- {mapkgsutils-0.0.2 → mapkgsutils-0.0.4}/src/mapkgsutils/diff.py +0 -0
- {mapkgsutils-0.0.2 → mapkgsutils-0.0.4}/src/mapkgsutils/download.py +0 -0
- {mapkgsutils-0.0.2 → mapkgsutils-0.0.4}/src/mapkgsutils/exports.py +0 -0
- {mapkgsutils-0.0.2 → mapkgsutils-0.0.4}/src/mapkgsutils/logging.py +0 -0
- {mapkgsutils-0.0.2 → mapkgsutils-0.0.4}/src/mapkgsutils/parsers/__init__.py +0 -0
- {mapkgsutils-0.0.2 → mapkgsutils-0.0.4}/src/mapkgsutils/py.typed +0 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mapkgsutils
|
|
3
|
+
Version: 0.0.4
|
|
4
|
+
Summary: Utils shared by several mapping set generation tools for biomedical databases
|
|
5
|
+
Keywords: snekpack,cookiecutter
|
|
6
|
+
Author: Javier Millan Acosta
|
|
7
|
+
Author-email: Javier Millan Acosta <javier.millanacosta@gmail.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 1 - Planning
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Framework :: Pytest
|
|
15
|
+
Classifier: Framework :: Sphinx
|
|
16
|
+
Classifier: Natural Language :: English
|
|
17
|
+
Classifier: Programming Language :: Python
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
23
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
24
|
+
Classifier: Typing :: Typed
|
|
25
|
+
Requires-Dist: more-itertools
|
|
26
|
+
Requires-Dist: tqdm
|
|
27
|
+
Requires-Dist: click
|
|
28
|
+
Requires-Dist: more-click
|
|
29
|
+
Requires-Dist: typing-extensions
|
|
30
|
+
Requires-Dist: pydantic>=2.0
|
|
31
|
+
Requires-Dist: pyyaml
|
|
32
|
+
Requires-Dist: sssom==0.4.21
|
|
33
|
+
Requires-Dist: sssom-schema==1.1.0a5
|
|
34
|
+
Requires-Dist: curies>=0.7.0
|
|
35
|
+
Requires-Dist: polars>=0.20.0
|
|
36
|
+
Requires-Dist: polars-runtime-32
|
|
37
|
+
Requires-Dist: pandas
|
|
38
|
+
Requires-Dist: numpy
|
|
39
|
+
Requires-Dist: httpx>=0.24.0
|
|
40
|
+
Maintainer: Javier Millan Acosta
|
|
41
|
+
Maintainer-email: Javier Millan Acosta <javier.millanacosta@gmail.com>
|
|
42
|
+
Requires-Python: >=3.10
|
|
43
|
+
Project-URL: Bug Tracker, https://github.com//mapkgsutils/issues
|
|
44
|
+
Project-URL: Homepage, https://github.com//mapkgsutils
|
|
45
|
+
Project-URL: Repository, https://github.com//mapkgsutils.git
|
|
46
|
+
Project-URL: Documentation, https://mapkgsutils.readthedocs.io
|
|
47
|
+
Project-URL: Funding, https://github.com/sponsors/jmillanacosta
|
|
48
|
+
Description-Content-Type: text/markdown
|
|
49
|
+
|
|
50
|
+
<!--
|
|
51
|
+
<p align="center">
|
|
52
|
+
<img src="https://github.com//mapkgsutils/raw/main/docs/source/logo.png" height="150">
|
|
53
|
+
</p>
|
|
54
|
+
-->
|
|
55
|
+
|
|
56
|
+
<h1 align="center">
|
|
57
|
+
MaPkgsUtilS
|
|
58
|
+
</h1>
|
|
59
|
+
|
|
60
|
+
<p align="center">
|
|
61
|
+
<a href="https://github.com//mapkgsutils/actions/workflows/tests.yml">
|
|
62
|
+
<img alt="Tests" src="https://github.com//mapkgsutils/actions/workflows/tests.yml/badge.svg" /></a>
|
|
63
|
+
<a href="https://pypi.org/project/mapkgsutils">
|
|
64
|
+
<img alt="PyPI" src="https://img.shields.io/pypi/v/mapkgsutils" /></a>
|
|
65
|
+
<a href="https://pypi.org/project/mapkgsutils">
|
|
66
|
+
<img alt="PyPI - Python Version" src="https://img.shields.io/pypi/pyversions/mapkgsutils" /></a>
|
|
67
|
+
<a href="https://github.com//mapkgsutils/blob/main/LICENSE">
|
|
68
|
+
<img alt="PyPI - License" src="https://img.shields.io/pypi/l/mapkgsutils" /></a>
|
|
69
|
+
<a href='https://mapkgsutils.readthedocs.io/en/latest/?badge=latest'>
|
|
70
|
+
<img src='https://readthedocs.org/projects/mapkgsutils/badge/?version=latest' alt='Documentation Status' /></a>
|
|
71
|
+
<a href="https://codecov.io/gh//mapkgsutils/branch/main">
|
|
72
|
+
<img src="https://codecov.io/gh//mapkgsutils/branch/main/graph/badge.svg" alt="Codecov status" /></a>
|
|
73
|
+
<a href="https://github.com/cthoyt/cookiecutter-python-package">
|
|
74
|
+
<img alt="Cookiecutter template from @cthoyt" src="https://img.shields.io/badge/Cookiecutter-snekpack-blue" /></a>
|
|
75
|
+
<a href="https://github.com/astral-sh/ruff">
|
|
76
|
+
<img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json" alt="Ruff" style="max-width:100%;"></a>
|
|
77
|
+
<a href="https://github.com//mapkgsutils/blob/main/.github/CODE_OF_CONDUCT.md">
|
|
78
|
+
<img src="https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg" alt="Contributor Covenant"/></a>
|
|
79
|
+
<!-- uncomment if you archive on zenodo
|
|
80
|
+
<a href="https://doi.org/10.5281/zenodo.XXXXXX">
|
|
81
|
+
<img src="https://zenodo.org/badge/DOI/10.5281/zenodo.XXXXXX.svg" alt="DOI"></a>
|
|
82
|
+
-->
|
|
83
|
+
</p>
|
|
84
|
+
|
|
85
|
+
Utils shared by several mapping set generation tools for biomedical databases.
|
|
86
|
+
|
|
87
|
+
## 🚀 Installation
|
|
88
|
+
|
|
89
|
+
<!-- Uncomment this section after your first ``just finish``
|
|
90
|
+
The most recent release can be installed from
|
|
91
|
+
[PyPI](https://pypi.org/project/mapkgsutils/) with uv:
|
|
92
|
+
|
|
93
|
+
```console
|
|
94
|
+
$ uv pip install mapkgsutils
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
or with pip:
|
|
98
|
+
|
|
99
|
+
```console
|
|
100
|
+
$ python3 -m pip install mapkgsutils
|
|
101
|
+
```
|
|
102
|
+
-->
|
|
103
|
+
|
|
104
|
+
The most recent code and data can be installed directly from GitHub with uv:
|
|
105
|
+
|
|
106
|
+
```console
|
|
107
|
+
$ uv pip install git+https://github.com//mapkgsutils.git
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
or with pip:
|
|
111
|
+
|
|
112
|
+
```console
|
|
113
|
+
$ python3 -m pip install git+https://github.com//mapkgsutils.git
|
|
114
|
+
```
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
<p align="center">
|
|
3
|
+
<img src="https://github.com//mapkgsutils/raw/main/docs/source/logo.png" height="150">
|
|
4
|
+
</p>
|
|
5
|
+
-->
|
|
6
|
+
|
|
7
|
+
<h1 align="center">
|
|
8
|
+
MaPkgsUtilS
|
|
9
|
+
</h1>
|
|
10
|
+
|
|
11
|
+
<p align="center">
|
|
12
|
+
<a href="https://github.com//mapkgsutils/actions/workflows/tests.yml">
|
|
13
|
+
<img alt="Tests" src="https://github.com//mapkgsutils/actions/workflows/tests.yml/badge.svg" /></a>
|
|
14
|
+
<a href="https://pypi.org/project/mapkgsutils">
|
|
15
|
+
<img alt="PyPI" src="https://img.shields.io/pypi/v/mapkgsutils" /></a>
|
|
16
|
+
<a href="https://pypi.org/project/mapkgsutils">
|
|
17
|
+
<img alt="PyPI - Python Version" src="https://img.shields.io/pypi/pyversions/mapkgsutils" /></a>
|
|
18
|
+
<a href="https://github.com//mapkgsutils/blob/main/LICENSE">
|
|
19
|
+
<img alt="PyPI - License" src="https://img.shields.io/pypi/l/mapkgsutils" /></a>
|
|
20
|
+
<a href='https://mapkgsutils.readthedocs.io/en/latest/?badge=latest'>
|
|
21
|
+
<img src='https://readthedocs.org/projects/mapkgsutils/badge/?version=latest' alt='Documentation Status' /></a>
|
|
22
|
+
<a href="https://codecov.io/gh//mapkgsutils/branch/main">
|
|
23
|
+
<img src="https://codecov.io/gh//mapkgsutils/branch/main/graph/badge.svg" alt="Codecov status" /></a>
|
|
24
|
+
<a href="https://github.com/cthoyt/cookiecutter-python-package">
|
|
25
|
+
<img alt="Cookiecutter template from @cthoyt" src="https://img.shields.io/badge/Cookiecutter-snekpack-blue" /></a>
|
|
26
|
+
<a href="https://github.com/astral-sh/ruff">
|
|
27
|
+
<img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json" alt="Ruff" style="max-width:100%;"></a>
|
|
28
|
+
<a href="https://github.com//mapkgsutils/blob/main/.github/CODE_OF_CONDUCT.md">
|
|
29
|
+
<img src="https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg" alt="Contributor Covenant"/></a>
|
|
30
|
+
<!-- uncomment if you archive on zenodo
|
|
31
|
+
<a href="https://doi.org/10.5281/zenodo.XXXXXX">
|
|
32
|
+
<img src="https://zenodo.org/badge/DOI/10.5281/zenodo.XXXXXX.svg" alt="DOI"></a>
|
|
33
|
+
-->
|
|
34
|
+
</p>
|
|
35
|
+
|
|
36
|
+
Utils shared by several mapping set generation tools for biomedical databases.
|
|
37
|
+
|
|
38
|
+
## 🚀 Installation
|
|
39
|
+
|
|
40
|
+
<!-- Uncomment this section after your first ``just finish``
|
|
41
|
+
The most recent release can be installed from
|
|
42
|
+
[PyPI](https://pypi.org/project/mapkgsutils/) with uv:
|
|
43
|
+
|
|
44
|
+
```console
|
|
45
|
+
$ uv pip install mapkgsutils
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
or with pip:
|
|
49
|
+
|
|
50
|
+
```console
|
|
51
|
+
$ python3 -m pip install mapkgsutils
|
|
52
|
+
```
|
|
53
|
+
-->
|
|
54
|
+
|
|
55
|
+
The most recent code and data can be installed directly from GitHub with uv:
|
|
56
|
+
|
|
57
|
+
```console
|
|
58
|
+
$ uv pip install git+https://github.com//mapkgsutils.git
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
or with pip:
|
|
62
|
+
|
|
63
|
+
```console
|
|
64
|
+
$ python3 -m pip install git+https://github.com//mapkgsutils.git
|
|
65
|
+
```
|
|
@@ -4,7 +4,7 @@ build-backend = "uv_build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "mapkgsutils"
|
|
7
|
-
version = "0.0.
|
|
7
|
+
version = "0.0.4"
|
|
8
8
|
description = "Utils shared by several mapping set generation tools for biomedical databases"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
authors = [
|
|
@@ -236,7 +236,7 @@ known-first-party = [
|
|
|
236
236
|
docstring-code-format = true
|
|
237
237
|
|
|
238
238
|
[tool.bumpversion]
|
|
239
|
-
current_version = "0.0.
|
|
239
|
+
current_version = "0.0.4"
|
|
240
240
|
parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)(?:-(?P<release>[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*))?(?:\\+(?P<build>[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*))?"
|
|
241
241
|
serialize = [
|
|
242
242
|
"{major}.{minor}.{patch}-{release}+{build}",
|
|
@@ -65,11 +65,11 @@ class DistributionEraSchema(BaseModel):
|
|
|
65
65
|
class DatasourceConfigSchema(BaseModel):
|
|
66
66
|
"""Top-level schema for a ``config/<datasource>.yaml`` file.
|
|
67
67
|
|
|
68
|
-
``name``, ``prefix``, and ``curie_base_url`` are
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
in shape
|
|
72
|
-
:func:`validate_config_dict`),
|
|
68
|
+
``name``, ``prefix``, and ``curie_base_url`` are the only fields
|
|
69
|
+
:class:`~mapkgsutils.parsers.base.DatasourceConfig` declares without a
|
|
70
|
+
default, so they're required here too. Every other recognized key is
|
|
71
|
+
optional and permissive in shape. Unrecognized top-level keys just get a
|
|
72
|
+
warning (see :func:`validate_config_dict`), not a hard error.
|
|
73
73
|
"""
|
|
74
74
|
|
|
75
75
|
model_config = ConfigDict(extra="allow")
|
|
@@ -99,7 +99,6 @@ class DatasourceConfigSchema(BaseModel):
|
|
|
99
99
|
distribution_eras: list[DistributionEraSchema] = Field(default_factory=list)
|
|
100
100
|
xref_sources: list[XrefSourceSchema] = Field(default_factory=list)
|
|
101
101
|
species: dict[str, Any] = Field(default_factory=dict)
|
|
102
|
-
genome_build: dict[str, Any] = Field(default_factory=dict)
|
|
103
102
|
subset: dict[str, Any] = Field(default_factory=dict)
|
|
104
103
|
mappingset: dict[str, Any] = Field(default_factory=dict)
|
|
105
104
|
mapping: dict[str, Any] = Field(default_factory=dict)
|
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
"""Generic mapping-date consolidation: cache I/O and the release/date walk skeleton.
|
|
2
|
+
|
|
3
|
+
A versioned datasource doesn't always record when a mapping first appeared.
|
|
4
|
+
Recovering that date means walking every historical release once and
|
|
5
|
+
recording the first/last release each mapping (keyed by its
|
|
6
|
+
version-independent pair hash) was seen in.
|
|
7
|
+
|
|
8
|
+
This module provides the cache I/O, the release-walk and single-pass-date
|
|
9
|
+
loop shapes, and rebuilding the cache back into a real SSSOM mapping set.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
from collections.abc import Callable, Iterable, Mapping
|
|
16
|
+
from datetime import datetime
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
from mapkgsutils.logging import logger
|
|
21
|
+
from mapkgsutils.parsers.base import _cmp_versions
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"CACHE_COLUMNS",
|
|
25
|
+
"build_consolidated_mapping_set",
|
|
26
|
+
"consolidate_by_date",
|
|
27
|
+
"consolidate_by_release",
|
|
28
|
+
"load_mapping_dates",
|
|
29
|
+
"read_cache",
|
|
30
|
+
"read_meta",
|
|
31
|
+
"sssom_output_path",
|
|
32
|
+
"write_cache",
|
|
33
|
+
"write_consolidated_sssom",
|
|
34
|
+
"write_meta",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
CACHE_COLUMNS = (
|
|
38
|
+
"record_id",
|
|
39
|
+
"first_seen_version",
|
|
40
|
+
"first_seen_date",
|
|
41
|
+
"last_seen_version",
|
|
42
|
+
"last_seen_date",
|
|
43
|
+
# JSON-encoded snapshot of the mapping's own fields (subject_id,
|
|
44
|
+
# object_id, predicate_id, ...) as last seen. Used to rebuild the cache
|
|
45
|
+
# into a real SSSOM mapping set later (see build_consolidated_mapping_set).
|
|
46
|
+
# Empty for legacy/hand-built rows.
|
|
47
|
+
"fields_json",
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def sssom_output_path(cache_path: Path) -> Path:
|
|
52
|
+
"""Return the companion SSSOM mapping-set path for a consolidated cache file."""
|
|
53
|
+
return cache_path.with_name(cache_path.stem + "_sssom.tsv")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def read_cache(cache_path: Path) -> dict[str, dict[str, str]]:
|
|
57
|
+
"""Read a consolidated mapping-date cache TSV into a dict keyed by record_id."""
|
|
58
|
+
if not cache_path.exists():
|
|
59
|
+
return {}
|
|
60
|
+
|
|
61
|
+
import polars as pl
|
|
62
|
+
|
|
63
|
+
df = pl.read_csv(cache_path, separator="\t", schema_overrides={"record_id": pl.Utf8})
|
|
64
|
+
cols = [c for c in CACHE_COLUMNS[1:] if c in df.columns]
|
|
65
|
+
return {
|
|
66
|
+
str(row["record_id"]): {col: str(row[col]) for col in cols}
|
|
67
|
+
for row in df.iter_rows(named=True)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def write_cache(cache_path: Path, records: dict[str, dict[str, str]]) -> None:
|
|
72
|
+
"""Write the merged ``record_id -> first/last seen`` dict to a TSV file."""
|
|
73
|
+
import polars as pl
|
|
74
|
+
|
|
75
|
+
cache_path.parent.mkdir(parents=True, exist_ok=True)
|
|
76
|
+
rows = [
|
|
77
|
+
{"record_id": rid, **{c: fields.get(c, "") for c in CACHE_COLUMNS[1:]}}
|
|
78
|
+
for rid, fields in records.items()
|
|
79
|
+
]
|
|
80
|
+
schema = list(CACHE_COLUMNS)
|
|
81
|
+
df = pl.DataFrame(rows, schema=schema) if rows else pl.DataFrame(schema=schema)
|
|
82
|
+
df.write_csv(cache_path, separator="\t")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def read_meta(meta_path: Path) -> str | None:
|
|
86
|
+
"""Read the ``last_version`` sidecar, or ``None`` if absent/unreadable."""
|
|
87
|
+
if not meta_path.exists():
|
|
88
|
+
return None
|
|
89
|
+
try:
|
|
90
|
+
data: dict[str, Any] = json.loads(meta_path.read_text(encoding="utf-8"))
|
|
91
|
+
except (OSError, json.JSONDecodeError):
|
|
92
|
+
return None
|
|
93
|
+
last_version = data.get("last_version")
|
|
94
|
+
return str(last_version) if last_version is not None else None
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def write_meta(meta_path: Path, last_version: str) -> None:
|
|
98
|
+
"""Write the ``last_version`` sidecar used to resume an interrupted walk."""
|
|
99
|
+
meta_path.parent.mkdir(parents=True, exist_ok=True)
|
|
100
|
+
meta_path.write_text(json.dumps({"last_version": last_version}), encoding="utf-8")
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def load_mapping_dates(cache_path: Path) -> dict[str, str]:
|
|
104
|
+
"""Load the consolidated ``record_id -> first_seen_date`` index at *cache_path*.
|
|
105
|
+
|
|
106
|
+
Safe to call even when the index hasn't been built yet: returns ``{}``.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
cache_path: Path to the cache TSV (see :func:`write_cache`).
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
Dict mapping each ``record_id`` to its first-seen ISO date string.
|
|
113
|
+
Records walked but never assigned a real release date (e.g. an
|
|
114
|
+
unresolvable ``Last-Modified``) are omitted, leaving their
|
|
115
|
+
``mapping_date`` unset rather than passing through a non-date value.
|
|
116
|
+
"""
|
|
117
|
+
records = read_cache(cache_path)
|
|
118
|
+
return {
|
|
119
|
+
rid: fields["first_seen_date"]
|
|
120
|
+
for rid, fields in records.items()
|
|
121
|
+
if fields.get("first_seen_date")
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _mapping_fields_json(m: Any) -> str:
|
|
126
|
+
"""JSON-encode a mapping's own fields (excluding mapping_date/record_id).
|
|
127
|
+
|
|
128
|
+
Snapshots a mapping's current shape (subject_id, object_id,
|
|
129
|
+
predicate_id, ...) so :func:`build_consolidated_mapping_set` can rebuild
|
|
130
|
+
it into a real SSSOM mapping set later. ``default=str`` handles
|
|
131
|
+
enum-valued fields like ``mapping_cardinality``, which round-trip
|
|
132
|
+
cleanly back through ``Mapping(**fields)``. Returns ``"{}"`` for
|
|
133
|
+
non-dataclass stand-ins (e.g. test doubles); callers skip those rows
|
|
134
|
+
rather than materializing them.
|
|
135
|
+
"""
|
|
136
|
+
from dataclasses import fields as dataclass_fields
|
|
137
|
+
from dataclasses import is_dataclass
|
|
138
|
+
|
|
139
|
+
if not is_dataclass(m):
|
|
140
|
+
return "{}"
|
|
141
|
+
fields = {
|
|
142
|
+
f.name: getattr(m, f.name)
|
|
143
|
+
for f in dataclass_fields(m)
|
|
144
|
+
if f.name not in ("mapping_date", "record_id") and getattr(m, f.name, None) is not None
|
|
145
|
+
}
|
|
146
|
+
return json.dumps(fields, default=str)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def consolidate_by_date(
|
|
150
|
+
cache_path: Path,
|
|
151
|
+
meta_path: Path,
|
|
152
|
+
*,
|
|
153
|
+
run_one_version: Callable[[], Any],
|
|
154
|
+
) -> bool:
|
|
155
|
+
"""Single-pass "date" mode: capture each row's own real ``mapping_date``.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
cache_path: Path to the cache TSV to write.
|
|
159
|
+
meta_path: Path to the ``last_version`` sidecar to write.
|
|
160
|
+
run_one_version: Zero-arg callable returning a freshly parsed
|
|
161
|
+
mapping set (the current/latest release, already bound to
|
|
162
|
+
whatever datasource/mapping-kind/kwargs the caller needs).
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
``True`` and writes the cache when the mapping set produced at
|
|
166
|
+
least one real per-row date; ``False`` (cache untouched) when it
|
|
167
|
+
produced none, so the caller can fall back to a release-walk.
|
|
168
|
+
"""
|
|
169
|
+
mapping_set = run_one_version()
|
|
170
|
+
dated = [m for m in (mapping_set.mappings or []) if getattr(m, "mapping_date", None)]
|
|
171
|
+
if not dated:
|
|
172
|
+
return False
|
|
173
|
+
|
|
174
|
+
version_label = str(getattr(mapping_set, "mapping_set_version", None) or "current")
|
|
175
|
+
records: dict[str, dict[str, str]] = {}
|
|
176
|
+
for m in dated:
|
|
177
|
+
# record_id is release-scoped; its trailing 16 hex chars are always
|
|
178
|
+
# the version-independent pair hash (see mapkgsutils.parsers.base.pair_hash),
|
|
179
|
+
# which is what must match across releases.
|
|
180
|
+
pair_key = str(getattr(m, "record_id", None) or "")[-16:]
|
|
181
|
+
if not pair_key:
|
|
182
|
+
continue
|
|
183
|
+
date_str = str(m.mapping_date)
|
|
184
|
+
records[pair_key] = {
|
|
185
|
+
"first_seen_version": version_label,
|
|
186
|
+
"first_seen_date": date_str,
|
|
187
|
+
"last_seen_version": version_label,
|
|
188
|
+
"last_seen_date": date_str,
|
|
189
|
+
"fields_json": _mapping_fields_json(m),
|
|
190
|
+
}
|
|
191
|
+
write_cache(cache_path, records)
|
|
192
|
+
write_meta(meta_path, version_label)
|
|
193
|
+
return True
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def build_consolidated_mapping_set(
|
|
197
|
+
records: dict[str, dict[str, str]],
|
|
198
|
+
last_version: str | None,
|
|
199
|
+
*,
|
|
200
|
+
mapping_set_class: type[Any],
|
|
201
|
+
record_namespace: str,
|
|
202
|
+
mapping_set_metadata: Mapping[str, Any],
|
|
203
|
+
cardinality_on: str = "id",
|
|
204
|
+
) -> Any:
|
|
205
|
+
"""Materialize the consolidated index as a real SSSOM mapping set.
|
|
206
|
+
|
|
207
|
+
Each row's ``mapping_date`` and ``subject_source_version``/
|
|
208
|
+
``object_source_version`` are overridden from the record's
|
|
209
|
+
``first_seen_date``/``first_seen_version``: the date of first
|
|
210
|
+
appearance and the release it first appeared in, rather than whatever
|
|
211
|
+
the last-seen snapshot happened to carry. The two stay in their own
|
|
212
|
+
fields. A release version (e.g. ChEBI's "183") is not a date and never
|
|
213
|
+
goes in ``mapping_date``, and a real date never goes in the version
|
|
214
|
+
fields.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
records: ``record_id -> fields`` dict as read by :func:`read_cache`.
|
|
218
|
+
last_version: The most recent release the walk has processed.
|
|
219
|
+
mapping_set_class: Concrete ``BaseMappingSet`` subclass to build.
|
|
220
|
+
record_namespace: Base IRI namespace prefixed to each rebuilt
|
|
221
|
+
``record_id``.
|
|
222
|
+
mapping_set_metadata: ``mapping_set_id``/``curie_map``/``license``/...
|
|
223
|
+
metadata for the resulting mapping set.
|
|
224
|
+
cardinality_on: ``"id"`` or ``"label"``, forwarded to
|
|
225
|
+
:meth:`~mapkgsutils.parsers.base.BaseMappingSet._compute_cardinalities`.
|
|
226
|
+
|
|
227
|
+
Returns:
|
|
228
|
+
A ``mapping_set_class`` instance with cardinalities computed.
|
|
229
|
+
"""
|
|
230
|
+
from sssom_schema import Mapping as SSSOMMapping
|
|
231
|
+
|
|
232
|
+
version_label = str(last_version) if last_version else "current"
|
|
233
|
+
|
|
234
|
+
mappings = []
|
|
235
|
+
for pair_key, fields in records.items():
|
|
236
|
+
fields_json = fields.get("fields_json") or ""
|
|
237
|
+
if not fields_json:
|
|
238
|
+
continue
|
|
239
|
+
try:
|
|
240
|
+
row_fields = json.loads(fields_json)
|
|
241
|
+
except json.JSONDecodeError:
|
|
242
|
+
continue
|
|
243
|
+
if not row_fields:
|
|
244
|
+
# Non-dataclass stand-ins (see _mapping_fields_json) have
|
|
245
|
+
# nothing to materialize into a real Mapping; date bookkeeping
|
|
246
|
+
# for them still lives in the TSV cache, just not in the SSSOM.
|
|
247
|
+
continue
|
|
248
|
+
# mapping_date and subject/object_source_version are semantically
|
|
249
|
+
# distinct SSSOM fields: a release version (e.g. ChEBI's "183") is
|
|
250
|
+
# never a date, and must never end up in mapping_date or vice versa.
|
|
251
|
+
# Override both from first_seen_*, not whatever the snapshot's own
|
|
252
|
+
# (last-seen) values were.
|
|
253
|
+
row_fields["mapping_date"] = fields.get("first_seen_date") or None
|
|
254
|
+
first_seen_version = fields.get("first_seen_version") or None
|
|
255
|
+
row_fields["subject_source_version"] = first_seen_version
|
|
256
|
+
row_fields["object_source_version"] = first_seen_version
|
|
257
|
+
row_fields["record_id"] = f"{record_namespace}{version_label}/consolidate/{pair_key}"
|
|
258
|
+
mappings.append(SSSOMMapping(**row_fields))
|
|
259
|
+
|
|
260
|
+
base_ms_id = str(mapping_set_metadata.get("mapping_set_id") or "")
|
|
261
|
+
mapping_set = mapping_set_class(
|
|
262
|
+
mappings=mappings,
|
|
263
|
+
curie_map=mapping_set_metadata.get("curie_map") or {},
|
|
264
|
+
mapping_set_id=f"{base_ms_id}/{version_label}/consolidate",
|
|
265
|
+
mapping_set_version=version_label,
|
|
266
|
+
mapping_set_title=mapping_set_metadata.get("mapping_set_title"),
|
|
267
|
+
mapping_set_description=mapping_set_metadata.get("mapping_set_description"),
|
|
268
|
+
license=mapping_set_metadata.get("license"),
|
|
269
|
+
)
|
|
270
|
+
mapping_set._compute_cardinalities(on=cardinality_on)
|
|
271
|
+
return mapping_set
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def write_consolidated_sssom(
|
|
275
|
+
cache_path: Path,
|
|
276
|
+
meta_path: Path,
|
|
277
|
+
*,
|
|
278
|
+
mapping_set_class: type[Any],
|
|
279
|
+
record_namespace: str,
|
|
280
|
+
mapping_set_metadata: Mapping[str, Any],
|
|
281
|
+
cardinality_on: str = "id",
|
|
282
|
+
) -> Path:
|
|
283
|
+
"""Build and save the companion SSSOM mapping set next to the cache file.
|
|
284
|
+
|
|
285
|
+
Args:
|
|
286
|
+
cache_path: Path to the cache TSV (see :func:`read_cache`).
|
|
287
|
+
meta_path: Path to the ``last_version`` sidecar (see :func:`read_meta`).
|
|
288
|
+
mapping_set_class: Concrete ``BaseMappingSet`` subclass to build.
|
|
289
|
+
record_namespace: Base IRI namespace for rebuilt ``record_id`` values.
|
|
290
|
+
mapping_set_metadata: ``mapping_set_id``/``curie_map``/``license``/...
|
|
291
|
+
metadata for the resulting mapping set.
|
|
292
|
+
cardinality_on: ``"id"`` or ``"label"``, see
|
|
293
|
+
:func:`build_consolidated_mapping_set`.
|
|
294
|
+
|
|
295
|
+
Returns:
|
|
296
|
+
Path to the written SSSOM TSV (see :func:`sssom_output_path`).
|
|
297
|
+
"""
|
|
298
|
+
records = read_cache(cache_path)
|
|
299
|
+
last_version = read_meta(meta_path)
|
|
300
|
+
mapping_set = build_consolidated_mapping_set(
|
|
301
|
+
records,
|
|
302
|
+
last_version,
|
|
303
|
+
mapping_set_class=mapping_set_class,
|
|
304
|
+
record_namespace=record_namespace,
|
|
305
|
+
mapping_set_metadata=mapping_set_metadata,
|
|
306
|
+
cardinality_on=cardinality_on,
|
|
307
|
+
)
|
|
308
|
+
output_path = sssom_output_path(cache_path)
|
|
309
|
+
mapping_set.save("sssom", output_path)
|
|
310
|
+
return output_path
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def consolidate_by_release(
|
|
314
|
+
cache_path: Path,
|
|
315
|
+
meta_path: Path,
|
|
316
|
+
*,
|
|
317
|
+
label: str,
|
|
318
|
+
list_versions: Callable[[], list[str]],
|
|
319
|
+
run_one_version: Callable[[str], Any],
|
|
320
|
+
resolve_release_date: Callable[[str], datetime | None],
|
|
321
|
+
show_progress: bool = True,
|
|
322
|
+
force: bool = False,
|
|
323
|
+
) -> None:
|
|
324
|
+
"""Historical-walk "release" mode: track first/last-seen release per mapping.
|
|
325
|
+
|
|
326
|
+
Args:
|
|
327
|
+
cache_path: Path to the cache TSV to read/write.
|
|
328
|
+
meta_path: Path to the ``last_version`` sidecar to read/write.
|
|
329
|
+
label: Datasource name, used only for the progress bar/log messages.
|
|
330
|
+
list_versions: Zero-arg callable returning every available version,
|
|
331
|
+
oldest first.
|
|
332
|
+
run_one_version: Callable taking a version string and returning a
|
|
333
|
+
freshly downloaded+parsed mapping set for it.
|
|
334
|
+
resolve_release_date: Callable taking a version string and
|
|
335
|
+
returning its upstream release date, or ``None`` when
|
|
336
|
+
unresolvable.
|
|
337
|
+
show_progress: Whether to show a progress bar over releases.
|
|
338
|
+
force: Re-scan every release from scratch, ignoring any existing
|
|
339
|
+
cache/resume state.
|
|
340
|
+
"""
|
|
341
|
+
records: dict[str, dict[str, str]] = {} if force else read_cache(cache_path)
|
|
342
|
+
last_version = None if force else read_meta(meta_path)
|
|
343
|
+
|
|
344
|
+
versions = list_versions()
|
|
345
|
+
if last_version is not None:
|
|
346
|
+
versions = [v for v in versions if _cmp_versions(v, last_version) > 0]
|
|
347
|
+
|
|
348
|
+
iterator: Iterable[str] = versions
|
|
349
|
+
if show_progress:
|
|
350
|
+
from tqdm import tqdm
|
|
351
|
+
|
|
352
|
+
iterator = tqdm(versions, desc=f"Consolidating {label.upper()} mapping dates")
|
|
353
|
+
|
|
354
|
+
for v in iterator:
|
|
355
|
+
try:
|
|
356
|
+
mapping_set = run_one_version(v)
|
|
357
|
+
release_date = resolve_release_date(v)
|
|
358
|
+
# Empty when no real release date resolves. v is a version, not
|
|
359
|
+
# a date, and isn't always date-shaped (e.g. ChEBI's plain
|
|
360
|
+
# release numbers), so don't store it as one.
|
|
361
|
+
date_str = release_date.date().isoformat() if release_date else ""
|
|
362
|
+
|
|
363
|
+
for m in mapping_set.mappings or []:
|
|
364
|
+
# record_id is release-scoped; match across releases on its
|
|
365
|
+
# trailing pair hash instead (see mapkgsutils.parsers.base.pair_hash).
|
|
366
|
+
pair_key = str(getattr(m, "record_id", None) or "")[-16:]
|
|
367
|
+
if not pair_key:
|
|
368
|
+
continue
|
|
369
|
+
entry = records.setdefault(
|
|
370
|
+
pair_key,
|
|
371
|
+
{"first_seen_version": v, "first_seen_date": date_str},
|
|
372
|
+
)
|
|
373
|
+
entry["last_seen_version"] = v
|
|
374
|
+
entry["last_seen_date"] = date_str
|
|
375
|
+
entry["fields_json"] = _mapping_fields_json(m)
|
|
376
|
+
except Exception:
|
|
377
|
+
logger.warning("Skipping %s version %s during consolidation", label, v, exc_info=True)
|
|
378
|
+
continue
|
|
379
|
+
|
|
380
|
+
last_version = v
|
|
381
|
+
write_cache(cache_path, records)
|
|
382
|
+
write_meta(meta_path, last_version)
|
|
@@ -1,18 +1,13 @@
|
|
|
1
|
-
"""Disambiguate ambiguous
|
|
1
|
+
"""Disambiguate ambiguous mappings with context: label, id, and xref evidence.
|
|
2
2
|
|
|
3
3
|
:class:`ContextSpec` describes a per-row piece of evidence that can
|
|
4
4
|
be used to decide which of those two entities a given ambiguous cell actually
|
|
5
5
|
means:
|
|
6
6
|
|
|
7
|
-
* ``label``:
|
|
7
|
+
* ``label``: a label string.
|
|
8
8
|
* ``id`` : a related/foreign identifier string.
|
|
9
9
|
* ``xref`` : a cross-reference token (e.g. an Ensembl ID) resolved through
|
|
10
10
|
an independent :class:`XrefMapping` crosswalk table.
|
|
11
|
-
|
|
12
|
-
All three are resolved the same way: confirm *secondary usage* (the evidence
|
|
13
|
-
points to the mapping's target), confirm *primary usage* (the evidence points
|
|
14
|
-
to the token's own current identity), or leave the cell unresolved. Every
|
|
15
|
-
attempt can be recorded as a :class:`DecisionRecord` for an auditable TSV log.
|
|
16
11
|
"""
|
|
17
12
|
|
|
18
13
|
from __future__ import annotations
|
|
@@ -35,8 +30,8 @@ class XrefRecord:
|
|
|
35
30
|
|
|
36
31
|
Args:
|
|
37
32
|
subject_id: The cross-reference token, e.g. ``"ENSG00000197471"``.
|
|
38
|
-
object_id: The target
|
|
39
|
-
object_label: The target
|
|
33
|
+
object_id: The target id, e.g. ``"HGNC:11249"``.
|
|
34
|
+
object_label: The target label, e.g. ``"SPN"``.
|
|
40
35
|
predicate_id: The equivalence predicate, e.g. ``"skos:exactMatch"``.
|
|
41
36
|
``None`` when the source table carries no predicate column.
|
|
42
37
|
"""
|
|
@@ -93,8 +88,8 @@ def load_xref_mapping(
|
|
|
93
88
|
Args:
|
|
94
89
|
path: Path to the crosswalk file.
|
|
95
90
|
subject_col: Column with the cross-reference token.
|
|
96
|
-
object_col: Column with the target
|
|
97
|
-
object_label_col: Column with the target
|
|
91
|
+
object_col: Column with the target id.
|
|
92
|
+
object_label_col: Column with the target label (optional).
|
|
98
93
|
predicate_col: Column with the equivalence predicate (optional).
|
|
99
94
|
sep: Field delimiter.
|
|
100
95
|
|
|
@@ -257,13 +252,12 @@ def resolve_ambiguous_with_xref(
|
|
|
257
252
|
"""Attempt to resolve an ambiguous token using a cross-reference token.
|
|
258
253
|
|
|
259
254
|
Args:
|
|
260
|
-
token: The ambiguous label or ID
|
|
261
|
-
entry and as a secondary one pointing elsewhere).
|
|
255
|
+
token: The ambiguous label or ID.
|
|
262
256
|
xref_token: The row's cross-reference token, e.g. an Ensembl ID.
|
|
263
|
-
lkp: ``{
|
|
257
|
+
lkp: ``{token: resolved_token}`` lookup.
|
|
264
258
|
xref_index: ``{xref_token: [XrefRecord, ...]}`` (see
|
|
265
259
|
:meth:`XrefMapping.by_subject`).
|
|
266
|
-
token_to_id: ``{
|
|
260
|
+
token_to_id: ``{token: id}``. ``None`` when *token*
|
|
267
261
|
is already a CURIE (ID mode).
|
|
268
262
|
accepted_predicates: Equivalence predicates to accept. ``None``
|
|
269
263
|
accepts any predicate.
|