dsgrid-toolkit 0.2.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.

Potentially problematic release.


This version of dsgrid-toolkit might be problematic. Click here for more details.

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