threedi-cmd 0.3.0__py3-none-any.whl

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