ds_crawler 2.7.2__tar.gz → 2.8.0__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.
- {ds_crawler-2.7.2 → ds_crawler-2.8.0}/PKG-INFO +1 -1
- {ds_crawler-2.7.2 → ds_crawler-2.8.0}/README.md +77 -0
- {ds_crawler-2.7.2 → ds_crawler-2.8.0}/ds_crawler/__init__.py +3 -0
- {ds_crawler-2.7.2 → ds_crawler-2.8.0}/ds_crawler/artifact_builder.py +47 -8
- {ds_crawler-2.7.2 → ds_crawler-2.8.0}/ds_crawler/artifacts.py +15 -2
- {ds_crawler-2.7.2 → ds_crawler-2.8.0}/ds_crawler/cli.py +52 -2
- {ds_crawler-2.7.2 → ds_crawler-2.8.0}/ds_crawler/config.py +75 -7
- {ds_crawler-2.7.2 → ds_crawler-2.8.0}/ds_crawler/migration.py +81 -13
- {ds_crawler-2.7.2 → ds_crawler-2.8.0}/ds_crawler/operations.py +323 -69
- {ds_crawler-2.7.2 → ds_crawler-2.8.0}/ds_crawler/parser.py +52 -9
- {ds_crawler-2.7.2 → ds_crawler-2.8.0}/ds_crawler/schema.py +29 -6
- {ds_crawler-2.7.2 → ds_crawler-2.8.0}/ds_crawler/validation.py +37 -7
- {ds_crawler-2.7.2 → ds_crawler-2.8.0}/ds_crawler/writer.py +19 -18
- {ds_crawler-2.7.2 → ds_crawler-2.8.0}/ds_crawler/zip_utils.py +280 -20
- {ds_crawler-2.7.2 → ds_crawler-2.8.0}/pyproject.toml +1 -1
- {ds_crawler-2.7.2 → ds_crawler-2.8.0}/tests/test_artifact_builder.py +44 -0
- ds_crawler-2.8.0/tests/test_metadata_scopes.py +313 -0
- {ds_crawler-2.7.2 → ds_crawler-2.8.0}/tests/test_new_metadata.py +30 -0
- {ds_crawler-2.7.2 → ds_crawler-2.8.0}/.github/workflows/workflow.yml +0 -0
- {ds_crawler-2.7.2 → ds_crawler-2.8.0}/.gitignore +0 -0
- {ds_crawler-2.7.2 → ds_crawler-2.8.0}/config_example.json +0 -0
- {ds_crawler-2.7.2 → ds_crawler-2.8.0}/ds_crawler/_dataset_contract.py +0 -0
- {ds_crawler-2.7.2 → ds_crawler-2.8.0}/ds_crawler/_euler_modalities.py +0 -0
- {ds_crawler-2.7.2 → ds_crawler-2.8.0}/ds_crawler/_version.py +0 -0
- {ds_crawler-2.7.2 → ds_crawler-2.8.0}/ds_crawler/handlers/__init__.py +0 -0
- {ds_crawler-2.7.2 → ds_crawler-2.8.0}/ds_crawler/handlers/base.py +0 -0
- {ds_crawler-2.7.2 → ds_crawler-2.8.0}/ds_crawler/handlers/generic.py +0 -0
- {ds_crawler-2.7.2 → ds_crawler-2.8.0}/ds_crawler/handlers/zip_handler.py +0 -0
- {ds_crawler-2.7.2 → ds_crawler-2.8.0}/ds_crawler/layout.py +0 -0
- {ds_crawler-2.7.2 → ds_crawler-2.8.0}/ds_crawler/path_filters.py +0 -0
- {ds_crawler-2.7.2 → ds_crawler-2.8.0}/ds_crawler/traversal.py +0 -0
- {ds_crawler-2.7.2 → ds_crawler-2.8.0}/example-output-rds.json +0 -0
- {ds_crawler-2.7.2 → ds_crawler-2.8.0}/examples/augmented_rgb_example.py +0 -0
- {ds_crawler-2.7.2 → ds_crawler-2.8.0}/examples/config.json +0 -0
- {ds_crawler-2.7.2 → ds_crawler-2.8.0}/examples/example.py +0 -0
- {ds_crawler-2.7.2 → ds_crawler-2.8.0}/examples/example_output.json +0 -0
- {ds_crawler-2.7.2 → ds_crawler-2.8.0}/examples/run.sh +0 -0
- {ds_crawler-2.7.2 → ds_crawler-2.8.0}/examples/sample_realdrivesim.py +0 -0
- {ds_crawler-2.7.2 → ds_crawler-2.8.0}/meta/__init__.py +0 -0
- {ds_crawler-2.7.2 → ds_crawler-2.8.0}/meta/build_meta_schema.py +0 -0
- {ds_crawler-2.7.2 → ds_crawler-2.8.0}/meta/schema.json +0 -0
- {ds_crawler-2.7.2 → ds_crawler-2.8.0}/package-lock.json +0 -0
- {ds_crawler-2.7.2 → ds_crawler-2.8.0}/split_rds.py +0 -0
- {ds_crawler-2.7.2 → ds_crawler-2.8.0}/tests/__init__.py +0 -0
- {ds_crawler-2.7.2 → ds_crawler-2.8.0}/tests/conftest.py +0 -0
- {ds_crawler-2.7.2 → ds_crawler-2.8.0}/tests/current_helpers.py +0 -0
- {ds_crawler-2.7.2 → ds_crawler-2.8.0}/tests/test_align_datasets.py +0 -0
- {ds_crawler-2.7.2 → ds_crawler-2.8.0}/tests/test_attributes_passthrough.py +0 -0
- {ds_crawler-2.7.2 → ds_crawler-2.8.0}/tests/test_config.py +0 -0
- {ds_crawler-2.7.2 → ds_crawler-2.8.0}/tests/test_copy_dataset.py +0 -0
- {ds_crawler-2.7.2 → ds_crawler-2.8.0}/tests/test_extract_datasets.py +0 -0
- {ds_crawler-2.7.2 → ds_crawler-2.8.0}/tests/test_handlers.py +0 -0
- {ds_crawler-2.7.2 → ds_crawler-2.8.0}/tests/test_inline_splits.py +0 -0
- {ds_crawler-2.7.2 → ds_crawler-2.8.0}/tests/test_integration.py +0 -0
- {ds_crawler-2.7.2 → ds_crawler-2.8.0}/tests/test_layout.py +0 -0
- {ds_crawler-2.7.2 → ds_crawler-2.8.0}/tests/test_meta_schema.py +0 -0
- {ds_crawler-2.7.2 → ds_crawler-2.8.0}/tests/test_parser.py +0 -0
- {ds_crawler-2.7.2 → ds_crawler-2.8.0}/tests/test_split_dataset.py +0 -0
- {ds_crawler-2.7.2 → ds_crawler-2.8.0}/tests/test_validation.py +0 -0
- {ds_crawler-2.7.2 → ds_crawler-2.8.0}/tests/test_writer.py +0 -0
- {ds_crawler-2.7.2 → ds_crawler-2.8.0}/tests/test_zip.py +0 -0
|
@@ -43,6 +43,31 @@ The files have distinct roles:
|
|
|
43
43
|
| `.ds_crawler/index.json` | Materialized full dataset index. No duplicated head/config metadata. |
|
|
44
44
|
| `.ds_crawler/split_<name>.json` | Named split artifact with its own contract, provenance, and filtered `index` node. |
|
|
45
45
|
|
|
46
|
+
When several logical modalities share one physical root or zip, store each
|
|
47
|
+
metadata set under a scope:
|
|
48
|
+
|
|
49
|
+
```text
|
|
50
|
+
dataset_root/
|
|
51
|
+
├── .ds_crawler/
|
|
52
|
+
│ ├── scopes.json
|
|
53
|
+
│ ├── rgb/
|
|
54
|
+
│ │ ├── dataset-head.json
|
|
55
|
+
│ │ ├── ds-crawler.json
|
|
56
|
+
│ │ └── index.json
|
|
57
|
+
│ └── camera_extrinsics/
|
|
58
|
+
│ ├── dataset-head.json
|
|
59
|
+
│ ├── ds-crawler.json
|
|
60
|
+
│ └── index.json
|
|
61
|
+
├── calib.json
|
|
62
|
+
└── ...
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Pass `metadata_scope="rgb"` or `metadata_scope="camera_extrinsics"` to the
|
|
66
|
+
path-based APIs to read/write that scoped artifact set. Scoped writes update
|
|
67
|
+
`.ds_crawler/scopes.json`, a discoverability manifest listing known scopes and
|
|
68
|
+
their artifact filenames. Omitting `metadata_scope` preserves the legacy
|
|
69
|
+
unscoped layout exactly.
|
|
70
|
+
|
|
46
71
|
The in-memory objects returned by `index_dataset_from_path(...)` and
|
|
47
72
|
`load_dataset_split(...)` are hydrated outputs. They include `head` and
|
|
48
73
|
`indexing` for convenience, even though the on-disk `index.json` only stores
|
|
@@ -146,6 +171,21 @@ print(output["index"].keys())
|
|
|
146
171
|
- `.ds_crawler/ds-crawler.json`
|
|
147
172
|
- `.ds_crawler/index.json`
|
|
148
173
|
|
|
174
|
+
For a shared physical root, pass a metadata scope:
|
|
175
|
+
|
|
176
|
+
```python
|
|
177
|
+
rgb = index_dataset_from_path(
|
|
178
|
+
"/data/muses",
|
|
179
|
+
save_index=True,
|
|
180
|
+
metadata_scope="rgb",
|
|
181
|
+
)
|
|
182
|
+
extrinsics = index_dataset_from_path(
|
|
183
|
+
"/data/muses",
|
|
184
|
+
save_index=True,
|
|
185
|
+
metadata_scope="camera_extrinsics",
|
|
186
|
+
)
|
|
187
|
+
```
|
|
188
|
+
|
|
149
189
|
### 3. Align multiple modalities
|
|
150
190
|
|
|
151
191
|
```python
|
|
@@ -237,6 +277,19 @@ print(train_output["split"]["name"])
|
|
|
237
277
|
print(train_output["execution"]["split"])
|
|
238
278
|
```
|
|
239
279
|
|
|
280
|
+
Scoped split artifacts use the same namespace as the index:
|
|
281
|
+
|
|
282
|
+
```python
|
|
283
|
+
create_dataset_splits(
|
|
284
|
+
"/data/muses",
|
|
285
|
+
split_names=["train", "val"],
|
|
286
|
+
ratios=[80, 20],
|
|
287
|
+
metadata_scope="rgb",
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
train_output = load_dataset_split("/data/muses", "train", metadata_scope="rgb")
|
|
291
|
+
```
|
|
292
|
+
|
|
240
293
|
To reuse an existing partition on a sibling dataset (same qualified file
|
|
241
294
|
IDs), copy the splits over instead of recomputing them:
|
|
242
295
|
|
|
@@ -497,6 +550,9 @@ The main public entry points are re-exported from `ds_crawler`.
|
|
|
497
550
|
|
|
498
551
|
Indexing and config:
|
|
499
552
|
|
|
553
|
+
- `build_crawler_config(...)`
|
|
554
|
+
- `build_dataset_artifacts_from_files(...)`
|
|
555
|
+
- `build_dataset_head(...)`
|
|
500
556
|
- `index_dataset(...)`
|
|
501
557
|
- `index_dataset_from_files(...)`
|
|
502
558
|
- `index_dataset_from_path(...)`
|
|
@@ -505,14 +561,22 @@ Indexing and config:
|
|
|
505
561
|
- `validate_dataset(...)`
|
|
506
562
|
- `validate_output(...)`
|
|
507
563
|
|
|
564
|
+
`index_dataset(...)`, `index_dataset_from_path(...)`, `load_dataset_config(...)`,
|
|
565
|
+
and `validate_dataset(...)` accept `metadata_scope=...` for scoped metadata.
|
|
566
|
+
`build_dataset_artifacts_from_files(...)` also accepts `metadata_scope=...` and
|
|
567
|
+
returns scoped artifact names plus `scopes.json`.
|
|
568
|
+
|
|
508
569
|
Dataset metadata:
|
|
509
570
|
|
|
510
571
|
- `get_dataset_contract(source)`
|
|
511
572
|
- `get_dataset_properties(source)`
|
|
512
573
|
- `extract_dataset_properties(mapping)`
|
|
574
|
+
- `list_metadata_scopes(path)`
|
|
513
575
|
|
|
514
576
|
`get_dataset_contract(...)` returns a `DatasetHeadContract`. Use
|
|
515
577
|
`contract.get_namespace("euler_train")` to access addon payloads.
|
|
578
|
+
`get_dataset_contract(path, metadata_scope=...)` and
|
|
579
|
+
`get_dataset_properties(path, metadata_scope=...)` read scoped heads.
|
|
516
580
|
|
|
517
581
|
Traversal and filtering:
|
|
518
582
|
|
|
@@ -535,16 +599,29 @@ Operations:
|
|
|
535
599
|
- `list_dataset_splits(...)`
|
|
536
600
|
- `load_dataset_split(...)`
|
|
537
601
|
|
|
602
|
+
Split, copy, align, and extraction operations accept `metadata_scope=...`.
|
|
603
|
+
`copy_dataset(...)`, `copy_dataset_splits(...)`, and `split_dataset(...)` also
|
|
604
|
+
accept source/target scope overrides when the two sides use different scope
|
|
605
|
+
names. `split_datasets(...)` and `extract_datasets(...)` accept per-source or
|
|
606
|
+
per-config scope lists for multi-dataset workflows.
|
|
607
|
+
|
|
538
608
|
Writers:
|
|
539
609
|
|
|
540
610
|
- `DatasetWriter(...)`
|
|
541
611
|
- `ZipDatasetWriter(...)`
|
|
542
612
|
|
|
613
|
+
Both writers accept `metadata_scope=...` and write scoped artifacts plus the
|
|
614
|
+
`scopes.json` manifest.
|
|
615
|
+
|
|
543
616
|
Migration helpers:
|
|
544
617
|
|
|
545
618
|
- `migrate_dataset_metadata(...)`
|
|
546
619
|
- `migrate_dataset_zip(...)`
|
|
547
620
|
- `migrate_dataset_zips_in_folder(...)`
|
|
621
|
+
- `migrate_inline_splits(...)`
|
|
622
|
+
|
|
623
|
+
Migration helpers and `ds-crawler migrate` also accept `metadata_scope=...`
|
|
624
|
+
or `--metadata-scope` for scoped legacy metadata conversion.
|
|
548
625
|
|
|
549
626
|
## ZIP support
|
|
550
627
|
|
|
@@ -46,6 +46,7 @@ from .migration import (
|
|
|
46
46
|
migrate_dataset_zips_in_folder,
|
|
47
47
|
)
|
|
48
48
|
from .writer import DatasetWriter, ZipDatasetWriter
|
|
49
|
+
from .zip_utils import list_metadata_scopes, validate_metadata_scope
|
|
49
50
|
from .layout import (
|
|
50
51
|
EULER_LAYOUT_ADDON,
|
|
51
52
|
EULER_LAYOUT_VERSION,
|
|
@@ -86,6 +87,7 @@ __all__ = [
|
|
|
86
87
|
"index_dataset_from_files",
|
|
87
88
|
"index_dataset_from_path",
|
|
88
89
|
"list_dataset_splits",
|
|
90
|
+
"list_metadata_scopes",
|
|
89
91
|
"load_dataset_config",
|
|
90
92
|
"load_dataset_split",
|
|
91
93
|
"migrate_dataset_metadata",
|
|
@@ -96,6 +98,7 @@ __all__ = [
|
|
|
96
98
|
"split_qualified_ids",
|
|
97
99
|
"validate_crawler_config",
|
|
98
100
|
"validate_dataset",
|
|
101
|
+
"validate_metadata_scope",
|
|
99
102
|
"validate_layout_addon",
|
|
100
103
|
"validate_output",
|
|
101
104
|
"extract_dataset_properties",
|
|
@@ -15,7 +15,14 @@ from .artifacts import build_index_artifact
|
|
|
15
15
|
from .config import CONFIG_FILENAME, CRAWLER_CONFIG_KIND, CRAWLER_CONFIG_VERSION, DatasetConfig
|
|
16
16
|
from .parser import index_dataset_from_files
|
|
17
17
|
from .traversal import get_files
|
|
18
|
-
from .zip_utils import
|
|
18
|
+
from .zip_utils import (
|
|
19
|
+
DATASET_HEAD_FILENAME,
|
|
20
|
+
OUTPUT_FILENAME,
|
|
21
|
+
SCOPES_FILENAME,
|
|
22
|
+
_scopes_manifest_with,
|
|
23
|
+
get_metadata_filename,
|
|
24
|
+
validate_metadata_scope,
|
|
25
|
+
)
|
|
19
26
|
|
|
20
27
|
|
|
21
28
|
def _require_mapping(value: Any, context: str) -> dict[str, Any]:
|
|
@@ -125,6 +132,7 @@ def build_crawler_config(
|
|
|
125
132
|
source_path: str = ".",
|
|
126
133
|
head_file: str = DATASET_HEAD_FILENAME,
|
|
127
134
|
prebuilt_index_file: str | None = None,
|
|
135
|
+
metadata_scope: str | None = None,
|
|
128
136
|
) -> dict[str, Any]:
|
|
129
137
|
"""Build and validate a canonical ``ds-crawler.json`` mapping."""
|
|
130
138
|
indexing = _require_mapping(indexing, "indexing")
|
|
@@ -150,6 +158,8 @@ def build_crawler_config(
|
|
|
150
158
|
}
|
|
151
159
|
if prebuilt_index_file is not None:
|
|
152
160
|
config["source"]["prebuilt_index_file"] = prebuilt_index_file
|
|
161
|
+
if metadata_scope is not None:
|
|
162
|
+
config["metadata_scope"] = validate_metadata_scope(metadata_scope)
|
|
153
163
|
|
|
154
164
|
runtime_config = dict(config)
|
|
155
165
|
runtime_config["head"] = deepcopy(head)
|
|
@@ -172,10 +182,20 @@ def build_dataset_artifacts_from_files(
|
|
|
172
182
|
strict: bool = False,
|
|
173
183
|
sample: int | None = None,
|
|
174
184
|
match_index: dict[str, Any] | None = None,
|
|
185
|
+
metadata_scope: str | None = None,
|
|
175
186
|
) -> dict[str, Any]:
|
|
176
187
|
"""Build canonical dataset artifacts from a parameter set and file list."""
|
|
188
|
+
normalized_scope = (
|
|
189
|
+
validate_metadata_scope(metadata_scope)
|
|
190
|
+
if metadata_scope is not None
|
|
191
|
+
else None
|
|
192
|
+
)
|
|
177
193
|
head = build_dataset_head(dataset=dataset, modality=modality, addons=addons)
|
|
178
|
-
config = build_crawler_config(
|
|
194
|
+
config = build_crawler_config(
|
|
195
|
+
head=head,
|
|
196
|
+
indexing=indexing,
|
|
197
|
+
metadata_scope=normalized_scope,
|
|
198
|
+
)
|
|
179
199
|
|
|
180
200
|
runtime_config = dict(config)
|
|
181
201
|
runtime_config["head"] = deepcopy(head)
|
|
@@ -193,14 +213,33 @@ def build_dataset_artifacts_from_files(
|
|
|
193
213
|
CONFIG_FILENAME: config,
|
|
194
214
|
OUTPUT_FILENAME: build_index_artifact(output),
|
|
195
215
|
}
|
|
216
|
+
if normalized_scope is not None:
|
|
217
|
+
unscoped_artifact_names = list(artifacts)
|
|
218
|
+
artifacts = {
|
|
219
|
+
get_metadata_filename(
|
|
220
|
+
filename,
|
|
221
|
+
metadata_scope=normalized_scope,
|
|
222
|
+
): payload
|
|
223
|
+
for filename, payload in artifacts.items()
|
|
224
|
+
}
|
|
225
|
+
artifacts[SCOPES_FILENAME] = _scopes_manifest_with(
|
|
226
|
+
None,
|
|
227
|
+
metadata_scope=normalized_scope,
|
|
228
|
+
filenames=unscoped_artifact_names,
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
summary = {
|
|
232
|
+
"dataset_id": output["head"]["dataset"]["id"],
|
|
233
|
+
"dataset_name": output["head"]["dataset"]["name"],
|
|
234
|
+
"modality_key": output["head"]["modality"]["key"],
|
|
235
|
+
"file_count": len(get_files(output)),
|
|
236
|
+
}
|
|
237
|
+
if normalized_scope is not None:
|
|
238
|
+
summary["metadata_scope"] = normalized_scope
|
|
239
|
+
|
|
196
240
|
return {
|
|
197
241
|
"artifacts": artifacts,
|
|
198
|
-
"summary":
|
|
199
|
-
"dataset_id": output["head"]["dataset"]["id"],
|
|
200
|
-
"dataset_name": output["head"]["dataset"]["name"],
|
|
201
|
-
"modality_key": output["head"]["modality"]["key"],
|
|
202
|
-
"file_count": len(get_files(output)),
|
|
203
|
-
},
|
|
242
|
+
"summary": summary,
|
|
204
243
|
}
|
|
205
244
|
|
|
206
245
|
|
|
@@ -173,13 +173,21 @@ def load_saved_output(
|
|
|
173
173
|
dataset_path: str | Path,
|
|
174
174
|
*,
|
|
175
175
|
filename: str = OUTPUT_FILENAME,
|
|
176
|
+
metadata_scope: str | None = None,
|
|
176
177
|
) -> dict[str, Any] | None:
|
|
177
178
|
"""Load and hydrate a persisted dataset index artifact from disk."""
|
|
178
179
|
dataset_root = Path(dataset_path)
|
|
179
|
-
index_artifact = read_metadata_json(
|
|
180
|
+
index_artifact = read_metadata_json(
|
|
181
|
+
dataset_root,
|
|
182
|
+
filename,
|
|
183
|
+
metadata_scope=metadata_scope,
|
|
184
|
+
)
|
|
180
185
|
if index_artifact is None:
|
|
181
186
|
return None
|
|
182
|
-
ds_config = load_dataset_config(
|
|
187
|
+
ds_config = load_dataset_config(
|
|
188
|
+
{"path": str(dataset_root)},
|
|
189
|
+
metadata_scope=metadata_scope,
|
|
190
|
+
)
|
|
183
191
|
return hydrate_index_artifact(index_artifact, ds_config)
|
|
184
192
|
|
|
185
193
|
|
|
@@ -196,6 +204,7 @@ def load_prebuilt_output(ds_config: DatasetConfig) -> dict[str, Any]:
|
|
|
196
204
|
index_artifact = read_metadata_json(
|
|
197
205
|
Path(ds_config.dataset_root),
|
|
198
206
|
prebuilt_path.name,
|
|
207
|
+
metadata_scope=ds_config.metadata_scope,
|
|
199
208
|
)
|
|
200
209
|
if index_artifact is None:
|
|
201
210
|
raise FileNotFoundError(
|
|
@@ -210,6 +219,7 @@ def save_output_artifacts(
|
|
|
210
219
|
output: dict[str, Any],
|
|
211
220
|
*,
|
|
212
221
|
filename: str = OUTPUT_FILENAME,
|
|
222
|
+
metadata_scope: str | None = None,
|
|
213
223
|
) -> Path:
|
|
214
224
|
"""Persist head/config/index artifacts for a dataset output."""
|
|
215
225
|
dataset_root = Path(dataset_path)
|
|
@@ -227,8 +237,11 @@ def save_output_artifacts(
|
|
|
227
237
|
CONFIG_FILENAME: build_crawler_config_for_output(output),
|
|
228
238
|
filename: build_index_artifact(output),
|
|
229
239
|
},
|
|
240
|
+
metadata_scope=metadata_scope,
|
|
230
241
|
)
|
|
231
242
|
|
|
232
243
|
if is_zip_path(dataset_root):
|
|
233
244
|
return dataset_root
|
|
245
|
+
if metadata_scope is not None:
|
|
246
|
+
return dataset_root / METADATA_DIR / metadata_scope / filename
|
|
234
247
|
return dataset_root / METADATA_DIR / filename
|
|
@@ -85,13 +85,27 @@ def _run_index(argv: list[str]) -> int:
|
|
|
85
85
|
metavar="PATH",
|
|
86
86
|
help="Path to an index.json whose file IDs are used as a filter.",
|
|
87
87
|
)
|
|
88
|
+
parser.add_argument(
|
|
89
|
+
"--metadata-scope",
|
|
90
|
+
type=str,
|
|
91
|
+
default=None,
|
|
92
|
+
metavar="SCOPE",
|
|
93
|
+
help=(
|
|
94
|
+
"Read/write ds-crawler artifacts below .ds_crawler/SCOPE/ "
|
|
95
|
+
"instead of the root .ds_crawler directory."
|
|
96
|
+
),
|
|
97
|
+
)
|
|
88
98
|
|
|
89
99
|
args = parser.parse_args(argv)
|
|
90
100
|
|
|
91
101
|
setup_logging(args.verbose)
|
|
92
102
|
|
|
93
103
|
try:
|
|
94
|
-
config = Config.from_file(
|
|
104
|
+
config = Config.from_file(
|
|
105
|
+
args.config,
|
|
106
|
+
workdir=args.workdir,
|
|
107
|
+
metadata_scope=args.metadata_scope,
|
|
108
|
+
)
|
|
95
109
|
except FileNotFoundError as e:
|
|
96
110
|
print(f"Error: {e}", file=sys.stderr)
|
|
97
111
|
return 1
|
|
@@ -122,7 +136,9 @@ def _run_index(argv: list[str]) -> int:
|
|
|
122
136
|
parser_instance.write_output(args.output)
|
|
123
137
|
logger.info(f"Output written to: {args.output}")
|
|
124
138
|
else:
|
|
125
|
-
output_paths = parser_instance.write_outputs_per_dataset(
|
|
139
|
+
output_paths = parser_instance.write_outputs_per_dataset(
|
|
140
|
+
metadata_scope=args.metadata_scope,
|
|
141
|
+
)
|
|
126
142
|
for path in output_paths:
|
|
127
143
|
logger.info(f"Output written to: {path}")
|
|
128
144
|
except Exception as e:
|
|
@@ -165,6 +181,13 @@ def _run_migrate(argv: list[str]) -> int:
|
|
|
165
181
|
action="store_true",
|
|
166
182
|
help="When used with --scan-zips, only scan the provided directory itself.",
|
|
167
183
|
)
|
|
184
|
+
parser.add_argument(
|
|
185
|
+
"--metadata-scope",
|
|
186
|
+
help=(
|
|
187
|
+
"Read/write legacy migration artifacts below "
|
|
188
|
+
".ds_crawler/SCOPE/."
|
|
189
|
+
),
|
|
190
|
+
)
|
|
168
191
|
parser.add_argument(
|
|
169
192
|
"-v",
|
|
170
193
|
"--verbose",
|
|
@@ -188,6 +211,7 @@ def _run_migrate(argv: list[str]) -> int:
|
|
|
188
211
|
result = migrate_inline_splits(
|
|
189
212
|
path,
|
|
190
213
|
logger=logger,
|
|
214
|
+
metadata_scope=args.metadata_scope,
|
|
191
215
|
)
|
|
192
216
|
logger.info(
|
|
193
217
|
"Migrated inline splits %s (splits=%d)",
|
|
@@ -202,6 +226,7 @@ def _run_migrate(argv: list[str]) -> int:
|
|
|
202
226
|
recursive=not args.top_level_only,
|
|
203
227
|
write_output=not args.no_index,
|
|
204
228
|
logger=logger,
|
|
229
|
+
metadata_scope=args.metadata_scope,
|
|
205
230
|
)
|
|
206
231
|
if result["failed"]:
|
|
207
232
|
failed = True
|
|
@@ -222,6 +247,7 @@ def _run_migrate(argv: list[str]) -> int:
|
|
|
222
247
|
path,
|
|
223
248
|
write_output=not args.no_index,
|
|
224
249
|
logger=logger,
|
|
250
|
+
metadata_scope=args.metadata_scope,
|
|
225
251
|
)
|
|
226
252
|
except Exception as exc:
|
|
227
253
|
failed = True
|
|
@@ -274,6 +300,27 @@ def _run_copy_splits(argv: list[str]) -> int:
|
|
|
274
300
|
action="store_true",
|
|
275
301
|
help="Replace existing splits on the target if they share a name.",
|
|
276
302
|
)
|
|
303
|
+
parser.add_argument(
|
|
304
|
+
"--metadata-scope",
|
|
305
|
+
type=str,
|
|
306
|
+
default=None,
|
|
307
|
+
metavar="SCOPE",
|
|
308
|
+
help="Use the same scoped .ds_crawler/SCOPE metadata for source and target.",
|
|
309
|
+
)
|
|
310
|
+
parser.add_argument(
|
|
311
|
+
"--source-metadata-scope",
|
|
312
|
+
type=str,
|
|
313
|
+
default=None,
|
|
314
|
+
metavar="SCOPE",
|
|
315
|
+
help="Read source splits from .ds_crawler/SCOPE/.",
|
|
316
|
+
)
|
|
317
|
+
parser.add_argument(
|
|
318
|
+
"--target-metadata-scope",
|
|
319
|
+
type=str,
|
|
320
|
+
default=None,
|
|
321
|
+
metavar="SCOPE",
|
|
322
|
+
help="Write target splits to .ds_crawler/SCOPE/.",
|
|
323
|
+
)
|
|
277
324
|
parser.add_argument(
|
|
278
325
|
"-v",
|
|
279
326
|
"--verbose",
|
|
@@ -291,6 +338,9 @@ def _run_copy_splits(argv: list[str]) -> int:
|
|
|
291
338
|
args.target,
|
|
292
339
|
split_names=args.splits,
|
|
293
340
|
override=args.override,
|
|
341
|
+
metadata_scope=args.metadata_scope,
|
|
342
|
+
source_metadata_scope=args.source_metadata_scope,
|
|
343
|
+
target_metadata_scope=args.target_metadata_scope,
|
|
294
344
|
)
|
|
295
345
|
except (FileNotFoundError, ValueError) as exc:
|
|
296
346
|
print(f"Error: {exc}", file=sys.stderr)
|
|
@@ -16,7 +16,11 @@ from ._dataset_contract import (
|
|
|
16
16
|
validate_contract_version,
|
|
17
17
|
)
|
|
18
18
|
from .path_filters import PathFilters
|
|
19
|
-
from .zip_utils import
|
|
19
|
+
from .zip_utils import (
|
|
20
|
+
DATASET_HEAD_FILENAME,
|
|
21
|
+
read_metadata_json,
|
|
22
|
+
validate_metadata_scope,
|
|
23
|
+
)
|
|
20
24
|
|
|
21
25
|
|
|
22
26
|
CONFIG_FILENAME = "ds-crawler.json"
|
|
@@ -81,6 +85,7 @@ class DatasetConfig:
|
|
|
81
85
|
path_filters: dict[str, Any] | None = None
|
|
82
86
|
prebuilt_index_file: str | None = None
|
|
83
87
|
file_extensions: list[str] | None = None
|
|
88
|
+
metadata_scope: str | None = None
|
|
84
89
|
compiled_path_filters: PathFilters = field(init=False, repr=False)
|
|
85
90
|
|
|
86
91
|
def __post_init__(self) -> None:
|
|
@@ -167,6 +172,8 @@ class DatasetConfig:
|
|
|
167
172
|
}
|
|
168
173
|
if self.prebuilt_index_file is not None:
|
|
169
174
|
result["source"]["prebuilt_index_file"] = self.prebuilt_index_file
|
|
175
|
+
if self.metadata_scope is not None:
|
|
176
|
+
result["metadata_scope"] = self.metadata_scope
|
|
170
177
|
return result
|
|
171
178
|
|
|
172
179
|
def _normalize_file_extensions(self) -> None:
|
|
@@ -242,11 +249,22 @@ class DatasetConfig:
|
|
|
242
249
|
workdir: str | Path | None = None,
|
|
243
250
|
dataset_root: str | Path | None = None,
|
|
244
251
|
dataset_head: DatasetHeadContract | dict[str, Any] | None = None,
|
|
252
|
+
metadata_scope: str | None = None,
|
|
245
253
|
) -> "DatasetConfig":
|
|
246
254
|
if not isinstance(data, dict):
|
|
247
255
|
raise ValueError("Crawler config must be a JSON object")
|
|
248
256
|
|
|
249
257
|
_normalize_config_contract(data, "config")
|
|
258
|
+
raw_metadata_scope = metadata_scope
|
|
259
|
+
if raw_metadata_scope is None:
|
|
260
|
+
config_scope = data.get("metadata_scope")
|
|
261
|
+
if config_scope is not None:
|
|
262
|
+
raw_metadata_scope = config_scope
|
|
263
|
+
normalized_metadata_scope = (
|
|
264
|
+
validate_metadata_scope(raw_metadata_scope)
|
|
265
|
+
if raw_metadata_scope is not None
|
|
266
|
+
else None
|
|
267
|
+
)
|
|
250
268
|
|
|
251
269
|
source = _require_mapping(data.get("source", {}), "config.source")
|
|
252
270
|
raw_source_path = source.get("path", ".")
|
|
@@ -271,10 +289,19 @@ class DatasetConfig:
|
|
|
271
289
|
if embedded_head is not None:
|
|
272
290
|
dataset_head = embedded_head
|
|
273
291
|
else:
|
|
274
|
-
raw_head = read_metadata_json(
|
|
292
|
+
raw_head = read_metadata_json(
|
|
293
|
+
dataset_root_path,
|
|
294
|
+
head_file,
|
|
295
|
+
metadata_scope=normalized_metadata_scope,
|
|
296
|
+
)
|
|
275
297
|
if raw_head is None:
|
|
276
298
|
raise FileNotFoundError(
|
|
277
299
|
f"No {head_file} found at: {dataset_root_path}"
|
|
300
|
+
+ (
|
|
301
|
+
f" (metadata_scope={normalized_metadata_scope!r})"
|
|
302
|
+
if normalized_metadata_scope is not None
|
|
303
|
+
else ""
|
|
304
|
+
)
|
|
278
305
|
)
|
|
279
306
|
dataset_head = raw_head
|
|
280
307
|
|
|
@@ -375,30 +402,61 @@ class DatasetConfig:
|
|
|
375
402
|
path_filters=files_cfg.get("path_filters"),
|
|
376
403
|
prebuilt_index_file=prebuilt_index_file,
|
|
377
404
|
file_extensions=file_extensions,
|
|
405
|
+
metadata_scope=normalized_metadata_scope,
|
|
378
406
|
)
|
|
379
407
|
|
|
380
408
|
|
|
381
409
|
def load_dataset_config(
|
|
382
410
|
data: dict[str, Any],
|
|
383
411
|
workdir: str | Path | None = None,
|
|
412
|
+
*,
|
|
413
|
+
metadata_scope: str | None = None,
|
|
384
414
|
) -> DatasetConfig:
|
|
385
415
|
"""Load a DatasetConfig, resolving from ``ds-crawler.json`` when needed."""
|
|
386
416
|
if not isinstance(data, dict):
|
|
387
417
|
raise ValueError("Crawler config must be a JSON object")
|
|
388
418
|
|
|
419
|
+
raw_metadata_scope = metadata_scope
|
|
420
|
+
if raw_metadata_scope is None:
|
|
421
|
+
config_scope = data.get("metadata_scope")
|
|
422
|
+
if config_scope is not None:
|
|
423
|
+
raw_metadata_scope = config_scope
|
|
424
|
+
normalized_metadata_scope = (
|
|
425
|
+
validate_metadata_scope(raw_metadata_scope)
|
|
426
|
+
if raw_metadata_scope is not None
|
|
427
|
+
else None
|
|
428
|
+
)
|
|
429
|
+
|
|
389
430
|
if "indexing" in data or "source" in data:
|
|
390
|
-
return DatasetConfig.from_dict(
|
|
431
|
+
return DatasetConfig.from_dict(
|
|
432
|
+
data,
|
|
433
|
+
workdir=workdir,
|
|
434
|
+
metadata_scope=normalized_metadata_scope,
|
|
435
|
+
)
|
|
391
436
|
|
|
392
437
|
if "path" not in data:
|
|
393
438
|
raise ValueError("Crawler config shorthand requires a 'path' field")
|
|
394
439
|
|
|
395
440
|
ds_root = Path(_resolve_path(data["path"], base_path=workdir))
|
|
396
|
-
file_config = read_metadata_json(
|
|
441
|
+
file_config = read_metadata_json(
|
|
442
|
+
ds_root,
|
|
443
|
+
CONFIG_FILENAME,
|
|
444
|
+
metadata_scope=normalized_metadata_scope,
|
|
445
|
+
)
|
|
397
446
|
if file_config is None:
|
|
398
447
|
raise FileNotFoundError(
|
|
399
448
|
f"No {CONFIG_FILENAME} found at: {ds_root}"
|
|
449
|
+
+ (
|
|
450
|
+
f" (metadata_scope={normalized_metadata_scope!r})"
|
|
451
|
+
if normalized_metadata_scope is not None
|
|
452
|
+
else ""
|
|
453
|
+
)
|
|
400
454
|
)
|
|
401
|
-
return DatasetConfig.from_dict(
|
|
455
|
+
return DatasetConfig.from_dict(
|
|
456
|
+
file_config,
|
|
457
|
+
dataset_root=ds_root,
|
|
458
|
+
metadata_scope=normalized_metadata_scope,
|
|
459
|
+
)
|
|
402
460
|
|
|
403
461
|
|
|
404
462
|
@dataclass
|
|
@@ -412,6 +470,8 @@ class Config:
|
|
|
412
470
|
cls,
|
|
413
471
|
path: str | Path,
|
|
414
472
|
workdir: str | Path | None = None,
|
|
473
|
+
*,
|
|
474
|
+
metadata_scope: str | None = None,
|
|
415
475
|
) -> "Config":
|
|
416
476
|
path = Path(path)
|
|
417
477
|
if not path.exists():
|
|
@@ -423,13 +483,21 @@ class Config:
|
|
|
423
483
|
base_workdir = Path(workdir) if workdir is not None else path.parent
|
|
424
484
|
|
|
425
485
|
if "datasets" not in data:
|
|
426
|
-
dataset = load_dataset_config(
|
|
486
|
+
dataset = load_dataset_config(
|
|
487
|
+
data,
|
|
488
|
+
workdir=base_workdir,
|
|
489
|
+
metadata_scope=metadata_scope,
|
|
490
|
+
)
|
|
427
491
|
return cls(datasets=[dataset])
|
|
428
492
|
|
|
429
493
|
datasets: list[DatasetConfig] = []
|
|
430
494
|
for i, ds_data in enumerate(data["datasets"]):
|
|
431
495
|
try:
|
|
432
|
-
ds_config = load_dataset_config(
|
|
496
|
+
ds_config = load_dataset_config(
|
|
497
|
+
ds_data,
|
|
498
|
+
workdir=base_workdir,
|
|
499
|
+
metadata_scope=metadata_scope,
|
|
500
|
+
)
|
|
433
501
|
datasets.append(ds_config)
|
|
434
502
|
except FileNotFoundError:
|
|
435
503
|
raise
|