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
File without changes
@@ -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,45 @@
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
+ # Env vars from other SAF components used to configure them through the CLI options
18
+ from pathlib import Path
19
+
20
+ GLOW_DEBUG = "GLOW_DEBUG"
21
+ GLOW_LOGGING_LEVEL = "GLOW_LOGGING_LEVEL"
22
+ PORTAL_UI_PORT = "PORTAL_UI_PORT"
23
+ GLOW_API_PORT = "GLOW_API_PORT"
24
+ GLOW_UI_PORT = "GLOW_UI_PORT"
25
+ GLOW_UI_DEBUG = "GLOW_UI_PYTHON_DEBUGGING"
26
+ GLOW_SOLUTION_DEFINITION = "GLOW_SOLUTION_DEFINITION"
27
+ GLOW_UI_MODULE = "GLOW_UI_MODULE"
28
+
29
+ # Env vars for solution scaffolding
30
+ _root_path = Path(__file__).resolve().parent.parent
31
+ SOLUTION_TEMPLATE_PATH = _root_path / "_solutions" / "templates" / "solution"
32
+ DEFAULT_SOLUTION_NAME = "my_solution"
33
+ DEFAULT_SOLUTION_DISPLAY_NAME = "My Solution"
34
+ DEFAULT_UI_FRAMEWORK = "dash"
35
+ UI_FRAMEWORKS = ["dash", "none"]
36
+ DEFAULT_STEP_NAME = "my_new_step"
37
+ DEFAULT_TEMPLATE_NAME = "calculator-step"
38
+ DEFAULT_SOLUTION_NAMESPACE = "saf.solutions"
39
+
40
+ # Env vars for solution installation
41
+ WORKSPACE_CLEAR_OPTIONS = ["hard", "soft"]
42
+
43
+ # Others
44
+ SAF_CLI_EXTERNAL_URL = "https://ansys.github.io/saf-cli-documentation"
45
+ SAF_DESKTOP_LOG_TO_FILES = "SAF_DESKTOP_LOG_TO_FILES"
File without changes
@@ -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,94 @@
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 json
18
+ import os
19
+ from pathlib import Path
20
+ import platform
21
+
22
+ import click
23
+ from pydantic import ValidationError
24
+
25
+ from ansys.saf.cli._database.models import SolutionDatabase, SolutionRegistry
26
+
27
+
28
+ class SolutionDatabaseManager:
29
+ def __init__(self) -> None:
30
+ self._db_path = self.get_solution_database_path()
31
+ self._db = self._load_solution_database()
32
+
33
+ @classmethod
34
+ def get_solution_database_path(cls) -> Path:
35
+ if platform.system() == "Windows":
36
+ directory = Path(os.environ["APPDATA"])
37
+ elif platform.system() == "Linux":
38
+ directory = Path(os.getenv("XDG_DATA_HOME", "~/.local/share")).expanduser()
39
+ else:
40
+ raise ValueError("Unsupported operating system")
41
+ return directory / "ansys" / "saf" / "cli" / "solution_db.json"
42
+
43
+ def _load_solution_database(self) -> SolutionDatabase:
44
+ db_raw = self._db_path.read_text() if self._db_path.is_file() else ""
45
+ try:
46
+ return SolutionDatabase.model_validate_json(db_raw or json.dumps({}))
47
+ except ValidationError:
48
+ click.secho(
49
+ "The solution database is invalid.",
50
+ err=True,
51
+ fg="red",
52
+ )
53
+ click.secho(
54
+ f"You may need to delete it from '{SolutionDatabaseManager.get_solution_database_path()}'",
55
+ err=True,
56
+ fg="red",
57
+ )
58
+ raise
59
+
60
+ def _save_solution_database(self):
61
+ if not self._db_path.is_file():
62
+ self._db_path.parent.mkdir(parents=True, exist_ok=True)
63
+ self._db_path.write_text(self._db.model_dump_json())
64
+
65
+ def store_solution(self, solution: SolutionRegistry) -> None:
66
+ if solution not in self._db.solutions:
67
+ self._db.solutions.append(solution)
68
+ self._save_solution_database()
69
+ else:
70
+ solution_in_db = next(db_solution for db_solution in self._db.solutions if db_solution == solution)
71
+ if solution_in_db.display_name != solution.display_name:
72
+ solution_in_db.display_name = solution.display_name
73
+ self._save_solution_database()
74
+
75
+ def get_solutions_grouped_by_name(self) -> dict[str, list[SolutionRegistry]]:
76
+ solutions_by_name: dict[str, list[SolutionRegistry]] = {}
77
+ for solution in self._db.solutions:
78
+ if not solution.is_valid:
79
+ continue
80
+ if solution.name not in solutions_by_name:
81
+ solutions_by_name[solution.name] = []
82
+ solutions_by_name[solution.name].append(solution)
83
+ return solutions_by_name
84
+
85
+ def get_solutions_by_name(self, solution_name: str) -> list[SolutionRegistry]:
86
+ return [solution for solution in self._db.solutions if solution.name == solution_name and solution.is_valid]
87
+
88
+ def get_solution_by_root_dir(self, solution_root_dir: Path) -> SolutionRegistry | None:
89
+ solutions = [
90
+ solution for solution in self._db.solutions if solution.root_dir == solution_root_dir and solution.is_valid
91
+ ]
92
+ if len(solutions) > 1:
93
+ raise ValueError(f"Multiple solutions found with the root directory {solution_root_dir}")
94
+ return solutions[0] if solutions else None
@@ -0,0 +1,53 @@
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
+ from pathlib import Path
18
+ import platform
19
+ import re
20
+
21
+ from pydantic import BaseModel, field_validator
22
+
23
+
24
+ class SolutionRegistry(BaseModel):
25
+ name: str
26
+ root_dir: Path
27
+ display_name: str
28
+
29
+ @property
30
+ def is_valid(self) -> bool:
31
+ return self.root_dir.is_dir()
32
+
33
+ @field_validator("name")
34
+ @classmethod
35
+ def _validate_solution_name(cls, solution_name: str) -> str:
36
+ if platform.system() == "Windows":
37
+ invalid_chars = r'[<>:"/\\|?*\x00-\x1F]'
38
+ elif platform.system() == "Linux":
39
+ invalid_chars = r"[/\x00]"
40
+ else:
41
+ raise ValueError("Unsupported operating system")
42
+ if bool(re.search(invalid_chars, solution_name)):
43
+ raise ValueError(f"Solution name contains invalid characters: {invalid_chars}")
44
+ return solution_name
45
+
46
+ def __eq__(self, other: object):
47
+ if not isinstance(other, type(self)):
48
+ return False
49
+ return self.root_dir == other.root_dir
50
+
51
+
52
+ class SolutionDatabase(BaseModel):
53
+ solutions: list[SolutionRegistry] = []
File without changes
@@ -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,256 @@
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
+ from pathlib import Path
18
+ import re
19
+ import tempfile
20
+
21
+ import click
22
+ from cookiecutter.main import cookiecutter # pyright: ignore[reportMissingTypeStubs, reportUnknownVariableType]
23
+ from packaging.version import Version
24
+ import tomlkit
25
+
26
+ from ansys.saf.cli._solutions.dependencies import DependencyManager
27
+ from ansys.saf.cli._solutions.plugins import SafTemplate
28
+ from ansys.saf.cli._utilities.conversion import namespace_to_path, to_class_name, to_module_name
29
+ from ansys.saf.cli._utilities.solution_modules import (
30
+ create_solution_environment,
31
+ get_exec_from_solution_venv,
32
+ get_main_module,
33
+ resolve_solution_main_file,
34
+ )
35
+
36
+ WRONG_FRAMEWORK_ERROR_MSG = (
37
+ "The step you are creating is using a different framework than the one you used to create the solution with."
38
+ )
39
+ WRONG_DASH_STRUCTURE_ERROR_MSG = (
40
+ "The target solution does not use the expected multi-page Dash structure required by 'saf add-step'."
41
+ )
42
+
43
+
44
+ def _insert_step_line(pattern: re.Pattern[str], content: str, new_line: str) -> str:
45
+ content_lines = content.splitlines()
46
+
47
+ if new_line not in content_lines:
48
+ insert_position = 0
49
+ last_non_empty_line = 0
50
+
51
+ for i, line in enumerate(content_lines):
52
+ if pattern.match(line):
53
+ insert_position = last_non_empty_line + 1
54
+ break
55
+ if line.strip():
56
+ last_non_empty_line = i
57
+
58
+ content_lines.insert(insert_position, new_line)
59
+
60
+ return "\n".join(content_lines)
61
+
62
+
63
+ def _resolve_solution_namespace_info(
64
+ solution_root_dir: Path,
65
+ solution_package_name: str | None = None,
66
+ ) -> tuple[str, str, str]:
67
+ """
68
+ Resolve namespace information from a solution's main.py file.
69
+ """
70
+ solution_main_file = resolve_solution_main_file(solution_root_dir)
71
+ main_module = get_main_module(solution_main_file)
72
+ resolved_solution_module = main_module.removesuffix(".main")
73
+ namespace, resolved_solution_package_name = resolved_solution_module.rsplit(".", 1)
74
+
75
+ # Trust the discovered module path; this supports both default and custom namespaces
76
+ if solution_package_name and solution_package_name != resolved_solution_package_name:
77
+ resolved_solution_package_name = solution_package_name
78
+
79
+ namespace_path = namespace_to_path(namespace)
80
+
81
+ return namespace, namespace_path, resolved_solution_package_name
82
+
83
+
84
+ def step_name_exists(solution_root_dir: Path, solution_package_name: str, step_name: str) -> bool:
85
+ """Return True if the step name already exists in the given solution."""
86
+ step_module_name = to_module_name(step_name, "step")
87
+
88
+ _, namespace_path, resolved_solution_package_name = _resolve_solution_namespace_info(
89
+ solution_root_dir,
90
+ solution_package_name,
91
+ )
92
+
93
+ solution_definition_path = (
94
+ solution_root_dir / "src" / namespace_path / resolved_solution_package_name / "solution" / "definition.py"
95
+ )
96
+
97
+ if not solution_definition_path.is_file():
98
+ raise FileNotFoundError(f"Solution definition not found in expected location: {solution_definition_path}")
99
+
100
+ return step_module_name in solution_definition_path.read_text()
101
+
102
+
103
+ def check_template_cli_compatibility(
104
+ solution_root_dir: Path,
105
+ saf_step_template: SafTemplate,
106
+ ) -> None:
107
+ if saf_step_template.saf_cli_compatibility_range is None:
108
+ return
109
+
110
+ pyproject_path = solution_root_dir / "pyproject.toml"
111
+ try:
112
+ pyproject_data = tomlkit.loads(pyproject_path.read_bytes()).unwrap()
113
+ solution_saf_cli_version = Version(pyproject_data.get("saf-cli-version", {}).get("saf-cli-version"))
114
+ except Exception:
115
+ solution_saf_cli_version = None
116
+
117
+ if solution_saf_cli_version is None:
118
+ click.secho(
119
+ f"Warning: Template '{saf_step_template.name}' declares the saf-cli compatibility constraint "
120
+ f"'{saf_step_template.saf_cli_compatibility_range}', but the solution does not declare a valid saf-cli "
121
+ f"version in pyproject.toml. The step might not work as expected.",
122
+ fg="yellow",
123
+ )
124
+ return
125
+
126
+ if not saf_step_template.saf_cli_compatibility_range.contains(solution_saf_cli_version, prereleases=True):
127
+ raise ValueError(
128
+ f"Template '{saf_step_template.name}' requires a saf-cli version in the range "
129
+ f"'{saf_step_template.saf_cli_compatibility_range}', "
130
+ f"but the solution's saf-cli version is {solution_saf_cli_version}.",
131
+ ) from None
132
+
133
+
134
+ def _add_step_to_definition(
135
+ solution_root_dir: Path,
136
+ solution_module_name: str,
137
+ step_module_name: str,
138
+ step_definition_class_name: str,
139
+ namespace: str,
140
+ namespace_path: str,
141
+ ) -> None:
142
+ solution_definition_path = (
143
+ solution_root_dir / "src" / namespace_path / solution_module_name / "solution" / "definition.py"
144
+ )
145
+
146
+ if not solution_definition_path.is_file():
147
+ raise FileNotFoundError(f"Solution definition not found in {solution_definition_path}.")
148
+
149
+ solution_definition_content = solution_definition_path.read_text()
150
+
151
+ new_step_line = f" {step_module_name}: {step_definition_class_name}"
152
+
153
+ solution_definition_content = _insert_step_line(
154
+ pattern=re.compile(r"^class .*?\(Solution\):$"),
155
+ content=solution_definition_content,
156
+ new_line=new_step_line,
157
+ )
158
+ solution_definition_content = _insert_step_line(
159
+ pattern=re.compile(r"^class .*?\(StepsModel\):$"),
160
+ content=solution_definition_content,
161
+ new_line=f"from {namespace}.{solution_module_name}.solution.{step_module_name} import {step_definition_class_name}", # noqa: E501
162
+ )
163
+ solution_definition_path.write_text(solution_definition_content)
164
+
165
+
166
+ def _validate_multi_page_dash_solution(
167
+ solution_root_dir: Path,
168
+ solution_module_name: str,
169
+ namespace_path: str,
170
+ ) -> None:
171
+ main_page_path = solution_root_dir / "src" / namespace_path / solution_module_name / "ui" / "pages" / "page.py"
172
+ app_path = solution_root_dir / "src" / namespace_path / solution_module_name / "ui" / "app.py"
173
+ # temporary fix until saf-cli supports detecting which framework the solution was created with.
174
+ if not main_page_path.is_file() or not app_path.is_file():
175
+ raise RuntimeError(WRONG_FRAMEWORK_ERROR_MSG)
176
+
177
+ app_content = app_path.read_text(encoding="utf-8")
178
+ if not re.search(r"\buse_pages\s*=\s*True\b", app_content):
179
+ raise RuntimeError(WRONG_DASH_STRUCTURE_ERROR_MSG)
180
+
181
+
182
+ def add_step_to_solution(
183
+ solution_name: str,
184
+ solution_root_dir: Path,
185
+ solution_module_name: str,
186
+ step_name: str,
187
+ ui_type: str,
188
+ saf_step_template: SafTemplate,
189
+ ) -> None:
190
+ # see docs' glossary for a detailed explanation of every term used in the cookiecutter kwargs
191
+
192
+ namespace, namespace_path, resolved_solution_module_name = _resolve_solution_namespace_info(
193
+ solution_root_dir,
194
+ solution_module_name,
195
+ )
196
+ solution_module_name = resolved_solution_module_name
197
+
198
+ solution_definition_class_name = to_class_name(solution_name, "Solution")
199
+ step_module_name = to_module_name(step_name, "step")
200
+ step_definition_class_name = to_class_name(step_name, "Step")
201
+ ui_page_file_name = to_module_name(step_module_name.removesuffix("_step"), "page")
202
+
203
+ if ui_type == "dash":
204
+ _validate_multi_page_dash_solution(
205
+ solution_root_dir,
206
+ solution_module_name,
207
+ namespace_path,
208
+ )
209
+
210
+ # Generate into a temporary directory and then copy generated files to the solution.
211
+ # This avoids cookiecutter complaining about an existing output directory, and lets the
212
+ # template hooks enforce that existing step files are not overwritten.
213
+ with tempfile.TemporaryDirectory() as baking_path:
214
+ cookiecutter(
215
+ template=saf_step_template.location.as_posix(),
216
+ output_dir=baking_path,
217
+ no_input=True,
218
+ # overwrite all cookiecutter.json terms
219
+ extra_context={
220
+ "__solution_name": solution_name,
221
+ "__solution_root_dir": solution_root_dir.absolute().as_posix(),
222
+ "__solution_module_name": solution_module_name,
223
+ "__solution_namespace": namespace,
224
+ "__solution_namespace_path": namespace_path,
225
+ "__solution_definition_class_name": solution_definition_class_name,
226
+ "__step_name": step_name,
227
+ "__step_module_name": step_module_name,
228
+ "__step_definition_class_name": step_definition_class_name,
229
+ "__ui_page_file_name": ui_page_file_name,
230
+ "__ui_framework": ui_type,
231
+ },
232
+ )
233
+
234
+ _add_step_to_definition(
235
+ solution_root_dir,
236
+ solution_module_name,
237
+ step_module_name,
238
+ step_definition_class_name,
239
+ namespace,
240
+ namespace_path,
241
+ )
242
+ if saf_step_template.dependencies:
243
+ click.secho(
244
+ "Updating solution dependencies. Do not modify solution files until completion.",
245
+ fg="cyan",
246
+ bold=True,
247
+ )
248
+
249
+ executable = get_exec_from_solution_venv(solution_root_dir, "poetry")
250
+ solution_env = create_solution_environment(solution_root_dir)
251
+
252
+ dependency_manager = DependencyManager(
253
+ saf_template=saf_step_template,
254
+ solution_root_dir=solution_root_dir,
255
+ )
256
+ dependency_manager.update_solution_dependencies(executable, solution_env)
@@ -0,0 +1,162 @@
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
+ from pathlib import Path
18
+ import subprocess
19
+ from typing import Any
20
+
21
+ import click
22
+ from pydantic import BaseModel
23
+ import tomlkit
24
+ from tomlkit.items import Table
25
+
26
+ from ansys.saf.cli._solutions.plugins import SafTemplate
27
+
28
+
29
+ class ManualDependency(BaseModel):
30
+ name: str
31
+ specification: str | dict[str, Any]
32
+ group: str
33
+
34
+
35
+ class DependencyManager:
36
+ def __init__(
37
+ self,
38
+ saf_template: SafTemplate,
39
+ solution_root_dir: Path,
40
+ ) -> None:
41
+ self._saf_template = saf_template
42
+ self._solution_root_dir = solution_root_dir
43
+ self._pyproject_path = solution_root_dir / "pyproject.toml"
44
+ if not self._pyproject_path.is_file():
45
+ raise FileNotFoundError(f"pyproject.toml not found in solution root directory '{self._solution_root_dir}'.")
46
+
47
+ @staticmethod
48
+ def _create_new_dependency_group() -> Table:
49
+ new_group = tomlkit.table()
50
+ new_group.add("optional", True)
51
+ deps = tomlkit.table()
52
+ new_group.add("dependencies", deps)
53
+ new_group.add(tomlkit.nl())
54
+ return new_group
55
+
56
+ def _get_group_str(self, group_name: str) -> str:
57
+ return "main" if group_name == "main" else f"{group_name} group"
58
+
59
+ def _get_solution_dependency_groups(self, poetry_section: dict[str, Any]) -> dict[str, Any]:
60
+ if "group" not in poetry_section: # type: ignore
61
+ if any(step_dep_group != "main" for step_dep_group in self._saf_template.dependencies):
62
+ poetry_section["group"] = tomlkit.table(is_super_table=True) # type: ignore
63
+ else:
64
+ return {}
65
+ return poetry_section["group"] # type: ignore
66
+
67
+ def _find_dependency_in_solution(
68
+ self,
69
+ dependency_name: str,
70
+ main_dependencies: dict[str, Any],
71
+ dependency_groups: dict[str, Any],
72
+ ) -> bool:
73
+ if dependency_name in main_dependencies:
74
+ return True
75
+ for _, group_dependencies in dependency_groups.items():
76
+ if dependency_name in group_dependencies.get("dependencies", {}):
77
+ return True
78
+ return False
79
+
80
+ def _add_dependency_to_solution(
81
+ self,
82
+ step_dependency_name: str,
83
+ step_dependency_spec: str | dict[str, Any],
84
+ solution_dependencies: dict[str, Any],
85
+ ) -> None:
86
+ if isinstance(step_dependency_spec, str):
87
+ # Dependencies in the format dependency_name: "dependency_specification"
88
+ solution_dependencies[step_dependency_name] = step_dependency_spec
89
+ else:
90
+ # Dependencies in the format dependency_name: { "key": "value", ... }
91
+ inline_dependency = tomlkit.inline_table()
92
+ inline_dependency.update(step_dependency_spec) # pyright: ignore[reportUnknownMemberType]
93
+ inline_dependency.trivia.trail = "\n"
94
+ solution_dependencies[step_dependency_name] = inline_dependency
95
+
96
+ def _render_manual_dependency_warning(self, manual_dependencies: list[ManualDependency], groups: set[str]) -> None:
97
+ click.secho(
98
+ f"WARNING: the step template '{self._saf_template.name}' has some dependencies in common with the "
99
+ f"solution's pyproject.toml at {self._pyproject_path}. Review the solution's pyproject.toml and manually "
100
+ f"update it if necessary, to ensure that it is compatible with the step template's dependencies.\n",
101
+ fg="yellow",
102
+ )
103
+ click.secho("Step specification of the common dependencies:", fg="yellow")
104
+ for manual_dependency in manual_dependencies:
105
+ click.secho(
106
+ f" - {manual_dependency.name} ({self._get_group_str(manual_dependency.group)}): "
107
+ f"{manual_dependency.specification}",
108
+ fg="yellow",
109
+ )
110
+ install_command = "poetry install"
111
+ if groups:
112
+ install_command += f" --with {','.join(sorted(groups))}"
113
+ click.secho(
114
+ f"\nAfter reviewing the dependencies, please run:\n"
115
+ f' - saf execute {self._solution_root_dir.name} "poetry lock"\n'
116
+ f' - saf execute {self._solution_root_dir.name} "{install_command}"',
117
+ fg="yellow",
118
+ )
119
+
120
+ def update_solution_dependencies(self, executable: Path, solution_env: dict[str, str]) -> None:
121
+ pyproject_content = tomlkit.loads(self._pyproject_path.read_bytes())
122
+ if not pyproject_content.get("tool", {}).get("poetry", {}): # type: ignore
123
+ raise ValueError(f"No [tool.poetry] section found in pyproject.toml at '{self._pyproject_path}'.")
124
+ poetry_section = pyproject_content["tool"]["poetry"] # type: ignore
125
+ main_deps = poetry_section.get("dependencies", {}) # type: ignore
126
+ if not main_deps:
127
+ raise ValueError(f"No main dependencies found in pyproject.toml at '{self._pyproject_path}'.")
128
+ dep_groups = self._get_solution_dependency_groups(poetry_section) # type: ignore
129
+
130
+ manual_deps: list[ManualDependency] = []
131
+ for step_dep_group in self._saf_template.dependencies:
132
+ if step_dep_group != "main" and step_dep_group not in dep_groups:
133
+ dep_groups[step_dep_group] = self._create_new_dependency_group()
134
+ group_deps = dep_groups.get(step_dep_group, {}).get("dependencies", {})
135
+ for step_dep_name, step_dep_spec in self._saf_template.dependencies[step_dep_group].items():
136
+ if self._find_dependency_in_solution(step_dep_name, main_deps, dep_groups): # type: ignore
137
+ manual_deps.append(
138
+ ManualDependency(
139
+ name=step_dep_name,
140
+ specification=step_dep_spec,
141
+ group=step_dep_group,
142
+ ),
143
+ )
144
+ else:
145
+ self._add_dependency_to_solution(
146
+ step_dep_name,
147
+ step_dep_spec,
148
+ main_deps if step_dep_group == "main" else group_deps, # type: ignore
149
+ )
150
+
151
+ self._pyproject_path.write_text(tomlkit.dumps(pyproject_content), newline="") # type: ignore
152
+
153
+ groups = {group for group in self._saf_template.dependencies if group != "main"}
154
+ if not manual_deps:
155
+ cmd = [executable.as_posix(), "lock"]
156
+ subprocess.run(cmd, env=solution_env, cwd=self._solution_root_dir, check=True)
157
+
158
+ cmd = [executable.as_posix(), "install"]
159
+ cmd += ["--with", ",".join(sorted(groups))] if groups else []
160
+ subprocess.run(cmd, env=solution_env, cwd=self._solution_root_dir, check=True)
161
+ else:
162
+ self._render_manual_dependency_warning(manual_deps, groups)