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.
- ansys/saf/cli/__init__.py +15 -0
- ansys/saf/cli/_cli/__init__.py +15 -0
- ansys/saf/cli/_cli/main.py +816 -0
- ansys/saf/cli/_cli/py.typed +0 -0
- ansys/saf/cli/_config/__init__.py +15 -0
- ansys/saf/cli/_config/const.py +45 -0
- ansys/saf/cli/_config/py.typed +0 -0
- ansys/saf/cli/_database/__init__.py +15 -0
- ansys/saf/cli/_database/manager.py +94 -0
- ansys/saf/cli/_database/models.py +53 -0
- ansys/saf/cli/_database/py.typed +0 -0
- ansys/saf/cli/_solutions/__init__.py +15 -0
- ansys/saf/cli/_solutions/add_step.py +256 -0
- ansys/saf/cli/_solutions/dependencies.py +162 -0
- ansys/saf/cli/_solutions/environment.py +1145 -0
- ansys/saf/cli/_solutions/plugins.py +254 -0
- ansys/saf/cli/_solutions/py.typed +0 -0
- ansys/saf/cli/_solutions/scaffolding.py +73 -0
- ansys/saf/cli/_solutions/templates/__init__.py +15 -0
- ansys/saf/cli/_solutions/templates/py.typed +0 -0
- ansys/saf/cli/_solutions/templates/solution/cookiecutter.json +18 -0
- ansys/saf/cli/_solutions/templates/solution/hooks/post_gen_project.py +54 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/.codespell.ignore +2 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/.devcontainer/devcontainer.json +26 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/.env +5 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/.flake8 +8 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/.github/labeler.yml +9 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/.github/labels.yml +27 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/.github/workflows/build-release.yml +175 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/.github/workflows/label.yml +89 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/.gitignore +137 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/.pre-commit-config.yaml +48 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/.release-please-manifest.json +3 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/.reuse/templates/copyright.jinja2 +1 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/.vscode/extensions.json +8 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/.vscode/launch.json +28 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/AUTHORS +12 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/CHANGELOG.md +1 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/CODEOWNERS +3 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/CODE_OF_CONDUCT.md +65 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/CONTRIBUTING.md +2 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/CONTRIBUTORS.md +9 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/LICENSE.rst +41 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/README.md +69 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/deployments/Dockerfile +43 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/deployments/README.md +13 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/deployments/distributed-deployment-template/.env +27 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/deployments/distributed-deployment-template/compose.yaml +132 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/deployments/standalone/.env +20 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/deployments/standalone/compose.yaml +118 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/deployments/standalone-with-hps/.env +28 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/deployments/standalone-with-hps/compose.yaml +146 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/deployments/standalone-with-minerva/.env +25 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/deployments/standalone-with-minerva/compose.yaml +122 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/doc/.vale.ini +28 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/doc/Makefile +30 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/doc/changelog.d/changelog_template.jinja +17 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/doc/make.bat +46 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/doc/source/_static/css/custom.css +8 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/doc/source/_static/images/repository-banner.png +0 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/doc/source/_templates/README.md +1 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/doc/source/changelog.rst +12 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/doc/source/conf.py +253 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/doc/source/examples.rst +8 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/doc/source/getting_started/desktop_installation.rst +165 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/doc/source/getting_started/docker_installation.rst +68 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/doc/source/index.rst +51 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/doc/source/user_guide/index.rst +6 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/doc/styles/config/vocabularies/ANSYS/accept.txt +5 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/doc/styles/config/vocabularies/ANSYS/reject.txt +0 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/examples/README.md +1 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/lock_files/dash/poetry.lock +7621 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/lock_files/no_ui/poetry.lock +7173 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/minerva/cli/README.md +3 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/minerva/oauth/App_Data/Certificates/README.md +3 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/minerva/oauth/oauth.config +9 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/pyproject.toml +192 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/release-please-config.json +9 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/sonar-project.properties +12 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/__init__.py +3 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/main.py +19 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/portal_assets/application.svg +36 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/portal_assets/description.json +3 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/portal_assets/project.svg +1 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/solution/definition.py +24 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/solution/first_step.py +34 -0
- 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
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/solution/scripts/README.md +1 -0
- 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
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/solution/second_step.py +12 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/app.py +30 -0
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/components/README.md +1 -0
- 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
- 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
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/pages/page.py +367 -0
- 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
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/tests/common_test_files/README.md +1 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/tests/conftest.py +37 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/tests/unit/test_solution_api.py +42 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/tests/unit/test_solution_ui.py +30 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/tox.ini +56 -0
- ansys/saf/cli/_utilities/__init__.py +15 -0
- ansys/saf/cli/_utilities/archiver.py +67 -0
- ansys/saf/cli/_utilities/backup.py +181 -0
- ansys/saf/cli/_utilities/conversion.py +64 -0
- ansys/saf/cli/_utilities/platform.py +26 -0
- ansys/saf/cli/_utilities/py.typed +0 -0
- ansys/saf/cli/_utilities/solution_modules.py +173 -0
- ansys/saf/cli/integrations/solutions_manager.py +64 -0
- ansys/saf/cli/py.typed +0 -0
- ansys_saf_cli-4.1.dev0.dist-info/METADATA +201 -0
- ansys_saf_cli-4.1.dev0.dist-info/RECORD +143 -0
- ansys_saf_cli-4.1.dev0.dist-info/WHEEL +4 -0
- ansys_saf_cli-4.1.dev0.dist-info/entry_points.txt +3 -0
- ansys_saf_cli-4.1.dev0.dist-info/licenses/AUTHORS +12 -0
- 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)
|