mapkgsutils 0.0.2__py3-none-any.whl
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/__init__.py +1 -0
- mapkgsutils/config/__init__.py +1 -0
- mapkgsutils/config/schema.py +170 -0
- mapkgsutils/context.py +358 -0
- mapkgsutils/diff.py +338 -0
- mapkgsutils/download.py +661 -0
- mapkgsutils/exports.py +180 -0
- mapkgsutils/logging.py +65 -0
- mapkgsutils/parsers/__init__.py +1 -0
- mapkgsutils/parsers/base.py +1899 -0
- mapkgsutils/py.typed +1 -0
- mapkgsutils/version.py +39 -0
- mapkgsutils-0.0.2.dist-info/METADATA +385 -0
- mapkgsutils-0.0.2.dist-info/RECORD +16 -0
- mapkgsutils-0.0.2.dist-info/WHEEL +4 -0
- mapkgsutils-0.0.2.dist-info/licenses/LICENSE +21 -0
mapkgsutils/diff.py
ADDED
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
"""Diff operations for comparing MappingSets between releases.
|
|
2
|
+
|
|
3
|
+
Uses Polars for efficient comparison of large mapping datasets.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import TYPE_CHECKING
|
|
11
|
+
|
|
12
|
+
import polars as pl
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from sssom_schema import MappingSet
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"MappingDiff",
|
|
19
|
+
"diff_mapping_sets",
|
|
20
|
+
"diff_sssom_files",
|
|
21
|
+
"summarize_diff",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class MappingDiff:
|
|
27
|
+
"""Result of comparing two MappingSets."""
|
|
28
|
+
|
|
29
|
+
old_version: str | None
|
|
30
|
+
new_version: str | None
|
|
31
|
+
datasource: str
|
|
32
|
+
|
|
33
|
+
# Added mappings (object_id, subject_id) - in new but not in old
|
|
34
|
+
added: pl.DataFrame = field(default_factory=pl.DataFrame)
|
|
35
|
+
# Removed mappings (object_id, subject_id) - in old but not in new
|
|
36
|
+
removed: pl.DataFrame = field(default_factory=pl.DataFrame)
|
|
37
|
+
# Changed mappings (same object_id, different subject_id)
|
|
38
|
+
changed: pl.DataFrame = field(default_factory=pl.DataFrame)
|
|
39
|
+
# Intersection mappings (in both old and new)
|
|
40
|
+
intersection: pl.DataFrame = field(default_factory=pl.DataFrame)
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def added_count(self) -> int:
|
|
44
|
+
"""Number of added mappings."""
|
|
45
|
+
return len(self.added)
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def removed_count(self) -> int:
|
|
49
|
+
"""Number of removed mappings."""
|
|
50
|
+
return len(self.removed)
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def changed_count(self) -> int:
|
|
54
|
+
"""Number of changed mappings."""
|
|
55
|
+
return len(self.changed)
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def intersection_count(self) -> int:
|
|
59
|
+
"""Number of mappings in both sets."""
|
|
60
|
+
return len(self.intersection)
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def total_changes(self) -> int:
|
|
64
|
+
"""Total number of changes."""
|
|
65
|
+
return self.added_count + self.removed_count + self.changed_count
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def has_changes(self) -> bool:
|
|
69
|
+
"""Whether there are any changes."""
|
|
70
|
+
return self.total_changes > 0
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def mapping_set_to_dataframe(mapping_set: MappingSet) -> pl.DataFrame:
|
|
74
|
+
"""Convert a MappingSet to a Polars DataFrame for comparison.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
mapping_set: The MappingSet to convert (sssom_schema.MappingSet).
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
Polars DataFrame with columns: subject_id, object_id.
|
|
81
|
+
"""
|
|
82
|
+
rows = []
|
|
83
|
+
mappings = mapping_set.mappings or []
|
|
84
|
+
for mapping in mappings:
|
|
85
|
+
rows.append(
|
|
86
|
+
{
|
|
87
|
+
"subject_id": mapping.subject_id,
|
|
88
|
+
"object_id": mapping.object_id,
|
|
89
|
+
}
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
if not rows:
|
|
93
|
+
return pl.DataFrame(schema={"subject_id": pl.Utf8, "object_id": pl.Utf8})
|
|
94
|
+
|
|
95
|
+
return pl.DataFrame(rows)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def diff_mapping_sets(
|
|
99
|
+
old_set: MappingSet,
|
|
100
|
+
new_set: MappingSet,
|
|
101
|
+
datasource: str = "unknown",
|
|
102
|
+
) -> MappingDiff:
|
|
103
|
+
"""Compare two MappingSets and find differences.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
old_set: The older/previous MappingSet (sssom_schema.MappingSet).
|
|
107
|
+
new_set: The newer/current MappingSet (sssom_schema.MappingSet).
|
|
108
|
+
datasource: Name of the datasource for the diff.
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
MappingDiff with added, removed, and changed mappings.
|
|
112
|
+
"""
|
|
113
|
+
old_df = mapping_set_to_dataframe(old_set)
|
|
114
|
+
new_df = mapping_set_to_dataframe(new_set)
|
|
115
|
+
|
|
116
|
+
return _diff_dataframes(
|
|
117
|
+
old_df=old_df,
|
|
118
|
+
new_df=new_df,
|
|
119
|
+
old_version=old_set.mapping_set_version,
|
|
120
|
+
new_version=new_set.mapping_set_version,
|
|
121
|
+
datasource=datasource,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def diff_sssom_files(
|
|
126
|
+
old_file: Path | str,
|
|
127
|
+
new_file: Path | str,
|
|
128
|
+
datasource: str = "unknown",
|
|
129
|
+
) -> MappingDiff:
|
|
130
|
+
"""Compare two SSSOM TSV files and find differences.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
old_file: Path to the older SSSOM file.
|
|
134
|
+
new_file: Path to the newer SSSOM file.
|
|
135
|
+
datasource: Name of the datasource.
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
MappingDiff with added, removed, and changed mappings.
|
|
139
|
+
"""
|
|
140
|
+
old_path = Path(old_file)
|
|
141
|
+
new_path = Path(new_file)
|
|
142
|
+
|
|
143
|
+
# Read SSSOM files (skip metadata lines starting with #)
|
|
144
|
+
old_df = _read_sssom_to_dataframe(old_path)
|
|
145
|
+
new_df = _read_sssom_to_dataframe(new_path)
|
|
146
|
+
|
|
147
|
+
# Extract versions from metadata if available
|
|
148
|
+
old_version = _extract_sssom_version(old_path)
|
|
149
|
+
new_version = _extract_sssom_version(new_path)
|
|
150
|
+
|
|
151
|
+
return _diff_dataframes(
|
|
152
|
+
old_df=old_df,
|
|
153
|
+
new_df=new_df,
|
|
154
|
+
old_version=old_version,
|
|
155
|
+
new_version=new_version,
|
|
156
|
+
datasource=datasource,
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _read_sssom_to_dataframe(path: Path) -> pl.DataFrame:
|
|
161
|
+
"""Read an SSSOM TSV file to a Polars DataFrame.
|
|
162
|
+
|
|
163
|
+
Handles the SSSOM metadata header (lines starting with #).
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
path: Path to the SSSOM file.
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
Polars DataFrame with subject_id and object_id columns.
|
|
170
|
+
"""
|
|
171
|
+
# Count header lines
|
|
172
|
+
header_lines = 0
|
|
173
|
+
with path.open() as f:
|
|
174
|
+
for line in f:
|
|
175
|
+
if line.startswith("#"):
|
|
176
|
+
header_lines += 1
|
|
177
|
+
else:
|
|
178
|
+
break
|
|
179
|
+
|
|
180
|
+
# Read the TSV, skipping metadata
|
|
181
|
+
df = pl.read_csv(
|
|
182
|
+
path,
|
|
183
|
+
separator="\t",
|
|
184
|
+
skip_rows=header_lines,
|
|
185
|
+
infer_schema_length=10000,
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
if "subject_id" in df.columns and "object_id" in df.columns:
|
|
189
|
+
df = df.select(["subject_id", "object_id"])
|
|
190
|
+
|
|
191
|
+
return df
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _extract_sssom_version(path: Path) -> str | None:
|
|
195
|
+
"""Extract version from SSSOM metadata header.
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
path: Path to the SSSOM file.
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
Version string or None if not found.
|
|
202
|
+
"""
|
|
203
|
+
with path.open() as f:
|
|
204
|
+
for line in f:
|
|
205
|
+
if not line.startswith("#"):
|
|
206
|
+
break
|
|
207
|
+
if "mapping_set_version" in line.lower():
|
|
208
|
+
# Format: #mapping_set_version: "1.0"
|
|
209
|
+
parts = line.split(":", 1)
|
|
210
|
+
if len(parts) == 2:
|
|
211
|
+
return parts[1].strip().strip('"').strip("'")
|
|
212
|
+
return None
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _diff_dataframes(
|
|
216
|
+
old_df: pl.DataFrame,
|
|
217
|
+
new_df: pl.DataFrame,
|
|
218
|
+
old_version: str | None,
|
|
219
|
+
new_version: str | None,
|
|
220
|
+
datasource: str,
|
|
221
|
+
) -> MappingDiff:
|
|
222
|
+
"""Compare two DataFrames and find differences.
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
old_df: The older DataFrame.
|
|
226
|
+
new_df: The newer DataFrame.
|
|
227
|
+
old_version: Version of the old set.
|
|
228
|
+
new_version: Version of the new set.
|
|
229
|
+
datasource: Datasource name.
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
MappingDiff with added, removed, and changed mappings.
|
|
233
|
+
"""
|
|
234
|
+
# Ensure consistent schema
|
|
235
|
+
if old_df.is_empty():
|
|
236
|
+
old_df = pl.DataFrame(schema={"subject_id": pl.Utf8, "object_id": pl.Utf8})
|
|
237
|
+
if new_df.is_empty():
|
|
238
|
+
new_df = pl.DataFrame(schema={"subject_id": pl.Utf8, "object_id": pl.Utf8})
|
|
239
|
+
|
|
240
|
+
# Find mappings by (subject_id, object_id) pairs
|
|
241
|
+
old_pairs = old_df.select(["subject_id", "object_id"]).unique()
|
|
242
|
+
new_pairs = new_df.select(["subject_id", "object_id"]).unique()
|
|
243
|
+
|
|
244
|
+
# Added: in new but not in old
|
|
245
|
+
added = new_pairs.join(
|
|
246
|
+
old_pairs,
|
|
247
|
+
on=["subject_id", "object_id"],
|
|
248
|
+
how="anti",
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
# Removed: in old but not in new
|
|
252
|
+
removed = old_pairs.join(
|
|
253
|
+
new_pairs,
|
|
254
|
+
on=["subject_id", "object_id"],
|
|
255
|
+
how="anti",
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
# Changed: same object_id but different subject_id
|
|
259
|
+
# Get unique secondary IDs with their primary mappings
|
|
260
|
+
old_by_sec = old_df.select(
|
|
261
|
+
[
|
|
262
|
+
pl.col("object_id"),
|
|
263
|
+
pl.col("subject_id").alias("old_subject_id"),
|
|
264
|
+
]
|
|
265
|
+
).unique()
|
|
266
|
+
|
|
267
|
+
new_by_sec = new_df.select(
|
|
268
|
+
[
|
|
269
|
+
pl.col("object_id"),
|
|
270
|
+
pl.col("subject_id").alias("new_subject_id"),
|
|
271
|
+
]
|
|
272
|
+
).unique()
|
|
273
|
+
|
|
274
|
+
# Join on object_id and filter where primary changed
|
|
275
|
+
changed = old_by_sec.join(new_by_sec, on="object_id", how="inner").filter(
|
|
276
|
+
pl.col("old_subject_id") != pl.col("new_subject_id")
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
# Intersection: in both old and new (same subject_id, object_id pair)
|
|
280
|
+
intersection = old_pairs.join(
|
|
281
|
+
new_pairs,
|
|
282
|
+
on=["subject_id", "object_id"],
|
|
283
|
+
how="inner",
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
return MappingDiff(
|
|
287
|
+
old_version=old_version,
|
|
288
|
+
new_version=new_version,
|
|
289
|
+
datasource=datasource,
|
|
290
|
+
added=added,
|
|
291
|
+
removed=removed,
|
|
292
|
+
changed=changed,
|
|
293
|
+
intersection=intersection,
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def summarize_diff(diff: MappingDiff) -> str:
|
|
298
|
+
"""Generate a human-readable summary of a diff.
|
|
299
|
+
|
|
300
|
+
Args:
|
|
301
|
+
diff: The MappingDiff to summarize.
|
|
302
|
+
|
|
303
|
+
Returns:
|
|
304
|
+
A formatted string summary.
|
|
305
|
+
"""
|
|
306
|
+
lines = [
|
|
307
|
+
f"Diff Summary for {diff.datasource}",
|
|
308
|
+
f" Old version: {diff.old_version or 'unknown'}",
|
|
309
|
+
f" New version: {diff.new_version or 'unknown'}",
|
|
310
|
+
"",
|
|
311
|
+
f" Intersection: {diff.intersection_count:>8}",
|
|
312
|
+
f" Added mappings: {diff.added_count:>8}",
|
|
313
|
+
f" Removed mappings: {diff.removed_count:>8}",
|
|
314
|
+
f" Changed mappings: {diff.changed_count:>8}",
|
|
315
|
+
f" Total changes: {diff.total_changes:>8}",
|
|
316
|
+
]
|
|
317
|
+
|
|
318
|
+
if diff.added_count > 0 and diff.added_count <= 10:
|
|
319
|
+
lines.append("")
|
|
320
|
+
lines.append(" Added:")
|
|
321
|
+
for row in diff.added.iter_rows(named=True):
|
|
322
|
+
lines.append(f" {row['object_id']} -> {row['subject_id']}")
|
|
323
|
+
|
|
324
|
+
if diff.removed_count > 0 and diff.removed_count <= 10:
|
|
325
|
+
lines.append("")
|
|
326
|
+
lines.append(" Removed:")
|
|
327
|
+
for row in diff.removed.iter_rows(named=True):
|
|
328
|
+
lines.append(f" {row['object_id']} -> {row['subject_id']}")
|
|
329
|
+
|
|
330
|
+
if diff.changed_count > 0 and diff.changed_count <= 10:
|
|
331
|
+
lines.append("")
|
|
332
|
+
lines.append(" Changed:")
|
|
333
|
+
for row in diff.changed.iter_rows(named=True):
|
|
334
|
+
lines.append(
|
|
335
|
+
f" {row['object_id']}: {row['old_subject_id']} -> {row['new_subject_id']}"
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
return "\n".join(lines)
|