dsgrid-toolkit 0.3.3__cp313-cp313-win_amd64.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 (157) hide show
  1. build_backend.py +93 -0
  2. dsgrid/__init__.py +22 -0
  3. dsgrid/api/__init__.py +0 -0
  4. dsgrid/api/api_manager.py +179 -0
  5. dsgrid/api/app.py +419 -0
  6. dsgrid/api/models.py +60 -0
  7. dsgrid/api/response_models.py +116 -0
  8. dsgrid/apps/__init__.py +0 -0
  9. dsgrid/apps/project_viewer/app.py +216 -0
  10. dsgrid/apps/registration_gui.py +444 -0
  11. dsgrid/chronify.py +32 -0
  12. dsgrid/cli/__init__.py +0 -0
  13. dsgrid/cli/common.py +120 -0
  14. dsgrid/cli/config.py +176 -0
  15. dsgrid/cli/download.py +13 -0
  16. dsgrid/cli/dsgrid.py +157 -0
  17. dsgrid/cli/dsgrid_admin.py +92 -0
  18. dsgrid/cli/install_notebooks.py +62 -0
  19. dsgrid/cli/query.py +729 -0
  20. dsgrid/cli/registry.py +1862 -0
  21. dsgrid/cloud/__init__.py +0 -0
  22. dsgrid/cloud/cloud_storage_interface.py +140 -0
  23. dsgrid/cloud/factory.py +31 -0
  24. dsgrid/cloud/fake_storage_interface.py +37 -0
  25. dsgrid/cloud/s3_storage_interface.py +156 -0
  26. dsgrid/common.py +36 -0
  27. dsgrid/config/__init__.py +0 -0
  28. dsgrid/config/annual_time_dimension_config.py +194 -0
  29. dsgrid/config/common.py +142 -0
  30. dsgrid/config/config_base.py +148 -0
  31. dsgrid/config/dataset_config.py +907 -0
  32. dsgrid/config/dataset_schema_handler_factory.py +46 -0
  33. dsgrid/config/date_time_dimension_config.py +136 -0
  34. dsgrid/config/dimension_config.py +54 -0
  35. dsgrid/config/dimension_config_factory.py +65 -0
  36. dsgrid/config/dimension_mapping_base.py +350 -0
  37. dsgrid/config/dimension_mappings_config.py +48 -0
  38. dsgrid/config/dimensions.py +1025 -0
  39. dsgrid/config/dimensions_config.py +71 -0
  40. dsgrid/config/file_schema.py +190 -0
  41. dsgrid/config/index_time_dimension_config.py +80 -0
  42. dsgrid/config/input_dataset_requirements.py +31 -0
  43. dsgrid/config/mapping_tables.py +209 -0
  44. dsgrid/config/noop_time_dimension_config.py +42 -0
  45. dsgrid/config/project_config.py +1462 -0
  46. dsgrid/config/registration_models.py +188 -0
  47. dsgrid/config/representative_period_time_dimension_config.py +194 -0
  48. dsgrid/config/simple_models.py +49 -0
  49. dsgrid/config/supplemental_dimension.py +29 -0
  50. dsgrid/config/time_dimension_base_config.py +192 -0
  51. dsgrid/data_models.py +155 -0
  52. dsgrid/dataset/__init__.py +0 -0
  53. dsgrid/dataset/dataset.py +123 -0
  54. dsgrid/dataset/dataset_expression_handler.py +86 -0
  55. dsgrid/dataset/dataset_mapping_manager.py +121 -0
  56. dsgrid/dataset/dataset_schema_handler_base.py +945 -0
  57. dsgrid/dataset/dataset_schema_handler_one_table.py +209 -0
  58. dsgrid/dataset/dataset_schema_handler_two_table.py +322 -0
  59. dsgrid/dataset/growth_rates.py +162 -0
  60. dsgrid/dataset/models.py +51 -0
  61. dsgrid/dataset/table_format_handler_base.py +257 -0
  62. dsgrid/dataset/table_format_handler_factory.py +17 -0
  63. dsgrid/dataset/unpivoted_table.py +121 -0
  64. dsgrid/dimension/__init__.py +0 -0
  65. dsgrid/dimension/base_models.py +230 -0
  66. dsgrid/dimension/dimension_filters.py +308 -0
  67. dsgrid/dimension/standard.py +252 -0
  68. dsgrid/dimension/time.py +352 -0
  69. dsgrid/dimension/time_utils.py +103 -0
  70. dsgrid/dsgrid_rc.py +88 -0
  71. dsgrid/exceptions.py +105 -0
  72. dsgrid/filesystem/__init__.py +0 -0
  73. dsgrid/filesystem/cloud_filesystem.py +32 -0
  74. dsgrid/filesystem/factory.py +32 -0
  75. dsgrid/filesystem/filesystem_interface.py +136 -0
  76. dsgrid/filesystem/local_filesystem.py +74 -0
  77. dsgrid/filesystem/s3_filesystem.py +118 -0
  78. dsgrid/loggers.py +132 -0
  79. dsgrid/minimal_patterns.cp313-win_amd64.pyd +0 -0
  80. dsgrid/notebooks/connect_to_dsgrid_registry.ipynb +949 -0
  81. dsgrid/notebooks/registration.ipynb +48 -0
  82. dsgrid/notebooks/start_notebook.sh +11 -0
  83. dsgrid/project.py +451 -0
  84. dsgrid/query/__init__.py +0 -0
  85. dsgrid/query/dataset_mapping_plan.py +142 -0
  86. dsgrid/query/derived_dataset.py +388 -0
  87. dsgrid/query/models.py +728 -0
  88. dsgrid/query/query_context.py +287 -0
  89. dsgrid/query/query_submitter.py +994 -0
  90. dsgrid/query/report_factory.py +19 -0
  91. dsgrid/query/report_peak_load.py +70 -0
  92. dsgrid/query/reports_base.py +20 -0
  93. dsgrid/registry/__init__.py +0 -0
  94. dsgrid/registry/bulk_register.py +165 -0
  95. dsgrid/registry/common.py +287 -0
  96. dsgrid/registry/config_update_checker_base.py +63 -0
  97. dsgrid/registry/data_store_factory.py +34 -0
  98. dsgrid/registry/data_store_interface.py +74 -0
  99. dsgrid/registry/dataset_config_generator.py +158 -0
  100. dsgrid/registry/dataset_registry_manager.py +950 -0
  101. dsgrid/registry/dataset_update_checker.py +16 -0
  102. dsgrid/registry/dimension_mapping_registry_manager.py +575 -0
  103. dsgrid/registry/dimension_mapping_update_checker.py +16 -0
  104. dsgrid/registry/dimension_registry_manager.py +413 -0
  105. dsgrid/registry/dimension_update_checker.py +16 -0
  106. dsgrid/registry/duckdb_data_store.py +207 -0
  107. dsgrid/registry/filesystem_data_store.py +150 -0
  108. dsgrid/registry/filter_registry_manager.py +123 -0
  109. dsgrid/registry/project_config_generator.py +57 -0
  110. dsgrid/registry/project_registry_manager.py +1623 -0
  111. dsgrid/registry/project_update_checker.py +48 -0
  112. dsgrid/registry/registration_context.py +223 -0
  113. dsgrid/registry/registry_auto_updater.py +316 -0
  114. dsgrid/registry/registry_database.py +667 -0
  115. dsgrid/registry/registry_interface.py +446 -0
  116. dsgrid/registry/registry_manager.py +558 -0
  117. dsgrid/registry/registry_manager_base.py +367 -0
  118. dsgrid/registry/versioning.py +92 -0
  119. dsgrid/rust_ext/__init__.py +14 -0
  120. dsgrid/rust_ext/find_minimal_patterns.py +129 -0
  121. dsgrid/spark/__init__.py +0 -0
  122. dsgrid/spark/functions.py +589 -0
  123. dsgrid/spark/types.py +110 -0
  124. dsgrid/tests/__init__.py +0 -0
  125. dsgrid/tests/common.py +140 -0
  126. dsgrid/tests/make_us_data_registry.py +265 -0
  127. dsgrid/tests/register_derived_datasets.py +103 -0
  128. dsgrid/tests/utils.py +25 -0
  129. dsgrid/time/__init__.py +0 -0
  130. dsgrid/time/time_conversions.py +80 -0
  131. dsgrid/time/types.py +67 -0
  132. dsgrid/units/__init__.py +0 -0
  133. dsgrid/units/constants.py +113 -0
  134. dsgrid/units/convert.py +71 -0
  135. dsgrid/units/energy.py +145 -0
  136. dsgrid/units/power.py +87 -0
  137. dsgrid/utils/__init__.py +0 -0
  138. dsgrid/utils/dataset.py +830 -0
  139. dsgrid/utils/files.py +179 -0
  140. dsgrid/utils/filters.py +125 -0
  141. dsgrid/utils/id_remappings.py +100 -0
  142. dsgrid/utils/py_expression_eval/LICENSE +19 -0
  143. dsgrid/utils/py_expression_eval/README.md +8 -0
  144. dsgrid/utils/py_expression_eval/__init__.py +847 -0
  145. dsgrid/utils/py_expression_eval/tests.py +283 -0
  146. dsgrid/utils/run_command.py +70 -0
  147. dsgrid/utils/scratch_dir_context.py +65 -0
  148. dsgrid/utils/spark.py +918 -0
  149. dsgrid/utils/spark_partition.py +98 -0
  150. dsgrid/utils/timing.py +239 -0
  151. dsgrid/utils/utilities.py +221 -0
  152. dsgrid/utils/versioning.py +36 -0
  153. dsgrid_toolkit-0.3.3.dist-info/METADATA +193 -0
  154. dsgrid_toolkit-0.3.3.dist-info/RECORD +157 -0
  155. dsgrid_toolkit-0.3.3.dist-info/WHEEL +4 -0
  156. dsgrid_toolkit-0.3.3.dist-info/entry_points.txt +4 -0
  157. dsgrid_toolkit-0.3.3.dist-info/licenses/LICENSE +29 -0
dsgrid/cli/registry.py ADDED
@@ -0,0 +1,1862 @@
1
+ """Manages a dsgrid registry."""
2
+
3
+ import getpass
4
+ import logging
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ import rich_click as click
9
+ from rich import print
10
+ from semver import VersionInfo
11
+
12
+ from dsgrid.cli.common import (
13
+ get_value_from_context,
14
+ handle_dsgrid_exception,
15
+ path_callback,
16
+ )
17
+ from dsgrid.common import REMOTE_REGISTRY
18
+ from dsgrid.dataset.models import TableFormat
19
+ from dsgrid.dimension.base_models import DimensionType
20
+ from dsgrid.dimension.time import TimeDimensionType
21
+ from dsgrid.config.common import SUPPORTED_METRIC_TYPES
22
+ from dsgrid.config.project_config import ProjectConfig
23
+ from dsgrid.registry.bulk_register import bulk_register
24
+ from dsgrid.registry.common import (
25
+ DatabaseConnection,
26
+ DatasetRegistryStatus,
27
+ VersionUpdateType,
28
+ )
29
+ from dsgrid.registry.dataset_config_generator import generate_config_from_dataset
30
+ from dsgrid.registry.registry_manager import RegistryManager
31
+ from dsgrid.registry.project_config_generator import generate_project_config
32
+ from dsgrid.utils.filters import ACCEPTED_OPS
33
+
34
+
35
+ logger = logging.getLogger(__name__)
36
+
37
+
38
+ def _version_info_callback(*args) -> VersionInfo | None:
39
+ val = args[2]
40
+ if val is None:
41
+ return val
42
+ return VersionInfo.parse(val)
43
+
44
+
45
+ def _version_info_required_callback(*args) -> VersionInfo:
46
+ val = args[2]
47
+ return VersionInfo.parse(val)
48
+
49
+
50
+ def _version_update_callback(*args) -> VersionUpdateType:
51
+ val = args[2]
52
+ return VersionUpdateType(val)
53
+
54
+
55
+ """
56
+ Click Group Definitions
57
+ """
58
+
59
+
60
+ @click.group()
61
+ @click.pass_context
62
+ def registry(ctx):
63
+ """Manage a registry."""
64
+ conn = DatabaseConnection(
65
+ url=get_value_from_context(ctx, "url"),
66
+ )
67
+ scratch_dir = get_value_from_context(ctx, "scratch_dir")
68
+ no_prompts = ctx.parent.params["no_prompts"]
69
+ if "--help" in sys.argv:
70
+ ctx.obj = None
71
+ else:
72
+ ctx.obj = RegistryManager.load(
73
+ conn,
74
+ REMOTE_REGISTRY,
75
+ offline_mode=True,
76
+ no_prompts=no_prompts,
77
+ scratch_dir=scratch_dir,
78
+ )
79
+
80
+
81
+ @registry.result_callback()
82
+ @click.pass_context
83
+ def cleanup_registry_manager(ctx, result, **kwargs):
84
+ """Cleanup the registry manager after the command finishes."""
85
+ if ctx.obj is not None:
86
+ ctx.obj.dispose()
87
+
88
+
89
+ @click.group()
90
+ @click.pass_obj
91
+ def dimensions(registry_manager: RegistryManager):
92
+ """Dimension subcommands"""
93
+
94
+
95
+ @click.group()
96
+ @click.pass_obj
97
+ def dimension_mappings(registry_manager: RegistryManager):
98
+ """Dimension mapping subcommands"""
99
+
100
+
101
+ @click.group()
102
+ @click.pass_obj
103
+ def projects(registry_manager: RegistryManager):
104
+ """Project subcommands"""
105
+
106
+
107
+ @click.group()
108
+ @click.pass_obj
109
+ def datasets(registry_manager: RegistryManager):
110
+ """Dataset subcommands"""
111
+
112
+
113
+ """
114
+ Registry Commands
115
+ """
116
+
117
+
118
+ # TODO: Support registry file reads without syncing using something like sfs3
119
+ @click.command(name="list")
120
+ @click.pass_obj
121
+ def list_(registry_manager):
122
+ """List the contents of a registry."""
123
+ print(f"Registry: {registry_manager.path}")
124
+ registry_manager.show()
125
+
126
+
127
+ """
128
+ Dimension Commands
129
+ """
130
+
131
+
132
+ _list_dimensions_epilog = """
133
+ Examples:\n
134
+ $ dsgrid registry dimensions list\n
135
+ $ dsgrid registry dimensions list -f "Type == sector"\n
136
+ $ dsgrid registry dimensions list -f "Submitter == username"\n
137
+ """
138
+
139
+
140
+ @click.command(name="list", epilog=_list_dimensions_epilog)
141
+ @click.option(
142
+ "-f",
143
+ "--filter",
144
+ multiple=True,
145
+ type=str,
146
+ help=f"""
147
+ Filter table with a case-insensitive expression in the format 'column operation value',
148
+ accepts multiple flags\b\n
149
+ valid operations: {ACCEPTED_OPS}\n
150
+ """,
151
+ )
152
+ @click.pass_obj
153
+ def list_dimensions(registry_manager, filter):
154
+ """List the registered dimensions."""
155
+ registry_manager.dimension_manager.show(filters=filter)
156
+
157
+
158
+ _register_dimensions_epilog = """
159
+ Examples:\n
160
+ $ dsgrid registry dimensions register -l "Register dimensions for my-project" dimensions.json5\n
161
+ """
162
+
163
+
164
+ @click.command(name="register", epilog=_register_dimensions_epilog)
165
+ @click.argument("dimension-config-file", type=click.Path(exists=True), callback=path_callback)
166
+ @click.option(
167
+ "-l",
168
+ "--log-message",
169
+ required=True,
170
+ help="reason for submission",
171
+ )
172
+ @click.pass_obj
173
+ @click.pass_context
174
+ def register_dimensions(
175
+ ctx,
176
+ registry_manager: RegistryManager,
177
+ dimension_config_file: Path,
178
+ log_message: str,
179
+ ):
180
+ """Register new dimensions with the dsgrid repository. The contents of the JSON/JSON5 file
181
+ must match the data model defined by this documentation:
182
+ https://dsgrid.github.io/dsgrid/reference/data_models/dimension.html#dsgrid.config.dimensions.DimensionsConfigModel
183
+ """
184
+ manager = registry_manager.dimension_manager
185
+ submitter = getpass.getuser()
186
+ res = handle_dsgrid_exception(
187
+ ctx, manager.register, dimension_config_file, submitter, log_message
188
+ )
189
+ if res[1] != 0:
190
+ ctx.exit(res[1])
191
+
192
+
193
+ _dump_dimension_epilog = """
194
+ Examples:\n
195
+ $ dsgrid registry dimensions dump 8c575746-18fa-4b65-bf1f-516079d634a5\n
196
+ """
197
+
198
+
199
+ @click.command(name="dump", epilog=_dump_dimension_epilog)
200
+ @click.argument("dimension-id")
201
+ @click.option(
202
+ "-v",
203
+ "--version",
204
+ callback=_version_info_callback,
205
+ help="Version to dump; defaults to latest",
206
+ )
207
+ @click.option(
208
+ "-d",
209
+ "--directory",
210
+ default=".",
211
+ type=click.Path(exists=True),
212
+ help="Directory in which to create config and data files",
213
+ )
214
+ @click.option(
215
+ "--force",
216
+ is_flag=True,
217
+ default=False,
218
+ show_default=True,
219
+ help="Overwrite files if they exist.",
220
+ )
221
+ @click.pass_obj
222
+ @click.pass_context
223
+ def dump_dimension(ctx, registry_manager, dimension_id, version, directory, force):
224
+ """Dump a dimension config file (and any related data) from the registry."""
225
+ manager = registry_manager.dimension_manager
226
+ res = handle_dsgrid_exception(
227
+ ctx, manager.dump, dimension_id, Path(directory), version=version, force=force
228
+ )
229
+ if res[1] != 0:
230
+ ctx.exit(res[1])
231
+
232
+
233
+ _show_dimension_epilog = """
234
+ Examples:\n
235
+ $ dsgrid registry dimensions show 8c575746-18fa-4b65-bf1f-516079d634a5
236
+ """
237
+
238
+
239
+ @click.command(name="show", epilog=_show_dimension_epilog)
240
+ @click.argument("dimension-id", type=str)
241
+ @click.option(
242
+ "-v",
243
+ "--version",
244
+ default="1.0.0",
245
+ show_default=True,
246
+ callback=_version_info_required_callback,
247
+ help="Version to show.",
248
+ )
249
+ @click.pass_obj
250
+ @click.pass_context
251
+ def show_dimension(
252
+ ctx,
253
+ registry_manager: RegistryManager,
254
+ dimension_id: str,
255
+ version: str,
256
+ ):
257
+ """Show an existing dimension in the registry."""
258
+ manager = registry_manager.dimension_manager
259
+ res = handle_dsgrid_exception(
260
+ ctx,
261
+ manager.get_by_id,
262
+ dimension_id,
263
+ version,
264
+ )
265
+ if res[1] != 0:
266
+ ctx.exit(res[1])
267
+ dim = res[0]
268
+ print(
269
+ f"""id={dim.model.dimension_id}
270
+ type={dim.model.dimension_type.value}
271
+ name={dim.model.name}
272
+ description={dim.model.description}
273
+ """
274
+ )
275
+ if dim.model.dimension_type != DimensionType.TIME:
276
+ records = dim.get_records_dataframe()
277
+ records.show(n=4000)
278
+
279
+
280
+ _update_dimension_epilog = """
281
+ Examples:\n
282
+ $ dsgrid registry dimensions update -d 8c575746-18fa-4b65-bf1f-516079d634a5 -l "Update county dimension" -u major -v 1.0.0 dimension.json5\n
283
+ """
284
+
285
+
286
+ @click.command(name="update", epilog=_update_dimension_epilog)
287
+ @click.argument("dimension-config-file", type=click.Path(exists=True), callback=path_callback)
288
+ @click.option(
289
+ "-d",
290
+ "--dimension-id",
291
+ required=True,
292
+ type=str,
293
+ help="dimension ID",
294
+ )
295
+ @click.option(
296
+ "-l",
297
+ "--log-message",
298
+ required=True,
299
+ type=str,
300
+ help="reason for submission",
301
+ )
302
+ @click.option(
303
+ "-t",
304
+ "--update-type",
305
+ required=True,
306
+ type=click.Choice([x.value for x in VersionUpdateType]),
307
+ callback=_version_update_callback,
308
+ )
309
+ @click.option(
310
+ "-v",
311
+ "--version",
312
+ required=True,
313
+ callback=_version_info_required_callback,
314
+ help="Version to update; must be the current version.",
315
+ )
316
+ @click.pass_obj
317
+ @click.pass_context
318
+ def update_dimension(
319
+ ctx,
320
+ registry_manager: RegistryManager,
321
+ dimension_config_file: Path,
322
+ dimension_id: str,
323
+ log_message: str,
324
+ update_type: VersionUpdateType,
325
+ version: str,
326
+ ):
327
+ """Update an existing dimension in the registry."""
328
+ manager = registry_manager.dimension_manager
329
+ submitter = getpass.getuser()
330
+ res = handle_dsgrid_exception(
331
+ ctx,
332
+ manager.update_from_file,
333
+ dimension_config_file,
334
+ dimension_id,
335
+ submitter,
336
+ update_type,
337
+ log_message,
338
+ version,
339
+ )
340
+ if res[1] != 0:
341
+ ctx.exit(res[1])
342
+
343
+
344
+ @click.command(name="remove")
345
+ @click.argument("dimension-id")
346
+ @click.pass_obj
347
+ def remove_dimension(registry_manager: RegistryManager, dimension_id: str):
348
+ """Remove a dimension from the dsgrid repository."""
349
+ registry_manager.dimension_manager.remove(dimension_id)
350
+
351
+
352
+ """
353
+ Dimension Mapping Commands
354
+ """
355
+
356
+
357
+ _list_dimension_mappings_epilog = """
358
+ Examples:\n
359
+ $ dsgrid registry dimension-mappings list\n
360
+ $ dsgrid registry dimension-mappings list -f "Type [From, To] contains geography" -f "Submitter == username"\n
361
+ """
362
+
363
+
364
+ @click.command(name="list", epilog=_list_dimension_mappings_epilog)
365
+ @click.option(
366
+ "-f",
367
+ "--filter",
368
+ multiple=True,
369
+ type=str,
370
+ help=f"""
371
+ Filter table with a case-insensitive expression in the format 'column operation value',
372
+ accepts multiple flags\b\n
373
+ valid operations: {ACCEPTED_OPS}\n
374
+ """,
375
+ )
376
+ @click.pass_obj
377
+ def list_dimension_mappings(registry_manager, filter):
378
+ """List the registered dimension mappings."""
379
+ registry_manager.dimension_mapping_manager.show(filters=filter)
380
+
381
+
382
+ _register_dimension_mappings_epilog = """
383
+ Examples:\\
384
+ $ dsgrid registry dimension-mappings register -l "Register dimension mappings for my-project" dimension_mappings.json5\n
385
+ """
386
+
387
+
388
+ @click.command(name="register", epilog=_register_dimension_mappings_epilog)
389
+ @click.argument(
390
+ "dimension-mapping-config-file",
391
+ type=click.Path(exists=True),
392
+ callback=path_callback,
393
+ )
394
+ @click.option(
395
+ "-l",
396
+ "--log-message",
397
+ required=True,
398
+ help="reason for submission",
399
+ )
400
+ @click.pass_obj
401
+ @click.pass_context
402
+ def register_dimension_mappings(
403
+ ctx,
404
+ registry_manager: RegistryManager,
405
+ dimension_mapping_config_file: Path,
406
+ log_message: str,
407
+ ):
408
+ """Register new dimension mappings with the dsgrid repository. The contents of the JSON/JSON5
409
+ file must match the data model defined by this documentation:
410
+ https://dsgrid.github.io/dsgrid/reference/data_models/dimension_mapping.html#dsgrid.config.dimension_mappings_config.DimensionMappingsConfigModel
411
+ """
412
+ submitter = getpass.getuser()
413
+ manager = registry_manager.dimension_mapping_manager
414
+ res = handle_dsgrid_exception(
415
+ ctx, manager.register, dimension_mapping_config_file, submitter, log_message
416
+ )
417
+ if res[1] != 0:
418
+ ctx.exit(res[1])
419
+
420
+
421
+ _dump_dimension_mapping_epilog = """
422
+ Examples:\n
423
+ $ dsgrid registry dimension-mappings dump 8c575746-18fa-4b65-bf1f-516079d634a5\n
424
+ """
425
+
426
+
427
+ @click.command(name="dump", epilog=_dump_dimension_mapping_epilog)
428
+ @click.argument("dimension-mapping-id")
429
+ @click.option(
430
+ "-v",
431
+ "--version",
432
+ callback=_version_info_callback,
433
+ help="Version to dump; defaults to latest",
434
+ )
435
+ @click.option(
436
+ "-d",
437
+ "--directory",
438
+ default=".",
439
+ type=click.Path(exists=True),
440
+ help="Directory in which to create config and data files",
441
+ )
442
+ @click.option(
443
+ "--force",
444
+ is_flag=True,
445
+ default=False,
446
+ show_default=True,
447
+ help="Overwrite files if they exist.",
448
+ )
449
+ @click.pass_obj
450
+ @click.pass_context
451
+ def dump_dimension_mapping(
452
+ ctx,
453
+ registry_manager: RegistryManager,
454
+ dimension_mapping_id: str,
455
+ version: str,
456
+ directory: Path,
457
+ force: bool,
458
+ ):
459
+ """Dump a dimension mapping config file (and any related data) from the registry."""
460
+ manager = registry_manager.dimension_mapping_manager
461
+ res = handle_dsgrid_exception(
462
+ ctx,
463
+ manager.dump,
464
+ dimension_mapping_id,
465
+ Path(directory),
466
+ version=version,
467
+ force=force,
468
+ )
469
+ if res[1] != 0:
470
+ ctx.exit(res[1])
471
+
472
+
473
+ _show_dimension_mapping_epilog = """
474
+ Examples:\n
475
+ $ dsgrid registry dimension-mappings show 8c575746-18fa-4b65-bf1f-516079d634a5
476
+ """
477
+
478
+
479
+ @click.command(name="show", epilog=_show_dimension_mapping_epilog)
480
+ @click.argument("mapping-id", type=str)
481
+ @click.option(
482
+ "-v",
483
+ "--version",
484
+ default="1.0.0",
485
+ show_default=True,
486
+ callback=_version_info_required_callback,
487
+ help="Version to show.",
488
+ )
489
+ @click.pass_obj
490
+ @click.pass_context
491
+ def show_dimension_mapping(
492
+ ctx,
493
+ registry_manager: RegistryManager,
494
+ mapping_id: str,
495
+ version: str,
496
+ ):
497
+ """Show an existing dimension mapping in the registry."""
498
+ manager = registry_manager.dimension_mapping_manager
499
+ res = handle_dsgrid_exception(
500
+ ctx,
501
+ manager.get_by_id,
502
+ mapping_id,
503
+ version,
504
+ )
505
+ if res[1] != 0:
506
+ ctx.exit(res[1])
507
+ mapping = res[0]
508
+ from_dim = registry_manager.dimension_manager.get_by_id(
509
+ mapping.model.from_dimension.dimension_id
510
+ )
511
+ to_dim = registry_manager.dimension_manager.get_by_id(mapping.model.to_dimension.dimension_id)
512
+ print(
513
+ f"""
514
+ type={mapping.model.from_dimension.dimension_type}
515
+ from_id={mapping.model.from_dimension.dimension_id}
516
+ from_name={from_dim.model.name}
517
+ from_description={from_dim.model.description}
518
+ to_id={mapping.model.to_dimension.dimension_id}
519
+ to_name={to_dim.model.name}
520
+ to_description={to_dim.model.description}
521
+ """
522
+ )
523
+ records = mapping.get_records_dataframe()
524
+ records.show(n=4000)
525
+
526
+
527
+ _update_dimension_mapping_epilog = """
528
+ Examples:\n
529
+ $ dsgrid registry dimension-mappings update \\ \n
530
+ -d 8c575746-18fa-4b65-bf1f-516079d634a5 \\ \n
531
+ -l "Swap out the state to county mapping for my-dataset to that-project" \\ \n
532
+ -u major \\ \n
533
+ -v 1.0.0 dimension_mappings.json5"
534
+ """
535
+
536
+
537
+ @click.command(name="update", epilog=_update_dimension_mapping_epilog)
538
+ @click.argument(
539
+ "dimension-mapping-config-file",
540
+ type=click.Path(exists=True),
541
+ callback=path_callback,
542
+ )
543
+ @click.option(
544
+ "-d",
545
+ "--dimension-mapping-id",
546
+ required=True,
547
+ type=str,
548
+ help="dimension mapping ID",
549
+ )
550
+ @click.option(
551
+ "-l",
552
+ "--log-message",
553
+ required=True,
554
+ type=str,
555
+ help="reason for submission",
556
+ )
557
+ @click.option(
558
+ "-t",
559
+ "--update-type",
560
+ required=True,
561
+ type=click.Choice([x.value for x in VersionUpdateType]),
562
+ callback=_version_update_callback,
563
+ )
564
+ @click.option(
565
+ "-v",
566
+ "--version",
567
+ required=True,
568
+ callback=_version_info_required_callback,
569
+ help="Version to update; must be the current version.",
570
+ )
571
+ @click.pass_obj
572
+ def update_dimension_mapping(
573
+ registry_manager: RegistryManager,
574
+ dimension_mapping_config_file: Path,
575
+ dimension_mapping_id: str,
576
+ log_message: str,
577
+ update_type: VersionUpdateType,
578
+ version: str,
579
+ ):
580
+ """Update an existing dimension mapping registry. The contents of the JSON/JSON5 file must
581
+ match the data model defined by this documentation:
582
+ https://dsgrid.github.io/dsgrid/reference/data_models/dimension_mapping.html#dsgrid.config.mapping_tables.MappingTableModel
583
+ """
584
+ manager = registry_manager.dimension_mapping_manager
585
+ submitter = getpass.getuser()
586
+ manager.update_from_file(
587
+ dimension_mapping_config_file,
588
+ dimension_mapping_id,
589
+ submitter,
590
+ update_type,
591
+ log_message,
592
+ version,
593
+ )
594
+
595
+
596
+ @click.command(name="remove")
597
+ @click.argument("dimension-mapping-id")
598
+ @click.pass_obj
599
+ def remove_dimension_mapping(registry_manager: RegistryManager, dimension_mapping_id: str):
600
+ """Remove a dimension mapping from the dsgrid repository."""
601
+ registry_manager.dimension_mapping_manager.remove(dimension_mapping_id)
602
+
603
+
604
+ """
605
+ Project Commands
606
+ """
607
+
608
+
609
+ _list_projects_epilog = """
610
+ Examples:\n
611
+ $ dsgrid registry projects list\n
612
+ $ dsgrid registry projects list -f "ID contains efs"\n
613
+ """
614
+
615
+
616
+ @click.command(name="list", epilog=_list_projects_epilog)
617
+ @click.option(
618
+ "-f",
619
+ "--filter",
620
+ multiple=True,
621
+ type=str,
622
+ help=f"""
623
+ Filter table with a case-insensitive expression in the format 'column operation value',
624
+ accepts multiple flags\b\n
625
+ valid operations: {ACCEPTED_OPS}\n
626
+ """,
627
+ )
628
+ @click.pass_obj
629
+ @click.pass_context
630
+ def list_projects(ctx, registry_manager, filter):
631
+ """List the registered projects."""
632
+ res = handle_dsgrid_exception(ctx, registry_manager.project_manager.show, filters=filter)
633
+ if res[1] != 0:
634
+ ctx.exit(res[1])
635
+
636
+
637
+ _register_project_epilog = """
638
+ Examples:\n
639
+ $ dsgrid registry projects register -l "Register project my-project" project.json5\n
640
+ """
641
+
642
+
643
+ @click.command(name="register", epilog=_register_project_epilog)
644
+ @click.argument("project-config-file", type=click.Path(exists=True), callback=path_callback)
645
+ @click.option(
646
+ "-l",
647
+ "--log-message",
648
+ required=True,
649
+ help="reason for submission",
650
+ )
651
+ @click.pass_obj
652
+ @click.pass_context
653
+ def register_project(
654
+ ctx,
655
+ registry_manager,
656
+ project_config_file,
657
+ log_message,
658
+ ):
659
+ """Register a new project with the dsgrid repository. The contents of the JSON/JSON5 file must
660
+ match the data model defined by this documentation:
661
+ https://dsgrid.github.io/dsgrid/reference/data_models/project.html#dsgrid.config.project_config.ProjectConfigModel
662
+ """
663
+ submitter = getpass.getuser()
664
+ res = handle_dsgrid_exception(
665
+ ctx,
666
+ registry_manager.project_manager.register,
667
+ project_config_file,
668
+ submitter,
669
+ log_message,
670
+ )
671
+ if res[1] != 0:
672
+ ctx.exit(res[1])
673
+
674
+
675
+ _submit_dataset_epilog = """
676
+ Examples:\n
677
+ $ dsgrid registry projects submit-dataset \\ \n
678
+ -p my-project-id \\ \n
679
+ -d my-dataset-id \\ \n
680
+ -m dimension_mappings.json5 \\ \n
681
+ -l "Submit dataset my-dataset to project my-project."\n
682
+ """
683
+
684
+
685
+ @click.command(epilog=_submit_dataset_epilog)
686
+ @click.option(
687
+ "-d",
688
+ "--dataset-id",
689
+ required=True,
690
+ type=str,
691
+ help="dataset identifier",
692
+ )
693
+ @click.option(
694
+ "-p",
695
+ "--project-id",
696
+ required=True,
697
+ type=str,
698
+ help="project identifier",
699
+ )
700
+ @click.option(
701
+ "-m",
702
+ "--dimension-mapping-file",
703
+ type=click.Path(exists=True),
704
+ show_default=True,
705
+ help="Dimension mapping file. Must match the data model defined by "
706
+ "https://dsgrid.github.io/dsgrid/reference/data_models/dimension_mapping.html#dsgrid.config.dimension_mappings_config.DimensionMappingsConfigModel",
707
+ callback=path_callback,
708
+ )
709
+ @click.option(
710
+ "-r",
711
+ "--dimension-mapping-references-file",
712
+ type=click.Path(exists=True),
713
+ show_default=True,
714
+ help="dimension mapping references file. Mutually exclusive with dimension_mapping_file. "
715
+ "Use it when the mappings are already registered. Must mach the data model defined by "
716
+ "https://dsgrid.github.io/dsgrid/reference/data_models/dimension_mapping.html#dsgrid.config.dimension_mapping_base.DimensionMappingReferenceListModel",
717
+ callback=path_callback,
718
+ )
719
+ @click.option(
720
+ "-a",
721
+ "--autogen-reverse-supplemental-mappings",
722
+ type=click.Choice([x.value for x in DimensionType]),
723
+ callback=lambda _, __, x: [DimensionType(y) for y in x],
724
+ multiple=True,
725
+ help="For any dimension listed here, if the dataset's dimension is a project's supplemental "
726
+ "dimension and no mapping is provided, create a reverse mapping from that supplemental "
727
+ "dimension.",
728
+ )
729
+ @click.option(
730
+ "-l",
731
+ "--log-message",
732
+ required=True,
733
+ type=str,
734
+ help="reason for submission",
735
+ )
736
+ @click.pass_obj
737
+ def submit_dataset(
738
+ registry_manager: RegistryManager,
739
+ dataset_id: str,
740
+ project_id: str,
741
+ dimension_mapping_file: Path,
742
+ dimension_mapping_references_file: Path,
743
+ autogen_reverse_supplemental_mappings: list[DimensionType],
744
+ log_message: str,
745
+ ):
746
+ """Submit a dataset to a dsgrid project."""
747
+ submitter = getpass.getuser()
748
+ manager = registry_manager.project_manager
749
+ manager.submit_dataset(
750
+ project_id,
751
+ dataset_id,
752
+ submitter,
753
+ log_message,
754
+ dimension_mapping_file=dimension_mapping_file,
755
+ dimension_mapping_references_file=dimension_mapping_references_file,
756
+ autogen_reverse_supplemental_mappings=autogen_reverse_supplemental_mappings,
757
+ )
758
+
759
+
760
+ _register_and_submit_dataset_epilog = """
761
+ Examples:\n
762
+ $ dsgrid registry projects register-and-submit-dataset \\ \n
763
+ -c dataset.json5 \\ \n
764
+ -p my-project-id \\ \n
765
+ -m dimension_mappings.json5 \\ \n
766
+ -l "Register and submit dataset my-dataset to project my-project." \n
767
+
768
+ $ dsgrid registry projects register-and-submit-dataset \\ \n
769
+ -c dataset.json5 \\ \n
770
+ --data-base-dir /path/to/data \\ \n
771
+ -p my-project-id \\ \n
772
+ -m dimension_mappings.json5 \\ \n
773
+ -l "Register and submit dataset my-dataset to project my-project." \n
774
+ """
775
+
776
+
777
+ @click.command(epilog=_register_and_submit_dataset_epilog)
778
+ @click.option(
779
+ "-c",
780
+ "--dataset-config-file",
781
+ required=True,
782
+ type=click.Path(exists=True),
783
+ callback=path_callback,
784
+ help="Dataset config file (must include data_layout with data_file paths)",
785
+ )
786
+ @click.option(
787
+ "-m",
788
+ "--dimension-mapping-file",
789
+ type=click.Path(exists=True),
790
+ help="Dimension mapping file. Must match the data model defined by "
791
+ "https://dsgrid.github.io/dsgrid/reference/data_models/dimension_mapping.html#dsgrid.config.dimension_mappings_config.DimensionMappingsConfigModel",
792
+ callback=path_callback,
793
+ )
794
+ @click.option(
795
+ "-r",
796
+ "--dimension-mapping-references-file",
797
+ type=click.Path(exists=True),
798
+ show_default=True,
799
+ help="dimension mapping references file. Mutually exclusive with dimension_mapping_file. "
800
+ "Use it when the mappings are already registered. Must mach the data model defined by "
801
+ "https://dsgrid.github.io/dsgrid/reference/data_models/dimension_mapping.html#dsgrid.config.dimension_mapping_base.DimensionMappingReferenceListModel",
802
+ callback=path_callback,
803
+ )
804
+ @click.option(
805
+ "-a",
806
+ "--autogen-reverse-supplemental-mappings",
807
+ type=click.Choice([x.value for x in DimensionType]),
808
+ callback=lambda _, __, x: [DimensionType(y) for y in x],
809
+ multiple=True,
810
+ help="For any dimension listed here, if the dataset's dimension is a project's supplemental "
811
+ "dimension and no mapping is provided, create a reverse mapping from that supplemental "
812
+ "dimension.",
813
+ )
814
+ @click.option(
815
+ "-p",
816
+ "--project-id",
817
+ required=True,
818
+ type=str,
819
+ help="project identifier",
820
+ )
821
+ @click.option(
822
+ "-l",
823
+ "--log-message",
824
+ required=True,
825
+ type=str,
826
+ help="reason for submission",
827
+ )
828
+ @click.option(
829
+ "-D",
830
+ "--data-base-dir",
831
+ type=click.Path(exists=True),
832
+ callback=path_callback,
833
+ help="Base directory for data files. If set and data file paths are relative, "
834
+ "prepend them with this path.",
835
+ )
836
+ @click.option(
837
+ "-M",
838
+ "--missing-associations-base-dir",
839
+ type=click.Path(exists=True),
840
+ callback=path_callback,
841
+ help="Base directory for missing associations files. If set and missing associations "
842
+ "paths are relative, prepend them with this path.",
843
+ )
844
+ @click.pass_obj
845
+ @click.pass_context
846
+ def register_and_submit_dataset(
847
+ ctx,
848
+ registry_manager,
849
+ dataset_config_file,
850
+ dimension_mapping_file,
851
+ dimension_mapping_references_file,
852
+ autogen_reverse_supplemental_mappings,
853
+ project_id,
854
+ log_message,
855
+ data_base_dir,
856
+ missing_associations_base_dir,
857
+ ):
858
+ """Register a dataset and then submit it to a dsgrid project.
859
+
860
+ The dataset config file must include a data_layout with data_file and optional
861
+ lookup_data_file paths pointing to the dataset files.
862
+ """
863
+ submitter = getpass.getuser()
864
+ manager = registry_manager.project_manager
865
+ res = handle_dsgrid_exception(
866
+ ctx,
867
+ manager.register_and_submit_dataset,
868
+ dataset_config_file,
869
+ project_id,
870
+ submitter,
871
+ log_message,
872
+ dimension_mapping_file=dimension_mapping_file,
873
+ dimension_mapping_references_file=dimension_mapping_references_file,
874
+ autogen_reverse_supplemental_mappings=autogen_reverse_supplemental_mappings,
875
+ data_base_dir=data_base_dir,
876
+ missing_associations_base_dir=missing_associations_base_dir,
877
+ )
878
+ if res[1] != 0:
879
+ ctx.exit(res[1])
880
+
881
+
882
+ _dump_project_epilog = """
883
+ Examples:\n
884
+ $ dsgrid registry projects dump my-project-id\n
885
+ """
886
+
887
+
888
+ @click.command(name="dump", epilog=_dump_project_epilog)
889
+ @click.argument("project-id")
890
+ @click.option(
891
+ "-v",
892
+ "--version",
893
+ callback=_version_info_callback,
894
+ help="Version to dump; defaults to latest",
895
+ )
896
+ @click.option(
897
+ "-d",
898
+ "--directory",
899
+ default=".",
900
+ type=click.Path(exists=True),
901
+ help="Directory in which to create the config file",
902
+ )
903
+ @click.option(
904
+ "--force",
905
+ is_flag=True,
906
+ default=False,
907
+ show_default=True,
908
+ help="Overwrite files if they exist.",
909
+ )
910
+ @click.pass_obj
911
+ @click.pass_context
912
+ def dump_project(
913
+ ctx,
914
+ registry_manager: RegistryManager,
915
+ project_id: str,
916
+ version: str,
917
+ directory: Path,
918
+ force: bool,
919
+ ):
920
+ """Dump a project config file from the registry."""
921
+ manager = registry_manager.project_manager
922
+ res = handle_dsgrid_exception(
923
+ ctx, manager.dump, project_id, directory, version=version, force=force
924
+ )
925
+ if res[1] != 0:
926
+ ctx.exit(res[1])
927
+
928
+
929
+ _update_project_epilog = """
930
+ Examples: \n
931
+ $ dsgrid registry projects update \\ \n
932
+ -p my-project-id \\ \n
933
+ -u patch \\ \n
934
+ -v 1.5.0 \\ \n
935
+ -l "Update description for project my-project-id." \n
936
+ """
937
+
938
+
939
+ @click.command(name="update", epilog=_update_project_epilog)
940
+ @click.argument("project-config-file", type=click.Path(exists=True), callback=path_callback)
941
+ @click.option(
942
+ "-p",
943
+ "--project-id",
944
+ required=True,
945
+ type=str,
946
+ help="project ID",
947
+ )
948
+ @click.option(
949
+ "-l",
950
+ "--log-message",
951
+ required=True,
952
+ type=str,
953
+ help="reason for submission",
954
+ )
955
+ @click.option(
956
+ "-t",
957
+ "--update-type",
958
+ required=True,
959
+ type=click.Choice([x.value for x in VersionUpdateType]),
960
+ callback=_version_update_callback,
961
+ )
962
+ @click.option(
963
+ "-v",
964
+ "--version",
965
+ required=True,
966
+ callback=_version_info_required_callback,
967
+ help="Version to update; must be the current version.",
968
+ )
969
+ @click.pass_obj
970
+ @click.pass_context
971
+ def update_project(
972
+ ctx,
973
+ registry_manager: RegistryManager,
974
+ project_config_file: Path,
975
+ project_id: str,
976
+ log_message: str,
977
+ update_type: VersionUpdateType,
978
+ version: str,
979
+ ):
980
+ """Update an existing project in the registry."""
981
+ manager = registry_manager.project_manager
982
+ submitter = getpass.getuser()
983
+ res = handle_dsgrid_exception(
984
+ ctx,
985
+ manager.update_from_file,
986
+ project_config_file,
987
+ project_id,
988
+ submitter,
989
+ update_type,
990
+ log_message,
991
+ version,
992
+ )
993
+ if res[1] != 0:
994
+ ctx.exit(res[1])
995
+
996
+
997
+ _register_subset_dimensions_epilog = """
998
+ Examples:\n
999
+ $ dsgrid registry projects register-subset-dimensions \\ \n
1000
+ -l "Register subset dimensions for end uses by fuel type for my-project-id." \\ \n
1001
+ my-project-id \\ \n
1002
+ subset_dimensions.json5 \n
1003
+ """
1004
+
1005
+
1006
+ @click.command(epilog=_register_subset_dimensions_epilog)
1007
+ @click.pass_obj
1008
+ @click.pass_context
1009
+ @click.argument("project_id")
1010
+ @click.argument("filename", callback=path_callback)
1011
+ @click.option(
1012
+ "-l",
1013
+ "--log-message",
1014
+ required=True,
1015
+ type=str,
1016
+ help="Please specify the reason for this addition.",
1017
+ )
1018
+ @click.option(
1019
+ "-t",
1020
+ "--update-type",
1021
+ default="patch",
1022
+ type=click.Choice([x.value for x in VersionUpdateType]),
1023
+ callback=_version_update_callback,
1024
+ )
1025
+ def register_subset_dimensions(
1026
+ ctx,
1027
+ registry_manager: RegistryManager,
1028
+ project_id: str,
1029
+ filename: Path,
1030
+ log_message: str,
1031
+ update_type: VersionUpdateType,
1032
+ ):
1033
+ """Register new subset dimensions with a project. The contents of the JSON/JSON5 file must
1034
+ match the data model defined by this documentation:
1035
+
1036
+ https://dsgrid.github.io/dsgrid/reference/data_models/project.html#dsgrid.config.project_config.SubsetDimensionGroupListModel
1037
+ """
1038
+
1039
+ submitter = getpass.getuser()
1040
+ project_mgr = registry_manager.project_manager
1041
+ res = handle_dsgrid_exception(
1042
+ ctx,
1043
+ project_mgr.register_subset_dimensions,
1044
+ project_id,
1045
+ filename,
1046
+ submitter,
1047
+ log_message,
1048
+ update_type,
1049
+ )
1050
+ if res[1] != 0:
1051
+ ctx.exit(res[1])
1052
+
1053
+
1054
+ _register_supplemental_dimensions_epilog = """
1055
+ Examples:\n
1056
+ $ dsgrid registry projects register-supplemental-dimensions \\ \n
1057
+ -l "Register states supplemental dimension for my-project-id" \\ \n
1058
+ my-project-id \\ \n
1059
+ supplemental_dimensions.json5\n
1060
+ """
1061
+
1062
+
1063
+ @click.command(epilog=_register_supplemental_dimensions_epilog)
1064
+ @click.pass_obj
1065
+ @click.pass_context
1066
+ @click.argument("project_id")
1067
+ @click.argument("filename", callback=path_callback)
1068
+ @click.option(
1069
+ "-l",
1070
+ "--log-message",
1071
+ required=True,
1072
+ type=str,
1073
+ help="Please specify the reason for this addition.",
1074
+ )
1075
+ @click.option(
1076
+ "-t",
1077
+ "--update-type",
1078
+ default="patch",
1079
+ type=click.Choice([x.value for x in VersionUpdateType]),
1080
+ callback=_version_update_callback,
1081
+ )
1082
+ def register_supplemental_dimensions(
1083
+ ctx, registry_manager, project_id, filename: Path, log_message, update_type
1084
+ ):
1085
+ """Register new supplemental dimensions with a project. The contents of the JSON/JSON5 file
1086
+ must match the data model defined by this documentation:
1087
+ https://dsgrid.github.io/dsgrid/reference/data_models/project.html#dsgrid.config.supplemental_dimension.SupplementalDimensionsListModel
1088
+ """
1089
+
1090
+ submitter = getpass.getuser()
1091
+ project_mgr = registry_manager.project_manager
1092
+ res = handle_dsgrid_exception(
1093
+ ctx,
1094
+ project_mgr.register_supplemental_dimensions,
1095
+ project_id,
1096
+ filename,
1097
+ submitter,
1098
+ log_message,
1099
+ update_type,
1100
+ )
1101
+ if res[1] != 0:
1102
+ ctx.exit(res[1])
1103
+
1104
+
1105
+ _add_dataset_requirements_epilog = """
1106
+ Examples:\n
1107
+ $ dsgrid registry projects add-dataset-requirements \\ \n
1108
+ -l "Add requirements for dataset my-dataset-id to my-project-id." \\ \n
1109
+ my-project-id \\ \n
1110
+ dataset_requirements.json5\n
1111
+ """
1112
+
1113
+
1114
+ @click.command(epilog=_add_dataset_requirements_epilog)
1115
+ @click.pass_obj
1116
+ @click.pass_context
1117
+ @click.argument("project_id")
1118
+ @click.argument("filename", callback=path_callback)
1119
+ @click.option(
1120
+ "-l",
1121
+ "--log-message",
1122
+ required=True,
1123
+ type=str,
1124
+ help="Please specify the reason for the new datasets.",
1125
+ )
1126
+ @click.option(
1127
+ "-t",
1128
+ "--update-type",
1129
+ default="patch",
1130
+ type=click.Choice([x.value for x in VersionUpdateType]),
1131
+ callback=_version_update_callback,
1132
+ )
1133
+ def add_dataset_requirements(
1134
+ ctx, registry_manager, project_id, filename: Path, log_message, update_type
1135
+ ):
1136
+ """Add requirements for one or more datasets to a project. The contents of the JSON/JSON5 file
1137
+ must match the data model defined by this documentation:
1138
+ https://dsgrid.github.io/dsgrid/reference/data_models/project.html#dsgrid.config.input_dataset_requirements.InputDatasetListModel
1139
+ """
1140
+ submitter = getpass.getuser()
1141
+ project_mgr = registry_manager.project_manager
1142
+ res = handle_dsgrid_exception(
1143
+ ctx,
1144
+ project_mgr.add_dataset_requirements,
1145
+ project_id,
1146
+ filename,
1147
+ submitter,
1148
+ log_message,
1149
+ update_type,
1150
+ )
1151
+ if res[1] != 0:
1152
+ ctx.exit(res[1])
1153
+
1154
+
1155
+ _replace_dataset_dimension_requirements_epilog = """
1156
+ Examples:\n
1157
+ $ dsgrid registry projects replace-dataset-dimension-requirements \\ \n
1158
+ -l "Replace dimension requirements for dataset my-dataset-id in my-project-id." \\ \n
1159
+ project_id \\ \n
1160
+ dataset_dimension_requirements.json5\n
1161
+ """
1162
+
1163
+
1164
+ @click.command(epilog=_replace_dataset_dimension_requirements_epilog)
1165
+ @click.pass_obj
1166
+ @click.pass_context
1167
+ @click.argument("project_id")
1168
+ @click.argument("filename", callback=path_callback)
1169
+ @click.option(
1170
+ "-l",
1171
+ "--log-message",
1172
+ required=True,
1173
+ type=str,
1174
+ help="Please specify the reason for the new requirements.",
1175
+ )
1176
+ @click.option(
1177
+ "-t",
1178
+ "--update-type",
1179
+ default="major",
1180
+ type=click.Choice([x.value for x in VersionUpdateType]),
1181
+ callback=_version_update_callback,
1182
+ )
1183
+ def replace_dataset_dimension_requirements(
1184
+ ctx, registry_manager, project_id, filename: Path, log_message, update_type
1185
+ ):
1186
+ """Replace dimension requirements for one or more datasets in a project. The contents of the
1187
+ JSON/JSON5 file must match the data model defined by this documentation:
1188
+
1189
+ https://dsgrid.github.io/dsgrid/reference/data_models/project.html#dsgrid.config.input_dataset_requirements.InputDatasetDimensionRequirementsListModel
1190
+ """
1191
+ submitter = getpass.getuser()
1192
+ project_mgr = registry_manager.project_manager
1193
+ res = handle_dsgrid_exception(
1194
+ ctx,
1195
+ project_mgr.replace_dataset_dimension_requirements,
1196
+ project_id,
1197
+ filename,
1198
+ submitter,
1199
+ log_message,
1200
+ update_type,
1201
+ )
1202
+ if res[1] != 0:
1203
+ ctx.exit(res[1])
1204
+
1205
+
1206
+ _list_project_dimension_names_epilog = """
1207
+ Examples:\n
1208
+ $ dsgrid registry projects list-dimension-names my_project_id\n
1209
+ $ dsgrid registry projects list-dimension-names --exclude-subset my_project_id\n
1210
+ $ dsgrid registry projects list-dimension-names --exclude-supplemental my_project_id\n
1211
+ """
1212
+
1213
+
1214
+ @click.command(name="list-dimension-names", epilog=_list_project_dimension_names_epilog)
1215
+ @click.argument("project-id")
1216
+ @click.option(
1217
+ "-b",
1218
+ "--exclude-base",
1219
+ is_flag=True,
1220
+ default=False,
1221
+ show_default=True,
1222
+ help="Exclude base dimension names.",
1223
+ )
1224
+ @click.option(
1225
+ "-S",
1226
+ "--exclude-subset",
1227
+ is_flag=True,
1228
+ default=False,
1229
+ show_default=True,
1230
+ help="Exclude subset dimension names.",
1231
+ )
1232
+ @click.option(
1233
+ "-s",
1234
+ "--exclude-supplemental",
1235
+ is_flag=True,
1236
+ default=False,
1237
+ show_default=True,
1238
+ help="Exclude supplemental dimension names.",
1239
+ )
1240
+ @click.pass_obj
1241
+ @click.pass_context
1242
+ def list_project_dimension_names(
1243
+ ctx,
1244
+ registry_manager: RegistryManager,
1245
+ project_id,
1246
+ exclude_base,
1247
+ exclude_subset,
1248
+ exclude_supplemental,
1249
+ ):
1250
+ """List the project's dimension names."""
1251
+ if exclude_base and exclude_subset and exclude_supplemental:
1252
+ print(
1253
+ "exclude_base, exclude_subset, and exclude_supplemental cannot all be set",
1254
+ file=sys.stderr,
1255
+ )
1256
+ ctx.exit(1)
1257
+
1258
+ manager = registry_manager.project_manager
1259
+ res = handle_dsgrid_exception(ctx, manager.get_by_id, project_id)
1260
+ if res[1] != 0:
1261
+ ctx.exit(res[1])
1262
+
1263
+ project_config = res[0]
1264
+ assert isinstance(project_config, ProjectConfig)
1265
+ base = None if exclude_base else project_config.get_dimension_type_to_base_name_mapping()
1266
+ sub = None if exclude_subset else project_config.get_subset_dimension_to_name_mapping()
1267
+ supp = (
1268
+ None
1269
+ if exclude_supplemental
1270
+ else project_config.get_supplemental_dimension_to_name_mapping()
1271
+ )
1272
+
1273
+ dimensions = sorted(DimensionType, key=lambda x: x.value)
1274
+ print("Dimension names:")
1275
+ for dim_type in dimensions:
1276
+ print(f" {dim_type.value}:")
1277
+ if base:
1278
+ base_str = " ".join(base[dim_type])
1279
+ print(f" base: {base_str}")
1280
+ if sub:
1281
+ print(" subset: " + " ".join(sub[dim_type]))
1282
+ if supp:
1283
+ print(" supplemental: " + " ".join(supp[dim_type]))
1284
+
1285
+
1286
+ _generate_project_config_epilog = """
1287
+ Examples:\n
1288
+ $ dsgrid registry projects generate-config \\ \n
1289
+ -o "./my-project-dir" \\ \n
1290
+ my-project-id dataset-id1 dataset-id2 dataset-id3\n
1291
+ """
1292
+
1293
+
1294
+ @click.command(name="generate-config", epilog=_generate_project_config_epilog)
1295
+ @click.argument("project_id")
1296
+ @click.argument("dataset_ids", nargs=-1)
1297
+ @click.option(
1298
+ "-m",
1299
+ "--metric-type",
1300
+ multiple=True,
1301
+ type=click.Choice(sorted(SUPPORTED_METRIC_TYPES)),
1302
+ help="Metric types available in the project",
1303
+ )
1304
+ @click.option(
1305
+ "-n",
1306
+ "--name",
1307
+ type=str,
1308
+ help="Project name, optional",
1309
+ )
1310
+ @click.option(
1311
+ "-d",
1312
+ "--description",
1313
+ type=str,
1314
+ help="Project description, optional",
1315
+ )
1316
+ @click.option(
1317
+ "-t",
1318
+ "--time-type",
1319
+ type=click.Choice([x.value for x in TimeDimensionType]),
1320
+ default=TimeDimensionType.DATETIME.value,
1321
+ show_default=True,
1322
+ help="Type of the time dimension",
1323
+ callback=lambda *x: TimeDimensionType(x[2]),
1324
+ )
1325
+ @click.option(
1326
+ "-o",
1327
+ "--output",
1328
+ type=click.Path(),
1329
+ default=".",
1330
+ show_default=True,
1331
+ callback=path_callback,
1332
+ help="Path in which to generate project config files.",
1333
+ )
1334
+ @click.option(
1335
+ "-O",
1336
+ "--overwrite",
1337
+ is_flag=True,
1338
+ default=False,
1339
+ show_default=True,
1340
+ help="Overwrite files if they exist.",
1341
+ )
1342
+ @click.pass_context
1343
+ def generate_project_config_from_ids(
1344
+ ctx: click.Context,
1345
+ project_id: str,
1346
+ dataset_ids: tuple[str],
1347
+ metric_type: tuple[str],
1348
+ name: str | None,
1349
+ description: str | None,
1350
+ time_type: TimeDimensionType,
1351
+ output: Path | None,
1352
+ overwrite: bool,
1353
+ ):
1354
+ """Generate project config files from a project ID and one or more dataset IDs."""
1355
+ res = handle_dsgrid_exception(
1356
+ ctx,
1357
+ generate_project_config,
1358
+ project_id,
1359
+ dataset_ids,
1360
+ metric_type,
1361
+ name=name,
1362
+ description=description,
1363
+ time_type=time_type,
1364
+ output_directory=output,
1365
+ overwrite=overwrite,
1366
+ )
1367
+ if res[1] != 0:
1368
+ ctx.exit(res[1])
1369
+
1370
+
1371
+ @click.command(name="remove")
1372
+ @click.argument("project-id")
1373
+ @click.pass_obj
1374
+ def remove_project(registry_manager: RegistryManager, project_id: str):
1375
+ """Remove a project from the dsgrid repository."""
1376
+ registry_manager.project_manager.remove(project_id)
1377
+
1378
+
1379
+ """
1380
+ Dataset Commands
1381
+ """
1382
+
1383
+
1384
+ _list_datasets_epilog = """
1385
+ Examples:\n
1386
+ $ dsgrid registry datasets list\n
1387
+ $ dsgrid registry datasets list -f "ID contains com" -f "Submitter == username"\n
1388
+ """
1389
+
1390
+
1391
+ @click.command(name="list", epilog=_list_datasets_epilog)
1392
+ @click.option(
1393
+ "-f",
1394
+ "--filter",
1395
+ multiple=True,
1396
+ type=str,
1397
+ help=f"""
1398
+ Filter table with a case-insensitive expression in the format 'column operation value',
1399
+ accepts multiple flags\b\n
1400
+ valid operations: {ACCEPTED_OPS}\n
1401
+ """,
1402
+ )
1403
+ @click.pass_obj
1404
+ def list_datasets(registry_manager, filter):
1405
+ """List the registered dimensions."""
1406
+ registry_manager.dataset_manager.show(filters=filter)
1407
+
1408
+
1409
+ _register_dataset_epilog = """
1410
+ Examples:\n
1411
+ $ dsgrid registry datasets register dataset.json5 -l "Register dataset my-dataset-id."\n
1412
+ $ dsgrid registry datasets register dataset.json5 --data-base-dir /path/to/data -l "Register dataset my-dataset-id."\n
1413
+ """
1414
+
1415
+
1416
+ @click.command(name="register", epilog=_register_dataset_epilog)
1417
+ @click.argument("dataset-config-file", type=click.Path(exists=True), callback=path_callback)
1418
+ @click.option(
1419
+ "-l",
1420
+ "--log-message",
1421
+ required=True,
1422
+ help="reason for submission",
1423
+ )
1424
+ @click.option(
1425
+ "-D",
1426
+ "--data-base-dir",
1427
+ type=click.Path(exists=True),
1428
+ callback=path_callback,
1429
+ help="Base directory for data files. If set and data file paths are relative, "
1430
+ "prepend them with this path.",
1431
+ )
1432
+ @click.option(
1433
+ "-M",
1434
+ "--missing-associations-base-dir",
1435
+ type=click.Path(exists=True),
1436
+ callback=path_callback,
1437
+ help="Base directory for missing associations files. If set and missing associations "
1438
+ "paths are relative, prepend them with this path.",
1439
+ )
1440
+ @click.pass_obj
1441
+ @click.pass_context
1442
+ def register_dataset(
1443
+ ctx: click.Context,
1444
+ registry_manager: RegistryManager,
1445
+ dataset_config_file: Path,
1446
+ log_message: str,
1447
+ data_base_dir: Path | None,
1448
+ missing_associations_base_dir: Path | None,
1449
+ ):
1450
+ """Register a new dataset with the registry. The contents of the JSON/JSON5 file
1451
+ must match the data model defined by this documentation:
1452
+ https://dsgrid.github.io/dsgrid/reference/data_models/dataset.html#dsgrid.config.dataset_config.DatasetConfigModel
1453
+
1454
+ The config file must include a data_layout with data_file and optional
1455
+ lookup_data_file paths pointing to the dataset files.
1456
+ """
1457
+ manager = registry_manager.dataset_manager
1458
+ submitter = getpass.getuser()
1459
+ res = handle_dsgrid_exception(
1460
+ ctx,
1461
+ manager.register,
1462
+ dataset_config_file,
1463
+ submitter,
1464
+ log_message,
1465
+ data_base_dir=data_base_dir,
1466
+ missing_associations_base_dir=missing_associations_base_dir,
1467
+ )
1468
+ if res[1] != 0:
1469
+ ctx.exit(res[1])
1470
+
1471
+
1472
+ _dump_dataset_epilog = """
1473
+ Examples:\n
1474
+ $ dsgrid registry datasets dump my-dataset-id\n
1475
+ """
1476
+
1477
+
1478
+ @click.command(name="dump", epilog=_dump_dataset_epilog)
1479
+ @click.argument("dataset-id")
1480
+ @click.option(
1481
+ "-v",
1482
+ "--version",
1483
+ callback=_version_info_callback,
1484
+ help="Version to dump; defaults to latest",
1485
+ )
1486
+ @click.option(
1487
+ "-d",
1488
+ "--directory",
1489
+ default=".",
1490
+ type=click.Path(exists=True),
1491
+ help="Directory in which to create the config file",
1492
+ )
1493
+ @click.option(
1494
+ "--force",
1495
+ is_flag=True,
1496
+ default=False,
1497
+ show_default=True,
1498
+ help="Overwrite files if they exist.",
1499
+ )
1500
+ @click.pass_obj
1501
+ def dump_dataset(registry_manager, dataset_id, version, directory, force):
1502
+ """Dump a dataset config file from the registry."""
1503
+ manager = registry_manager.dataset_manager
1504
+ manager.dump(dataset_id, directory, version=version, force=force)
1505
+
1506
+
1507
+ _update_dataset_epilog = """
1508
+ Examples:\n
1509
+ $ dsgrid registry datasets update \\ \n
1510
+ -l "Update the description for dataset my-dataset-id." \\ \n
1511
+ -u patch \\ \n
1512
+ -v 1.0.0 \\ \n
1513
+ dataset.json5\n
1514
+ """
1515
+
1516
+
1517
+ @click.command(name="update", epilog=_update_dataset_epilog)
1518
+ @click.argument("dataset-config-file", type=click.Path(exists=True), callback=path_callback)
1519
+ @click.option(
1520
+ "-d",
1521
+ "--dataset-id",
1522
+ required=True,
1523
+ type=str,
1524
+ help="dataset ID",
1525
+ )
1526
+ @click.option(
1527
+ "-l",
1528
+ "--log-message",
1529
+ required=True,
1530
+ type=str,
1531
+ help="reason for submission",
1532
+ )
1533
+ @click.option(
1534
+ "-t",
1535
+ "--update-type",
1536
+ required=True,
1537
+ type=click.Choice([x.value for x in VersionUpdateType]),
1538
+ callback=_version_update_callback,
1539
+ )
1540
+ @click.option(
1541
+ "-v",
1542
+ "--version",
1543
+ required=True,
1544
+ callback=_version_info_required_callback,
1545
+ help="Version to update; must be the current version.",
1546
+ )
1547
+ @click.pass_obj
1548
+ @click.pass_context
1549
+ def update_dataset(
1550
+ ctx: click.Context,
1551
+ registry_manager: RegistryManager,
1552
+ dataset_config_file: Path,
1553
+ dataset_id: str,
1554
+ log_message: str,
1555
+ update_type: VersionUpdateType,
1556
+ version: str,
1557
+ ):
1558
+ """Update an existing dataset in the registry. The contents of the JSON/JSON5 file
1559
+ must match the data model defined by this documentation:
1560
+ https://dsgrid.github.io/dsgrid/reference/data_models/dataset.html#dsgrid.config.dataset_config.DatasetConfigModel
1561
+
1562
+ If the config file includes a UserDatasetSchema with file paths, the data will be
1563
+ re-read from those paths. Otherwise, the existing data in the registry is used.
1564
+ """
1565
+ manager = registry_manager.dataset_manager
1566
+ submitter = getpass.getuser()
1567
+ res = handle_dsgrid_exception(
1568
+ ctx,
1569
+ manager.update_from_file,
1570
+ dataset_config_file,
1571
+ dataset_id,
1572
+ submitter,
1573
+ update_type,
1574
+ log_message,
1575
+ version,
1576
+ )
1577
+ if res[1] != 0:
1578
+ ctx.exit(res[1])
1579
+
1580
+
1581
+ _generate_dataset_config_from_dataset_epilog = """
1582
+ Examples:\n
1583
+ $ dsgrid registry datasets generate-config-from-dataset \\ \n
1584
+ -o "./my-dataset-dir" \\ \n
1585
+ -P my-project-id \\ \n
1586
+ my-dataset-id \\ \n
1587
+ /path/to/table.parquet\n
1588
+ """
1589
+
1590
+
1591
+ @click.command(name="generate-config", epilog=_generate_dataset_config_from_dataset_epilog)
1592
+ @click.argument("dataset-id")
1593
+ @click.argument("dataset-path")
1594
+ @click.option(
1595
+ "-s",
1596
+ "--table-format",
1597
+ type=click.Choice([x.value for x in TableFormat]),
1598
+ default=TableFormat.ONE_TABLE.value,
1599
+ show_default=True,
1600
+ callback=lambda *x: TableFormat(x[2]),
1601
+ )
1602
+ @click.option(
1603
+ "-m",
1604
+ "--metric-type",
1605
+ type=click.Choice(sorted(SUPPORTED_METRIC_TYPES)),
1606
+ default="EnergyEndUse",
1607
+ show_default=True,
1608
+ )
1609
+ @click.option(
1610
+ "-p",
1611
+ "--pivoted-dimension-type",
1612
+ type=click.Choice([x.value for x in DimensionType if x != DimensionType.TIME]),
1613
+ default=None,
1614
+ callback=lambda *x: None if x[2] is None else DimensionType(x[2]),
1615
+ help="Optional, if one dimension has its records pivoted as columns, its type.",
1616
+ )
1617
+ @click.option(
1618
+ "-t",
1619
+ "--time-type",
1620
+ type=click.Choice([x.value for x in TimeDimensionType]),
1621
+ default=TimeDimensionType.DATETIME.value,
1622
+ show_default=True,
1623
+ help="Type of the time dimension",
1624
+ callback=lambda *x: TimeDimensionType(x[2]),
1625
+ )
1626
+ @click.option(
1627
+ "-T",
1628
+ "--time-columns",
1629
+ multiple=True,
1630
+ help="Names of time columns in the table. Required if pivoted_dimension_type is set "
1631
+ "and the time column is not 'timestamp'.",
1632
+ )
1633
+ @click.option(
1634
+ "-o",
1635
+ "--output",
1636
+ type=click.Path(),
1637
+ default=".",
1638
+ show_default=True,
1639
+ callback=path_callback,
1640
+ help="Path in which to create dataset config files.",
1641
+ )
1642
+ @click.option(
1643
+ "-P",
1644
+ "--project-id",
1645
+ required=False,
1646
+ type=str,
1647
+ help="Project ID, optional. If provided, prioritize project base dimensions when "
1648
+ "searching for matching dimensions.",
1649
+ )
1650
+ @click.option(
1651
+ "-n",
1652
+ "--no-prompts",
1653
+ is_flag=True,
1654
+ default=False,
1655
+ show_default=True,
1656
+ help="Do not prompt for matches. Automatically accept the first one.",
1657
+ )
1658
+ @click.option(
1659
+ "-O",
1660
+ "--overwrite",
1661
+ is_flag=True,
1662
+ default=False,
1663
+ show_default=True,
1664
+ help="Overwrite files if they exist.",
1665
+ )
1666
+ @click.pass_obj
1667
+ @click.pass_context
1668
+ def generate_dataset_config_from_dataset(
1669
+ ctx: click.Context,
1670
+ registry_manager: RegistryManager,
1671
+ dataset_id: str,
1672
+ dataset_path: Path,
1673
+ table_format: TableFormat,
1674
+ metric_type: str,
1675
+ pivoted_dimension_type: DimensionType | None,
1676
+ time_type: TimeDimensionType,
1677
+ time_columns: tuple[str],
1678
+ output: Path | None,
1679
+ project_id: str | None,
1680
+ no_prompts: bool,
1681
+ overwrite: bool,
1682
+ ):
1683
+ """Generate dataset config files from a dataset table.
1684
+
1685
+ Fill out the dimension record files based on the unique values in the dataset.
1686
+
1687
+ Look for matches for dimensions in the registry. Prompt the user for confirmation unless
1688
+ --no-prompts is set. If --no-prompts is set, the first match is automatically accepted.
1689
+ """
1690
+ res = handle_dsgrid_exception(
1691
+ ctx,
1692
+ generate_config_from_dataset,
1693
+ registry_manager,
1694
+ dataset_id,
1695
+ dataset_path,
1696
+ table_format,
1697
+ metric_type,
1698
+ pivoted_dimension_type=pivoted_dimension_type,
1699
+ time_type=time_type,
1700
+ time_columns=set(time_columns),
1701
+ output_directory=output,
1702
+ project_id=project_id,
1703
+ no_prompts=no_prompts,
1704
+ overwrite=overwrite,
1705
+ )
1706
+ if res[1] != 0:
1707
+ ctx.exit(res[1])
1708
+
1709
+
1710
+ @click.command(name="remove")
1711
+ @click.argument("dataset-ids", nargs=-1)
1712
+ @click.pass_obj
1713
+ def remove_datasets(registry_manager: RegistryManager, dataset_ids: list[str]):
1714
+ """Remove one or more datasets from the dsgrid repository."""
1715
+ dataset_mgr = registry_manager.dataset_manager
1716
+ project_mgr = registry_manager.project_manager
1717
+
1718
+ # Ensure that all dataset IDs are valid before removing any of them.
1719
+ for dataset_id in dataset_ids:
1720
+ dataset_mgr.get_by_id(dataset_id)
1721
+
1722
+ for dataset_id in dataset_ids:
1723
+ registry_manager.dataset_manager.remove(dataset_id)
1724
+
1725
+ dataset_ids_set = set(dataset_ids)
1726
+ for project_id in project_mgr.list_ids():
1727
+ config = project_mgr.get_by_id(project_id)
1728
+ removed_dataset_ids = []
1729
+ for dataset in config.iter_datasets():
1730
+ if (
1731
+ dataset.dataset_id in dataset_ids_set
1732
+ and dataset.status == DatasetRegistryStatus.REGISTERED
1733
+ ):
1734
+ dataset.status = DatasetRegistryStatus.UNREGISTERED
1735
+ dataset.mapping_references.clear()
1736
+ removed_dataset_ids.append(dataset.dataset_id)
1737
+ if removed_dataset_ids:
1738
+ ids = ", ".join(removed_dataset_ids)
1739
+ msg = (
1740
+ f"Set status for datasets {ids} to unregistered in project {project_id} "
1741
+ "after removal."
1742
+ )
1743
+ project_mgr.update(config, VersionUpdateType.MAJOR, msg)
1744
+
1745
+
1746
+ _bulk_register_epilog = """
1747
+ Examples:\n
1748
+ $ dsgrid registry bulk-register registration.json5
1749
+ $ dsgrid registry bulk-register registration.json5 -j journal__11f733f6-ac9b-4f70-ad4b-df75b291f150.json5
1750
+ """
1751
+
1752
+
1753
+ @click.command(name="bulk-register", epilog=_bulk_register_epilog)
1754
+ @click.argument("registration_file", type=click.Path(exists=True))
1755
+ @click.option(
1756
+ "-D",
1757
+ "--data-base-dir",
1758
+ type=click.Path(exists=True),
1759
+ callback=path_callback,
1760
+ help="Base directory for data files. If set and data file paths are relative, "
1761
+ "prepend them with this path.",
1762
+ )
1763
+ @click.option(
1764
+ "-M",
1765
+ "--missing-associations-base-dir",
1766
+ type=click.Path(exists=True),
1767
+ callback=path_callback,
1768
+ help="Base directory for missing associations files. If set and missing associations "
1769
+ "paths are relative, prepend them with this path.",
1770
+ )
1771
+ @click.option(
1772
+ "-r",
1773
+ "--repo-base-dir",
1774
+ type=click.Path(exists=True),
1775
+ callback=path_callback,
1776
+ help="Base directory for dsgrid project/dataset repository. If set, and if the config file "
1777
+ "paths are relative, prepend them with this path.",
1778
+ )
1779
+ @click.option(
1780
+ "-j",
1781
+ "--journal-file",
1782
+ type=click.Path(exists=True),
1783
+ callback=path_callback,
1784
+ help="Journal file created by a previous bulk register operation. If passed, the code will "
1785
+ "read it and skip all projects and datasets that were successfully registered. "
1786
+ "The file will be updated with IDs that are successfully registered.",
1787
+ )
1788
+ @click.pass_obj
1789
+ @click.pass_context
1790
+ def bulk_register_cli(
1791
+ ctx,
1792
+ registry_manager: RegistryManager,
1793
+ registration_file: Path,
1794
+ data_base_dir: Path | None,
1795
+ missing_associations_base_dir: Path | None,
1796
+ repo_base_dir: Path | None,
1797
+ journal_file: Path | None,
1798
+ ):
1799
+ """Bulk register projects, datasets, and their dimensions. If any failure occurs, the code
1800
+ records successfully registered project and dataset IDs to a journal file and prints its
1801
+ filename to the console. Users can pass that filename with the --journal-file option to
1802
+ avoid re-registering those projects and datasets on subsequent attempts.
1803
+
1804
+ The JSON/JSON5 filename must match the data model defined by this documentation:
1805
+
1806
+ https://dsgrid.github.io/dsgrid/reference/data_models/project.html#dsgrid.config.registration_models.RegistrationModel
1807
+ """
1808
+ res = handle_dsgrid_exception(
1809
+ ctx,
1810
+ bulk_register,
1811
+ registry_manager,
1812
+ registration_file,
1813
+ data_base_dir=data_base_dir,
1814
+ missing_associations_base_dir=missing_associations_base_dir,
1815
+ repo_base_dir=repo_base_dir,
1816
+ journal_file=journal_file,
1817
+ )
1818
+ if res[1] != 0:
1819
+ ctx.exit(res[1])
1820
+
1821
+
1822
+ dimensions.add_command(list_dimensions)
1823
+ dimensions.add_command(register_dimensions)
1824
+ dimensions.add_command(dump_dimension)
1825
+ dimensions.add_command(show_dimension)
1826
+ dimensions.add_command(update_dimension)
1827
+ dimensions.add_command(remove_dimension)
1828
+
1829
+ dimension_mappings.add_command(list_dimension_mappings)
1830
+ dimension_mappings.add_command(register_dimension_mappings)
1831
+ dimension_mappings.add_command(dump_dimension_mapping)
1832
+ dimension_mappings.add_command(show_dimension_mapping)
1833
+ dimension_mappings.add_command(update_dimension_mapping)
1834
+ dimension_mappings.add_command(remove_dimension_mapping)
1835
+
1836
+ projects.add_command(list_projects)
1837
+ projects.add_command(register_project)
1838
+ projects.add_command(submit_dataset)
1839
+ projects.add_command(register_and_submit_dataset)
1840
+ projects.add_command(dump_project)
1841
+ projects.add_command(update_project)
1842
+ projects.add_command(register_subset_dimensions)
1843
+ projects.add_command(register_supplemental_dimensions)
1844
+ projects.add_command(add_dataset_requirements)
1845
+ projects.add_command(replace_dataset_dimension_requirements)
1846
+ projects.add_command(list_project_dimension_names)
1847
+ projects.add_command(generate_project_config_from_ids)
1848
+ projects.add_command(remove_project)
1849
+
1850
+ datasets.add_command(list_datasets)
1851
+ datasets.add_command(register_dataset)
1852
+ datasets.add_command(dump_dataset)
1853
+ datasets.add_command(update_dataset)
1854
+ datasets.add_command(generate_dataset_config_from_dataset)
1855
+ datasets.add_command(remove_datasets)
1856
+
1857
+ registry.add_command(list_)
1858
+ registry.add_command(dimensions)
1859
+ registry.add_command(dimension_mappings)
1860
+ registry.add_command(projects)
1861
+ registry.add_command(datasets)
1862
+ registry.add_command(bulk_register_cli)