lsst-daf-butler 29.0.1rc1__py3-none-any.whl → 29.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. lsst/daf/butler/__init__.py +1 -0
  2. lsst/daf/butler/_butler.py +57 -10
  3. lsst/daf/butler/_butler_collections.py +4 -0
  4. lsst/daf/butler/_butler_instance_options.py +3 -0
  5. lsst/daf/butler/_butler_metrics.py +117 -0
  6. lsst/daf/butler/_config.py +1 -1
  7. lsst/daf/butler/_dataset_ref.py +99 -16
  8. lsst/daf/butler/_file_dataset.py +78 -3
  9. lsst/daf/butler/_limited_butler.py +42 -3
  10. lsst/daf/butler/_quantum_backed.py +23 -4
  11. lsst/daf/butler/arrow_utils.py +7 -9
  12. lsst/daf/butler/cli/butler.py +1 -1
  13. lsst/daf/butler/cli/cmd/_remove_runs.py +2 -0
  14. lsst/daf/butler/cli/cmd/commands.py +25 -1
  15. lsst/daf/butler/cli/utils.py +33 -5
  16. lsst/daf/butler/column_spec.py +77 -34
  17. lsst/daf/butler/configs/datastores/formatters.yaml +1 -0
  18. lsst/daf/butler/configs/storageClasses.yaml +2 -0
  19. lsst/daf/butler/datastore/__init__.py +1 -0
  20. lsst/daf/butler/datastore/_datastore.py +48 -19
  21. lsst/daf/butler/datastore/_transfer.py +102 -0
  22. lsst/daf/butler/datastore/generic_base.py +2 -2
  23. lsst/daf/butler/datastore/stored_file_info.py +34 -0
  24. lsst/daf/butler/datastores/chainedDatastore.py +112 -95
  25. lsst/daf/butler/datastores/fileDatastore.py +296 -151
  26. lsst/daf/butler/datastores/file_datastore/transfer.py +104 -0
  27. lsst/daf/butler/datastores/inMemoryDatastore.py +33 -5
  28. lsst/daf/butler/dimensions/_coordinate.py +7 -15
  29. lsst/daf/butler/dimensions/_group.py +15 -5
  30. lsst/daf/butler/dimensions/_record_set.py +469 -4
  31. lsst/daf/butler/dimensions/_record_table.py +1 -1
  32. lsst/daf/butler/dimensions/_records.py +127 -6
  33. lsst/daf/butler/dimensions/_universe.py +12 -8
  34. lsst/daf/butler/dimensions/record_cache.py +1 -2
  35. lsst/daf/butler/direct_butler/_direct_butler.py +429 -245
  36. lsst/daf/butler/direct_query_driver/_driver.py +30 -11
  37. lsst/daf/butler/direct_query_driver/_query_builder.py +74 -17
  38. lsst/daf/butler/direct_query_driver/_sql_column_visitor.py +28 -1
  39. lsst/daf/butler/formatters/parquet.py +7 -3
  40. lsst/daf/butler/pydantic_utils.py +26 -0
  41. lsst/daf/butler/queries/_expression_strings.py +24 -0
  42. lsst/daf/butler/queries/_identifiers.py +4 -1
  43. lsst/daf/butler/queries/_query.py +48 -1
  44. lsst/daf/butler/queries/expression_factory.py +16 -0
  45. lsst/daf/butler/queries/overlaps.py +1 -1
  46. lsst/daf/butler/{direct_query_driver/_predicate_constraints_summary.py → queries/predicate_constraints_summary.py} +2 -2
  47. lsst/daf/butler/queries/tree/_column_expression.py +39 -0
  48. lsst/daf/butler/queries/tree/_column_set.py +1 -1
  49. lsst/daf/butler/queries/tree/_predicate.py +19 -9
  50. lsst/daf/butler/registry/bridge/ephemeral.py +16 -6
  51. lsst/daf/butler/registry/bridge/monolithic.py +78 -37
  52. lsst/daf/butler/registry/collections/_base.py +23 -6
  53. lsst/daf/butler/registry/connectionString.py +5 -10
  54. lsst/daf/butler/registry/databases/postgresql.py +50 -0
  55. lsst/daf/butler/registry/databases/sqlite.py +46 -0
  56. lsst/daf/butler/registry/datasets/byDimensions/_manager.py +77 -64
  57. lsst/daf/butler/registry/datasets/byDimensions/summaries.py +4 -4
  58. lsst/daf/butler/registry/dimensions/static.py +20 -8
  59. lsst/daf/butler/registry/interfaces/_bridge.py +13 -1
  60. lsst/daf/butler/registry/interfaces/_database.py +22 -2
  61. lsst/daf/butler/registry/interfaces/_datasets.py +4 -16
  62. lsst/daf/butler/registry/interfaces/_dimensions.py +7 -2
  63. lsst/daf/butler/registry/obscore/_config.py +5 -0
  64. lsst/daf/butler/registry/obscore/_records.py +4 -2
  65. lsst/daf/butler/registry/queries/expressions/_predicate.py +35 -19
  66. lsst/daf/butler/registry/queries/expressions/check.py +29 -10
  67. lsst/daf/butler/registry/queries/expressions/normalForm.py +15 -0
  68. lsst/daf/butler/registry/queries/expressions/parser/exprTree.py +136 -23
  69. lsst/daf/butler/registry/queries/expressions/parser/parserLex.py +10 -1
  70. lsst/daf/butler/registry/queries/expressions/parser/parserYacc.py +47 -24
  71. lsst/daf/butler/registry/queries/expressions/parser/treeVisitor.py +49 -10
  72. lsst/daf/butler/registry/sql_registry.py +17 -45
  73. lsst/daf/butler/registry/tests/_registry.py +60 -32
  74. lsst/daf/butler/remote_butler/_http_connection.py +21 -5
  75. lsst/daf/butler/remote_butler/_query_driver.py +5 -7
  76. lsst/daf/butler/remote_butler/_registry.py +3 -2
  77. lsst/daf/butler/remote_butler/_remote_butler.py +55 -27
  78. lsst/daf/butler/remote_butler/_remote_file_transfer_source.py +124 -0
  79. lsst/daf/butler/remote_butler/server/_config.py +68 -13
  80. lsst/daf/butler/remote_butler/server/_dependencies.py +68 -3
  81. lsst/daf/butler/remote_butler/server/_factory.py +4 -0
  82. lsst/daf/butler/remote_butler/server/_gafaelfawr.py +125 -0
  83. lsst/daf/butler/remote_butler/server/_server.py +11 -4
  84. lsst/daf/butler/remote_butler/server/_telemetry.py +105 -0
  85. lsst/daf/butler/remote_butler/server/handlers/_external.py +100 -5
  86. lsst/daf/butler/remote_butler/server/handlers/_query_serialization.py +5 -7
  87. lsst/daf/butler/remote_butler/server/handlers/_query_streaming.py +7 -3
  88. lsst/daf/butler/remote_butler/server/handlers/_utils.py +15 -1
  89. lsst/daf/butler/remote_butler/server_models.py +17 -1
  90. lsst/daf/butler/script/ingest_zip.py +13 -1
  91. lsst/daf/butler/script/queryCollections.py +185 -29
  92. lsst/daf/butler/script/removeRuns.py +2 -5
  93. lsst/daf/butler/script/retrieveArtifacts.py +1 -0
  94. lsst/daf/butler/script/transferDatasets.py +5 -0
  95. lsst/daf/butler/tests/butler_queries.py +236 -23
  96. lsst/daf/butler/tests/cliCmdTestBase.py +1 -1
  97. lsst/daf/butler/tests/hybrid_butler.py +42 -9
  98. lsst/daf/butler/tests/hybrid_butler_registry.py +15 -2
  99. lsst/daf/butler/tests/server.py +28 -3
  100. lsst/daf/butler/version.py +1 -1
  101. {lsst_daf_butler-29.0.1rc1.dist-info → lsst_daf_butler-29.1.0.dist-info}/METADATA +1 -1
  102. {lsst_daf_butler-29.0.1rc1.dist-info → lsst_daf_butler-29.1.0.dist-info}/RECORD +110 -104
  103. {lsst_daf_butler-29.0.1rc1.dist-info → lsst_daf_butler-29.1.0.dist-info}/WHEEL +1 -1
  104. {lsst_daf_butler-29.0.1rc1.dist-info → lsst_daf_butler-29.1.0.dist-info}/entry_points.txt +0 -0
  105. {lsst_daf_butler-29.0.1rc1.dist-info → lsst_daf_butler-29.1.0.dist-info}/licenses/COPYRIGHT +0 -0
  106. {lsst_daf_butler-29.0.1rc1.dist-info → lsst_daf_butler-29.1.0.dist-info}/licenses/LICENSE +0 -0
  107. {lsst_daf_butler-29.0.1rc1.dist-info → lsst_daf_butler-29.1.0.dist-info}/licenses/bsd_license.txt +0 -0
  108. {lsst_daf_butler-29.0.1rc1.dist-info → lsst_daf_butler-29.1.0.dist-info}/licenses/gpl-v3.0.txt +0 -0
  109. {lsst_daf_butler-29.0.1rc1.dist-info → lsst_daf_butler-29.1.0.dist-info}/top_level.txt +0 -0
  110. {lsst_daf_butler-29.0.1rc1.dist-info → lsst_daf_butler-29.1.0.dist-info}/zip-safe +0 -0
@@ -43,6 +43,7 @@ import pydantic
43
43
  from lsst.resources import ResourcePath, ResourcePathExpression
44
44
 
45
45
  from ._butler_config import ButlerConfig
46
+ from ._butler_metrics import ButlerMetrics
46
47
  from ._config import Config
47
48
  from ._dataset_provenance import DatasetProvenance
48
49
  from ._dataset_ref import DatasetId, DatasetRef
@@ -118,6 +119,8 @@ class QuantumBackedButler(LimitedButler):
118
119
  Object managing all storage class definitions.
119
120
  dataset_types : `~collections.abc.Mapping` [`str`, `DatasetType`]
120
121
  The registry dataset type definitions, indexed by name.
122
+ metrics : `lsst.daf.butler.ButlerMetrics` or `None`, optional
123
+ Metrics object for tracking butler statistics.
121
124
 
122
125
  Notes
123
126
  -----
@@ -164,6 +167,7 @@ class QuantumBackedButler(LimitedButler):
164
167
  datastore: Datastore,
165
168
  storageClasses: StorageClassFactory,
166
169
  dataset_types: Mapping[str, DatasetType] | None = None,
170
+ metrics: ButlerMetrics | None = None,
167
171
  ):
168
172
  self._dimensions = dimensions
169
173
  self._predicted_inputs = set(predicted_inputs)
@@ -175,6 +179,7 @@ class QuantumBackedButler(LimitedButler):
175
179
  self._datastore = datastore
176
180
  self.storageClasses = storageClasses
177
181
  self._dataset_types: Mapping[str, DatasetType] = {}
182
+ self._metrics = metrics if metrics is not None else ButlerMetrics()
178
183
  if dataset_types is not None:
179
184
  self._dataset_types = dataset_types
180
185
  self._datastore.set_retrieve_dataset_type_method(self._retrieve_dataset_type)
@@ -190,6 +195,7 @@ class QuantumBackedButler(LimitedButler):
190
195
  BridgeManagerClass: type[DatastoreRegistryBridgeManager] = MonolithicDatastoreRegistryBridgeManager,
191
196
  search_paths: list[str] | None = None,
192
197
  dataset_types: Mapping[str, DatasetType] | None = None,
198
+ metrics: ButlerMetrics | None = None,
193
199
  ) -> QuantumBackedButler:
194
200
  """Construct a new `QuantumBackedButler` from repository configuration
195
201
  and helper types.
@@ -219,6 +225,8 @@ class QuantumBackedButler(LimitedButler):
219
225
  dataset_types : `~collections.abc.Mapping` [`str`, `DatasetType`], \
220
226
  optional
221
227
  Mapping of the dataset type name to its registry definition.
228
+ metrics : `lsst.daf.butler.ButlerMetrics` or `None`, optional
229
+ Metrics object for gathering butler statistics.
222
230
  """
223
231
  predicted_inputs = [ref.id for ref in itertools.chain.from_iterable(quantum.inputs.values())]
224
232
  predicted_inputs += [ref.id for ref in quantum.initInputs.values()]
@@ -234,6 +242,7 @@ class QuantumBackedButler(LimitedButler):
234
242
  BridgeManagerClass=BridgeManagerClass,
235
243
  search_paths=search_paths,
236
244
  dataset_types=dataset_types,
245
+ metrics=metrics,
237
246
  )
238
247
 
239
248
  @classmethod
@@ -249,6 +258,7 @@ class QuantumBackedButler(LimitedButler):
249
258
  BridgeManagerClass: type[DatastoreRegistryBridgeManager] = MonolithicDatastoreRegistryBridgeManager,
250
259
  search_paths: list[str] | None = None,
251
260
  dataset_types: Mapping[str, DatasetType] | None = None,
261
+ metrics: ButlerMetrics | None = None,
252
262
  ) -> QuantumBackedButler:
253
263
  """Construct a new `QuantumBackedButler` from sets of input and output
254
264
  dataset IDs.
@@ -281,6 +291,8 @@ class QuantumBackedButler(LimitedButler):
281
291
  dataset_types : `~collections.abc.Mapping` [`str`, `DatasetType`], \
282
292
  optional
283
293
  Mapping of the dataset type name to its registry definition.
294
+ metrics : `lsst.daf.butler.ButlerMetrics` or `None`, optional
295
+ Metrics object for gathering butler statistics.
284
296
  """
285
297
  return cls._initialize(
286
298
  config=config,
@@ -293,6 +305,7 @@ class QuantumBackedButler(LimitedButler):
293
305
  BridgeManagerClass=BridgeManagerClass,
294
306
  search_paths=search_paths,
295
307
  dataset_types=dataset_types,
308
+ metrics=metrics,
296
309
  )
297
310
 
298
311
  @classmethod
@@ -309,6 +322,7 @@ class QuantumBackedButler(LimitedButler):
309
322
  BridgeManagerClass: type[DatastoreRegistryBridgeManager] = MonolithicDatastoreRegistryBridgeManager,
310
323
  search_paths: list[str] | None = None,
311
324
  dataset_types: Mapping[str, DatasetType] | None = None,
325
+ metrics: ButlerMetrics | None = None,
312
326
  ) -> QuantumBackedButler:
313
327
  """Initialize quantum-backed butler.
314
328
 
@@ -341,6 +355,8 @@ class QuantumBackedButler(LimitedButler):
341
355
  Additional search paths for butler configuration.
342
356
  dataset_types : `~collections.abc.Mapping` [`str`, `DatasetType`]
343
357
  Mapping of the dataset type name to its registry definition.
358
+ metrics : `lsst.daf.butler.ButlerMetrics` or `None`, optional
359
+ Metrics object for gathering butler statistics.
344
360
  """
345
361
  butler_config = ButlerConfig(config, searchPaths=search_paths)
346
362
  butler_root = butler_config.get("root", butler_config.configDir)
@@ -373,6 +389,7 @@ class QuantumBackedButler(LimitedButler):
373
389
  datastore,
374
390
  storageClasses=storageClasses,
375
391
  dataset_types=dataset_types,
392
+ metrics=metrics,
376
393
  )
377
394
 
378
395
  def _retrieve_dataset_type(self, name: str) -> DatasetType | None:
@@ -459,8 +476,9 @@ class QuantumBackedButler(LimitedButler):
459
476
  # Docstring inherited.
460
477
  if ref.id not in self._predicted_outputs:
461
478
  raise RuntimeError("Cannot `put` dataset that was not predicted as an output.")
462
- self._datastore.put(obj, ref, provenance=provenance)
463
- self._actual_output_refs.add(ref)
479
+ with self._metrics.instrument_put(log=_LOG, msg="Put QBB dataset"):
480
+ self._datastore.put(obj, ref, provenance=provenance)
481
+ self._actual_output_refs.add(ref)
464
482
  return ref
465
483
 
466
484
  def pruneDatasets(
@@ -498,8 +516,9 @@ class QuantumBackedButler(LimitedButler):
498
516
  self._actual_output_refs.discard(ref)
499
517
 
500
518
  if unstore:
501
- # Point of no return for removing artifacts
502
- self._datastore.emptyTrash()
519
+ # Point of no return for removing artifacts. Only try to remove
520
+ # refs associated with this pruning.
521
+ self._datastore.emptyTrash(refs=refs)
503
522
 
504
523
  def retrieve_artifacts_zip(
505
524
  self,
@@ -383,7 +383,7 @@ class _ToArrowTimespan(ToArrow):
383
383
  # Docstring inherited.
384
384
  return TimespanArrowType()
385
385
 
386
- def append(self, value: Timespan | None, column: list[pa.StructScalar | None]) -> None:
386
+ def append(self, value: Timespan | None, column: list[dict[str, int] | None]) -> None:
387
387
  # Docstring inherited.
388
388
  column.append({"begin_nsec": value.nsec[0], "end_nsec": value.nsec[1]} if value is not None else None)
389
389
 
@@ -432,12 +432,10 @@ class _ToArrowDateTime(ToArrow):
432
432
 
433
433
  @final
434
434
  class UUIDArrowType(pa.ExtensionType):
435
- """An Arrow extension type for `astropy.time.Time`, stored as TAI
436
- nanoseconds since 1970-01-01.
437
- """
435
+ """An Arrow extension type for `uuid.UUID`, stored as 16 bytes."""
438
436
 
439
437
  def __init__(self) -> None:
440
- super().__init__(_ToArrowTimespan.storage_type, "astropy.time.Time")
438
+ super().__init__(_ToArrowUUID.storage_type, "uuid.UUID")
441
439
 
442
440
  def __arrow_ext_serialize__(self) -> bytes:
443
441
  return b""
@@ -458,7 +456,7 @@ class UUIDArrowScalar(pa.ExtensionScalar):
458
456
  instance.
459
457
  """
460
458
 
461
- def as_py(self) -> astropy.time.Time:
459
+ def as_py(self, **_unused: Any) -> uuid.UUID:
462
460
  return uuid.UUID(bytes=self.value.as_py())
463
461
 
464
462
 
@@ -487,7 +485,7 @@ class RegionArrowScalar(pa.ExtensionScalar):
487
485
  Use the standard `as_py` method to convert to an actual region.
488
486
  """
489
487
 
490
- def as_py(self) -> Region:
488
+ def as_py(self, **_unused: Any) -> Region:
491
489
  return Region.decode(self.value.as_py())
492
490
 
493
491
 
@@ -516,7 +514,7 @@ class TimespanArrowScalar(pa.ExtensionScalar):
516
514
  Use the standard `as_py` method to convert to an actual timespan.
517
515
  """
518
516
 
519
- def as_py(self) -> Timespan | None:
517
+ def as_py(self, **_unused: Any) -> Timespan | None:
520
518
  if self.value is None:
521
519
  return None
522
520
  else:
@@ -554,7 +552,7 @@ class DateTimeArrowScalar(pa.ExtensionScalar):
554
552
  instance.
555
553
  """
556
554
 
557
- def as_py(self) -> astropy.time.Time:
555
+ def as_py(self, **_unused: Any) -> astropy.time.Time:
558
556
  return TimeConverter().nsec_to_astropy(self.value.as_py())
559
557
 
560
558
 
@@ -102,7 +102,7 @@ class PluginCommand:
102
102
  """Where the command came from (`str`)."""
103
103
 
104
104
 
105
- class LoaderCLI(click.MultiCommand, abc.ABC):
105
+ class LoaderCLI(click.Group, abc.ABC):
106
106
  """Extends `click.MultiCommand`, which dispatches to subcommands, to load
107
107
  subcommands at runtime.
108
108
 
@@ -81,7 +81,9 @@ def _print_remove(will: bool, runs: Sequence[script.RemoveRun], datasets: Mappin
81
81
  else:
82
82
  print(run.name)
83
83
  print("\n" + willRemoveDatasetsMsg if will else didRemoveDatasetsMsg)
84
+ total = sum(datasets.values())
84
85
  print(", ".join([f"{i[0]}({i[1]})" for i in datasets.items()]))
86
+ print("Total number of datasets to remove: ", total)
85
87
 
86
88
 
87
89
  def _print_requires_confirmation(runs: Sequence[script.RemoveRun], datasets: Mapping[str, int]) -> None:
@@ -424,6 +424,21 @@ def prune_datasets(**kwargs: Any) -> None:
424
424
  case_sensitive=False,
425
425
  ),
426
426
  )
427
+ @click.option(
428
+ "-t",
429
+ "--show-dataset-types",
430
+ is_flag=True,
431
+ help="Also show the dataset types registered within each collection.",
432
+ )
433
+ @click.option(
434
+ "--exclude-dataset-types",
435
+ type=click.STRING,
436
+ multiple=True,
437
+ default=["*_config,*_log,*_metadata,packages"],
438
+ callback=split_commas,
439
+ show_default=True,
440
+ help="Dataset types (comma-separated) to exclude. Only valid with --show-dataset-types.",
441
+ )
427
442
  @options_file_option()
428
443
  def query_collections(*args: Any, **kwargs: Any) -> None:
429
444
  """Get the collections whose names match an expression."""
@@ -454,7 +469,7 @@ def query_dataset_types(*args: Any, **kwargs: Any) -> None:
454
469
  """Get the dataset types in a repository."""
455
470
  table = script.queryDatasetTypes(*args, **kwargs)
456
471
  if table:
457
- table.pprint_all()
472
+ table.pprint_all(align="<")
458
473
  else:
459
474
  print("No results. Try --help for more information.")
460
475
 
@@ -638,6 +653,9 @@ def retrieve_artifacts(**kwargs: Any) -> None:
638
653
  @transfer_option()
639
654
  @register_dataset_types_option()
640
655
  @transfer_dimensions_option()
656
+ @click.option(
657
+ "--dry-run/--no-dry-run", default=False, help="Enable dry run mode and do not transfer any datasets."
658
+ )
641
659
  @options_file_option()
642
660
  def transfer_datasets(**kwargs: Any) -> None:
643
661
  """Transfer datasets from a source butler to a destination butler.
@@ -826,6 +844,12 @@ def export_calibs(*args: Any, **kwargs: Any) -> None:
826
844
  @repo_argument(required=True)
827
845
  @click.argument("zip", required=True)
828
846
  @transfer_option()
847
+ @transfer_dimensions_option(
848
+ default=False, help="Attempt to register missing dimension records during ingest."
849
+ )
850
+ @click.option(
851
+ "--dry-run/--no-dry-run", default=False, help="Enable dry run mode and do not ingest any datasets."
852
+ )
829
853
  def ingest_zip(**kwargs: Any) -> None:
830
854
  """Ingest a Zip file created by retrieve-artifacts.
831
855
 
@@ -55,6 +55,7 @@ __all__ = (
55
55
  )
56
56
 
57
57
 
58
+ import importlib.metadata
58
59
  import itertools
59
60
  import logging
60
61
  import os
@@ -76,6 +77,7 @@ import click
76
77
  import click.exceptions
77
78
  import click.testing
78
79
  import yaml
80
+ from packaging.version import Version
79
81
 
80
82
  from lsst.utils.iteration import ensure_iterable
81
83
 
@@ -87,6 +89,12 @@ if TYPE_CHECKING:
87
89
 
88
90
  from lsst.daf.butler import Dimension
89
91
 
92
+ _click_version = Version(importlib.metadata.version("click"))
93
+ if _click_version >= Version("8.2.0"):
94
+ _click_make_metavar_has_context = True
95
+ else:
96
+ _click_make_metavar_has_context = False
97
+
90
98
  log = logging.getLogger(__name__)
91
99
 
92
100
  # This is used as the metavar argument to Options that accept multiple string
@@ -741,9 +749,16 @@ class MWPath(click.Path):
741
749
  class MWOption(click.Option):
742
750
  """Overrides click.Option with desired behaviors."""
743
751
 
744
- def make_metavar(self) -> str:
752
+ def make_metavar(self, ctx: click.Context | None = None) -> str:
745
753
  """Make the metavar for the help menu.
746
754
 
755
+ Parameters
756
+ ----------
757
+ ctx : `click.Context` or `None`
758
+ Context from the command.
759
+
760
+ Notes
761
+ -----
747
762
  Overrides `click.Option.make_metavar`.
748
763
  Adds a space and an ellipsis after the metavar name if
749
764
  the option accepts multiple inputs, otherwise defers to the base
@@ -758,7 +773,10 @@ class MWOption(click.Option):
758
773
  transformation that must apply to all types should be applied in
759
774
  get_help_record.
760
775
  """
761
- metavar = super().make_metavar()
776
+ if _click_make_metavar_has_context:
777
+ metavar = super().make_metavar(ctx=ctx) # type: ignore
778
+ else:
779
+ metavar = super().make_metavar() # type: ignore
762
780
  if self.multiple and self.nargs == 1:
763
781
  metavar += " ..."
764
782
  elif self.nargs != 1:
@@ -769,9 +787,16 @@ class MWOption(click.Option):
769
787
  class MWArgument(click.Argument):
770
788
  """Overrides click.Argument with desired behaviors."""
771
789
 
772
- def make_metavar(self) -> str:
790
+ def make_metavar(self, ctx: click.Context | None = None) -> str:
773
791
  """Make the metavar for the help menu.
774
792
 
793
+ Parameters
794
+ ----------
795
+ ctx : `click.Context` or `None`
796
+ Context from the command.
797
+
798
+ Notes
799
+ -----
775
800
  Overrides `click.Option.make_metavar`.
776
801
  Always adds a space and an ellipsis (' ...') after the
777
802
  metavar name if the option accepts multiple inputs.
@@ -784,7 +809,10 @@ class MWArgument(click.Argument):
784
809
  metavar : `str`
785
810
  The metavar value.
786
811
  """
787
- metavar = super().make_metavar()
812
+ if _click_make_metavar_has_context:
813
+ metavar = super().make_metavar(ctx=ctx) # type: ignore
814
+ else:
815
+ metavar = super().make_metavar() # type: ignore
788
816
  if self.nargs != 1:
789
817
  metavar = f"{metavar[:-3]} ..."
790
818
  return metavar
@@ -1029,7 +1057,7 @@ class MWCommand(click.Command):
1029
1057
  return ret
1030
1058
 
1031
1059
  @epilog.setter
1032
- def epilog(self, val: str) -> None:
1060
+ def epilog(self, val: str | None) -> None:
1033
1061
  self._epilog = val
1034
1062
 
1035
1063
 
@@ -39,12 +39,24 @@ __all__ = (
39
39
  "StringColumnSpec",
40
40
  "TimespanColumnSpec",
41
41
  "UUIDColumnSpec",
42
+ "make_tuple_type_adapter",
42
43
  )
43
44
 
44
45
  import textwrap
45
46
  import uuid
46
47
  from abc import ABC, abstractmethod
47
- from typing import TYPE_CHECKING, Annotated, Any, ClassVar, Literal, TypeAlias, Union, final
48
+ from collections.abc import Iterable
49
+ from typing import (
50
+ TYPE_CHECKING,
51
+ Annotated,
52
+ Any,
53
+ ClassVar,
54
+ Literal,
55
+ Optional,
56
+ TypeAlias,
57
+ Union,
58
+ final,
59
+ )
48
60
 
49
61
  import astropy.time
50
62
  import pyarrow as pa
@@ -54,7 +66,7 @@ from lsst.sphgeom import Region
54
66
 
55
67
  from . import arrow_utils, ddl
56
68
  from ._timespan import Timespan
57
- from .pydantic_utils import SerializableRegion, SerializableTime
69
+ from .pydantic_utils import SerializableBytesHex, SerializableRegion, SerializableTime
58
70
 
59
71
  if TYPE_CHECKING:
60
72
  from .name_shrinker import NameShrinker
@@ -125,18 +137,6 @@ class ColumnValueSerializer(ABC):
125
137
  raise NotImplementedError
126
138
 
127
139
 
128
- class _DefaultColumnValueSerializer(ColumnValueSerializer):
129
- """Default implementation of serializer for basic types."""
130
-
131
- def serialize(self, value: Any) -> Any:
132
- # Docstring inherited.
133
- return value
134
-
135
- def deserialize(self, value: Any) -> Any:
136
- # Docstring inherited.
137
- return value
138
-
139
-
140
140
  class _TypeAdapterColumnValueSerializer(ColumnValueSerializer):
141
141
  """Implementation of serializer that uses pydantic type adapter."""
142
142
 
@@ -156,6 +156,8 @@ class _TypeAdapterColumnValueSerializer(ColumnValueSerializer):
156
156
  class _BaseColumnSpec(pydantic.BaseModel, ABC):
157
157
  """Base class for descriptions of table columns."""
158
158
 
159
+ pytype: ClassVar[type]
160
+
159
161
  name: str = pydantic.Field(description="""Name of the column.""")
160
162
 
161
163
  doc: str = pydantic.Field(default="", description="Documentation for the column.")
@@ -200,7 +202,6 @@ class _BaseColumnSpec(pydantic.BaseModel, ABC):
200
202
  """
201
203
  raise NotImplementedError()
202
204
 
203
- @abstractmethod
204
205
  def serializer(self) -> ColumnValueSerializer:
205
206
  """Return object that converts values of this column to or from
206
207
  serializable format.
@@ -210,7 +211,7 @@ class _BaseColumnSpec(pydantic.BaseModel, ABC):
210
211
  serializer : `ColumnValueSerializer`
211
212
  A converter instance.
212
213
  """
213
- raise NotImplementedError()
214
+ return _TypeAdapterColumnValueSerializer(pydantic.TypeAdapter(self.annotated_type))
214
215
 
215
216
  def display(self, level: int = 0, tab: str = " ") -> list[str]:
216
217
  """Return a human-reader-focused string description of this column as
@@ -243,6 +244,48 @@ class _BaseColumnSpec(pydantic.BaseModel, ABC):
243
244
  def __str__(self) -> str:
244
245
  return "\n".join(self.display())
245
246
 
247
+ @property
248
+ def annotated_type(self) -> Any:
249
+ """Return a Pydantic-friendly type annotation for this column type.
250
+
251
+ Since this is a runtime object and most type annotations must be
252
+ static, this is really only useful for `pydantic.TypeAdapter`
253
+ construction and dynamic `pydantic.create_model` construction.
254
+ """
255
+ base = self._get_base_annotated_type()
256
+ if self.nullable:
257
+ return Optional[base]
258
+ return base
259
+
260
+ @abstractmethod
261
+ def _get_base_annotated_type(self) -> Any:
262
+ """Return the base annotated type (not taking into account `nullable`)
263
+ for this column type.
264
+ """
265
+ raise NotImplementedError()
266
+
267
+
268
+ def make_tuple_type_adapter(
269
+ columns: Iterable[ColumnSpec],
270
+ ) -> pydantic.TypeAdapter[tuple[Any, ...]]:
271
+ """Return a `pydantic.TypeAdapter` for a `tuple` with types defined by an
272
+ iterable of `ColumnSpec` objects.
273
+
274
+ Parameters
275
+ ----------
276
+ columns : `~collections.abc.Iterable` [ `ColumnSpec` ]
277
+ Iterable of column specifications.
278
+
279
+ Returns
280
+ -------
281
+ adapter : `pydantic.TypeAdapter`
282
+ A Pydantic type adapter for the `tuple` representation of a row with
283
+ the given columns.
284
+ """
285
+ # Static type-checkers don't like this runtime use of static-typing
286
+ # constructs, but that's how Pydantic works.
287
+ return pydantic.TypeAdapter(tuple[*[spec.annotated_type for spec in columns]]) # type: ignore
288
+
246
289
 
247
290
  @final
248
291
  class IntColumnSpec(_BaseColumnSpec):
@@ -256,9 +299,9 @@ class IntColumnSpec(_BaseColumnSpec):
256
299
  # Docstring inherited.
257
300
  return arrow_utils.ToArrow.for_primitive(self.name, pa.uint64(), nullable=self.nullable)
258
301
 
259
- def serializer(self) -> ColumnValueSerializer:
302
+ def _get_base_annotated_type(self) -> Any:
260
303
  # Docstring inherited.
261
- return _DefaultColumnValueSerializer()
304
+ return pydantic.StrictInt
262
305
 
263
306
 
264
307
  @final
@@ -280,9 +323,9 @@ class StringColumnSpec(_BaseColumnSpec):
280
323
  # Docstring inherited.
281
324
  return arrow_utils.ToArrow.for_primitive(self.name, pa.string(), nullable=self.nullable)
282
325
 
283
- def serializer(self) -> ColumnValueSerializer:
326
+ def _get_base_annotated_type(self) -> Any:
284
327
  # Docstring inherited.
285
- return _DefaultColumnValueSerializer()
328
+ return pydantic.StrictStr
286
329
 
287
330
 
288
331
  @final
@@ -310,9 +353,9 @@ class HashColumnSpec(_BaseColumnSpec):
310
353
  nullable=self.nullable,
311
354
  )
312
355
 
313
- def serializer(self) -> ColumnValueSerializer:
356
+ def _get_base_annotated_type(self) -> Any:
314
357
  # Docstring inherited.
315
- return _DefaultColumnValueSerializer()
358
+ return SerializableBytesHex
316
359
 
317
360
 
318
361
  @final
@@ -328,9 +371,9 @@ class FloatColumnSpec(_BaseColumnSpec):
328
371
  assert self.nullable is not None, "nullable=None should be resolved by validators"
329
372
  return arrow_utils.ToArrow.for_primitive(self.name, pa.float64(), nullable=self.nullable)
330
373
 
331
- def serializer(self) -> ColumnValueSerializer:
374
+ def _get_base_annotated_type(self) -> Any:
332
375
  # Docstring inherited.
333
- return _DefaultColumnValueSerializer()
376
+ return pydantic.StrictFloat
334
377
 
335
378
 
336
379
  @final
@@ -345,9 +388,9 @@ class BoolColumnSpec(_BaseColumnSpec):
345
388
  # Docstring inherited.
346
389
  return arrow_utils.ToArrow.for_primitive(self.name, pa.bool_(), nullable=self.nullable)
347
390
 
348
- def serializer(self) -> ColumnValueSerializer:
391
+ def _get_base_annotated_type(self) -> Any:
349
392
  # Docstring inherited.
350
- return _DefaultColumnValueSerializer()
393
+ return pydantic.StrictBool
351
394
 
352
395
 
353
396
  @final
@@ -363,9 +406,9 @@ class UUIDColumnSpec(_BaseColumnSpec):
363
406
  assert self.nullable is not None, "nullable=None should be resolved by validators"
364
407
  return arrow_utils.ToArrow.for_uuid(self.name, nullable=self.nullable)
365
408
 
366
- def serializer(self) -> ColumnValueSerializer:
409
+ def _get_base_annotated_type(self) -> Any:
367
410
  # Docstring inherited.
368
- return _TypeAdapterColumnValueSerializer(pydantic.TypeAdapter(self.pytype))
411
+ return uuid.UUID
369
412
 
370
413
 
371
414
  @final
@@ -386,9 +429,9 @@ class RegionColumnSpec(_BaseColumnSpec):
386
429
  assert self.nullable is not None, "nullable=None should be resolved by validators"
387
430
  return arrow_utils.ToArrow.for_region(self.name, nullable=self.nullable)
388
431
 
389
- def serializer(self) -> ColumnValueSerializer:
432
+ def _get_base_annotated_type(self) -> Any:
390
433
  # Docstring inherited.
391
- return _TypeAdapterColumnValueSerializer(pydantic.TypeAdapter(SerializableRegion))
434
+ return SerializableRegion
392
435
 
393
436
 
394
437
  @final
@@ -405,9 +448,9 @@ class TimespanColumnSpec(_BaseColumnSpec):
405
448
  # Docstring inherited.
406
449
  return arrow_utils.ToArrow.for_timespan(self.name, nullable=self.nullable)
407
450
 
408
- def serializer(self) -> ColumnValueSerializer:
451
+ def _get_base_annotated_type(self) -> Any:
409
452
  # Docstring inherited.
410
- return _TypeAdapterColumnValueSerializer(pydantic.TypeAdapter(self.pytype))
453
+ return Timespan
411
454
 
412
455
 
413
456
  @final
@@ -425,9 +468,9 @@ class DateTimeColumnSpec(_BaseColumnSpec):
425
468
  assert self.nullable is not None, "nullable=None should be resolved by validators"
426
469
  return arrow_utils.ToArrow.for_datetime(self.name, nullable=self.nullable)
427
470
 
428
- def serializer(self) -> ColumnValueSerializer:
471
+ def _get_base_annotated_type(self) -> Any:
429
472
  # Docstring inherited.
430
- return _TypeAdapterColumnValueSerializer(pydantic.TypeAdapter(SerializableTime))
473
+ return SerializableTime
431
474
 
432
475
 
433
476
  ColumnSpec = Annotated[
@@ -94,3 +94,4 @@ Timespan: lsst.daf.butler.formatters.json.JsonFormatter
94
94
  RegionTimeInfo: lsst.daf.butler.formatters.json.JsonFormatter
95
95
  QPEnsemble: lsst.meas.pz.qp_formatter.QPFormatter
96
96
  PZModel: lsst.meas.pz.model_formatter.ModelFormatter
97
+ VisitBackgroundModel: lsst.daf.butler.formatters.json.JsonFormatter
@@ -423,3 +423,5 @@ storageClasses:
423
423
  pytype: qp.Ensemble
424
424
  PZModel:
425
425
  pytype: rail.core.model.Model
426
+ VisitBackgroundModel:
427
+ pytype: lsst.drp.tasks.fit_visit_background.VisitBackgroundModel
@@ -26,3 +26,4 @@
26
26
  # along with this program. If not, see <http://www.gnu.org/licenses/>.
27
27
 
28
28
  from ._datastore import *
29
+ from ._transfer import *