threedi-cmd 0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. threedi_cmd/__init__.py +0 -0
  2. threedi_cmd/commands/__init__.py +0 -0
  3. threedi_cmd/commands/active_simulations.py +43 -0
  4. threedi_cmd/commands/api.py +387 -0
  5. threedi_cmd/commands/app_definitions.py +36 -0
  6. threedi_cmd/commands/callbacks.py +20 -0
  7. threedi_cmd/commands/main.py +23 -0
  8. threedi_cmd/commands/models.py +7 -0
  9. threedi_cmd/commands/scenarios.py +55 -0
  10. threedi_cmd/commands/settings.py +294 -0
  11. threedi_cmd/commands/slack_notify.py +16 -0
  12. threedi_cmd/commands/suite.py +377 -0
  13. threedi_cmd/commands/utils.py +77 -0
  14. threedi_cmd/console.py +14 -0
  15. threedi_cmd/errors.py +23 -0
  16. threedi_cmd/example.py +35 -0
  17. threedi_cmd/jinja2_time.py +65 -0
  18. threedi_cmd/logger.py +57 -0
  19. threedi_cmd/models/__init__.py +46 -0
  20. threedi_cmd/models/actions.py +109 -0
  21. threedi_cmd/models/base.py +435 -0
  22. threedi_cmd/models/boundary_conditions.py +50 -0
  23. threedi_cmd/models/breach.py +11 -0
  24. threedi_cmd/models/errors.py +2 -0
  25. threedi_cmd/models/initial_waterlevels.py +119 -0
  26. threedi_cmd/models/lateral.py +98 -0
  27. threedi_cmd/models/leakage.py +102 -0
  28. threedi_cmd/models/monitor.py +275 -0
  29. threedi_cmd/models/postprocessing.py +31 -0
  30. threedi_cmd/models/rain.py +134 -0
  31. threedi_cmd/models/rasteredit.py +42 -0
  32. threedi_cmd/models/revision.py +124 -0
  33. threedi_cmd/models/savedstate.py +18 -0
  34. threedi_cmd/models/scenario.py +378 -0
  35. threedi_cmd/models/settings.py +39 -0
  36. threedi_cmd/models/simulation.py +8 -0
  37. threedi_cmd/models/sources_sinks.py +120 -0
  38. threedi_cmd/models/structure_control.py +133 -0
  39. threedi_cmd/models/waitfor.py +231 -0
  40. threedi_cmd/models/wind.py +31 -0
  41. threedi_cmd/parser.py +108 -0
  42. threedi_cmd/plugins/__init__.py +0 -0
  43. threedi_cmd/plugins/models.py +17 -0
  44. threedi_cmd/plugins/tools.py +20 -0
  45. threedi_cmd/schematisation_uploader/__init__.py +0 -0
  46. threedi_cmd/schematisation_uploader/db.py +112 -0
  47. threedi_cmd/schematisation_uploader/example.py +14 -0
  48. threedi_cmd/schematisation_uploader/main.py +203 -0
  49. threedi_cmd/schematisation_uploader/yaml_converter.py +193 -0
  50. threedi_cmd/test.py +45 -0
  51. threedi_cmd/version.py +1 -0
  52. threedi_cmd/websockets/__init__.py +0 -0
  53. threedi_cmd/websockets/clients.py +55 -0
  54. threedi_cmd/websockets/settings.py +17 -0
  55. threedi_cmd-0.1.dist-info/AUTHORS.rst +15 -0
  56. threedi_cmd-0.1.dist-info/LICENSE +22 -0
  57. threedi_cmd-0.1.dist-info/METADATA +245 -0
  58. threedi_cmd-0.1.dist-info/RECORD +61 -0
  59. threedi_cmd-0.1.dist-info/WHEEL +5 -0
  60. threedi_cmd-0.1.dist-info/entry_points.txt +6 -0
  61. threedi_cmd-0.1.dist-info/top_level.txt +1 -0
File without changes
File without changes
@@ -0,0 +1,43 @@
1
+ import asyncio
2
+ from datetime import datetime
3
+ import typer
4
+
5
+ from threedi_cmd.console import console
6
+ from threedi_cmd.models.monitor import ActiveSimulations
7
+ from threedi_cmd.commands.callbacks import default_callback
8
+
9
+ active_sims_app = typer.Typer(callback=default_callback)
10
+
11
+
12
+ async def monitor_active_simulations(endpoint: str) -> None:
13
+ """
14
+ executes ActiveSimlations "run_monitor" task in the background
15
+ """
16
+ active_simulations = ActiveSimulations(endpoint)
17
+ result = await asyncio.gather(
18
+ active_simulations.run_monitor(), return_exceptions=True
19
+ )
20
+ if result:
21
+ console.print(result, style="error")
22
+
23
+
24
+ @active_sims_app.command()
25
+ def simulations(ctx: typer.Context):
26
+ """
27
+ Show currently running simulations
28
+ """
29
+ start_time = datetime.now().strftime("%Y-%m-%d %H:%M")
30
+ try:
31
+ console.print(f"[{start_time}] Starting active simulations worker")
32
+ asyncio.run(monitor_active_simulations(ctx.obj._endpoint.name))
33
+ except KeyboardInterrupt:
34
+ pass
35
+ finally:
36
+ console.print(
37
+ f":sparkles: Bye bye, hope to see you soon! :sparkles:",
38
+ style="success",
39
+ )
40
+
41
+
42
+ if __name__ == "__main__":
43
+ active_sims_app()
@@ -0,0 +1,387 @@
1
+ import asyncio
2
+ import sys
3
+ from datetime import datetime
4
+ from pathlib import Path
5
+ import click
6
+ import typer
7
+ import websockets
8
+ from rich import box
9
+ from rich.padding import Padding
10
+ from rich.panel import Panel
11
+ from rich.prompt import Confirm, IntPrompt, Prompt
12
+ from rich.table import Table
13
+ from threedi_api_client.api import Configuration
14
+ from threedi_api_client.openapi.api.v3_api import V3Api
15
+ from threedi_api_client.openapi.exceptions import ApiException
16
+ from threedi_cmd.commands.callbacks import default_callback
17
+ from threedi_cmd.commands.scenarios import scenarios
18
+ from threedi_cmd.commands.utils import PathPrompt, download_files
19
+ from threedi_cmd.console import console
20
+ from threedi_cmd.errors import ExitCodes, LoadScenarioError
21
+ from threedi_cmd.logger import get_logger
22
+ from threedi_cmd.models.errors import ApiModelError
23
+ from threedi_cmd.models.scenario import FailedStep, ResolveError
24
+ from threedi_cmd.parser import ScenarioParser
25
+
26
+ logger = get_logger("INFO")
27
+
28
+ DEFAULT_PAGER_SIZE = 20
29
+
30
+
31
+ app = typer.Typer()
32
+ api_app = typer.Typer(callback=default_callback)
33
+ app.add_typer(api_app, name="api")
34
+
35
+
36
+ @api_app.command()
37
+ def models(ctx: typer.Context):
38
+ """List available threedimodels"""
39
+ threedi_models_api: V3Api = ctx.obj.api_client
40
+ threedi_models = threedi_models_api.threedimodels_list()
41
+
42
+ table = Table(
43
+ show_header=True,
44
+ box=box.HORIZONTALS,
45
+ show_lines=False,
46
+ width=(console.width * 80) / 100,
47
+ )
48
+ table.add_column("Id", width=5)
49
+ table.add_column(
50
+ "Name",
51
+ width=20,
52
+ justify="left",
53
+ style="bold cyan",
54
+ header_style="bold cyan",
55
+ )
56
+ table.add_column("Revision", justify="left")
57
+ table.add_column("Inp success", justify="left")
58
+
59
+ remaining = threedi_models.count
60
+ limit = min(DEFAULT_PAGER_SIZE, remaining)
61
+
62
+ while remaining == threedi_models.count or (
63
+ remaining > 0 and Confirm.ask("Show more?")
64
+ ):
65
+ threedi_models = threedi_models_api.threedimodels_list(
66
+ limit=limit, offset=remaining - limit
67
+ )
68
+ for i, model in enumerate(threedi_models.results):
69
+ if model.inp_success is True:
70
+ txt = (
71
+ f":heavy_check_mark: [bold green]{model.inp_success}[/bold green] "
72
+ )
73
+ else:
74
+ txt = f"[bold red]{model.inp_success}[/bold red]"
75
+ table.add_row(f"{model.id}", f"{model.name}", f"{model.revision_hash}", txt)
76
+ remaining -= limit
77
+ limit = min(remaining, DEFAULT_PAGER_SIZE)
78
+ console.print(table)
79
+
80
+
81
+ @api_app.command()
82
+ def organisations(ctx: typer.Context):
83
+ """List available organisations"""
84
+ organisations_api: V3Api = ctx.obj.api_client
85
+ organisations = organisations_api.organisations_list()
86
+ table = Table(
87
+ show_header=True,
88
+ box=box.HORIZONTALS,
89
+ show_lines=False,
90
+ width=(console.width * 80) / 100,
91
+ )
92
+ table.add_column("Unique Id", width=15)
93
+ table.add_column(
94
+ "Name",
95
+ width=20,
96
+ justify="left",
97
+ style="bold cyan",
98
+ header_style="bold cyan",
99
+ )
100
+
101
+ remaining = organisations.count
102
+ limit = min(DEFAULT_PAGER_SIZE, remaining)
103
+
104
+ while remaining == organisations.count or (
105
+ remaining > 0 and Confirm.ask("Show more?")
106
+ ):
107
+ organisations = organisations_api.organisations_list(
108
+ limit=limit, offset=remaining - limit
109
+ )
110
+ for i, org in enumerate(organisations.results):
111
+ table.add_row(f"{org.unique_id}", f"{org.name}")
112
+ remaining -= limit
113
+ limit = min(remaining, DEFAULT_PAGER_SIZE)
114
+ console.print(table)
115
+
116
+
117
+ @api_app.command()
118
+ def simulations(
119
+ ctx: typer.Context,
120
+ queued: bool = typer.Option(
121
+ False,
122
+ "--queued",
123
+ help="Show the current queue",
124
+ ),
125
+ ):
126
+ """List simulations"""
127
+ simulations_api: V3Api = ctx.obj.api_client
128
+ simulations = simulations_api.simulations_list()
129
+
130
+ table = Table(
131
+ show_header=True,
132
+ box=box.HORIZONTALS,
133
+ show_lines=False,
134
+ width=(console.width * 80) / 100,
135
+ )
136
+ table.add_column(
137
+ "Id", width=5
138
+ ) # , style="dark_green", header_style="bold dark_green")
139
+ table.add_column("Name", width=20, justify="left")
140
+ table.add_column("Status", justify="left")
141
+
142
+ remaining = simulations.count
143
+ limit = DEFAULT_PAGER_SIZE
144
+ offset = 0
145
+ while remaining == simulations.count or (
146
+ remaining > 0 and Confirm.ask("Show more?")
147
+ ):
148
+ simulations = simulations_api.simulations_list(limit=limit, offset=offset)
149
+ for i, simulation in enumerate(simulations.results):
150
+ status = simulations_api.simulations_status_list(simulation.id)
151
+ if status.name == "finished":
152
+ txt = f":heavy_check_mark: [bold green]{status.name}[/bold green] "
153
+ elif status.name == "created":
154
+ txt = f"[dim grey27]{status.name}[/dim grey27] "
155
+ elif status.name == "initialized":
156
+ txt = f"[bold dark_violet]{status.name}[/bold dark_violet] "
157
+ else:
158
+ txt = f"[bold red]{status.name}[/bold red]"
159
+ table.add_row(f"{simulation.id}", f"{simulation.name}", txt)
160
+ console.print(table)
161
+ remaining -= limit
162
+ offset += DEFAULT_PAGER_SIZE
163
+
164
+
165
+ @api_app.command()
166
+ def settings(
167
+ ctx: typer.Context,
168
+ organisation: str = typer.Option(
169
+ None, help="Unique-id of the organisation to set as default"
170
+ ),
171
+ scenario_folder: Path = typer.Option(
172
+ None,
173
+ dir_okay=True,
174
+ writable=True,
175
+ resolve_path=True,
176
+ help="Specify a the folder for your scenario's.",
177
+ ),
178
+ result_folder: Path = typer.Option(
179
+ None,
180
+ dir_okay=True,
181
+ writable=True,
182
+ resolve_path=True,
183
+ help="Specify a results folder.",
184
+ ),
185
+ ):
186
+ """Set default settings"""
187
+ console.rule(":wrench: Configuring defaults for the 3Di cli", style="bold blue")
188
+ console.print(Padding("", (1, 0)))
189
+ if organisation is None:
190
+ console.rule("Please choose an organisation", style="bold blue")
191
+ if (
192
+ ctx.obj.organisation_uuid
193
+ and Confirm.ask(f"Change current organisation {ctx.obj.organisation_uuid}?")
194
+ or not ctx.obj.organisation_uuid
195
+ ):
196
+ organisations(ctx)
197
+ console.rule(":pencil2:", style="bold blue")
198
+ organisation = Prompt.ask(
199
+ "Set default organisation. UNIQUE_ID",
200
+ default=ctx.obj.organisation_uuid,
201
+ )
202
+ else:
203
+ organisation = ctx.obj.organisation_uuid
204
+ if not scenario_folder:
205
+ default_scenario_path = ctx.obj.scenario_folder or Path("./scenarios").resolve()
206
+ print(default_scenario_path)
207
+ scenario_folder = PathPrompt.ask(
208
+ "Where do you keep your scenario files?", default=default_scenario_path
209
+ )
210
+ if not result_folder:
211
+ results_folder_default = ctx.obj.result_folder or Path("./results").resolve()
212
+ result_folder = PathPrompt.ask(
213
+ "Where do you want to store your result files?",
214
+ default=results_folder_default,
215
+ )
216
+ ctx.obj.organisation_uuid = organisation
217
+ ctx.obj.result_folder = result_folder
218
+ ctx.obj.scenario_folder = scenario_folder
219
+ console.print(
220
+ f":heavy_check_mark: Default settings are saved in {ctx.obj.config_file}"
221
+ )
222
+
223
+
224
+ @api_app.command()
225
+ def run_scenario(
226
+ ctx: typer.Context,
227
+ scenario: int = typer.Option(
228
+ None,
229
+ help="The ID of the scenario, as returned by the 'scenarios' command. If not provided the scenarios command will be invoked so you can choose an ID",
230
+ ),
231
+ model_id: int = typer.Option(
232
+ None,
233
+ help="The ID of the model you want to use, as returned by the 'models' command. If not provided the models command will be invoked so you can choose an ID",
234
+ ),
235
+ organisation: str = typer.Option(
236
+ None, help="Unique-id of the organisation to set as default"
237
+ ),
238
+ ):
239
+ """Run a scenario"""
240
+ if scenario is None:
241
+ scenarios(ctx)
242
+ scenario = IntPrompt.ask("Which scenario do you want to run? ID")
243
+ scenario_to_run = ctx.obj.scenarios[scenario]["file"]
244
+ parser = ScenarioParser(scenario_to_run)
245
+
246
+ if parser.is_simulation_scenario:
247
+ if model_id is None:
248
+ models(ctx)
249
+ model_id = IntPrompt.ask("Which model do you want to run? ID")
250
+
251
+ if not organisation:
252
+ organisations(ctx)
253
+ organisation = Prompt.ask("Which organisation do you want to use? UUID")
254
+
255
+ name = ctx.obj.scenarios[scenario]["name"]
256
+
257
+ model = ctx.obj.api_client.threedimodels_read(model_id)
258
+ context = {
259
+ "threedimodel_id": model.id,
260
+ "threedimodel": model,
261
+ "organisation_uuid": organisation,
262
+ "simulation_name": name,
263
+ "schematisation_name": name,
264
+ "datetime_now": datetime.utcnow().isoformat(),
265
+ }
266
+
267
+ try:
268
+ scenario = parser.parse(
269
+ ctx.obj.api_client, ctx.obj.websocket_settings, context=context
270
+ )
271
+ except (ResolveError, ApiModelError, LoadScenarioError) as err:
272
+ console.print(f":collision: {err}", style="error")
273
+ sys.exit(ExitCodes.SCENARIO_CONFIG_ERROR.value)
274
+
275
+ console.rule(f"Loading scenario {name}", style="bold blue")
276
+ scenario.base_object.save()
277
+ console.print(f":link: URL: {scenario.base_object.instance.url}")
278
+
279
+ try:
280
+ console.rule(f"Starting scenario run...", style="bold blue")
281
+ asyncio.run(scenario.execute())
282
+ except KeyboardInterrupt:
283
+ pass
284
+ except FailedStep as err:
285
+ console.print(f"{err}", style="error")
286
+ sys.exit(ExitCodes.RUN_SCENARIO_ERROR.value)
287
+ except websockets.exceptions.InvalidStatusCode as err:
288
+ console.print(f"{err}", style="error")
289
+ sys.exit(ExitCodes.CONNECTION_ERROR.value)
290
+ else:
291
+ success_panel = Panel(
292
+ f"Run for scenario {name} successful",
293
+ expand=True,
294
+ box=box.DOUBLE,
295
+ border_style="bold spring_green4",
296
+ title=f":sparkles: Finished :sparkles:",
297
+ )
298
+ console.print(success_panel, justify="center")
299
+
300
+ @api_app.command()
301
+ def results(
302
+ ctx: typer.Context,
303
+ simulation: int = typer.Option(None, help="ID of the simulation"),
304
+ folder: Path = typer.Option(
305
+ None,
306
+ dir_okay=True,
307
+ writable=True,
308
+ resolve_path=True,
309
+ help="Absolute path to where the files will be stored.",
310
+ ),
311
+ ):
312
+ """Download results of a simulation"""
313
+ console.rule(":arrow_heading_down: Download simulation results", style="bold blue")
314
+ console.print(Padding("", (1, 0)))
315
+
316
+ if simulation is None:
317
+ console.rule("Please choose a simulation", style="bold blue")
318
+ simulations(ctx)
319
+ simulation = IntPrompt.ask("Which simulation results do you want download? ID")
320
+ if not folder:
321
+ result_folder = ctx.obj.result_folder
322
+ if not result_folder:
323
+ result_folder = Path.cwd()
324
+ folder = PathPrompt.ask(
325
+ "Where do you want to store the results files?",
326
+ default=Path(f"{result_folder}/simulation-{simulation}"),
327
+ )
328
+
329
+ try:
330
+ folder.resolve().mkdir(parents=True)
331
+ except FileExistsError:
332
+ click.confirm(
333
+ "Output folder already exists, we might override files in the folder. "
334
+ "Do you want to continue?",
335
+ abort=True,
336
+ )
337
+
338
+ try:
339
+ simulations_api: V3Api = ctx.obj.api_client
340
+ threedimodels_api = simulations_api
341
+ simulation = simulations_api.simulations_read(id=simulation)
342
+ except ApiException as err:
343
+ console.print(f"{err}", style="error")
344
+ sys.exit(ExitCodes.RETRIEVE_ERROR.value)
345
+ threedi_model_id = simulation.threedimodel_id
346
+
347
+ gridadmin_download = threedimodels_api.threedimodels_gridadmin_download(
348
+ threedi_model_id
349
+ )
350
+ f = [gridadmin_download]
351
+
352
+ result_files = simulations_api.simulations_results_files_list(simulation.id)
353
+ for result in result_files.results:
354
+ if result.file.state in ["error", "removed"]:
355
+ console.print(
356
+ f"{result.filename} is in state {result.file.state} and will be skipped.",
357
+ style="warning",
358
+ )
359
+ continue
360
+
361
+ result_download = simulations_api.simulations_results_files_download(
362
+ id=result.id, simulation_pk=simulation.id
363
+ )
364
+ f.append(result_download)
365
+ success = download_files(f, folder)
366
+ if success:
367
+ console.print(
368
+ ":heavy_check_mark: Finished downloading results", style="success"
369
+ )
370
+ else:
371
+ console.print(":collision: Not all files could be downloaded", style="error")
372
+
373
+
374
+ @api_app.command()
375
+ def login(ctx: typer.Context):
376
+ """Login to the specified endpoint."""
377
+ try:
378
+ configuration = Configuration(ctx.obj.endpoint)
379
+ ctx.obj.credentials_prompt(configuration)
380
+ console.print(f":unlock: Authenticated as {ctx.obj.username}", style="success")
381
+ except ApiException as e:
382
+ console.print(f":lock: Failed to authenticate: {e.reason}", style="warning")
383
+ sys.exit(ExitCodes.AUTHENTICATION_FAILED.value)
384
+
385
+
386
+ if __name__ == "__main__":
387
+ app()
@@ -0,0 +1,36 @@
1
+ import typer
2
+
3
+ from threedi_cmd.commands.active_simulations import active_sims_app
4
+ from threedi_cmd.commands.scenarios import scenario_app
5
+ from threedi_cmd.commands.api import api_app
6
+ from threedi_cmd.commands.suite import suite_app
7
+ from threedi_cmd.plugins.models import AppRegistry
8
+ from threedi_cmd.plugins.models import AppMeta
9
+
10
+
11
+ core = typer.Typer()
12
+
13
+ # main app definition
14
+ core = AppMeta(app=core, name="core", help="Interact with various parts of 3Di")
15
+
16
+ live = AppMeta(
17
+ app=active_sims_app,
18
+ name="live",
19
+ help="Get real time updates of running simulations",
20
+ add_to="core",
21
+ )
22
+
23
+ scenarios = AppMeta(
24
+ app=scenario_app,
25
+ name="scenarios",
26
+ help="Manage your local scenarios",
27
+ add_to="core",
28
+ )
29
+
30
+ api = AppMeta(
31
+ app=api_app, name="api", help="Interact with with the 3Di API", add_to="core"
32
+ )
33
+
34
+ suite = AppMeta(app=suite_app, name="suite", help="Run a scenario suite", add_to="core")
35
+
36
+ registry = AppRegistry(apps={inst.name: inst for inst in [core, live, scenarios, api]})
@@ -0,0 +1,20 @@
1
+ import typer
2
+
3
+ from threedi_cmd.commands.settings import (
4
+ Settings,
5
+ EndpointOption,
6
+ get_settings,
7
+ )
8
+ from threedi_cmd.commands.models import EndpointChoices
9
+
10
+
11
+ def default_callback(
12
+ ctx: typer.Context,
13
+ endpoint: EndpointChoices = typer.Option(
14
+ EndpointChoices.production, case_sensitive=False
15
+ ),
16
+ ):
17
+ endpoint_name = EndpointOption[endpoint.value].name
18
+ settings = get_settings(endpoint_name)
19
+ ctx.obj = settings
20
+ ctx.call_on_close(Settings.save_settings)
@@ -0,0 +1,23 @@
1
+ from functools import lru_cache
2
+
3
+ from threedi_cmd.commands.app_definitions import registry
4
+ from threedi_cmd.plugins.tools import discover
5
+
6
+
7
+ @lru_cache()
8
+ def get_app():
9
+ plugin_registry = discover()
10
+ registry.apps.update(plugin_registry.apps)
11
+ for app_name, app_meta in registry.apps.items():
12
+ if not app_meta.add_to:
13
+ continue
14
+ add_to_meta = registry.apps[app_meta.add_to]
15
+ add_to_meta.app.add_typer(app_meta.app, name=app_meta.name, help=app_meta.help)
16
+ return registry.apps["core"].app
17
+
18
+
19
+ app = get_app()
20
+
21
+ if __name__ == "__main__":
22
+ app = get_app()
23
+ app()
@@ -0,0 +1,7 @@
1
+ from enum import Enum
2
+
3
+
4
+ class EndpointChoices(str, Enum):
5
+ localhost = "localhost"
6
+ staging = "staging"
7
+ production = "production"
@@ -0,0 +1,55 @@
1
+ from rich.table import Table
2
+ from rich import box
3
+ import typer
4
+
5
+ from threedi_cmd.commands.settings import (
6
+ ScenariosMeta,
7
+ )
8
+ from threedi_cmd.commands.callbacks import default_callback
9
+ from threedi_cmd.console import console
10
+ from threedi_cmd.logger import get_logger
11
+
12
+ logger = get_logger("INFO")
13
+
14
+ DEFAULT_PAGER_SIZE = 20
15
+
16
+
17
+ scenario_app = typer.Typer(callback=default_callback)
18
+
19
+
20
+ @scenario_app.command()
21
+ def scenarios(ctx: typer.Context):
22
+ """List local scenarios"""
23
+ table = Table(show_header=True, box=box.HORIZONTALS, show_lines=True)
24
+ table.add_column("Id", width=5)
25
+ table.add_column(
26
+ "Name",
27
+ width=20,
28
+ justify="left",
29
+ style="bold cyan",
30
+ header_style="bold cyan",
31
+ )
32
+ table.add_column("Description", justify="left", no_wrap=False)
33
+ table.add_column(
34
+ "Caution", justify="left", style="orange3", header_style="bold orange3"
35
+ )
36
+ table.add_column("yaml", justify="left", style="dim blue", header_style="bold blue")
37
+ s = ScenariosMeta(ctx.obj.scenario_folder)
38
+ for i, scenario in enumerate(s.scenarios, start=0):
39
+ if isinstance(scenario, dict):
40
+ s = ""
41
+ if scenario.get("known_constraints"):
42
+ for k, v in scenario["known_constraints"].items():
43
+ s += f"{k}: {v} \n"
44
+ table.add_row(
45
+ f"{i}",
46
+ f"{scenario['name']}",
47
+ f"{scenario['description']}",
48
+ f"{s}",
49
+ f"{scenario['file'].stem}",
50
+ )
51
+ console.print(table)
52
+
53
+
54
+ if __name__ == "__main__":
55
+ scenario_app()