mapkgsutils 0.0.2__tar.gz → 0.0.3__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.2 → mapkgsutils-0.0.3}/PKG-INFO +1 -1
- {mapkgsutils-0.0.2 → mapkgsutils-0.0.3}/pyproject.toml +2 -2
- mapkgsutils-0.0.3/src/mapkgsutils/consolidate.py +368 -0
- {mapkgsutils-0.0.2 → mapkgsutils-0.0.3}/src/mapkgsutils/version.py +1 -1
- {mapkgsutils-0.0.2 → mapkgsutils-0.0.3}/LICENSE +0 -0
- {mapkgsutils-0.0.2 → mapkgsutils-0.0.3}/README.md +0 -0
- {mapkgsutils-0.0.2 → mapkgsutils-0.0.3}/src/mapkgsutils/__init__.py +0 -0
- {mapkgsutils-0.0.2 → mapkgsutils-0.0.3}/src/mapkgsutils/config/__init__.py +0 -0
- {mapkgsutils-0.0.2 → mapkgsutils-0.0.3}/src/mapkgsutils/config/schema.py +0 -0
- {mapkgsutils-0.0.2 → mapkgsutils-0.0.3}/src/mapkgsutils/context.py +0 -0
- {mapkgsutils-0.0.2 → mapkgsutils-0.0.3}/src/mapkgsutils/diff.py +0 -0
- {mapkgsutils-0.0.2 → mapkgsutils-0.0.3}/src/mapkgsutils/download.py +0 -0
- {mapkgsutils-0.0.2 → mapkgsutils-0.0.3}/src/mapkgsutils/exports.py +0 -0
- {mapkgsutils-0.0.2 → mapkgsutils-0.0.3}/src/mapkgsutils/logging.py +0 -0
- {mapkgsutils-0.0.2 → mapkgsutils-0.0.3}/src/mapkgsutils/parsers/__init__.py +0 -0
- {mapkgsutils-0.0.2 → mapkgsutils-0.0.3}/src/mapkgsutils/parsers/base.py +0 -0
- {mapkgsutils-0.0.2 → mapkgsutils-0.0.3}/src/mapkgsutils/py.typed +0 -0
|
@@ -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.3"
|
|
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.3"
|
|
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}",
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
"""Generic mapping-date consolidation: cache I/O and the release/date walk skeleton.
|
|
2
|
+
|
|
3
|
+
A versioned datasource's per-row deprecation/change date isn't always
|
|
4
|
+
recorded anywhere in a single release -- recovering it means walking every
|
|
5
|
+
historical release once and recording the first/last release each mapping
|
|
6
|
+
(keyed by its version-independent pair hash) was seen in. This module is the
|
|
7
|
+
datasource-agnostic half of that: cache I/O, the release-walk and
|
|
8
|
+
single-pass-date loop shapes, and materializing the cache back into a real
|
|
9
|
+
SSSOM mapping set. Per-datasource dispatch (which versions exist, how to
|
|
10
|
+
download/parse one, which mapping-set class to build) is supplied by the
|
|
11
|
+
caller as plain callables/values -- see :mod:`pysec2pri.consolidate` for a
|
|
12
|
+
concrete binding.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
from collections.abc import Callable, Iterable, Mapping
|
|
19
|
+
from datetime import datetime
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
from mapkgsutils.logging import logger
|
|
24
|
+
from mapkgsutils.parsers.base import _cmp_versions
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
"CACHE_COLUMNS",
|
|
28
|
+
"build_consolidated_mapping_set",
|
|
29
|
+
"consolidate_by_date",
|
|
30
|
+
"consolidate_by_release",
|
|
31
|
+
"load_mapping_dates",
|
|
32
|
+
"read_cache",
|
|
33
|
+
"read_meta",
|
|
34
|
+
"sssom_output_path",
|
|
35
|
+
"write_cache",
|
|
36
|
+
"write_consolidated_sssom",
|
|
37
|
+
"write_meta",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
CACHE_COLUMNS = (
|
|
41
|
+
"record_id",
|
|
42
|
+
"first_seen_version",
|
|
43
|
+
"first_seen_date",
|
|
44
|
+
"last_seen_version",
|
|
45
|
+
"last_seen_date",
|
|
46
|
+
# JSON-encoded snapshot of the mapping's own fields (subject_id,
|
|
47
|
+
# object_id, predicate_id, ...) as last seen -- used to materialize the
|
|
48
|
+
# consolidated index as a real SSSOM mapping set (see
|
|
49
|
+
# build_consolidated_mapping_set). Empty for legacy/hand-built rows.
|
|
50
|
+
"fields_json",
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def sssom_output_path(cache_path: Path) -> Path:
|
|
55
|
+
"""Return the companion SSSOM mapping-set path for a consolidated cache file."""
|
|
56
|
+
return cache_path.with_name(cache_path.stem + "_sssom.tsv")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def read_cache(cache_path: Path) -> dict[str, dict[str, str]]:
|
|
60
|
+
"""Read a consolidated mapping-date cache TSV into a dict keyed by record_id."""
|
|
61
|
+
if not cache_path.exists():
|
|
62
|
+
return {}
|
|
63
|
+
|
|
64
|
+
import polars as pl
|
|
65
|
+
|
|
66
|
+
df = pl.read_csv(cache_path, separator="\t", schema_overrides={"record_id": pl.Utf8})
|
|
67
|
+
cols = [c for c in CACHE_COLUMNS[1:] if c in df.columns]
|
|
68
|
+
return {
|
|
69
|
+
str(row["record_id"]): {col: str(row[col]) for col in cols}
|
|
70
|
+
for row in df.iter_rows(named=True)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def write_cache(cache_path: Path, records: dict[str, dict[str, str]]) -> None:
|
|
75
|
+
"""Write the merged ``record_id -> first/last seen`` dict to a TSV file."""
|
|
76
|
+
import polars as pl
|
|
77
|
+
|
|
78
|
+
cache_path.parent.mkdir(parents=True, exist_ok=True)
|
|
79
|
+
rows = [
|
|
80
|
+
{"record_id": rid, **{c: fields.get(c, "") for c in CACHE_COLUMNS[1:]}}
|
|
81
|
+
for rid, fields in records.items()
|
|
82
|
+
]
|
|
83
|
+
schema = list(CACHE_COLUMNS)
|
|
84
|
+
df = pl.DataFrame(rows, schema=schema) if rows else pl.DataFrame(schema=schema)
|
|
85
|
+
df.write_csv(cache_path, separator="\t")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def read_meta(meta_path: Path) -> str | None:
|
|
89
|
+
"""Read the ``last_version`` sidecar, or ``None`` if absent/unreadable."""
|
|
90
|
+
if not meta_path.exists():
|
|
91
|
+
return None
|
|
92
|
+
try:
|
|
93
|
+
data: dict[str, Any] = json.loads(meta_path.read_text(encoding="utf-8"))
|
|
94
|
+
except (OSError, json.JSONDecodeError):
|
|
95
|
+
return None
|
|
96
|
+
last_version = data.get("last_version")
|
|
97
|
+
return str(last_version) if last_version is not None else None
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def write_meta(meta_path: Path, last_version: str) -> None:
|
|
101
|
+
"""Write the ``last_version`` sidecar used to resume an interrupted walk."""
|
|
102
|
+
meta_path.parent.mkdir(parents=True, exist_ok=True)
|
|
103
|
+
meta_path.write_text(json.dumps({"last_version": last_version}), encoding="utf-8")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def load_mapping_dates(cache_path: Path) -> dict[str, str]:
|
|
107
|
+
"""Load the consolidated ``record_id -> first_seen_date`` index at *cache_path*.
|
|
108
|
+
|
|
109
|
+
Safe to call even when the index hasn't been built yet: returns ``{}``.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
cache_path: Path to the cache TSV (see :func:`write_cache`).
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
Dict mapping each ``record_id`` to its first-seen ISO date string.
|
|
116
|
+
Records walked but never assigned a real release date (e.g. an
|
|
117
|
+
unresolvable ``Last-Modified``) are omitted, leaving their
|
|
118
|
+
``mapping_date`` unset rather than passing through a non-date value.
|
|
119
|
+
"""
|
|
120
|
+
records = read_cache(cache_path)
|
|
121
|
+
return {
|
|
122
|
+
rid: fields["first_seen_date"]
|
|
123
|
+
for rid, fields in records.items()
|
|
124
|
+
if fields.get("first_seen_date")
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _mapping_fields_json(m: Any) -> str:
|
|
129
|
+
"""JSON-encode a mapping's own fields (excluding mapping_date/record_id).
|
|
130
|
+
|
|
131
|
+
Snapshots a mapping's current shape (subject_id, object_id,
|
|
132
|
+
predicate_id, ...) so the consolidated index can later be materialized
|
|
133
|
+
as a real SSSOM mapping set (see :func:`build_consolidated_mapping_set`).
|
|
134
|
+
``default=str`` handles enum-valued fields like ``mapping_cardinality``,
|
|
135
|
+
which round-trip cleanly back through ``Mapping(**fields)``. Returns
|
|
136
|
+
``"{}"`` for non-dataclass stand-ins (e.g. test doubles) that don't carry
|
|
137
|
+
a full field set -- callers skip materializing those rows.
|
|
138
|
+
"""
|
|
139
|
+
from dataclasses import fields as dataclass_fields
|
|
140
|
+
from dataclasses import is_dataclass
|
|
141
|
+
|
|
142
|
+
if not is_dataclass(m):
|
|
143
|
+
return "{}"
|
|
144
|
+
fields = {
|
|
145
|
+
f.name: getattr(m, f.name)
|
|
146
|
+
for f in dataclass_fields(m)
|
|
147
|
+
if f.name not in ("mapping_date", "record_id") and getattr(m, f.name, None) is not None
|
|
148
|
+
}
|
|
149
|
+
return json.dumps(fields, default=str)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def consolidate_by_date(
|
|
153
|
+
cache_path: Path,
|
|
154
|
+
meta_path: Path,
|
|
155
|
+
*,
|
|
156
|
+
run_one_version: Callable[[], Any],
|
|
157
|
+
) -> bool:
|
|
158
|
+
"""Single-pass "date" mode: capture each row's own real ``mapping_date``.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
cache_path: Path to the cache TSV to write.
|
|
162
|
+
meta_path: Path to the ``last_version`` sidecar to write.
|
|
163
|
+
run_one_version: Zero-arg callable returning a freshly parsed
|
|
164
|
+
mapping set (the current/latest release, already bound to
|
|
165
|
+
whatever datasource/mapping-kind/kwargs the caller needs).
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
``True`` and writes the cache when the mapping set produced at
|
|
169
|
+
least one real per-row date; ``False`` (cache untouched) when it
|
|
170
|
+
produced none, so the caller can fall back to a release-walk.
|
|
171
|
+
"""
|
|
172
|
+
mapping_set = run_one_version()
|
|
173
|
+
dated = [m for m in (mapping_set.mappings or []) if getattr(m, "mapping_date", None)]
|
|
174
|
+
if not dated:
|
|
175
|
+
return False
|
|
176
|
+
|
|
177
|
+
version_label = str(getattr(mapping_set, "mapping_set_version", None) or "current")
|
|
178
|
+
records: dict[str, dict[str, str]] = {}
|
|
179
|
+
for m in dated:
|
|
180
|
+
# record_id is release-scoped; its trailing 16 hex chars are always
|
|
181
|
+
# the version-independent pair hash (see mapkgsutils.parsers.base.pair_hash),
|
|
182
|
+
# which is what must match across releases.
|
|
183
|
+
pair_key = str(getattr(m, "record_id", None) or "")[-16:]
|
|
184
|
+
if not pair_key:
|
|
185
|
+
continue
|
|
186
|
+
date_str = str(m.mapping_date)
|
|
187
|
+
records[pair_key] = {
|
|
188
|
+
"first_seen_version": version_label,
|
|
189
|
+
"first_seen_date": date_str,
|
|
190
|
+
"last_seen_version": version_label,
|
|
191
|
+
"last_seen_date": date_str,
|
|
192
|
+
"fields_json": _mapping_fields_json(m),
|
|
193
|
+
}
|
|
194
|
+
write_cache(cache_path, records)
|
|
195
|
+
write_meta(meta_path, version_label)
|
|
196
|
+
return True
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def build_consolidated_mapping_set(
|
|
200
|
+
records: dict[str, dict[str, str]],
|
|
201
|
+
last_version: str | None,
|
|
202
|
+
*,
|
|
203
|
+
mapping_set_class: type[Any],
|
|
204
|
+
record_namespace: str,
|
|
205
|
+
mapping_set_metadata: Mapping[str, Any],
|
|
206
|
+
) -> Any:
|
|
207
|
+
"""Materialize the consolidated index as a real SSSOM mapping set.
|
|
208
|
+
|
|
209
|
+
Each record's stored field snapshot (from the most recent release where
|
|
210
|
+
it was seen) is rebuilt into a ``Mapping``, with ``mapping_date``
|
|
211
|
+
overridden to its ``first_seen_date`` -- the date of appearance, rather
|
|
212
|
+
than whatever date the snapshot's own release happened to carry -- and
|
|
213
|
+
``record_id`` re-scoped with a trailing ``consolidate`` segment, marking
|
|
214
|
+
the IRI as a derived, cross-release product.
|
|
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
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
A ``mapping_set_class`` instance with cardinalities computed.
|
|
227
|
+
"""
|
|
228
|
+
from sssom_schema import Mapping as SSSOMMapping
|
|
229
|
+
|
|
230
|
+
version_label = str(last_version) if last_version else "current"
|
|
231
|
+
|
|
232
|
+
mappings = []
|
|
233
|
+
for pair_key, fields in records.items():
|
|
234
|
+
fields_json = fields.get("fields_json") or ""
|
|
235
|
+
if not fields_json:
|
|
236
|
+
continue
|
|
237
|
+
try:
|
|
238
|
+
row_fields = json.loads(fields_json)
|
|
239
|
+
except json.JSONDecodeError:
|
|
240
|
+
continue
|
|
241
|
+
if not row_fields:
|
|
242
|
+
# Non-dataclass stand-ins (see _mapping_fields_json) have
|
|
243
|
+
# nothing to materialize into a real Mapping; date bookkeeping
|
|
244
|
+
# for them still lives in the TSV cache, just not in the SSSOM.
|
|
245
|
+
continue
|
|
246
|
+
row_fields["mapping_date"] = fields.get("first_seen_date") or None
|
|
247
|
+
row_fields["record_id"] = f"{record_namespace}{version_label}/consolidate/{pair_key}"
|
|
248
|
+
mappings.append(SSSOMMapping(**row_fields))
|
|
249
|
+
|
|
250
|
+
base_ms_id = str(mapping_set_metadata.get("mapping_set_id") or "")
|
|
251
|
+
mapping_set = mapping_set_class(
|
|
252
|
+
mappings=mappings,
|
|
253
|
+
curie_map=mapping_set_metadata.get("curie_map") or {},
|
|
254
|
+
mapping_set_id=f"{base_ms_id}/{version_label}/consolidate",
|
|
255
|
+
mapping_set_version=version_label,
|
|
256
|
+
mapping_set_title=mapping_set_metadata.get("mapping_set_title"),
|
|
257
|
+
mapping_set_description=mapping_set_metadata.get("mapping_set_description"),
|
|
258
|
+
license=mapping_set_metadata.get("license"),
|
|
259
|
+
)
|
|
260
|
+
mapping_set.compute_cardinalities()
|
|
261
|
+
return mapping_set
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def write_consolidated_sssom(
|
|
265
|
+
cache_path: Path,
|
|
266
|
+
meta_path: Path,
|
|
267
|
+
*,
|
|
268
|
+
mapping_set_class: type[Any],
|
|
269
|
+
record_namespace: str,
|
|
270
|
+
mapping_set_metadata: Mapping[str, Any],
|
|
271
|
+
) -> Path:
|
|
272
|
+
"""Build and save the companion SSSOM mapping set next to the cache file.
|
|
273
|
+
|
|
274
|
+
Args:
|
|
275
|
+
cache_path: Path to the cache TSV (see :func:`read_cache`).
|
|
276
|
+
meta_path: Path to the ``last_version`` sidecar (see :func:`read_meta`).
|
|
277
|
+
mapping_set_class: Concrete ``BaseMappingSet`` subclass to build.
|
|
278
|
+
record_namespace: Base IRI namespace for rebuilt ``record_id`` values.
|
|
279
|
+
mapping_set_metadata: ``mapping_set_id``/``curie_map``/``license``/...
|
|
280
|
+
metadata for the resulting mapping set.
|
|
281
|
+
|
|
282
|
+
Returns:
|
|
283
|
+
Path to the written SSSOM TSV (see :func:`sssom_output_path`).
|
|
284
|
+
"""
|
|
285
|
+
records = read_cache(cache_path)
|
|
286
|
+
last_version = read_meta(meta_path)
|
|
287
|
+
mapping_set = build_consolidated_mapping_set(
|
|
288
|
+
records,
|
|
289
|
+
last_version,
|
|
290
|
+
mapping_set_class=mapping_set_class,
|
|
291
|
+
record_namespace=record_namespace,
|
|
292
|
+
mapping_set_metadata=mapping_set_metadata,
|
|
293
|
+
)
|
|
294
|
+
output_path = sssom_output_path(cache_path)
|
|
295
|
+
mapping_set.save("sssom", output_path)
|
|
296
|
+
return output_path
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def consolidate_by_release(
|
|
300
|
+
cache_path: Path,
|
|
301
|
+
meta_path: Path,
|
|
302
|
+
*,
|
|
303
|
+
label: str,
|
|
304
|
+
list_versions: Callable[[], list[str]],
|
|
305
|
+
run_one_version: Callable[[str], Any],
|
|
306
|
+
resolve_release_date: Callable[[str], datetime | None],
|
|
307
|
+
show_progress: bool = True,
|
|
308
|
+
force: bool = False,
|
|
309
|
+
) -> None:
|
|
310
|
+
"""Historical-walk "release" mode: track first/last-seen release per mapping.
|
|
311
|
+
|
|
312
|
+
Args:
|
|
313
|
+
cache_path: Path to the cache TSV to read/write.
|
|
314
|
+
meta_path: Path to the ``last_version`` sidecar to read/write.
|
|
315
|
+
label: Datasource name, used only for the progress bar/log messages.
|
|
316
|
+
list_versions: Zero-arg callable returning every available version,
|
|
317
|
+
oldest first.
|
|
318
|
+
run_one_version: Callable taking a version string and returning a
|
|
319
|
+
freshly downloaded+parsed mapping set for it.
|
|
320
|
+
resolve_release_date: Callable taking a version string and
|
|
321
|
+
returning its upstream release date, or ``None`` when
|
|
322
|
+
unresolvable.
|
|
323
|
+
show_progress: Whether to show a progress bar over releases.
|
|
324
|
+
force: Re-scan every release from scratch, ignoring any existing
|
|
325
|
+
cache/resume state.
|
|
326
|
+
"""
|
|
327
|
+
records: dict[str, dict[str, str]] = {} if force else read_cache(cache_path)
|
|
328
|
+
last_version = None if force else read_meta(meta_path)
|
|
329
|
+
|
|
330
|
+
versions = list_versions()
|
|
331
|
+
if last_version is not None:
|
|
332
|
+
versions = [v for v in versions if _cmp_versions(v, last_version) > 0]
|
|
333
|
+
|
|
334
|
+
iterator: Iterable[str] = versions
|
|
335
|
+
if show_progress:
|
|
336
|
+
from tqdm import tqdm
|
|
337
|
+
|
|
338
|
+
iterator = tqdm(versions, desc=f"Consolidating {label.upper()} mapping dates")
|
|
339
|
+
|
|
340
|
+
for v in iterator:
|
|
341
|
+
try:
|
|
342
|
+
mapping_set = run_one_version(v)
|
|
343
|
+
release_date = resolve_release_date(v)
|
|
344
|
+
# Empty (not the version label v) when no real release date is
|
|
345
|
+
# resolvable -- v is a version, not a date, and isn't always
|
|
346
|
+
# date-shaped (e.g. ChEBI's plain release numbers).
|
|
347
|
+
date_str = release_date.date().isoformat() if release_date else ""
|
|
348
|
+
|
|
349
|
+
for m in mapping_set.mappings or []:
|
|
350
|
+
# record_id is release-scoped; match across releases on its
|
|
351
|
+
# trailing pair hash instead (see mapkgsutils.parsers.base.pair_hash).
|
|
352
|
+
pair_key = str(getattr(m, "record_id", None) or "")[-16:]
|
|
353
|
+
if not pair_key:
|
|
354
|
+
continue
|
|
355
|
+
entry = records.setdefault(
|
|
356
|
+
pair_key,
|
|
357
|
+
{"first_seen_version": v, "first_seen_date": date_str},
|
|
358
|
+
)
|
|
359
|
+
entry["last_seen_version"] = v
|
|
360
|
+
entry["last_seen_date"] = date_str
|
|
361
|
+
entry["fields_json"] = _mapping_fields_json(m)
|
|
362
|
+
except Exception:
|
|
363
|
+
logger.warning("Skipping %s version %s during consolidation", label, v, exc_info=True)
|
|
364
|
+
continue
|
|
365
|
+
|
|
366
|
+
last_version = v
|
|
367
|
+
write_cache(cache_path, records)
|
|
368
|
+
write_meta(meta_path, last_version)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|