polystore 0.2.7__tar.gz → 0.2.8__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 (96) hide show
  1. {polystore-0.2.7/src/polystore.egg-info → polystore-0.2.8}/PKG-INFO +1 -1
  2. {polystore-0.2.7 → polystore-0.2.8}/pyproject.toml +1 -1
  3. {polystore-0.2.7 → polystore-0.2.8}/src/polystore/__init__.py +2 -0
  4. {polystore-0.2.7 → polystore-0.2.8}/src/polystore/omero_address.py +221 -25
  5. {polystore-0.2.7 → polystore-0.2.8/src/polystore.egg-info}/PKG-INFO +1 -1
  6. {polystore-0.2.7 → polystore-0.2.8}/tests/test_omero_address.py +37 -0
  7. {polystore-0.2.7 → polystore-0.2.8}/LICENSE +0 -0
  8. {polystore-0.2.7 → polystore-0.2.8}/README.md +0 -0
  9. {polystore-0.2.7 → polystore-0.2.8}/setup.cfg +0 -0
  10. {polystore-0.2.7 → polystore-0.2.8}/src/polystore/array_payload.py +0 -0
  11. {polystore-0.2.7 → polystore-0.2.8}/src/polystore/async_init.py +0 -0
  12. {polystore-0.2.7 → polystore-0.2.8}/src/polystore/atomic.py +0 -0
  13. {polystore-0.2.7 → polystore-0.2.8}/src/polystore/backend_registry.py +0 -0
  14. {polystore-0.2.7 → polystore-0.2.8}/src/polystore/base.py +0 -0
  15. {polystore-0.2.7 → polystore-0.2.8}/src/polystore/bioformats_java.py +0 -0
  16. {polystore-0.2.7 → polystore-0.2.8}/src/polystore/bioformats_storage.py +0 -0
  17. {polystore-0.2.7 → polystore-0.2.8}/src/polystore/config.py +0 -0
  18. {polystore-0.2.7 → polystore-0.2.8}/src/polystore/constants.py +0 -0
  19. {polystore-0.2.7 → polystore-0.2.8}/src/polystore/disk.py +0 -0
  20. {polystore-0.2.7 → polystore-0.2.8}/src/polystore/exceptions.py +0 -0
  21. {polystore-0.2.7 → polystore-0.2.8}/src/polystore/fiji_stream.py +0 -0
  22. {polystore-0.2.7 → polystore-0.2.8}/src/polystore/filemanager.py +0 -0
  23. {polystore-0.2.7 → polystore-0.2.8}/src/polystore/formats.py +0 -0
  24. {polystore-0.2.7 → polystore-0.2.8}/src/polystore/imagej_distribution.py +0 -0
  25. {polystore-0.2.7 → polystore-0.2.8}/src/polystore/imagej_runtime.py +0 -0
  26. {polystore-0.2.7 → polystore-0.2.8}/src/polystore/lazy_imports.py +0 -0
  27. {polystore-0.2.7 → polystore-0.2.8}/src/polystore/memory.py +0 -0
  28. {polystore-0.2.7 → polystore-0.2.8}/src/polystore/metadata_migration.py +0 -0
  29. {polystore-0.2.7 → polystore-0.2.8}/src/polystore/metadata_writer.py +0 -0
  30. {polystore-0.2.7 → polystore-0.2.8}/src/polystore/napari_stream.py +0 -0
  31. {polystore-0.2.7 → polystore-0.2.8}/src/polystore/ome_zarr_storage.py +0 -0
  32. {polystore-0.2.7 → polystore-0.2.8}/src/polystore/omero_local.py +0 -0
  33. {polystore-0.2.7 → polystore-0.2.8}/src/polystore/omero_tables.py +0 -0
  34. {polystore-0.2.7 → polystore-0.2.8}/src/polystore/omero_text.py +0 -0
  35. {polystore-0.2.7 → polystore-0.2.8}/src/polystore/roi.py +0 -0
  36. {polystore-0.2.7 → polystore-0.2.8}/src/polystore/roi_converters.py +0 -0
  37. {polystore-0.2.7 → polystore-0.2.8}/src/polystore/streaming/__init__.py +0 -0
  38. {polystore-0.2.7 → polystore-0.2.8}/src/polystore/streaming/_streaming_backend.py +0 -0
  39. {polystore-0.2.7 → polystore-0.2.8}/src/polystore/streaming/base.py +0 -0
  40. {polystore-0.2.7 → polystore-0.2.8}/src/polystore/streaming/handlers/__init__.py +0 -0
  41. {polystore-0.2.7 → polystore-0.2.8}/src/polystore/streaming/handlers/fiji_images.py +0 -0
  42. {polystore-0.2.7 → polystore-0.2.8}/src/polystore/streaming/handlers/fiji_rois.py +0 -0
  43. {polystore-0.2.7 → polystore-0.2.8}/src/polystore/streaming/identity.py +0 -0
  44. {polystore-0.2.7 → polystore-0.2.8}/src/polystore/streaming/receivers/__init__.py +0 -0
  45. {polystore-0.2.7 → polystore-0.2.8}/src/polystore/streaming/receivers/batching/__init__.py +0 -0
  46. {polystore-0.2.7 → polystore-0.2.8}/src/polystore/streaming/receivers/core/__init__.py +0 -0
  47. {polystore-0.2.7 → polystore-0.2.8}/src/polystore/streaming/receivers/core/batch_engine.py +0 -0
  48. {polystore-0.2.7 → polystore-0.2.8}/src/polystore/streaming/receivers/core/contracts.py +0 -0
  49. {polystore-0.2.7 → polystore-0.2.8}/src/polystore/streaming/receivers/core/window_projection.py +0 -0
  50. {polystore-0.2.7 → polystore-0.2.8}/src/polystore/streaming/receivers/fiji/__init__.py +0 -0
  51. {polystore-0.2.7 → polystore-0.2.8}/src/polystore/streaming/receivers/fiji/fiji_batch_processor.py +0 -0
  52. {polystore-0.2.7 → polystore-0.2.8}/src/polystore/streaming/receivers/napari/__init__.py +0 -0
  53. {polystore-0.2.7 → polystore-0.2.8}/src/polystore/streaming/receivers/napari/layer_key.py +0 -0
  54. {polystore-0.2.7 → polystore-0.2.8}/src/polystore/streaming/receivers/napari/napari_batch_processor.py +0 -0
  55. {polystore-0.2.7 → polystore-0.2.8}/src/polystore/streaming/viewer_transport.py +0 -0
  56. {polystore-0.2.7 → polystore-0.2.8}/src/polystore/streaming_constants.py +0 -0
  57. {polystore-0.2.7 → polystore-0.2.8}/src/polystore/utils.py +0 -0
  58. {polystore-0.2.7 → polystore-0.2.8}/src/polystore/virtual_workspace.py +0 -0
  59. {polystore-0.2.7 → polystore-0.2.8}/src/polystore/zarr.py +0 -0
  60. {polystore-0.2.7 → polystore-0.2.8}/src/polystore/zarr_batch.py +0 -0
  61. {polystore-0.2.7 → polystore-0.2.8}/src/polystore/zmq_config.py +0 -0
  62. {polystore-0.2.7 → polystore-0.2.8}/src/polystore.egg-info/SOURCES.txt +0 -0
  63. {polystore-0.2.7 → polystore-0.2.8}/src/polystore.egg-info/dependency_links.txt +0 -0
  64. {polystore-0.2.7 → polystore-0.2.8}/src/polystore.egg-info/requires.txt +0 -0
  65. {polystore-0.2.7 → polystore-0.2.8}/src/polystore.egg-info/top_level.txt +0 -0
  66. {polystore-0.2.7 → polystore-0.2.8}/tests/test_array_payload.py +0 -0
  67. {polystore-0.2.7 → polystore-0.2.8}/tests/test_atomic_lock.py +0 -0
  68. {polystore-0.2.7 → polystore-0.2.8}/tests/test_backend_registry.py +0 -0
  69. {polystore-0.2.7 → polystore-0.2.8}/tests/test_basic.py +0 -0
  70. {polystore-0.2.7 → polystore-0.2.8}/tests/test_bioformats_java.py +0 -0
  71. {polystore-0.2.7 → polystore-0.2.8}/tests/test_bioformats_storage.py +0 -0
  72. {polystore-0.2.7 → polystore-0.2.8}/tests/test_disk_backend.py +0 -0
  73. {polystore-0.2.7 → polystore-0.2.8}/tests/test_disk_coverage.py +0 -0
  74. {polystore-0.2.7 → polystore-0.2.8}/tests/test_disk_more.py +0 -0
  75. {polystore-0.2.7 → polystore-0.2.8}/tests/test_filemanager_backends.py +0 -0
  76. {polystore-0.2.7 → polystore-0.2.8}/tests/test_filemanager_extended.py +0 -0
  77. {polystore-0.2.7 → polystore-0.2.8}/tests/test_imagej_distribution.py +0 -0
  78. {polystore-0.2.7 → polystore-0.2.8}/tests/test_imagej_runtime.py +0 -0
  79. {polystore-0.2.7 → polystore-0.2.8}/tests/test_lazy_package_exports.py +0 -0
  80. {polystore-0.2.7 → polystore-0.2.8}/tests/test_memory_backend.py +0 -0
  81. {polystore-0.2.7 → polystore-0.2.8}/tests/test_memory_coverage.py +0 -0
  82. {polystore-0.2.7 → polystore-0.2.8}/tests/test_memory_type_authority.py +0 -0
  83. {polystore-0.2.7 → polystore-0.2.8}/tests/test_napari_batch_processor.py +0 -0
  84. {polystore-0.2.7 → polystore-0.2.8}/tests/test_ome_zarr_storage.py +0 -0
  85. {polystore-0.2.7 → polystore-0.2.8}/tests/test_omero_image_plane_batch.py +0 -0
  86. {polystore-0.2.7 → polystore-0.2.8}/tests/test_omero_tables.py +0 -0
  87. {polystore-0.2.7 → polystore-0.2.8}/tests/test_omero_text.py +0 -0
  88. {polystore-0.2.7 → polystore-0.2.8}/tests/test_png_format.py +0 -0
  89. {polystore-0.2.7 → polystore-0.2.8}/tests/test_roi.py +0 -0
  90. {polystore-0.2.7 → polystore-0.2.8}/tests/test_streaming_identity.py +0 -0
  91. {polystore-0.2.7 → polystore-0.2.8}/tests/test_streaming_metadata.py +0 -0
  92. {polystore-0.2.7 → polystore-0.2.8}/tests/test_streaming_receiver_core.py +0 -0
  93. {polystore-0.2.7 → polystore-0.2.8}/tests/test_streaming_shared_memory.py +0 -0
  94. {polystore-0.2.7 → polystore-0.2.8}/tests/test_viewer_transport.py +0 -0
  95. {polystore-0.2.7 → polystore-0.2.8}/tests/test_zarr_backend.py +0 -0
  96. {polystore-0.2.7 → polystore-0.2.8}/tests/test_zarr_batch.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: polystore
3
- Version: 0.2.7
3
+ Version: 0.2.8
4
4
  Summary: Framework-agnostic multi-backend storage abstraction for ML and scientific computing
5
5
  Author-email: Tristan Simas <tristan.simas@mail.mcgill.ca>
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "polystore"
7
- version = "0.2.7"
7
+ version = "0.2.8"
8
8
  description = "Framework-agnostic multi-backend storage abstraction for ML and scientific computing"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -71,6 +71,7 @@ from .metadata_writer import (
71
71
  from .omero_address import (
72
72
  OMEROAddressComponent,
73
73
  OMEROPlaneAddress,
74
+ OMEROPlaneFilenameTemplate,
74
75
  OMEROWellAddress,
75
76
  )
76
77
  from .roi import (
@@ -161,6 +162,7 @@ __all__ = [
161
162
  "resolve_subdirectory_path",
162
163
  "OMEROAddressComponent",
163
164
  "OMEROPlaneAddress",
165
+ "OMEROPlaneFilenameTemplate",
164
166
  "OMEROWellAddress",
165
167
  "detect_legacy_format",
166
168
  "migrate_legacy_metadata",
@@ -14,6 +14,39 @@ from typing import cast
14
14
  OMEROAddressValue = str | int
15
15
 
16
16
 
17
+ @dataclass(frozen=True, slots=True)
18
+ class OMEROAddressTemplateField:
19
+ """One symbolic field in an OMERO plane filename template."""
20
+
21
+ name: str
22
+
23
+ @classmethod
24
+ def capture_pattern(cls) -> str:
25
+ """Return the filename grammar for a symbolic template field."""
26
+
27
+ return r"\{[A-Za-z_][A-Za-z0-9_]*\}"
28
+
29
+ @classmethod
30
+ def from_value(cls, value: object) -> OMEROAddressTemplateField | None:
31
+ """Parse a serialized template field at the filename boundary."""
32
+
33
+ if not isinstance(value, str) or re.fullmatch(cls.capture_pattern(), value) is None:
34
+ return None
35
+ return cls(value[1:-1])
36
+
37
+ def __post_init__(self) -> None:
38
+ if re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", self.name) is None:
39
+ raise ValueError(f"Invalid OMERO address template field: {self.name!r}")
40
+
41
+ def render(self) -> str:
42
+ """Render the field at the filename-template boundary."""
43
+
44
+ return f"{{{self.name}}}"
45
+
46
+
47
+ OMEROAddressTemplateValue = OMEROAddressValue | OMEROAddressTemplateField
48
+
49
+
17
50
  class OMEROAddressComponentBehaviorABC(ABC):
18
51
  """Leaf behavior owned by one OMERO address component declaration."""
19
52
 
@@ -34,6 +67,28 @@ class OMEROAddressComponentBehaviorABC(ABC):
34
67
  def filename_fragment(self, component_name: str, value: object) -> str:
35
68
  """Render one normalized component value."""
36
69
 
70
+ @abstractmethod
71
+ def filename_template_pattern(self, component_name: str) -> str:
72
+ """Return this component's exact-or-symbolic template grammar."""
73
+
74
+ @abstractmethod
75
+ def filename_template_fragment(self, component_name: str, value: object) -> str:
76
+ """Render one exact value or nominal symbolic template field."""
77
+
78
+ def normalize_template(
79
+ self,
80
+ component_name: str,
81
+ value: object,
82
+ ) -> OMEROAddressTemplateValue:
83
+ """Normalize one exact value or serialized template field."""
84
+
85
+ if isinstance(value, OMEROAddressTemplateField):
86
+ return value
87
+ template_field = OMEROAddressTemplateField.from_value(value)
88
+ if template_field is not None:
89
+ return template_field
90
+ return self.normalize(component_name, value)
91
+
37
92
  def zero_based(self, component_name: str, value: object) -> int:
38
93
  """Project a plane coordinate onto a zero-based OMERO array index."""
39
94
 
@@ -60,6 +115,18 @@ class OMEROWellComponentBehavior(OMEROAddressComponentBehaviorABC):
60
115
  def filename_fragment(self, component_name: str, value: object) -> str:
61
116
  return self.normalize(component_name, value)
62
117
 
118
+ def filename_template_pattern(self, component_name: str) -> str:
119
+ return (
120
+ f"(?P<{component_name}>(?:[A-Za-z]+\\d+|"
121
+ f"{OMEROAddressTemplateField.capture_pattern()}))"
122
+ )
123
+
124
+ def filename_template_fragment(self, component_name: str, value: object) -> str:
125
+ normalized = self.normalize_template(component_name, value)
126
+ if isinstance(normalized, OMEROAddressTemplateField):
127
+ return normalized.render()
128
+ return cast(str, normalized)
129
+
63
130
 
64
131
  @dataclass(frozen=True, slots=True)
65
132
  class OMEROPlaneCoordinateBehavior(OMEROAddressComponentBehaviorABC):
@@ -92,6 +159,18 @@ class OMEROPlaneCoordinateBehavior(OMEROAddressComponentBehaviorABC):
92
159
  normalized = self.normalize(component_name, value)
93
160
  return f"_{self.prefix}{normalized:0{self.padding}d}"
94
161
 
162
+ def filename_template_pattern(self, component_name: str) -> str:
163
+ return (
164
+ f"_{self.prefix}(?P<{component_name}>(?:\\d+|"
165
+ f"{OMEROAddressTemplateField.capture_pattern()}))"
166
+ )
167
+
168
+ def filename_template_fragment(self, component_name: str, value: object) -> str:
169
+ normalized = self.normalize_template(component_name, value)
170
+ if isinstance(normalized, OMEROAddressTemplateField):
171
+ return f"_{self.prefix}{normalized.render()}"
172
+ return f"_{self.prefix}{normalized:0{self.padding}d}"
173
+
95
174
  def zero_based(self, component_name: str, value: object) -> int:
96
175
  return self.normalize(component_name, value) - 1
97
176
 
@@ -122,6 +201,15 @@ class OMEROAddressComponent(Enum):
122
201
  def filename_fragment(self, value: object) -> str:
123
202
  return self.value.filename_fragment(self.component_name, value)
124
203
 
204
+ def normalize_template(self, value: object) -> OMEROAddressTemplateValue:
205
+ return self.value.normalize_template(self.component_name, value)
206
+
207
+ def filename_template_pattern(self) -> str:
208
+ return self.value.filename_template_pattern(self.component_name)
209
+
210
+ def filename_template_fragment(self, value: object) -> str:
211
+ return self.value.filename_template_fragment(self.component_name, value)
212
+
125
213
  def captured_value(self, match: re.Match[str]) -> OMEROAddressValue:
126
214
  return self.normalize(match.group(self.component_name))
127
215
 
@@ -229,6 +317,41 @@ class OMEROWellAddress:
229
317
  return f"{self.row_label}{self.column_index + 1:02d}"
230
318
 
231
319
 
320
+ def _require_complete_component_values(
321
+ component_values: Iterable[tuple[OMEROAddressComponent, object]],
322
+ ) -> dict[OMEROAddressComponent, object]:
323
+ """Validate and index one complete OMERO component projection."""
324
+
325
+ supplied_values = tuple(component_values)
326
+ if any(not isinstance(component, OMEROAddressComponent) for component, _ in supplied_values):
327
+ raise TypeError("OMERO addresses require OMEROAddressComponent members")
328
+ values_by_component = dict(supplied_values)
329
+ if len(values_by_component) != len(supplied_values):
330
+ raise ValueError("Each OMERO address component may be bound only once")
331
+ missing = tuple(
332
+ component for component in OMEROAddressComponent if component not in values_by_component
333
+ )
334
+ if missing:
335
+ raise ValueError(
336
+ "OMERO plane address must bind every declared component: "
337
+ + ", ".join(f"missing {component.component_name}" for component in missing)
338
+ )
339
+ return values_by_component
340
+
341
+
342
+ def _normalize_extension(extension: str) -> str:
343
+ """Normalize one OMERO plane filename extension."""
344
+
345
+ normalized_extension = str(extension).strip()
346
+ if not normalized_extension or any(
347
+ separator in normalized_extension for separator in ("/", "\\")
348
+ ):
349
+ raise ValueError(f"Invalid OMERO plane extension: {extension!r}")
350
+ if not normalized_extension.startswith("."):
351
+ normalized_extension = f".{normalized_extension}"
352
+ return normalized_extension
353
+
354
+
232
355
  @dataclass(frozen=True, slots=True, init=False)
233
356
  class OMEROPlaneAddress:
234
357
  """Canonical virtual filename identity for one OMERO image plane."""
@@ -242,36 +365,13 @@ class OMEROPlaneAddress:
242
365
  *,
243
366
  extension: str = ".tif",
244
367
  ) -> None:
245
- supplied_values = tuple(component_values)
246
- if any(
247
- not isinstance(component, OMEROAddressComponent) for component, _ in supplied_values
248
- ):
249
- raise TypeError("OMERO addresses require OMEROAddressComponent members")
250
- values_by_component = dict(supplied_values)
251
- if len(values_by_component) != len(supplied_values):
252
- raise ValueError("Each OMERO address component may be bound only once")
253
- missing = tuple(
254
- component for component in OMEROAddressComponent if component not in values_by_component
255
- )
256
- if missing:
257
- raise ValueError(
258
- "OMERO plane address must bind every declared component: "
259
- + ", ".join(f"missing {component.component_name}" for component in missing)
260
- )
261
-
368
+ values_by_component = _require_complete_component_values(component_values)
262
369
  normalized_values = tuple(
263
370
  (component, component.normalize(values_by_component[component]))
264
371
  for component in OMEROAddressComponent
265
372
  )
266
- normalized_extension = str(extension).strip()
267
- if not normalized_extension or any(
268
- separator in normalized_extension for separator in ("/", "\\")
269
- ):
270
- raise ValueError(f"Invalid OMERO plane extension: {extension!r}")
271
- if not normalized_extension.startswith("."):
272
- normalized_extension = f".{normalized_extension}"
273
373
  object.__setattr__(self, "_component_values", normalized_values)
274
- object.__setattr__(self, "extension", normalized_extension)
374
+ object.__setattr__(self, "extension", _normalize_extension(extension))
275
375
 
276
376
  @classmethod
277
377
  def from_component_values(
@@ -460,3 +560,99 @@ class OMEROPlaneAddress:
460
560
  )
461
561
  + self.extension
462
562
  )
563
+
564
+
565
+ @dataclass(frozen=True, slots=True, init=False)
566
+ class OMEROPlaneFilenameTemplate:
567
+ """Exact or symbolic OMERO plane identity for filename pattern systems."""
568
+
569
+ _component_values: tuple[tuple[OMEROAddressComponent, OMEROAddressTemplateValue], ...]
570
+ extension: str
571
+
572
+ def __init__(
573
+ self,
574
+ component_values: Iterable[tuple[OMEROAddressComponent, object]],
575
+ *,
576
+ extension: str = ".tif",
577
+ ) -> None:
578
+ values_by_component = _require_complete_component_values(component_values)
579
+ object.__setattr__(
580
+ self,
581
+ "_component_values",
582
+ tuple(
583
+ (
584
+ component,
585
+ component.normalize_template(values_by_component[component]),
586
+ )
587
+ for component in OMEROAddressComponent
588
+ ),
589
+ )
590
+ object.__setattr__(self, "extension", _normalize_extension(extension))
591
+
592
+ @classmethod
593
+ def from_member_projection(
594
+ cls,
595
+ source_values: Iterable[tuple[Enum, object]],
596
+ *,
597
+ extension: str = ".tif",
598
+ ) -> OMEROPlaneFilenameTemplate:
599
+ """Construct a template from another nominal component family."""
600
+
601
+ return cls(
602
+ OMEROAddressComponent.project_values(source_values),
603
+ extension=extension,
604
+ )
605
+
606
+ @classmethod
607
+ @cache
608
+ def filename_pattern(cls) -> re.Pattern[str]:
609
+ """Derive exact-or-symbolic grammar from the component owners."""
610
+
611
+ return re.compile(
612
+ "^"
613
+ + "".join(component.filename_template_pattern() for component in OMEROAddressComponent)
614
+ + r"(?:_[^.]*)?(?P<extension>(?:\.[A-Za-z0-9]+)+)$"
615
+ )
616
+
617
+ @classmethod
618
+ def from_filename(cls, filename: str) -> OMEROPlaneFilenameTemplate | None:
619
+ """Parse one exact or symbolic OMERO plane filename template."""
620
+
621
+ basename = PurePosixPath(str(filename).replace("\\", "/")).name
622
+ match = cls.filename_pattern().fullmatch(basename)
623
+ if match is None:
624
+ return None
625
+ return cls(
626
+ (
627
+ (
628
+ component,
629
+ component.normalize_template(match.group(component.component_name)),
630
+ )
631
+ for component in OMEROAddressComponent
632
+ ),
633
+ extension=match.group("extension"),
634
+ )
635
+
636
+ def projected_values(
637
+ self,
638
+ ) -> tuple[tuple[OMEROAddressComponent, OMEROAddressValue], ...]:
639
+ """Project nominal template fields onto serialized boundary values."""
640
+
641
+ return tuple(
642
+ (
643
+ component,
644
+ value.render() if isinstance(value, OMEROAddressTemplateField) else value,
645
+ )
646
+ for component, value in self._component_values
647
+ )
648
+
649
+ def filename(self) -> str:
650
+ """Render through component-owned exact-or-symbolic fragments."""
651
+
652
+ return (
653
+ "".join(
654
+ component.filename_template_fragment(value)
655
+ for component, value in self._component_values
656
+ )
657
+ + self.extension
658
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: polystore
3
- Version: 0.2.7
3
+ Version: 0.2.8
4
4
  Summary: Framework-agnostic multi-backend storage abstraction for ML and scientific computing
5
5
  Author-email: Tristan Simas <tristan.simas@mail.mcgill.ca>
6
6
  License-Expression: MIT
@@ -5,6 +5,7 @@ import pytest
5
5
  from polystore import (
6
6
  OMEROAddressComponent,
7
7
  OMEROPlaneAddress,
8
+ OMEROPlaneFilenameTemplate,
8
9
  OMEROWellAddress,
9
10
  )
10
11
 
@@ -92,6 +93,42 @@ def test_omero_plane_address_binds_one_based_site_and_zero_based_plane_indices()
92
93
  assert address.zero_based(OMEROAddressComponent.TIMEPOINT) == 2
93
94
 
94
95
 
96
+ def test_omero_plane_filename_template_owns_symbolic_component_round_trip() -> None:
97
+ template = OMEROPlaneFilenameTemplate(
98
+ (
99
+ (OMEROAddressComponent.WELL, "A01"),
100
+ (OMEROAddressComponent.SITE, "{iii}"),
101
+ (OMEROAddressComponent.CHANNEL, 2),
102
+ (OMEROAddressComponent.Z_INDEX, 3),
103
+ (OMEROAddressComponent.TIMEPOINT, 1),
104
+ )
105
+ )
106
+
107
+ assert template.filename() == "A01_s{iii}_w2_z003_t001.tif"
108
+ assert OMEROPlaneFilenameTemplate.from_filename(template.filename()) == template
109
+ assert OMEROPlaneFilenameTemplate.from_filename(
110
+ "A01_s001_w2_z003_t001_result_suffix.tif"
111
+ ) == OMEROPlaneFilenameTemplate(
112
+ (
113
+ (OMEROAddressComponent.WELL, "A01"),
114
+ (OMEROAddressComponent.SITE, 1),
115
+ (OMEROAddressComponent.CHANNEL, 2),
116
+ (OMEROAddressComponent.Z_INDEX, 3),
117
+ (OMEROAddressComponent.TIMEPOINT, 1),
118
+ )
119
+ )
120
+ assert dict(template.projected_values()) == {
121
+ OMEROAddressComponent.WELL: "A01",
122
+ OMEROAddressComponent.SITE: "{iii}",
123
+ OMEROAddressComponent.CHANNEL: 2,
124
+ OMEROAddressComponent.Z_INDEX: 3,
125
+ OMEROAddressComponent.TIMEPOINT: 1,
126
+ }
127
+
128
+ with pytest.raises(ValueError, match="site must be a positive integer"):
129
+ OMEROPlaneAddress(template.projected_values())
130
+
131
+
95
132
  def test_omero_wire_mapping_requires_complete_declared_components() -> None:
96
133
  with pytest.raises(ValueError, match="lacks declared fields: timepoint"):
97
134
  OMEROPlaneAddress.from_wire_mapping(
File without changes
File without changes
File without changes
File without changes
File without changes