ansys-saf-cli 4.1.dev0__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 (143) hide show
  1. ansys/saf/cli/__init__.py +15 -0
  2. ansys/saf/cli/_cli/__init__.py +15 -0
  3. ansys/saf/cli/_cli/main.py +816 -0
  4. ansys/saf/cli/_cli/py.typed +0 -0
  5. ansys/saf/cli/_config/__init__.py +15 -0
  6. ansys/saf/cli/_config/const.py +45 -0
  7. ansys/saf/cli/_config/py.typed +0 -0
  8. ansys/saf/cli/_database/__init__.py +15 -0
  9. ansys/saf/cli/_database/manager.py +94 -0
  10. ansys/saf/cli/_database/models.py +53 -0
  11. ansys/saf/cli/_database/py.typed +0 -0
  12. ansys/saf/cli/_solutions/__init__.py +15 -0
  13. ansys/saf/cli/_solutions/add_step.py +256 -0
  14. ansys/saf/cli/_solutions/dependencies.py +162 -0
  15. ansys/saf/cli/_solutions/environment.py +1145 -0
  16. ansys/saf/cli/_solutions/plugins.py +254 -0
  17. ansys/saf/cli/_solutions/py.typed +0 -0
  18. ansys/saf/cli/_solutions/scaffolding.py +73 -0
  19. ansys/saf/cli/_solutions/templates/__init__.py +15 -0
  20. ansys/saf/cli/_solutions/templates/py.typed +0 -0
  21. ansys/saf/cli/_solutions/templates/solution/cookiecutter.json +18 -0
  22. ansys/saf/cli/_solutions/templates/solution/hooks/post_gen_project.py +54 -0
  23. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/.codespell.ignore +2 -0
  24. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/.devcontainer/devcontainer.json +26 -0
  25. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/.env +5 -0
  26. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/.flake8 +8 -0
  27. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/.github/labeler.yml +9 -0
  28. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/.github/labels.yml +27 -0
  29. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/.github/workflows/build-release.yml +175 -0
  30. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/.github/workflows/label.yml +89 -0
  31. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/.gitignore +137 -0
  32. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/.pre-commit-config.yaml +48 -0
  33. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/.release-please-manifest.json +3 -0
  34. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/.reuse/templates/copyright.jinja2 +1 -0
  35. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/.vscode/extensions.json +8 -0
  36. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/.vscode/launch.json +28 -0
  37. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/AUTHORS +12 -0
  38. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/CHANGELOG.md +1 -0
  39. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/CODEOWNERS +3 -0
  40. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/CODE_OF_CONDUCT.md +65 -0
  41. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/CONTRIBUTING.md +2 -0
  42. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/CONTRIBUTORS.md +9 -0
  43. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/LICENSE.rst +41 -0
  44. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/README.md +69 -0
  45. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/deployments/Dockerfile +43 -0
  46. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/deployments/README.md +13 -0
  47. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/deployments/distributed-deployment-template/.env +27 -0
  48. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/deployments/distributed-deployment-template/compose.yaml +132 -0
  49. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/deployments/standalone/.env +20 -0
  50. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/deployments/standalone/compose.yaml +118 -0
  51. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/deployments/standalone-with-hps/.env +28 -0
  52. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/deployments/standalone-with-hps/compose.yaml +146 -0
  53. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/deployments/standalone-with-minerva/.env +25 -0
  54. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/deployments/standalone-with-minerva/compose.yaml +122 -0
  55. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/doc/.vale.ini +28 -0
  56. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/doc/Makefile +30 -0
  57. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/doc/changelog.d/changelog_template.jinja +17 -0
  58. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/doc/make.bat +46 -0
  59. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/doc/source/_static/css/custom.css +8 -0
  60. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/doc/source/_static/images/repository-banner.png +0 -0
  61. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/doc/source/_templates/README.md +1 -0
  62. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/doc/source/changelog.rst +12 -0
  63. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/doc/source/conf.py +253 -0
  64. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/doc/source/examples.rst +8 -0
  65. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/doc/source/getting_started/desktop_installation.rst +165 -0
  66. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/doc/source/getting_started/docker_installation.rst +68 -0
  67. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/doc/source/index.rst +51 -0
  68. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/doc/source/user_guide/index.rst +6 -0
  69. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/doc/styles/config/vocabularies/ANSYS/accept.txt +5 -0
  70. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/doc/styles/config/vocabularies/ANSYS/reject.txt +0 -0
  71. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/examples/README.md +1 -0
  72. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/lock_files/dash/poetry.lock +7621 -0
  73. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/lock_files/no_ui/poetry.lock +7173 -0
  74. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/minerva/cli/README.md +3 -0
  75. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/minerva/oauth/App_Data/Certificates/README.md +3 -0
  76. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/minerva/oauth/oauth.config +9 -0
  77. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/pyproject.toml +192 -0
  78. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/release-please-config.json +9 -0
  79. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/sonar-project.properties +12 -0
  80. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/__init__.py +3 -0
  81. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/main.py +19 -0
  82. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/portal_assets/application.svg +36 -0
  83. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/portal_assets/description.json +3 -0
  84. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/portal_assets/project.svg +1 -0
  85. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/solution/definition.py +24 -0
  86. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/solution/first_step.py +34 -0
  87. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/solution/method_assets/README.md +1 -0
  88. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/solution/scripts/README.md +1 -0
  89. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/solution/scripts/assets/README.md +1 -0
  90. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/solution/second_step.py +12 -0
  91. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/app.py +30 -0
  92. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/assets/css/all.css +9 -0
  93. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/assets/css/bootstrap.min.css +6 -0
  94. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/assets/css/style.css +47 -0
  95. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/assets/icons/dark/carbon--ibm-engineering-workflow-mgmt.svg +1 -0
  96. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/assets/icons/dark/carbon--return.svg +1 -0
  97. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/assets/icons/dark/game-icons--crossed-air-flows.svg +1 -0
  98. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/assets/icons/dark/material-symbols--home.svg +1 -0
  99. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/assets/icons/dark/radix-icons--moon.svg +1 -0
  100. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/assets/icons/dark/radix-icons--sun.svg +1 -0
  101. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/assets/icons/dark/streamline--startup-solid.svg +1 -0
  102. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/assets/icons/dark/teenyicons--doc-solid.svg +1 -0
  103. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/assets/icons/light/carbon--ibm-engineering-workflow-mgmt.svg +1 -0
  104. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/assets/icons/light/carbon--return.svg +1 -0
  105. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/assets/icons/light/game-icons--crossed-air-flows.svg +1 -0
  106. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/assets/icons/light/material-symbols--home.svg +1 -0
  107. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/assets/icons/light/radix-icons--moon.svg +1 -0
  108. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/assets/icons/light/radix-icons--sun.svg +1 -0
  109. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/assets/icons/light/streamline--startup-solid.svg +1 -0
  110. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/assets/icons/light/teenyicons--doc-solid.svg +1 -0
  111. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/assets/images/README.md +1 -0
  112. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/assets/images/workflow-placeholder.png +0 -0
  113. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/assets/installer/README.md +5 -0
  114. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/assets/logos/dark/placeholder_logo.png +0 -0
  115. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/assets/logos/light/placeholder_logo.png +0 -0
  116. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/assets/orchestrator/README.md +3 -0
  117. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/assets/pywebview/README.md +3 -0
  118. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/assets/scripts/README.md +1 -0
  119. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/components/README.md +1 -0
  120. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/pages/about_page.py +71 -0
  121. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/pages/first_page.py +147 -0
  122. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/pages/page.py +367 -0
  123. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/pages/second_page.py +32 -0
  124. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/tests/common_test_files/README.md +1 -0
  125. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/tests/conftest.py +37 -0
  126. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/tests/unit/test_solution_api.py +42 -0
  127. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/tests/unit/test_solution_ui.py +30 -0
  128. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/tox.ini +56 -0
  129. ansys/saf/cli/_utilities/__init__.py +15 -0
  130. ansys/saf/cli/_utilities/archiver.py +67 -0
  131. ansys/saf/cli/_utilities/backup.py +181 -0
  132. ansys/saf/cli/_utilities/conversion.py +64 -0
  133. ansys/saf/cli/_utilities/platform.py +26 -0
  134. ansys/saf/cli/_utilities/py.typed +0 -0
  135. ansys/saf/cli/_utilities/solution_modules.py +173 -0
  136. ansys/saf/cli/integrations/solutions_manager.py +64 -0
  137. ansys/saf/cli/py.typed +0 -0
  138. ansys_saf_cli-4.1.dev0.dist-info/METADATA +201 -0
  139. ansys_saf_cli-4.1.dev0.dist-info/RECORD +143 -0
  140. ansys_saf_cli-4.1.dev0.dist-info/WHEEL +4 -0
  141. ansys_saf_cli-4.1.dev0.dist-info/entry_points.txt +3 -0
  142. ansys_saf_cli-4.1.dev0.dist-info/licenses/AUTHORS +12 -0
  143. ansys_saf_cli-4.1.dev0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,367 @@
1
+ # Copyright (C) 2023 - 2026 ANSYS, Inc. and/or its affiliates.
2
+
3
+ """Initialization of the frontend layout across all the steps."""
4
+
5
+
6
+ import importlib
7
+ import inspect
8
+ import logging
9
+ from pathlib import Path
10
+ from typing import Any, Literal
11
+ import webbrowser
12
+
13
+ from ansys.saf.glow.client import DashClient, Deployment, NotFoundException, callback
14
+ from ansys.solutions.dash_super_components import Tree
15
+ import dash
16
+ import dash_bootstrap_components as dbc
17
+ from dash_extensions.enrich import Input, Output, State, clientside_callback, dcc, html, no_update
18
+ import dash_mantine_components as dmc
19
+
20
+ from {{ cookiecutter.__solution_namespace }}.{{ cookiecutter.__solution_module_name }}.solution.definition import {{ cookiecutter.__solution_definition_class_name }}
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+
25
+ def get_asset(asset_name: str, relative_path: str = "", theme: str = "dark") -> str:
26
+ """Return asset URL based on the current theme."""
27
+ path = f"{relative_path.strip('/')}/{theme}/{asset_name}"
28
+ return dash.get_asset_url(path)
29
+
30
+
31
+ def get_page_list(theme: str) -> list[dict[str, str | bool]]:
32
+ """Return the page list with icons based on the current theme and registered pages."""
33
+ pages = []
34
+ for i, page in enumerate(dash.page_registry.values()):
35
+ if page["module"].split(".")[-1] == "not_found_404":
36
+ continue
37
+ page_id = str(i)
38
+ page_name = page["name"]
39
+ icon_name = page.get("icon_asset_name", "carbon--ibm-engineering-workflow-mgmt.svg")
40
+ icon_path = page.get("icon_asset_path", "icons")
41
+ icon_url = get_asset(icon_name, icon_path, theme)
42
+ pages.append({"id": page_id, "text": page_name, "icon": icon_url, "expanded": True})
43
+ return pages
44
+
45
+
46
+ theme_toggle = dmc.Switch(
47
+ offLabel=html.Img(src=get_asset("radix-icons--sun.svg", "icons", "light")),
48
+ onLabel=html.Img(src=get_asset("radix-icons--moon.svg", "icons", "light")),
49
+ id="color-scheme-switch",
50
+ persistence=True,
51
+ color="grey",
52
+ checked=False,
53
+ )
54
+
55
+
56
+ header = dmc.AppShellHeader(
57
+ dmc.Flex(
58
+ [
59
+ html.Img(id="logo-image", src=get_asset("placeholder_logo.png", "logos", "light"), height="36px"),
60
+ dmc.Group(
61
+ [
62
+ dmc.Text(
63
+ "Project name:",
64
+ id="project-name",
65
+ size="sm",
66
+ style={"font-size": "16px", "color": "var(--mantine-color-text)"},
67
+ ),
68
+ theme_toggle,
69
+ dmc.ActionIcon(
70
+ html.Img(src=get_asset("teenyicons--doc-solid.svg", "icons", "light")),
71
+ id="access-solution-doc",
72
+ variant="transparent",
73
+ style={"color": "var(--mantine-color-text)"},
74
+ ),
75
+ dbc.Popover(
76
+ "Get access to the Solution's Documentation.",
77
+ target="access-solution-doc",
78
+ body=True,
79
+ trigger="hover",
80
+ ),
81
+ html.Div(id="return-to-portal"),
82
+ ],
83
+ gap=10,
84
+ ),
85
+ ],
86
+ justify="space-between",
87
+ align="center",
88
+ direction="row",
89
+ wrap="wrap",
90
+ h="100%",
91
+ px="md",
92
+ )
93
+ )
94
+
95
+
96
+ navbar = dmc.AppShellNavbar(
97
+ html.Div(),
98
+ id="navbar-content",
99
+ p="md",
100
+ )
101
+
102
+
103
+ main_layout = dmc.AppShellMain(
104
+ html.Div(
105
+ id="page-content",
106
+ style={
107
+ "padding-right": "0.7%",
108
+ "padding-left": "0.7%",
109
+ "backgroundColor": "var(--mantine-color-body)",
110
+ "color": "var(--mantine-color-text)",
111
+ "minHeight": "100vh",
112
+ },
113
+ )
114
+ )
115
+
116
+
117
+ layout = dmc.MantineProvider(
118
+ [
119
+ dmc.NotificationContainer(
120
+ id="notification-container",
121
+ position="bottom-left",
122
+ notificationMaxHeight=400,
123
+ ),
124
+ dmc.AppShell(
125
+ [
126
+ header,
127
+ navbar,
128
+ main_layout,
129
+ ],
130
+ header={"height": 70},
131
+ navbar={
132
+ "width": 300,
133
+ "breakpoint": "sm",
134
+ "collapsed": {"mobile": True},
135
+ },
136
+ ),
137
+ dcc.Location(id="url", refresh="callback-nav"),
138
+ html.Div(
139
+ id="alerts-container", style={"position": "fixed", "top": 90, "right": 10, "width": 350, "zIndex": 1000}
140
+ ),
141
+ dcc.Store(id="active-page-index", data=None),
142
+ dcc.Store(id="active-project-id", data=None),
143
+ ],
144
+ id="mantine-provider",
145
+ defaultColorScheme="light",
146
+ )
147
+
148
+
149
+ def _get_alert_toast(message: str, level: Literal["success", "info", "warning", "danger"]) -> dbc.Toast:
150
+ """Create a toast with the alert message."""
151
+ content = html.Div([message], style={"font-size": "small"})
152
+ toast = dbc.Toast(
153
+ content,
154
+ header=level.upper(),
155
+ is_open=True,
156
+ dismissable=True,
157
+ icon=level,
158
+ color=level,
159
+ duration=20000,
160
+ )
161
+
162
+ return toast
163
+
164
+
165
+ @callback(
166
+ Output("return-to-portal", "children"),
167
+ Input("url", "pathname"),
168
+ Input("color-scheme-switch", "checked"),
169
+ )
170
+ def return_to_portal(pathname: str, switch_on: bool) -> list[html.A]:
171
+ """Display Solution Portal when back-to-portal button gets selected."""
172
+ theme = "dark" if switch_on else "light"
173
+
174
+ portal_ui_url = DashClient.get_portal_ui_url()
175
+
176
+ if portal_ui_url is None:
177
+ return []
178
+
179
+ popover_text = "Back to Projects" if DashClient.get_deployment_type() == Deployment.Desktop else "Back to Portal"
180
+
181
+ return [
182
+ html.A(
183
+ [
184
+ dmc.ActionIcon(
185
+ html.Img(src=get_asset("carbon--return.svg", "icons", theme)),
186
+ id="back-to-projects-icon",
187
+ variant="transparent",
188
+ ),
189
+ dbc.Popover(
190
+ popover_text,
191
+ target="back-to-projects-icon",
192
+ body=True,
193
+ trigger="hover",
194
+ ),
195
+ ],
196
+ href=portal_ui_url,
197
+ )
198
+ ]
199
+
200
+
201
+ @callback(
202
+ Output("project-name", "children"),
203
+ Input("url", "pathname"),
204
+ )
205
+ def display_project_name(project: {{ cookiecutter.__solution_definition_class_name }}) -> str:
206
+ """Display current project name."""
207
+ return f"Project Name: {project.project_display_name}"
208
+
209
+
210
+ @callback(
211
+ Output("alerts-container", "children"),
212
+ Input("access-solution-doc", "n_clicks"),
213
+ prevent_initial_call=True,
214
+ )
215
+ def access_solution_documentation(n_clicks: int) -> list[dbc.Toast] | None:
216
+ """Open the Solution's Documentation home page in the web browser."""
217
+ solution_module = importlib.import_module("{{ cookiecutter.__solution_namespace }}.{{ cookiecutter.__solution_module_name }}")
218
+ doc_index_path = Path(solution_module.__path__[0]) / "html-doc" / "index.html"
219
+ if not doc_index_path.is_file():
220
+ doc_index_path = Path.cwd() / "doc" / "build" / "html" / "index.html"
221
+ if not doc_index_path.is_file():
222
+ return _get_alert_toast("Solution documentation is not available.", "warning")
223
+ logger.info(f"Opening documentation at {doc_index_path}")
224
+ webbrowser.open_new(doc_index_path.as_posix())
225
+ return no_update
226
+
227
+
228
+ @callback(
229
+ Output("active-page-index", "data"),
230
+ Output("active-project-id", "data"),
231
+ Input("url", "pathname"),
232
+ )
233
+ def resolve_active_page_and_project_information(pathname: str) -> tuple[str | None, str | None]:
234
+ """Resolve the active page index and project ID based on the current URL."""
235
+ path_parts = pathname.strip("/").split("/")
236
+ for i, page in enumerate(dash.page_registry.values()):
237
+ if page["module"].split(".")[-1] == "not_found_404":
238
+ continue
239
+ template_parts = page["path_template"].strip("/").split("/")
240
+ # look for same parts, excluding <project_id> variable
241
+ if len(path_parts) != len(template_parts):
242
+ continue
243
+ project_id = None
244
+ matched = True
245
+ for tp, pp in zip(template_parts, path_parts, strict=True):
246
+ if tp == "<project_id>":
247
+ project_id = pp
248
+ elif tp != pp:
249
+ matched = False
250
+ break
251
+ if matched:
252
+ return str(i), project_id
253
+ return None, None
254
+
255
+
256
+ @callback(
257
+ Output("navbar-content", "children"),
258
+ Input("active-page-index", "data"),
259
+ Input("color-scheme-switch", "checked"),
260
+ prevent_initial_call=True,
261
+ )
262
+ def render_nav_tree(active_index: str | None, switch_on: bool):
263
+ """Sync the active tree item based on the active page index."""
264
+ theme = "dark" if switch_on else "light"
265
+ return Tree(aio_id="navigation_tree", items=get_page_list(theme), active_item_id=active_index)
266
+
267
+
268
+ def _display_404_page() -> Any:
269
+ for module, page in dash.page_registry.items():
270
+ if module.split(".")[-1] == "not_found_404":
271
+ return page["layout"]
272
+ return html.H1("404 - Page not found")
273
+
274
+
275
+ @callback(
276
+ Output("page-content", "children"),
277
+ Input("url", "pathname"),
278
+ Input("active-page-index", "data"),
279
+ prevent_initial_call=True,
280
+ )
281
+ def display_page(project: {{ cookiecutter.__solution_definition_class_name }}, active_page_index: str | None):
282
+ """Return the page layout, passing the project instance.
283
+
284
+ Using dash.page_container does not allow to inject the project instance as argument to the layout function,
285
+ only supports passing path variables, query parameters or other Inputs/States.
286
+ """
287
+ if active_page_index is None:
288
+ # triggered if pathname with correct project information but incorrect sub-path.
289
+ # anyway, the 404 page will be rendered by another callback
290
+ return no_update
291
+ try:
292
+ # verify that the project information is valid, otherwise return 404.
293
+ # Cannot be done in display_404_page callback because we need that one to succeed if project injection fails.
294
+ _ = project.project_display_name
295
+ except NotFoundException:
296
+ return _display_404_page()
297
+
298
+ pages = list(dash.page_registry.values()) # page_registry is an ordered dict
299
+ index = int(active_page_index)
300
+ page = pages[index]
301
+ # support layout module attribute and layout function (with or without project argument)
302
+ layout_func = page["layout"]
303
+ if callable(layout_func):
304
+ params = inspect.signature(layout_func).parameters
305
+ if "project" in params:
306
+ return layout_func(project=project)
307
+ return layout_func()
308
+ return layout_func
309
+
310
+
311
+ @callback(
312
+ Output("page-content", "children"),
313
+ Input("active-page-index", "data"),
314
+ prevent_initial_call=True,
315
+ )
316
+ def display_404_page(active_page_index: str | None):
317
+ """Return the 404 page layout when the active page index is None."""
318
+ if active_page_index is None:
319
+ return _display_404_page()
320
+ return no_update
321
+
322
+
323
+ @callback(
324
+ Output("url", "href"),
325
+ Input(Tree.ids.selected_item("navigation_tree"), "data"),
326
+ State("active-project-id", "data"),
327
+ State("url", "pathname"),
328
+ prevent_initial_call=True,
329
+ )
330
+ def open_new_page(value, project_id: str | None, pathname: str):
331
+ """Navigate to the selected page."""
332
+ if project_id is None or not value:
333
+ return no_update
334
+ item_index = int(Tree.ids.get_index_from_navlink_item_id(value))
335
+ pages = list(dash.page_registry.values())
336
+ if item_index >= len(pages):
337
+ return no_update
338
+ page = pages[item_index]
339
+ target_path = page["path_template"].replace("<project_id>", project_id)
340
+ if target_path == pathname:
341
+ return no_update
342
+ return target_path
343
+
344
+
345
+ @callback(
346
+ Output("access-solution-doc", "children"),
347
+ Output("logo-image", "src"),
348
+ Input("color-scheme-switch", "checked"),
349
+ )
350
+ def update_nav_icons(switch_on: bool) -> tuple[html.Img, str]:
351
+ """Update navigation tree icons based on the current theme."""
352
+ theme = "dark" if switch_on else "light"
353
+ return (
354
+ html.Img(src=get_asset("teenyicons--doc-solid.svg", "icons", theme)),
355
+ get_asset("placeholder_logo.png", "logos", theme),
356
+ )
357
+
358
+
359
+ clientside_callback(
360
+ """
361
+ (switchOn) => {
362
+ return switchOn ? 'dark' : 'light';
363
+ }
364
+ """,
365
+ Output("mantine-provider", "forceColorScheme"),
366
+ Input("color-scheme-switch", "checked"),
367
+ )
@@ -0,0 +1,32 @@
1
+ # Copyright (C) 2023 - 2026 ANSYS, Inc. and/or its affiliates.
2
+
3
+ """Frontend of the second step."""
4
+
5
+
6
+ import dash
7
+ from dash_extensions.enrich import html
8
+
9
+ from {{ cookiecutter.__solution_namespace }}.{{ cookiecutter.__solution_module_name }}.solution.definition import {{cookiecutter.__solution_definition_class_name}}
10
+
11
+ dash.register_page(
12
+ __name__,
13
+ name="Second Step",
14
+ path_template="/projects/<project_id>/second-step",
15
+ icon_asset_name="carbon--ibm-engineering-workflow-mgmt.svg",
16
+ icon_asset_path="icons",
17
+ )
18
+
19
+
20
+ def layout(project: {{cookiecutter.__solution_definition_class_name}}):
21
+ """Layout of the second step page."""
22
+ return html.Div(
23
+ [
24
+ html.H1("Second Step", className="display-3", style={"font-size": "48px", "fontWeight": "bold"}),
25
+ html.P(
26
+ "This page is empty for now.",
27
+ className="lead",
28
+ style={"font-size": "20px"},
29
+ ),
30
+ html.Br(),
31
+ ]
32
+ )
@@ -0,0 +1,37 @@
1
+ # Copyright (C) 2023 - 2026 ANSYS, Inc. and/or its affiliates.
2
+
3
+ # ==================================================== [Imports] ==================================================== #
4
+
5
+ import os
6
+ from pathlib import Path
7
+
8
+ from dotenv import load_dotenv
9
+
10
+ # =================================================== [Variables] =================================================== #
11
+
12
+ # Get absolute path to tests directory
13
+ tests_directory = Path(__file__).parent.absolute()
14
+
15
+ # Get absolute path to project directory
16
+ project_directory = Path(__file__).parent.parent.absolute()
17
+
18
+ # =================================================== [Functions] =================================================== #
19
+
20
+
21
+ def _set_default_glow_env(project_directory: Path) -> None:
22
+ env_file = project_directory / ".env"
23
+ if env_file.is_file():
24
+ load_dotenv(env_file, override=False)
25
+
26
+ os.environ.setdefault(
27
+ "GLOW_SOLUTION_DEFINITION",
28
+ "{{cookiecutter.__solution_namespace}}.{{cookiecutter.__solution_module_name}}.solution.definition",
29
+ )
30
+ os.environ.setdefault(
31
+ "GLOW_UI_MODULE",
32
+ "{{cookiecutter.__solution_namespace}}.{{cookiecutter.__solution_module_name}}.ui.app",
33
+ )
34
+
35
+
36
+ def pytest_configure(config) -> None:
37
+ _set_default_glow_env(project_directory)
@@ -0,0 +1,42 @@
1
+ # Copyright (C) 2023 - 2026 ANSYS, Inc. and/or its affiliates.
2
+
3
+ from {{ cookiecutter.__solution_namespace }}.{{ cookiecutter.__solution_module_name }}.solution.definition import {{ cookiecutter.__solution_definition_class_name }}
4
+
5
+
6
+ def test_set_and_retrieve_basic_step_fields(client_project: {{ cookiecutter.__solution_definition_class_name }}):
7
+ first_step = client_project.steps.first_step
8
+ # retrieve step field
9
+ assert first_step.first_arg == 0
10
+ # set step field
11
+ first_step.first_arg = 1.0
12
+ # retrieve updated value
13
+ assert first_step.first_arg == 1.0
14
+ # set multiple fields
15
+ first_step.set_fields({"first_arg": 2.0, "second_arg": 3.0})
16
+ # retrieve updated values
17
+ assert first_step.first_arg == 2.0
18
+ assert first_step.second_arg == 3.0
19
+
20
+
21
+ def test_calculate(client_project: {{ cookiecutter.__solution_definition_class_name }}):
22
+ first_step = client_project.steps.first_step
23
+ first_step.first_arg = 10.0
24
+ first_step.second_arg = 20.0
25
+ first_step.calculate()
26
+ assert first_step.result == 30.0
27
+
28
+
29
+ def test_save_result(client_project: {{ cookiecutter.__solution_definition_class_name }}):
30
+ first_step = client_project.steps.first_step
31
+ first_step.result = 7.0
32
+ first_step.save_result()
33
+ assert client_project.storage_scope.get_text(first_step.result_file) == "7.0"
34
+
35
+
36
+ def test_load_result(client_project: {{ cookiecutter.__solution_definition_class_name }}):
37
+ result_file = client_project.storage_scope.get_storage_root() / "result_file.txt"
38
+ result_file.write_text("29.5")
39
+ first_step = client_project.steps.first_step
40
+ first_step.result_file = client_project.storage_scope.store(result_file)
41
+ result = first_step.load_result()
42
+ assert result == 29.5
@@ -0,0 +1,30 @@
1
+ # Copyright (C) 2023 - 2026 ANSYS, Inc. and/or its affiliates.
2
+
3
+ from dash.testing import ignore_register_page
4
+ import pytest
5
+
6
+ from {{ cookiecutter.__solution_namespace }}.{{ cookiecutter.__solution_module_name }}.solution.definition import {{ cookiecutter.__solution_definition_class_name }}
7
+
8
+ with ignore_register_page():
9
+ from {{ cookiecutter.__solution_namespace }}.{{ cookiecutter.__solution_module_name }}.ui.pages.first_page import calculate, load_result, save_result
10
+
11
+ pytestmark = [pytest.mark.usefixtures("init_dashclient")]
12
+
13
+
14
+ def test_calculate_callback(project_name: str):
15
+ assert calculate(1, 1, 2, project_name) == (3.0, True)
16
+
17
+
18
+ def test_save_result_callback(client_project: {{ cookiecutter.__solution_definition_class_name }}, project_name: str):
19
+ first_step = client_project.steps.first_step
20
+ first_step.result = 7.0
21
+ assert save_result(1, project_name) == True
22
+ assert client_project.storage_scope.get_text(first_step.result_file) == "7.0"
23
+
24
+
25
+ def test_load_result_callback(client_project: {{ cookiecutter.__solution_definition_class_name }}, project_name: str):
26
+ result_file = client_project.storage_scope.get_storage_root() / "result_file.txt"
27
+ result_file.write_text("29.5")
28
+ first_step = client_project.steps.first_step
29
+ first_step.result_file = client_project.storage_scope.store(result_file)
30
+ assert load_result(1, project_name) == (29.5, True)
@@ -0,0 +1,56 @@
1
+ [tox]
2
+ description = Default tox environments list
3
+ envlist =
4
+ style,{py39,py310}{,-coverage},doc,build
5
+ skip_missing_interpreters = true
6
+ isolated_build = true
7
+
8
+ [gh-actions]
9
+ description = The tox environment to be executed in gh-actions for a given python version
10
+ python =
11
+ 3.9: style,py39-coverage,doc,build
12
+ 3.10: style,py310-coverage,doc,build
13
+
14
+ [testenv]
15
+ description = Perform functional testing
16
+ basepython =
17
+ py39: python3.9
18
+ py310: python3.10
19
+ py: python3
20
+ {style,reformat,doc,build}: python3
21
+ skip_install = true
22
+ passenv = *
23
+ allowlist_externals = poetry
24
+ commands =
25
+ poetry config virtualenvs.create false
26
+ poetry install --only main,tests -vv
27
+ poetry run pytest -p no:faulthandler --cov={{ cookiecutter.__solution_namespace }} --cov-report=term --cov-report=xml --cov-report=html -vv
28
+ poetry run coverage lcov
29
+
30
+ [testenv:style]
31
+ description = Checks project code style
32
+ skip_install = true
33
+ deps =
34
+ pre-commit
35
+ setenv =
36
+ SKIP = poetry-check
37
+ commands =
38
+ pre-commit install
39
+ pre-commit run --all-files --show-diff-on-failure
40
+
41
+ [testenv:doc]
42
+ description = Check if documentation generates properly
43
+ skip_install = true
44
+ commands =
45
+ poetry config virtualenvs.create false
46
+ poetry install --only doc -vv
47
+ poetry run sphinx-build doc/source doc/build/html --color -vW -bhtml
48
+
49
+ [testenv:build]
50
+ description = Check source code build
51
+ skip_install = true
52
+ commands =
53
+ poetry config virtualenvs.create false
54
+ poetry install --only build -vv
55
+ poetry run python -m build
56
+
@@ -0,0 +1,15 @@
1
+ # Copyright (C) 2026 ANSYS, Inc. and/or its affiliates.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ #
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
@@ -0,0 +1,67 @@
1
+ # Copyright (C) 2026 ANSYS, Inc. and/or its affiliates.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ #
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ import os
18
+ from pathlib import Path
19
+ import zipfile
20
+
21
+ SOLUTION_EXT = ".saf"
22
+ DEFAULT_EXCLUDED_DIRS = [".venv", ".poetry", ".git", "glow", "portal", ".github"]
23
+
24
+
25
+ def archive_solution(
26
+ solution_root_dir: Path,
27
+ archive_name: str,
28
+ archive_path: Path | None = None,
29
+ archive_extension: str | None = None,
30
+ ) -> None:
31
+ """Archive a solution directory into a zip file.
32
+
33
+ This function takes a solution directory, compresses its contents into a zip file, and saves
34
+ it to the specified archive path or the current working directory if no path is provided.
35
+
36
+ Parameters
37
+ ----------
38
+ solution_root_dir : Path
39
+ The path to the solution's root directory (which contains src/) to be archived.
40
+ archive_name : str
41
+ The name of the archive file (without extension).
42
+ archive_path : Path, optional
43
+ The path where the archive file will be saved. Default path: current working directory.
44
+ archive_extension : str, optional
45
+ The extension for the archive file, including the initial dot. Default extension: ".saf".
46
+ """
47
+ if archive_path and not archive_path.is_dir():
48
+ raise NotADirectoryError(f"{archive_path} is not a valid directory.")
49
+
50
+ extension = f"{archive_extension}" if archive_extension is not None else SOLUTION_EXT
51
+
52
+ archive_path = (archive_path if archive_path else Path.cwd()) / f"{archive_name}{extension}"
53
+
54
+ with zipfile.ZipFile(archive_path, "w", zipfile.ZIP_DEFLATED) as zip_file:
55
+ stack = [solution_root_dir]
56
+ while stack:
57
+ current_path = stack.pop()
58
+ with os.scandir(current_path) as entries:
59
+ for entry in entries:
60
+ entry_path = current_path / entry.name
61
+ # Makes sure that the created .saf file is not part of the archive itself!
62
+ # (This typically happens when running "saf archive .").
63
+ if entry.is_file() and str(entry_path.resolve()) != zip_file.filename:
64
+ parent_path = os.path.relpath(entry_path, solution_root_dir)
65
+ zip_file.write(entry_path, parent_path)
66
+ elif entry.is_dir() and entry.name not in DEFAULT_EXCLUDED_DIRS:
67
+ stack.append(entry_path)