euler-loading 1.2.0__tar.gz → 2.0.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.
Files changed (46) hide show
  1. {euler_loading-1.2.0 → euler_loading-2.0.0}/PKG-INFO +2 -1
  2. {euler_loading-1.2.0 → euler_loading-2.0.0}/euler_loading/__init__.py +1 -0
  3. euler_loading-2.0.0/euler_loading/_dataset_contract.py +91 -0
  4. {euler_loading-1.2.0 → euler_loading-2.0.0}/euler_loading/_ds_crawler_utils.py +27 -19
  5. {euler_loading-1.2.0 → euler_loading-2.0.0}/euler_loading/_metadata.py +17 -7
  6. {euler_loading-1.2.0 → euler_loading-2.0.0}/euler_loading/_resolution.py +12 -2
  7. {euler_loading-1.2.0 → euler_loading-2.0.0}/euler_loading/_writing.py +8 -16
  8. {euler_loading-1.2.0 → euler_loading-2.0.0}/euler_loading/dataset.py +46 -9
  9. {euler_loading-1.2.0 → euler_loading-2.0.0}/package-lock.json +1 -1
  10. {euler_loading-1.2.0 → euler_loading-2.0.0}/pyproject.toml +2 -1
  11. {euler_loading-1.2.0 → euler_loading-2.0.0}/tests/test_dataset.py +68 -0
  12. {euler_loading-1.2.0 → euler_loading-2.0.0}/.github/workflows/workflow.yml +0 -0
  13. {euler_loading-1.2.0 → euler_loading-2.0.0}/.gitignore +0 -0
  14. {euler_loading-1.2.0 → euler_loading-2.0.0}/README.md +0 -0
  15. {euler_loading-1.2.0 → euler_loading-2.0.0}/euler_loading/indexing.py +0 -0
  16. {euler_loading-1.2.0 → euler_loading-2.0.0}/euler_loading/loaders/__init__.py +0 -0
  17. {euler_loading-1.2.0 → euler_loading-2.0.0}/euler_loading/loaders/_annotations.py +0 -0
  18. {euler_loading-1.2.0 → euler_loading-2.0.0}/euler_loading/loaders/_writer_utils.py +0 -0
  19. {euler_loading-1.2.0 → euler_loading-2.0.0}/euler_loading/loaders/contracts.py +0 -0
  20. {euler_loading-1.2.0 → euler_loading-2.0.0}/euler_loading/loaders/cpu/__init__.py +0 -0
  21. {euler_loading-1.2.0 → euler_loading-2.0.0}/euler_loading/loaders/cpu/generic.py +0 -0
  22. {euler_loading-1.2.0 → euler_loading-2.0.0}/euler_loading/loaders/cpu/generic_dense_depth.py +0 -0
  23. {euler_loading-1.2.0 → euler_loading-2.0.0}/euler_loading/loaders/cpu/real_drive_sim.py +0 -0
  24. {euler_loading-1.2.0 → euler_loading-2.0.0}/euler_loading/loaders/cpu/vkitti2.py +0 -0
  25. {euler_loading-1.2.0 → euler_loading-2.0.0}/euler_loading/loaders/generate/__init__.py +0 -0
  26. {euler_loading-1.2.0 → euler_loading-2.0.0}/euler_loading/loaders/generate/__main__.py +0 -0
  27. {euler_loading-1.2.0 → euler_loading-2.0.0}/euler_loading/loaders/generate/loaders.json +0 -0
  28. {euler_loading-1.2.0 → euler_loading-2.0.0}/euler_loading/loaders/generic.py +0 -0
  29. {euler_loading-1.2.0 → euler_loading-2.0.0}/euler_loading/loaders/gpu/__init__.py +0 -0
  30. {euler_loading-1.2.0 → euler_loading-2.0.0}/euler_loading/loaders/gpu/generic.py +0 -0
  31. {euler_loading-1.2.0 → euler_loading-2.0.0}/euler_loading/loaders/gpu/generic_dense_depth.py +0 -0
  32. {euler_loading-1.2.0 → euler_loading-2.0.0}/euler_loading/loaders/gpu/real_drive_sim.py +0 -0
  33. {euler_loading-1.2.0 → euler_loading-2.0.0}/euler_loading/loaders/gpu/vkitti2.py +0 -0
  34. {euler_loading-1.2.0 → euler_loading-2.0.0}/euler_loading/loaders/real_drive_sim.py +0 -0
  35. {euler_loading-1.2.0 → euler_loading-2.0.0}/euler_loading/loaders/vkitti2.py +0 -0
  36. {euler_loading-1.2.0 → euler_loading-2.0.0}/example.py +0 -0
  37. {euler_loading-1.2.0 → euler_loading-2.0.0}/sample_rds.py +0 -0
  38. {euler_loading-1.2.0 → euler_loading-2.0.0}/tests/__init__.py +0 -0
  39. {euler_loading-1.2.0 → euler_loading-2.0.0}/tests/conftest.py +0 -0
  40. {euler_loading-1.2.0 → euler_loading-2.0.0}/tests/example_rds_calib.json +0 -0
  41. {euler_loading-1.2.0 → euler_loading-2.0.0}/tests/test_indexing.py +0 -0
  42. {euler_loading-1.2.0 → euler_loading-2.0.0}/tests/test_loaders.py +0 -0
  43. {euler_loading-1.2.0 → euler_loading-2.0.0}/tests/test_real_dataset.py +0 -0
  44. {euler_loading-1.2.0 → euler_loading-2.0.0}/tests/test_writing.py +0 -0
  45. {euler_loading-1.2.0 → euler_loading-2.0.0}/vkitti_cpu_example_output.json +0 -0
  46. {euler_loading-1.2.0 → euler_loading-2.0.0}/vkitti_gpu_example_output.json +0 -0
@@ -1,9 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: euler-loading
3
- Version: 1.2.0
3
+ Version: 2.0.0
4
4
  Summary: Multi-modal PyTorch dataloader using ds-crawler indices
5
5
  Requires-Python: >=3.9
6
6
  Requires-Dist: ds-crawler
7
+ Requires-Dist: euler-dataset-contract
7
8
  Requires-Dist: numpy
8
9
  Requires-Dist: pillow
9
10
  Provides-Extra: dev
@@ -1,5 +1,6 @@
1
1
  """euler-loading: Multi-modal PyTorch dataloader using ds-crawler indices."""
2
2
 
3
+ from . import _dataset_contract # noqa: F401
3
4
  from ._resolution import resolve_loader_module, resolve_writer_module
4
5
  from ._writing import create_dataset_writer_from_index
5
6
  from .dataset import Modality, MultiModalDataset
@@ -0,0 +1,91 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from euler_dataset_contract import (
6
+ register_addon_validator,
7
+ validate_addon_version,
8
+ validate_slot,
9
+ validate_string_list,
10
+ validate_token,
11
+ )
12
+
13
+
14
+ _ALLOWED_USED_AS = {"condition", "input", "output", "target"}
15
+ _ALLOWED_KEYS = {
16
+ "version",
17
+ "used_as",
18
+ "slot",
19
+ "hierarchy_scope",
20
+ "applies_to",
21
+ "task",
22
+ "loader",
23
+ "function",
24
+ "writer_function",
25
+ }
26
+
27
+
28
+ def validate_euler_loading_addon(value: Any, context: str) -> None:
29
+ if not isinstance(value, dict):
30
+ raise ValueError(f"{context} must be an object")
31
+
32
+ unknown = sorted(set(value.keys()) - _ALLOWED_KEYS)
33
+ if unknown:
34
+ joined = ", ".join(unknown)
35
+ raise ValueError(f"Unknown {context} key(s): {joined}")
36
+
37
+ validate_addon_version(value.get("version"), f"{context}.version")
38
+
39
+ used_as = value.get("used_as")
40
+ if used_as is not None:
41
+ if not isinstance(used_as, str) or not used_as:
42
+ raise ValueError(f"{context}.used_as must be a non-empty string")
43
+ if used_as not in _ALLOWED_USED_AS:
44
+ allowed = ", ".join(sorted(_ALLOWED_USED_AS))
45
+ raise ValueError(
46
+ f"{context}.used_as must be one of {{{allowed}}}, got {used_as!r}"
47
+ )
48
+
49
+ slot = value.get("slot")
50
+ if slot is not None:
51
+ validate_slot(slot, f"{context}.slot")
52
+
53
+ task = value.get("task")
54
+ if task is not None:
55
+ validate_token(task, f"{context}.task")
56
+
57
+ loader = value.get("loader")
58
+ function = value.get("function")
59
+ if loader is not None:
60
+ validate_token(loader, f"{context}.loader")
61
+ if function is not None:
62
+ validate_token(function, f"{context}.function")
63
+ if (loader is None) != (function is None):
64
+ raise ValueError(
65
+ f"{context}.loader and {context}.function must either both be set "
66
+ "or both be omitted"
67
+ )
68
+
69
+ writer_function = value.get("writer_function")
70
+ if writer_function is not None:
71
+ validate_token(writer_function, f"{context}.writer_function")
72
+
73
+ hierarchy_scope = value.get("hierarchy_scope")
74
+ applies_to = value.get("applies_to")
75
+ if hierarchy_scope is not None or applies_to is not None:
76
+ if used_as != "condition":
77
+ raise ValueError(
78
+ f"{context}.hierarchy_scope and {context}.applies_to are only "
79
+ "allowed when used_as is 'condition'"
80
+ )
81
+ if hierarchy_scope is not None:
82
+ validate_token(hierarchy_scope, f"{context}.hierarchy_scope")
83
+ if applies_to is not None:
84
+ validate_string_list(
85
+ applies_to,
86
+ f"{context}.applies_to",
87
+ allow_wildcard=True,
88
+ )
89
+
90
+
91
+ register_addon_validator("euler_loading", validate_euler_loading_addon, overwrite=True)
@@ -5,6 +5,7 @@ from collections.abc import Mapping
5
5
  from pathlib import Path
6
6
  from typing import Any
7
7
 
8
+ from ds_crawler import get_dataset_contract
8
9
  from ds_crawler.zip_utils import read_metadata_json
9
10
 
10
11
  try:
@@ -13,19 +14,6 @@ except ImportError:
13
14
  _load_dataset_split = None
14
15
 
15
16
 
16
- DS_CRAWLER_STRUCTURAL_KEYS = frozenset({
17
- "name",
18
- "type",
19
- "id_regex",
20
- "id_regex_join_char",
21
- "euler_train",
22
- "dataset",
23
- "hierarchy_regex",
24
- "named_capture_group_value_separator",
25
- "sampled",
26
- })
27
-
28
-
29
17
  def as_non_empty_str(value: Any) -> str | None:
30
18
  if value is None:
31
19
  return None
@@ -61,16 +49,35 @@ def first_non_empty_list(*candidates: list[str] | None) -> list[str]:
61
49
 
62
50
 
63
51
  def extract_ds_crawler_properties(index_output: Mapping[str, Any]) -> dict[str, Any]:
64
- return {
65
- str(key): value
66
- for key, value in index_output.items()
67
- if key not in DS_CRAWLER_STRUCTURAL_KEYS
68
- }
52
+ return get_dataset_contract(dict(index_output)).to_properties_dict()
69
53
 
70
54
 
71
55
  _SPLIT_NAME_PATTERN = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_.-]*$")
72
56
 
73
57
 
58
+ def parse_path_with_split(path: str) -> tuple[str, str | None]:
59
+ """Extract an optional inline split suffix from a colon-separated path.
60
+
61
+ Supports paths like ``/data/ds.zip:train`` where the part after the
62
+ last colon is treated as the split name.
63
+
64
+ Returns a ``(path, split)`` tuple. When no valid split suffix is
65
+ found, *split* is ``None`` and *path* is returned unchanged.
66
+ """
67
+ colon_pos = path.rfind(":")
68
+ # No colon, or colon at position 1 is a Windows drive letter (e.g. "C:\\")
69
+ if colon_pos <= 1:
70
+ return path, None
71
+
72
+ candidate_split = path[colon_pos + 1:]
73
+ candidate_path = path[:colon_pos]
74
+
75
+ if not candidate_split or not _SPLIT_NAME_PATTERN.match(candidate_split):
76
+ return path, None
77
+
78
+ return candidate_path, candidate_split
79
+
80
+
74
81
  def validate_split_name(split_name: str) -> str:
75
82
  """Validate and normalize an inline split name."""
76
83
  if not isinstance(split_name, str):
@@ -141,5 +148,6 @@ def load_index_output(
141
148
  )
142
149
 
143
150
  result = dict(base_output)
144
- result["dataset"] = split_dataset
151
+ result["index"] = split_dataset
152
+ result.pop("dataset", None)
145
153
  return result
@@ -4,6 +4,8 @@ import re
4
4
  from collections.abc import Mapping
5
5
  from typing import TYPE_CHECKING, Any, Callable
6
6
 
7
+ from ds_crawler import get_dataset_contract
8
+
7
9
  from ._ds_crawler_utils import (
8
10
  as_non_empty_str,
9
11
  as_string_list,
@@ -44,6 +46,7 @@ def _build_runlog_entry(
44
46
  as_non_empty_str(
45
47
  _resolve_euler_loading_property(euler_loading_properties, "modality_type")
46
48
  ),
49
+ as_non_empty_str(descriptor.get("modality_key")),
47
50
  as_non_empty_str(descriptor.get("modality_type")),
48
51
  _infer_modality_type(name=name, path=modality.path),
49
52
  )
@@ -146,12 +149,19 @@ def _read_ds_crawler_descriptor(
146
149
 
147
150
  if isinstance(index_output, Mapping):
148
151
  properties.update(extract_ds_crawler_properties(index_output))
149
- index_type = as_non_empty_str(index_output.get("type"))
150
- if index_type is not None:
151
- descriptor["modality_type"] = index_type
152
- hierarchy_regex = as_non_empty_str(index_output.get("hierarchy_regex"))
153
- if hierarchy_regex is not None:
154
- descriptor["hierarchy_regex"] = hierarchy_regex
152
+ try:
153
+ contract = get_dataset_contract(dict(index_output))
154
+ except Exception:
155
+ contract = None
156
+ if contract is not None:
157
+ descriptor["modality_key"] = contract.modality_key
158
+ indexing = index_output.get("indexing")
159
+ if isinstance(indexing, Mapping):
160
+ hierarchy = indexing.get("hierarchy")
161
+ if isinstance(hierarchy, Mapping):
162
+ hierarchy_regex = as_non_empty_str(hierarchy.get("regex"))
163
+ if hierarchy_regex is not None:
164
+ descriptor["hierarchy_regex"] = hierarchy_regex
155
165
 
156
166
  try:
157
167
  cfg = load_dataset_config_fn({"path": path})
@@ -164,7 +174,7 @@ def _read_ds_crawler_descriptor(
164
174
 
165
175
  cfg_type = as_non_empty_str(getattr(cfg, "type", None))
166
176
  if cfg_type is not None:
167
- descriptor["modality_type"] = cfg_type
177
+ descriptor["modality_key"] = cfg_type
168
178
 
169
179
  cfg_hierarchy_regex = as_non_empty_str(getattr(cfg, "hierarchy_regex", None))
170
180
  if cfg_hierarchy_regex is not None:
@@ -6,6 +6,8 @@ from collections.abc import Mapping
6
6
  from types import ModuleType
7
7
  from typing import TYPE_CHECKING, Any, Callable
8
8
 
9
+ from ds_crawler import get_dataset_contract
10
+
9
11
  from ._ds_crawler_utils import as_non_empty_str
10
12
 
11
13
  if TYPE_CHECKING:
@@ -61,7 +63,11 @@ def _resolve_loader(
61
63
  if modality.loader is not None:
62
64
  return modality.loader
63
65
 
64
- euler_loading_meta = index.get("euler_loading")
66
+ try:
67
+ contract = get_dataset_contract(index)
68
+ euler_loading_meta = contract.get_addon("euler_loading")
69
+ except Exception:
70
+ euler_loading_meta = None
65
71
  if not isinstance(euler_loading_meta, Mapping):
66
72
  raise ValueError(
67
73
  f"Modality {modality_name!r}: no explicit loader provided and the "
@@ -126,7 +132,11 @@ def _resolve_writer(
126
132
  if modality.writer is not None:
127
133
  return modality.writer
128
134
 
129
- euler_loading_meta = index.get("euler_loading")
135
+ try:
136
+ contract = get_dataset_contract(index)
137
+ euler_loading_meta = contract.get_addon("euler_loading")
138
+ except Exception:
139
+ euler_loading_meta = None
130
140
  if not isinstance(euler_loading_meta, Mapping):
131
141
  return None
132
142
 
@@ -7,11 +7,10 @@ from collections.abc import Mapping
7
7
  from pathlib import Path
8
8
  from typing import Any, Callable
9
9
 
10
- from ds_crawler import DatasetWriter, ZipDatasetWriter
10
+ from ds_crawler import DatasetWriter, ZipDatasetWriter, get_dataset_contract
11
11
 
12
12
  from ._ds_crawler_utils import (
13
13
  as_non_empty_str,
14
- extract_ds_crawler_properties,
15
14
  first_non_empty,
16
15
  )
17
16
  from .loaders._writer_utils import supports_stream_target
@@ -30,27 +29,20 @@ def create_dataset_writer_from_index(
30
29
  zip: bool = False,
31
30
  ) -> DatasetWriter | ZipDatasetWriter:
32
31
  """Create a ds-crawler writer that mirrors an existing index's metadata."""
33
- name = as_non_empty_str(index_output.get("name")) or "dataset"
34
- type_name = as_non_empty_str(index_output.get("type")) or "other"
35
- euler_train = index_output.get("euler_train")
36
- if not isinstance(euler_train, Mapping):
37
- raise ValueError(
38
- "index_output must contain an 'euler_train' mapping to build a writer."
39
- )
32
+ contract = get_dataset_contract(dict(index_output))
40
33
 
34
+ indexing = index_output.get("indexing")
35
+ hierarchy = indexing.get("hierarchy") if isinstance(indexing, Mapping) else None
36
+ id_cfg = indexing.get("id") if isinstance(indexing, Mapping) else None
41
37
  separator = first_non_empty(
42
- as_non_empty_str(index_output.get("named_capture_group_value_separator")),
43
- as_non_empty_str(index_output.get("id_regex_join_char")),
38
+ as_non_empty_str(hierarchy.get("separator")) if isinstance(hierarchy, Mapping) else None,
39
+ as_non_empty_str(id_cfg.get("join_char")) if isinstance(id_cfg, Mapping) else None,
44
40
  )
45
- properties = extract_ds_crawler_properties(index_output)
46
41
  writer_cls = ZipDatasetWriter if zip else DatasetWriter
47
42
  return writer_cls(
48
43
  root,
49
- name=name,
50
- type=type_name,
51
- euler_train=dict(euler_train),
44
+ head=contract.to_mapping(),
52
45
  separator=separator,
53
- **properties,
54
46
  )
55
47
 
56
48
 
@@ -18,12 +18,13 @@ except ImportError:
18
18
  from ds_crawler import (
19
19
  DatasetWriter,
20
20
  ZipDatasetWriter,
21
+ get_dataset_contract,
21
22
  index_dataset_from_path,
22
23
  load_dataset_config,
23
24
  )
24
25
  from ds_crawler.zip_utils import get_zip_root_prefix, is_zip_path
25
26
 
26
- from ._ds_crawler_utils import load_index_output
27
+ from ._ds_crawler_utils import load_index_output, parse_path_with_split
27
28
  from ._metadata import _build_runlog_entry, _get_ds_crawler_descriptor
28
29
  from ._resolution import (
29
30
  _resolve_loader,
@@ -66,7 +67,9 @@ class Modality:
66
67
  Attributes:
67
68
  path: Absolute path to the dataset root directory for this modality.
68
69
  Must contain a ``ds-crawler.config`` file (or a cached
69
- ``output.json`` from a prior indexing run).
70
+ ``output.json`` from a prior indexing run). A colon-separated
71
+ split suffix is also accepted (e.g. ``/data/ds.zip:train``);
72
+ the suffix is extracted and used as the ``split`` parameter.
70
73
  origin_path: Optional original path string before any copying or symlinking for i.e. slurm staging.
71
74
  This is not used by euler-loading itself but can be useful for experiment
72
75
  logging to retain references to the original dataset location.
@@ -113,6 +116,18 @@ class Modality:
113
116
  split: str | None = None
114
117
  metadata: dict[str, Any] = field(default_factory=dict)
115
118
 
119
+ def __post_init__(self) -> None:
120
+ parsed_path, path_split = parse_path_with_split(self.path)
121
+ if path_split is not None:
122
+ if self.split is not None:
123
+ raise ValueError(
124
+ f"Modality path {self.path!r} contains an inline split "
125
+ f"({path_split!r}) but an explicit split={self.split!r} "
126
+ f"was also provided. Use one or the other, not both."
127
+ )
128
+ object.__setattr__(self, "path", parsed_path)
129
+ object.__setattr__(self, "split", path_split)
130
+
116
131
 
117
132
  class MultiModalDataset(_BaseDataset):
118
133
  """PyTorch Dataset that loads synchronized multi-modal samples.
@@ -305,7 +320,7 @@ class MultiModalDataset(_BaseDataset):
305
320
  self._resolved_writers[name] = _resolve_writer(
306
321
  modality_name=name, modality=modality, index=index,
307
322
  )
308
- records = collect_files(index["dataset"])
323
+ records = collect_files(index["index"])
309
324
  lookup = {
310
325
  "/".join(rec.hierarchy_path + (rec.file_entry["id"],)): rec
311
326
  for rec in records
@@ -337,7 +352,7 @@ class MultiModalDataset(_BaseDataset):
337
352
  self._resolved_writers[name] = _resolve_writer(
338
353
  modality_name=name, modality=modality, index=index,
339
354
  )
340
- files_by_level = collect_hierarchical_files(index["dataset"])
355
+ files_by_level = collect_hierarchical_files(index["index"])
341
356
  self._hierarchical_lookups[name] = files_by_level
342
357
  total_files = sum(len(f) for f in files_by_level.values())
343
358
  logger.info(
@@ -393,7 +408,11 @@ class MultiModalDataset(_BaseDataset):
393
408
 
394
409
  def get_modality_metadata(self, modality_name: str) -> dict[str, Any]:
395
410
  """Return the metadata dict for a given modality name."""
396
- return self._index_outputs.get(modality_name, {}).get("meta", {})
411
+ index = self._index_outputs.get(modality_name, {})
412
+ try:
413
+ return get_dataset_contract(index).meta or {}
414
+ except Exception:
415
+ return {}
397
416
 
398
417
  def get_modality_index(self, modality_name: str) -> dict[str, Any]:
399
418
  """Return the cached ds-crawler index for a modality."""
@@ -491,7 +510,12 @@ class MultiModalDataset(_BaseDataset):
491
510
  output_root=output_root, modality_name=modality_name
492
511
  )
493
512
  record = self._lookups[modality_name][sample_id]
494
- modality_meta = self._index_outputs[modality_name].get("meta")
513
+ try:
514
+ modality_meta = get_dataset_contract(
515
+ self._index_outputs[modality_name]
516
+ ).meta
517
+ except Exception:
518
+ modality_meta = None
495
519
  basename = Path(record.file_entry["path"]).name
496
520
  relative_path = record.file_entry["path"]
497
521
  full_id = _build_writer_full_id(
@@ -531,7 +555,10 @@ class MultiModalDataset(_BaseDataset):
531
555
  first_modality: str | None = None
532
556
 
533
557
  for modality_name, index in self._index_outputs.items():
534
- name = index.get("name")
558
+ try:
559
+ name = get_dataset_contract(index).name
560
+ except Exception:
561
+ name = None
535
562
  if name is None:
536
563
  continue
537
564
  if first_name is None:
@@ -590,7 +617,12 @@ class MultiModalDataset(_BaseDataset):
590
617
 
591
618
  for name, modality in self._modalities.items():
592
619
  record = self._lookups[name][sample_id]
593
- modality_meta = self._index_outputs[name].get("meta")
620
+ try:
621
+ modality_meta = get_dataset_contract(
622
+ self._index_outputs[name]
623
+ ).meta
624
+ except Exception:
625
+ modality_meta = None
594
626
 
595
627
  if name in self._zip_modalities:
596
628
  file_or_path = self._open_from_zip(
@@ -611,7 +643,12 @@ class MultiModalDataset(_BaseDataset):
611
643
  for name, modality in self._hierarchical_modalities.items():
612
644
  files_by_level = self._hierarchical_lookups[name]
613
645
  matched = match_hierarchical_files(hierarchy_path, files_by_level)
614
- modality_meta = self._hierarchical_index_outputs[name].get("meta")
646
+ try:
647
+ modality_meta = get_dataset_contract(
648
+ self._hierarchical_index_outputs[name]
649
+ ).meta
650
+ except Exception:
651
+ modality_meta = None
615
652
  loaded: dict[str, Any] = {}
616
653
  for entry in matched:
617
654
  cache_key = f"{modality.path}/{entry['path']}"
@@ -1,5 +1,5 @@
1
1
  {
2
- "name": "load-inline",
2
+ "name": "path-with-colon",
3
3
  "lockfileVersion": 3,
4
4
  "requires": true,
5
5
  "packages": {}
@@ -1,12 +1,13 @@
1
1
  [project]
2
2
  name = "euler-loading"
3
- version = "1.2.0"
3
+ version = "2.0.0"
4
4
  description = "Multi-modal PyTorch dataloader using ds-crawler indices"
5
5
  requires-python = ">=3.9"
6
6
  dependencies = [
7
7
  "numpy",
8
8
  "Pillow",
9
9
  "ds-crawler",
10
+ "euler-dataset-contract",
10
11
  ]
11
12
 
12
13
  [project.optional-dependencies]
@@ -301,6 +301,74 @@ class TestInlineSplitLoading:
301
301
  assert buf.read() == b"fake-png-002"
302
302
 
303
303
 
304
+ class TestPathWithColonSplit:
305
+ """Colon-separated path:split syntax."""
306
+
307
+ def test_split_extracted_from_path(self):
308
+ mod = Modality("/data/ds:train")
309
+ assert mod.path == "/data/ds"
310
+ assert mod.split == "train"
311
+
312
+ def test_split_extracted_from_zip_path(self):
313
+ mod = Modality("/data/ds.zip:val")
314
+ assert mod.path == "/data/ds.zip"
315
+ assert mod.split == "val"
316
+
317
+ def test_no_colon_leaves_split_none(self):
318
+ mod = Modality("/data/ds")
319
+ assert mod.path == "/data/ds"
320
+ assert mod.split is None
321
+
322
+ def test_explicit_split_without_colon_still_works(self):
323
+ mod = Modality("/data/ds", split="train")
324
+ assert mod.path == "/data/ds"
325
+ assert mod.split == "train"
326
+
327
+ def test_colon_and_explicit_split_raises(self):
328
+ with pytest.raises(ValueError, match="inline split"):
329
+ Modality("/data/ds:train", split="val")
330
+
331
+ def test_windows_drive_letter_not_treated_as_split(self):
332
+ mod = Modality("C:\\data\\ds")
333
+ assert mod.path == "C:\\data\\ds"
334
+ assert mod.split is None
335
+
336
+ def test_invalid_split_suffix_left_in_path(self):
337
+ # A colon followed by an invalid split name is left as-is
338
+ mod = Modality("/data/ds:bad/name")
339
+ assert mod.path == "/data/ds:bad/name"
340
+ assert mod.split is None
341
+
342
+ def test_colon_path_integration_with_dataset(self, tmp_path):
343
+ root = tmp_path / "rgb"
344
+ root.mkdir()
345
+ rgb_index = _flat_index("rgb", ["f001", "f002"])
346
+ rgb_split = _flat_index("rgb", ["f002"])["dataset"]
347
+ _write_inline_split(root, "train", rgb_split)
348
+
349
+ def mock_index(path, **kw):
350
+ return rgb_index
351
+
352
+ with patch(
353
+ "euler_loading.dataset.index_dataset_from_path",
354
+ side_effect=mock_index,
355
+ ):
356
+ ds = MultiModalDataset(
357
+ modalities={
358
+ "rgb": Modality(
359
+ f"{root}:train", loader=dummy_loader
360
+ ),
361
+ },
362
+ )
363
+
364
+ assert len(ds) == 1
365
+ assert ds[0]["id"] == "f002"
366
+ assert ds.modality_paths()["rgb"] == {
367
+ "path": str(root),
368
+ "origin_path": None,
369
+ "split": "train",
370
+ }
371
+
304
372
 
305
373
  class TestTransforms:
306
374
  """Transform application and ordering."""
File without changes
File without changes
File without changes