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
|
@@ -0,0 +1,816 @@
|
|
|
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 gettext import ngettext
|
|
18
|
+
import importlib.metadata
|
|
19
|
+
import keyword
|
|
20
|
+
import os
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
import re
|
|
23
|
+
import shlex
|
|
24
|
+
import shutil
|
|
25
|
+
import subprocess
|
|
26
|
+
import sys
|
|
27
|
+
import traceback
|
|
28
|
+
import webbrowser
|
|
29
|
+
|
|
30
|
+
import click
|
|
31
|
+
from dotenv import dotenv_values, load_dotenv
|
|
32
|
+
|
|
33
|
+
from ansys.saf.cli._config.const import (
|
|
34
|
+
DEFAULT_SOLUTION_DISPLAY_NAME,
|
|
35
|
+
DEFAULT_SOLUTION_NAME,
|
|
36
|
+
DEFAULT_SOLUTION_NAMESPACE,
|
|
37
|
+
DEFAULT_STEP_NAME,
|
|
38
|
+
DEFAULT_TEMPLATE_NAME,
|
|
39
|
+
DEFAULT_UI_FRAMEWORK,
|
|
40
|
+
GLOW_API_PORT,
|
|
41
|
+
GLOW_DEBUG,
|
|
42
|
+
GLOW_LOGGING_LEVEL,
|
|
43
|
+
GLOW_SOLUTION_DEFINITION,
|
|
44
|
+
GLOW_UI_DEBUG,
|
|
45
|
+
GLOW_UI_MODULE,
|
|
46
|
+
GLOW_UI_PORT,
|
|
47
|
+
PORTAL_UI_PORT,
|
|
48
|
+
SAF_CLI_EXTERNAL_URL,
|
|
49
|
+
SAF_DESKTOP_LOG_TO_FILES,
|
|
50
|
+
UI_FRAMEWORKS,
|
|
51
|
+
)
|
|
52
|
+
from ansys.saf.cli._database.manager import SolutionDatabaseManager
|
|
53
|
+
from ansys.saf.cli._database.models import SolutionRegistry
|
|
54
|
+
from ansys.saf.cli._solutions.add_step import (
|
|
55
|
+
add_step_to_solution,
|
|
56
|
+
check_template_cli_compatibility,
|
|
57
|
+
step_name_exists,
|
|
58
|
+
)
|
|
59
|
+
from ansys.saf.cli._solutions.environment import InstallError, setup_environment
|
|
60
|
+
from ansys.saf.cli._solutions.plugins import list_available_templates, resolve_template
|
|
61
|
+
from ansys.saf.cli._solutions.scaffolding import create_solution
|
|
62
|
+
from ansys.saf.cli._utilities.archiver import archive_solution
|
|
63
|
+
from ansys.saf.cli._utilities.backup import BackupManager
|
|
64
|
+
from ansys.saf.cli._utilities.solution_modules import (
|
|
65
|
+
create_solution_environment,
|
|
66
|
+
find_solution_display_name,
|
|
67
|
+
find_solution_root_dir,
|
|
68
|
+
get_exec_from_solution_venv,
|
|
69
|
+
get_main_module,
|
|
70
|
+
get_solution_venv_bin_dir,
|
|
71
|
+
resolve_solution_main_file,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
LIST_UI_FRAMEWORKS = "\n- ".join(UI_FRAMEWORKS)
|
|
75
|
+
UI_PROMPT = f"""What is the UI framework of the Solution?\n- {LIST_UI_FRAMEWORKS}\nChoose UI framework"""
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _validate_namespace_root(_ctx: click.Context, _param: click.Parameter, value: str) -> str:
|
|
79
|
+
"""Validate namespace root parameter for Click callback."""
|
|
80
|
+
|
|
81
|
+
value = value.strip()
|
|
82
|
+
if not value:
|
|
83
|
+
raise click.BadParameter("Namespace cannot be empty or just whitespace.")
|
|
84
|
+
|
|
85
|
+
if value.startswith(".") or value.endswith("."):
|
|
86
|
+
raise click.BadParameter("Namespace must not start or end with a dot.")
|
|
87
|
+
|
|
88
|
+
if ".." in value:
|
|
89
|
+
raise click.BadParameter("Namespace must not contain empty segments (for example: a..b).")
|
|
90
|
+
|
|
91
|
+
segment_pattern = re.compile(r"^[a-z_][a-z0-9_]*$")
|
|
92
|
+
for segment in value.split("."):
|
|
93
|
+
if segment != segment.lower():
|
|
94
|
+
raise click.BadParameter(f"Namespace segment '{segment}' must be lowercase.")
|
|
95
|
+
if keyword.iskeyword(segment):
|
|
96
|
+
raise click.BadParameter(f"Namespace segment '{segment}' is a Python keyword and is not allowed.")
|
|
97
|
+
if segment and segment[0].isdigit():
|
|
98
|
+
raise click.BadParameter(f"Namespace segment '{segment}' must start with a letter or underscore.")
|
|
99
|
+
if not segment_pattern.fullmatch(segment):
|
|
100
|
+
raise click.BadParameter(
|
|
101
|
+
f"Namespace segment '{segment}' can contain only lowercase letters, numbers, and underscores.",
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
return value
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _set_env_var_if_not_none(name: str, value: int | bool | str | None = None, env: dict[str, str] | None = None):
|
|
108
|
+
environment = env or os.environ
|
|
109
|
+
if value is not None:
|
|
110
|
+
environment[name] = str(value)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _resolve_input_solution(solution: str) -> tuple[SolutionRegistry, str]:
|
|
114
|
+
db_manager = SolutionDatabaseManager()
|
|
115
|
+
solution_root_dir = find_solution_root_dir(db_manager, solution)
|
|
116
|
+
solution_main_file = resolve_solution_main_file(solution_root_dir)
|
|
117
|
+
main_module = get_main_module(solution_main_file)
|
|
118
|
+
if stored_solution := db_manager.get_solution_by_root_dir(solution_root_dir):
|
|
119
|
+
return stored_solution, main_module
|
|
120
|
+
else:
|
|
121
|
+
solution_display_name = find_solution_display_name(solution_main_file) or solution_root_dir.name
|
|
122
|
+
new_solution = SolutionRegistry(
|
|
123
|
+
name=solution_root_dir.name,
|
|
124
|
+
root_dir=solution_root_dir,
|
|
125
|
+
display_name=solution_display_name,
|
|
126
|
+
)
|
|
127
|
+
db_manager.store_solution(new_solution)
|
|
128
|
+
return new_solution, main_module
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _resolve_effective_env_file(solution_root_dir: Path, env_file: Path | None) -> Path | None:
|
|
132
|
+
default_env_file = solution_root_dir / ".env"
|
|
133
|
+
return env_file or (default_env_file if default_env_file.is_file() else None)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _get_solution_runtime_modules(main_module: str) -> tuple[str, str]:
|
|
137
|
+
solution_module_root = main_module.removesuffix(".main")
|
|
138
|
+
return f"{solution_module_root}.solution.definition", f"{solution_module_root}.ui.app"
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _set_solution_runtime_env_vars(solution_env: dict[str, str], main_module: str, env_file: Path | None):
|
|
142
|
+
"""Set GLOW_SOLUTION_DEFINITION and GLOW_UI_MODULE environment variables.
|
|
143
|
+
|
|
144
|
+
For backward compatibility with existing solutions that may not define these values in .env,
|
|
145
|
+
derive defaults from the resolved main module. Allows explicit overrides from environment or .env.
|
|
146
|
+
"""
|
|
147
|
+
solution_definition_default, ui_module_default = _get_solution_runtime_modules(main_module)
|
|
148
|
+
env_file_values: dict[str, str] = {}
|
|
149
|
+
if env_file and env_file.is_file():
|
|
150
|
+
env_file_values = {k: str(v) for k, v in dotenv_values(env_file).items() if v is not None}
|
|
151
|
+
|
|
152
|
+
solution_definition = (
|
|
153
|
+
solution_env.get(GLOW_SOLUTION_DEFINITION)
|
|
154
|
+
or env_file_values.get(GLOW_SOLUTION_DEFINITION)
|
|
155
|
+
or solution_definition_default
|
|
156
|
+
)
|
|
157
|
+
ui_module = solution_env.get(GLOW_UI_MODULE) or env_file_values.get(GLOW_UI_MODULE) or ui_module_default
|
|
158
|
+
|
|
159
|
+
solution_env[GLOW_SOLUTION_DEFINITION] = solution_definition
|
|
160
|
+
solution_env[GLOW_UI_MODULE] = ui_module
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _get_executable_absolute_path(executable: str, solution_root_dir: Path) -> Path:
|
|
164
|
+
solution_venv_bin_dir = get_solution_venv_bin_dir(solution_root_dir)
|
|
165
|
+
# We could not find a way to force binaries such as python or pip to resolve to the solution's environment,
|
|
166
|
+
# so we force it by prepending the solution's environment bin directory.
|
|
167
|
+
if (solution_venv_bin_dir / executable).is_file():
|
|
168
|
+
return solution_venv_bin_dir / executable
|
|
169
|
+
elif (solution_venv_bin_dir / f"{executable}.exe").is_file():
|
|
170
|
+
return solution_venv_bin_dir / f"{executable}.exe"
|
|
171
|
+
else:
|
|
172
|
+
raise FileNotFoundError(f"Executable '{executable}' not found in solution '{solution_root_dir.name}'.")
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _generic_exception_handler(e: Exception):
|
|
176
|
+
click.secho(f"Error: {e}\n{traceback.format_exc()}", err=True, fg="red")
|
|
177
|
+
sys.exit(1)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
@click.group(invoke_without_command=True)
|
|
181
|
+
@click.option("--version", is_flag=True, help="Display the SAF-CLI version.")
|
|
182
|
+
@click.pass_context
|
|
183
|
+
def saf(ctx: click.Context, version: bool):
|
|
184
|
+
"""Utility script for solutions."""
|
|
185
|
+
try:
|
|
186
|
+
if version:
|
|
187
|
+
click.secho(importlib.metadata.version("ansys-saf-cli"), fg="cyan")
|
|
188
|
+
sys.exit(0)
|
|
189
|
+
elif ctx.invoked_subcommand is None:
|
|
190
|
+
click.secho(ctx.get_help())
|
|
191
|
+
sys.exit(0)
|
|
192
|
+
except Exception as e:
|
|
193
|
+
_generic_exception_handler(e)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
# By default, click first prompts for all options, and then validates the arguments. This causes an issue where an user
|
|
197
|
+
# may do something like "saf <command> extra-invalid-argument" and only after providing all the prompted inputs, they
|
|
198
|
+
# receive an error about the invalid argument. To avoid this, we create a custom Command class that first checks for
|
|
199
|
+
# extra arguments before prompting for inputs.
|
|
200
|
+
class CommandWithPromptsNoArgs(click.Command):
|
|
201
|
+
def parse_args(self, ctx: click.Context, args: list[str]) -> list[str]:
|
|
202
|
+
original_args = list(args)
|
|
203
|
+
# The following code snippet is copied from click.Command.parse_args
|
|
204
|
+
parser = self.make_parser(ctx)
|
|
205
|
+
_, args, _ = parser.parse_args(args=args)
|
|
206
|
+
if args and not ctx.allow_extra_args and not ctx.resilient_parsing:
|
|
207
|
+
ctx.fail(
|
|
208
|
+
ngettext(
|
|
209
|
+
"Got unexpected extra argument ({args})",
|
|
210
|
+
"Got unexpected extra arguments ({args})",
|
|
211
|
+
len(args),
|
|
212
|
+
).format(args=" ".join(map(str, args))),
|
|
213
|
+
)
|
|
214
|
+
return super().parse_args(ctx, original_args)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
@saf.command(short_help="Create a new solution", cls=CommandWithPromptsNoArgs)
|
|
218
|
+
@click.option(
|
|
219
|
+
"--solution-name",
|
|
220
|
+
prompt="What is the solution name?",
|
|
221
|
+
default=DEFAULT_SOLUTION_NAME,
|
|
222
|
+
help="The name of the solution to be created.",
|
|
223
|
+
type=str,
|
|
224
|
+
)
|
|
225
|
+
@click.option(
|
|
226
|
+
"--solution-display-name",
|
|
227
|
+
prompt="What is the solution display name?",
|
|
228
|
+
default=DEFAULT_SOLUTION_DISPLAY_NAME,
|
|
229
|
+
help="Name of the solution in the user interface.",
|
|
230
|
+
type=str,
|
|
231
|
+
)
|
|
232
|
+
@click.option(
|
|
233
|
+
"--ui-framework",
|
|
234
|
+
type=click.Choice(UI_FRAMEWORKS),
|
|
235
|
+
prompt=UI_PROMPT,
|
|
236
|
+
show_choices=False,
|
|
237
|
+
default=DEFAULT_UI_FRAMEWORK,
|
|
238
|
+
help="Type of solution UI.",
|
|
239
|
+
)
|
|
240
|
+
@click.option(
|
|
241
|
+
"--namespace",
|
|
242
|
+
default=DEFAULT_SOLUTION_NAMESPACE,
|
|
243
|
+
prompt="What is the solution namespace? (e.g. myorg.apps)",
|
|
244
|
+
help=(
|
|
245
|
+
"Provide a dot-separated namespace using lowercase letters, numbers, or underscores. "
|
|
246
|
+
"Example: my_namespace.project"
|
|
247
|
+
),
|
|
248
|
+
type=str,
|
|
249
|
+
callback=_validate_namespace_root,
|
|
250
|
+
)
|
|
251
|
+
def new(solution_name: str, solution_display_name: str, ui_framework: str, namespace: str):
|
|
252
|
+
"""Create a new Solution in the current working directory."""
|
|
253
|
+
solution_created: bool = False
|
|
254
|
+
solution_root_dir: Path | None = None
|
|
255
|
+
try:
|
|
256
|
+
# solution_name will be used for the name of the python project directory (solution_root_dir).
|
|
257
|
+
# SolutionRegistry contains validation for solution_name to check that the resulting path is valid.
|
|
258
|
+
# In the same way, when registering a solution based on its solution_root_dir path, we extract the solution name
|
|
259
|
+
# from the directory name.
|
|
260
|
+
solution_root_dir = Path.cwd() / solution_name
|
|
261
|
+
if solution_root_dir.exists():
|
|
262
|
+
raise FileExistsError(f"A file or directory already exists at {solution_root_dir}")
|
|
263
|
+
|
|
264
|
+
# create model before creating the solution to validate inputs
|
|
265
|
+
solution = SolutionRegistry(name=solution_name, root_dir=solution_root_dir, display_name=solution_display_name)
|
|
266
|
+
|
|
267
|
+
# Let's load the db before creating the solution, so that we fail fast in case something is wrong with it.
|
|
268
|
+
db_manager = SolutionDatabaseManager()
|
|
269
|
+
create_solution(solution_name, solution_display_name, ui_framework, namespace)
|
|
270
|
+
solution_created = True
|
|
271
|
+
db_manager.store_solution(solution)
|
|
272
|
+
except Exception as e:
|
|
273
|
+
if solution_root_dir and solution_root_dir.is_dir() and solution_created:
|
|
274
|
+
shutil.rmtree(solution_root_dir)
|
|
275
|
+
_generic_exception_handler(e)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
@saf.command(short_help="Create the virtual environment and install the dependencies of a solution")
|
|
279
|
+
@click.argument("solution", type=str, default="")
|
|
280
|
+
@click.option(
|
|
281
|
+
"-d",
|
|
282
|
+
"--dependencies",
|
|
283
|
+
type=str,
|
|
284
|
+
help=(
|
|
285
|
+
"List of dependency groups to install separated by comas. Example: tests,doc,style. [default: desktop,ui,doc]"
|
|
286
|
+
),
|
|
287
|
+
)
|
|
288
|
+
@click.option(
|
|
289
|
+
"-F",
|
|
290
|
+
"--force-clear-all",
|
|
291
|
+
is_flag=True,
|
|
292
|
+
default=False,
|
|
293
|
+
help="Clean-up the workspace. Delete existing .venv, .poetry/.venv, .poetry/.cache and poetry.lock.",
|
|
294
|
+
)
|
|
295
|
+
@click.option(
|
|
296
|
+
"-f",
|
|
297
|
+
"--force-clear",
|
|
298
|
+
is_flag=True,
|
|
299
|
+
default=False,
|
|
300
|
+
help="Clean-up the workspace. Delete existing .venv., .poetry/.venv and .poetry/.cache.",
|
|
301
|
+
)
|
|
302
|
+
@click.option(
|
|
303
|
+
"--env-file",
|
|
304
|
+
type=click.Path(exists=True, file_okay=True, path_type=Path),
|
|
305
|
+
help="Load environment variables from this file. [default: .env in the solution root directory]",
|
|
306
|
+
)
|
|
307
|
+
def install(
|
|
308
|
+
solution: str,
|
|
309
|
+
dependencies: str | None,
|
|
310
|
+
force_clear_all: bool,
|
|
311
|
+
force_clear: bool,
|
|
312
|
+
env_file: Path | None,
|
|
313
|
+
):
|
|
314
|
+
"""Create the virtual environment and install the dependencies of a solution
|
|
315
|
+
|
|
316
|
+
SOLUTION: The name of a registered Solution, or the path to a solution directory (relative or absolute).
|
|
317
|
+
When specifying a path, it must point to the root dir of the solution, which contains the src directory. If left
|
|
318
|
+
empty, the current working directory is assumed to be the root dir of a solution. When using a path or left empty,
|
|
319
|
+
if the resolved directory is a valid solution directory, the solution is registered in the database.
|
|
320
|
+
"""
|
|
321
|
+
try:
|
|
322
|
+
stored_solution, main_module = _resolve_input_solution(solution)
|
|
323
|
+
dependency_groups = dependencies.split(",") if dependencies else None
|
|
324
|
+
workspace_clear = "hard" if force_clear_all else "soft" if force_clear else None
|
|
325
|
+
default_env_file = stored_solution.root_dir / ".env"
|
|
326
|
+
env_file = env_file or (default_env_file if default_env_file.is_file() else None)
|
|
327
|
+
setup_environment(
|
|
328
|
+
solution_root_dir=stored_solution.root_dir,
|
|
329
|
+
solution_main_module_name=main_module,
|
|
330
|
+
dependency_groups=dependency_groups,
|
|
331
|
+
workspace_clear=workspace_clear,
|
|
332
|
+
env_file=env_file,
|
|
333
|
+
)
|
|
334
|
+
except InstallError as ie:
|
|
335
|
+
click.secho(str(ie), err=True, fg="red")
|
|
336
|
+
sys.exit(1)
|
|
337
|
+
except Exception as e:
|
|
338
|
+
_generic_exception_handler(e)
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
@saf.command(short_help="List stored solutions")
|
|
342
|
+
def solutions():
|
|
343
|
+
"""List all the Solutions stored in the database. Solutions marked as invalid are not shown."""
|
|
344
|
+
try:
|
|
345
|
+
db_manager = SolutionDatabaseManager()
|
|
346
|
+
solutions_by_name = db_manager.get_solutions_grouped_by_name()
|
|
347
|
+
if not solutions_by_name:
|
|
348
|
+
click.secho("No solutions found.", fg="yellow")
|
|
349
|
+
return
|
|
350
|
+
for solution_name, solutions in solutions_by_name.items():
|
|
351
|
+
click.secho(f"{solution_name}", fg="cyan", bold=True)
|
|
352
|
+
for solution in solutions:
|
|
353
|
+
click.secho(f" Root directory: {solution.root_dir}", fg="green")
|
|
354
|
+
click.secho(f" Display Name: {solution.display_name}", fg="green")
|
|
355
|
+
click.secho()
|
|
356
|
+
except Exception as e:
|
|
357
|
+
_generic_exception_handler(e)
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
@saf.command(short_help="List installed templates")
|
|
361
|
+
@click.option("--steps", is_flag=True, help="List only steps.")
|
|
362
|
+
@click.option("--solutions", is_flag=True, help="List only solutions.")
|
|
363
|
+
@click.option("--verbose", is_flag=True, help="Enable verbose output.")
|
|
364
|
+
def templates(steps: bool, solutions: bool, verbose: bool):
|
|
365
|
+
"""List all the templates installed in the system."""
|
|
366
|
+
try:
|
|
367
|
+
if steps and solutions:
|
|
368
|
+
raise click.UsageError("Cannot use both --steps and --solutions flags together.")
|
|
369
|
+
list_available_templates(steps_only=steps, solutions_only=solutions, verbose=verbose)
|
|
370
|
+
except Exception as e:
|
|
371
|
+
_generic_exception_handler(e)
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
@saf.command(short_help="Run a solution from source code.")
|
|
375
|
+
@click.argument("solution", type=str, default="")
|
|
376
|
+
@click.option(
|
|
377
|
+
"--portal",
|
|
378
|
+
is_flag=True,
|
|
379
|
+
default=False,
|
|
380
|
+
help="Run the solution with Portal. If set, the portal UI will open instead of the solution UI.",
|
|
381
|
+
)
|
|
382
|
+
@click.option("--no-ui", is_flag=True, default=False, help="Run the solution without the UI server.")
|
|
383
|
+
@click.option(
|
|
384
|
+
"--browser",
|
|
385
|
+
is_flag=True,
|
|
386
|
+
default=False,
|
|
387
|
+
help="Run the solution without webview, opening the UI in the browser.",
|
|
388
|
+
)
|
|
389
|
+
@click.option("--streamlit-ui", is_flag=True, default=False, help="Run the solution with Streamlit UI framework.")
|
|
390
|
+
@click.option(
|
|
391
|
+
"--loglevel",
|
|
392
|
+
envvar=GLOW_LOGGING_LEVEL,
|
|
393
|
+
default=None,
|
|
394
|
+
type=str,
|
|
395
|
+
help="Logging level: DEBUG, INFO, WARNING, ERROR, CRITICAL.",
|
|
396
|
+
)
|
|
397
|
+
@click.option("--debug", envvar=GLOW_DEBUG, is_flag=True, default=False, help="Run the solution in debug mode.")
|
|
398
|
+
@click.option(
|
|
399
|
+
"--ui-debugger",
|
|
400
|
+
envvar=GLOW_UI_DEBUG,
|
|
401
|
+
is_flag=True,
|
|
402
|
+
default=False,
|
|
403
|
+
help="Enable debugpy.listen in UI process and disable Dash/Flask debug functionality.",
|
|
404
|
+
)
|
|
405
|
+
@click.option(
|
|
406
|
+
"--portal-ui-port",
|
|
407
|
+
envvar=PORTAL_UI_PORT,
|
|
408
|
+
type=int,
|
|
409
|
+
help="The port on which the portal is running. [default: random]",
|
|
410
|
+
)
|
|
411
|
+
@click.option(
|
|
412
|
+
"--solution-api-port",
|
|
413
|
+
envvar=GLOW_API_PORT,
|
|
414
|
+
type=int,
|
|
415
|
+
help="The port on which the solution api is running. [default: random]",
|
|
416
|
+
)
|
|
417
|
+
@click.option(
|
|
418
|
+
"--solution-ui-port",
|
|
419
|
+
envvar=GLOW_UI_PORT,
|
|
420
|
+
type=int,
|
|
421
|
+
help="The port on which the solution ui is running. [default: random]",
|
|
422
|
+
)
|
|
423
|
+
@click.option(
|
|
424
|
+
"--project",
|
|
425
|
+
default=None,
|
|
426
|
+
type=str,
|
|
427
|
+
help="Display name of the project to open or create. This option cannot be used if Portal is running.",
|
|
428
|
+
)
|
|
429
|
+
@click.option(
|
|
430
|
+
"--env-file",
|
|
431
|
+
type=click.Path(exists=True, file_okay=True, path_type=Path),
|
|
432
|
+
help="Load environment variables from this file. [default: .env in the solution root directory]",
|
|
433
|
+
)
|
|
434
|
+
@click.option(
|
|
435
|
+
"--no-automatic-project-migration",
|
|
436
|
+
is_flag=True,
|
|
437
|
+
default=False,
|
|
438
|
+
help="Disable automatic schema migration for old projects.",
|
|
439
|
+
)
|
|
440
|
+
@click.option(
|
|
441
|
+
"--log-to-files",
|
|
442
|
+
envvar=SAF_DESKTOP_LOG_TO_FILES,
|
|
443
|
+
is_flag=True,
|
|
444
|
+
default=False,
|
|
445
|
+
help="Log to files and disable OTEL dashboard.",
|
|
446
|
+
)
|
|
447
|
+
def run(
|
|
448
|
+
solution: str,
|
|
449
|
+
portal: bool,
|
|
450
|
+
no_ui: bool,
|
|
451
|
+
browser: bool,
|
|
452
|
+
streamlit_ui: bool,
|
|
453
|
+
loglevel: str | None,
|
|
454
|
+
debug: bool,
|
|
455
|
+
ui_debugger: bool,
|
|
456
|
+
portal_ui_port: int | None,
|
|
457
|
+
solution_api_port: int | None,
|
|
458
|
+
solution_ui_port: int | None,
|
|
459
|
+
project: str | None,
|
|
460
|
+
env_file: Path | None,
|
|
461
|
+
no_automatic_project_migration: bool,
|
|
462
|
+
log_to_files: bool,
|
|
463
|
+
):
|
|
464
|
+
"""Run a Solution.
|
|
465
|
+
|
|
466
|
+
SOLUTION: The name of a registered Solution, or the path to a solution directory (relative or absolute).
|
|
467
|
+
When specifying a path, it must point to the root dir of the solution, which contains the src directory. If left
|
|
468
|
+
empty, the current working directory is assumed to be the root dir of a solution. When using a path or left empty,
|
|
469
|
+
if the resolved directory is a valid solution directory, the solution is registered in the database.
|
|
470
|
+
"""
|
|
471
|
+
try:
|
|
472
|
+
stored_solution, main_module = _resolve_input_solution(solution)
|
|
473
|
+
env_file = _resolve_effective_env_file(stored_solution.root_dir, env_file)
|
|
474
|
+
|
|
475
|
+
solution_env = create_solution_environment(stored_solution.root_dir)
|
|
476
|
+
_set_solution_runtime_env_vars(solution_env, main_module, env_file)
|
|
477
|
+
_set_env_var_if_not_none(GLOW_LOGGING_LEVEL, loglevel, solution_env)
|
|
478
|
+
if debug: # don't overwrite env var if user has not set the flag explicitly, bool variables are always not None
|
|
479
|
+
_set_env_var_if_not_none(GLOW_DEBUG, debug, solution_env)
|
|
480
|
+
if ui_debugger:
|
|
481
|
+
_set_env_var_if_not_none(GLOW_UI_DEBUG, ui_debugger, solution_env)
|
|
482
|
+
_set_env_var_if_not_none(PORTAL_UI_PORT, portal_ui_port, solution_env)
|
|
483
|
+
_set_env_var_if_not_none(GLOW_API_PORT, solution_api_port, solution_env)
|
|
484
|
+
_set_env_var_if_not_none(GLOW_UI_PORT, solution_ui_port, solution_env)
|
|
485
|
+
|
|
486
|
+
executable = get_exec_from_solution_venv(stored_solution.root_dir, "python")
|
|
487
|
+
cmd: list[str] = [
|
|
488
|
+
executable.as_posix(),
|
|
489
|
+
"-m",
|
|
490
|
+
"ansys.saf.desktop.orchestrator",
|
|
491
|
+
"--solution-main-module-name",
|
|
492
|
+
f"{main_module}",
|
|
493
|
+
]
|
|
494
|
+
cmd += ["--no-ui"] if no_ui else []
|
|
495
|
+
cmd += ["--portal"] if portal else []
|
|
496
|
+
cmd += ["--browser"] if browser else []
|
|
497
|
+
cmd += ["--streamlit-ui"] if streamlit_ui else []
|
|
498
|
+
cmd += ["--project-display-name", project] if project else []
|
|
499
|
+
cmd += ["--env-file", env_file.resolve().as_posix()] if env_file else []
|
|
500
|
+
cmd += ["--no-automatic-project-migration"] if no_automatic_project_migration else []
|
|
501
|
+
cmd += ["--log-to-files"] if log_to_files else []
|
|
502
|
+
subprocess.run(cmd, env=solution_env, cwd=stored_solution.root_dir, check=True)
|
|
503
|
+
except Exception as e:
|
|
504
|
+
_generic_exception_handler(e)
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
@saf.command(short_help="Execute a command using the solution's python environment")
|
|
508
|
+
@click.argument("solution", type=str, default="")
|
|
509
|
+
@click.argument("command", type=str, default="", required=True)
|
|
510
|
+
@click.option(
|
|
511
|
+
"--cwd",
|
|
512
|
+
type=click.Path(exists=True, file_okay=False, path_type=Path),
|
|
513
|
+
help="Directory in which the command will be executed. [default: solution root directory]",
|
|
514
|
+
)
|
|
515
|
+
@click.option(
|
|
516
|
+
"--env-file",
|
|
517
|
+
type=click.Path(exists=True, file_okay=True, path_type=Path),
|
|
518
|
+
help="Load environment variables from this file. [default: .env in the solution root directory]",
|
|
519
|
+
)
|
|
520
|
+
def execute(solution: str, command: str, cwd: Path | None, env_file: Path | None = None):
|
|
521
|
+
try:
|
|
522
|
+
# If a single argument is passed it is interpreted as the command and it will be executed in the
|
|
523
|
+
# current working directory's solution, if any.
|
|
524
|
+
if not command:
|
|
525
|
+
command = solution
|
|
526
|
+
solution = ""
|
|
527
|
+
|
|
528
|
+
stored_solution, main_module = _resolve_input_solution(solution)
|
|
529
|
+
|
|
530
|
+
cmd = shlex.split(command)
|
|
531
|
+
executable = cmd[0]
|
|
532
|
+
executable_abs_path = _get_executable_absolute_path(executable, stored_solution.root_dir)
|
|
533
|
+
|
|
534
|
+
cwd = cwd or stored_solution.root_dir
|
|
535
|
+
|
|
536
|
+
env_file = _resolve_effective_env_file(stored_solution.root_dir, env_file)
|
|
537
|
+
if env_file and env_file.is_file():
|
|
538
|
+
click.secho(f"Environment variables loaded from {env_file.resolve()}")
|
|
539
|
+
load_dotenv(env_file)
|
|
540
|
+
solution_env = create_solution_environment(stored_solution.root_dir)
|
|
541
|
+
_set_solution_runtime_env_vars(solution_env, main_module=main_module, env_file=env_file)
|
|
542
|
+
|
|
543
|
+
subprocess.run([executable_abs_path.as_posix()] + cmd[1:], cwd=cwd, env=solution_env, check=True)
|
|
544
|
+
except Exception as e:
|
|
545
|
+
_generic_exception_handler(e)
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
@saf.command(short_help="Archive a solution's source code.")
|
|
549
|
+
@click.argument("solution", type=str, default="")
|
|
550
|
+
@click.option(
|
|
551
|
+
"--filename",
|
|
552
|
+
default=None,
|
|
553
|
+
type=str,
|
|
554
|
+
help="The name of the archive file to create. If not provided, the solution name is used.",
|
|
555
|
+
)
|
|
556
|
+
@click.option(
|
|
557
|
+
"--path",
|
|
558
|
+
default=None,
|
|
559
|
+
type=str,
|
|
560
|
+
help="The path to a directory where to save the solution. If not provided, the current parent directory is used.",
|
|
561
|
+
)
|
|
562
|
+
@click.option(
|
|
563
|
+
"--extension",
|
|
564
|
+
default=None,
|
|
565
|
+
type=str,
|
|
566
|
+
help="The extension of the archive file to create. If not provided, .saf will be used.",
|
|
567
|
+
)
|
|
568
|
+
def archive(
|
|
569
|
+
solution: str,
|
|
570
|
+
filename: str | None,
|
|
571
|
+
path: Path | None,
|
|
572
|
+
extension: str | None,
|
|
573
|
+
):
|
|
574
|
+
"""Create a .saf archive from the given solution.
|
|
575
|
+
|
|
576
|
+
SOLUTION: The name of a registered Solution, or the path to a solution directory (relative or absolute).
|
|
577
|
+
When specifying a path, it must point to the root dir of the solution, which contains the src directory. If left
|
|
578
|
+
empty, the current working directory is assumed to be the root dir of a solution. When using a path or left empty,
|
|
579
|
+
if the resolved directory is a valid solution directory, the solution is registered in the database.
|
|
580
|
+
"""
|
|
581
|
+
try:
|
|
582
|
+
stored_solution, _ = _resolve_input_solution(solution)
|
|
583
|
+
|
|
584
|
+
archive_solution(
|
|
585
|
+
solution_root_dir=stored_solution.root_dir,
|
|
586
|
+
archive_name=filename or stored_solution.name,
|
|
587
|
+
archive_path=Path(path) if path else None,
|
|
588
|
+
archive_extension=extension,
|
|
589
|
+
)
|
|
590
|
+
except Exception as e:
|
|
591
|
+
_generic_exception_handler(e)
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
@saf.command(short_help="Open SAF documentation in a web browser.")
|
|
595
|
+
def doc():
|
|
596
|
+
"""Opens SAF documentation in a web browser."""
|
|
597
|
+
try:
|
|
598
|
+
webbrowser.open(SAF_CLI_EXTERNAL_URL)
|
|
599
|
+
except Exception as e:
|
|
600
|
+
_generic_exception_handler(e)
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
@saf.command(short_help="Package the solution for distribution.")
|
|
604
|
+
@click.argument("solution", type=str, default="")
|
|
605
|
+
@click.option(
|
|
606
|
+
"--display-console-window",
|
|
607
|
+
is_flag=True,
|
|
608
|
+
default=False,
|
|
609
|
+
help="Display the console window when running the solution.",
|
|
610
|
+
)
|
|
611
|
+
@click.option("--encrypt", is_flag=True, default=False, help="Encrypt the solution code before packaging.")
|
|
612
|
+
@click.option(
|
|
613
|
+
"--encryption-file",
|
|
614
|
+
help="The file containing the list of files to encrypt.",
|
|
615
|
+
default="",
|
|
616
|
+
)
|
|
617
|
+
@click.option("--encryption-key", help="The encryption key to use for obfuscation.", default="")
|
|
618
|
+
@click.option(
|
|
619
|
+
"--no-executable",
|
|
620
|
+
is_flag=True,
|
|
621
|
+
default=False,
|
|
622
|
+
help="Do not create an executable installer for the solution.",
|
|
623
|
+
)
|
|
624
|
+
@click.option(
|
|
625
|
+
"--executable-as-dir",
|
|
626
|
+
is_flag=True,
|
|
627
|
+
default=False,
|
|
628
|
+
help=(
|
|
629
|
+
"Bundle the executable in a directory containing all the supporting files. "
|
|
630
|
+
"This is mandatory if the executable file size exceeds the pyinstaller limit of 4GB."
|
|
631
|
+
),
|
|
632
|
+
)
|
|
633
|
+
@click.option("--obfuscate", is_flag=True, default=False, help="Obfuscate the solution code before packaging.")
|
|
634
|
+
@click.option(
|
|
635
|
+
"--offline-package",
|
|
636
|
+
is_flag=True,
|
|
637
|
+
default=False,
|
|
638
|
+
help="Create a package that installs without internet access.",
|
|
639
|
+
)
|
|
640
|
+
@click.option(
|
|
641
|
+
"--exclude-python",
|
|
642
|
+
is_flag=True,
|
|
643
|
+
default=False,
|
|
644
|
+
help="Do not ship the Python interpreter with the packaged solution.",
|
|
645
|
+
)
|
|
646
|
+
@click.option(
|
|
647
|
+
"--python-version",
|
|
648
|
+
help="The version of Python to use for the solution.",
|
|
649
|
+
default="",
|
|
650
|
+
)
|
|
651
|
+
@click.option(
|
|
652
|
+
"--force-python-from-source",
|
|
653
|
+
is_flag=True,
|
|
654
|
+
default=False,
|
|
655
|
+
help="Force building Python from source instead of downloading it from NuGet packages (Windows only).",
|
|
656
|
+
)
|
|
657
|
+
@click.option(
|
|
658
|
+
"--solution-entry-point",
|
|
659
|
+
help=(
|
|
660
|
+
"The main solution entry point to start the solution. "
|
|
661
|
+
"Module should be a dotted import ('ansys.solutions.mysoln.main'). "
|
|
662
|
+
"If not provided, the solution will be run with the default entry point."
|
|
663
|
+
),
|
|
664
|
+
default="",
|
|
665
|
+
)
|
|
666
|
+
@click.option("--github-token", help="Personal Access Token for downloading dependencies from GitHub.", default="")
|
|
667
|
+
@click.option("--solution-ui-framework", help="The UI framework used in the solution.", default="")
|
|
668
|
+
@click.option("--no-glow", is_flag=True, default=False, help="Add this flag if the solution does not use SAF/GLOW.")
|
|
669
|
+
@click.option(
|
|
670
|
+
"--env-file",
|
|
671
|
+
type=click.Path(exists=True, file_okay=True, path_type=Path),
|
|
672
|
+
help="Load environment variables from this file. [default: .env in the solution root directory]",
|
|
673
|
+
)
|
|
674
|
+
def build(
|
|
675
|
+
solution: str,
|
|
676
|
+
display_console_window: bool,
|
|
677
|
+
encrypt: bool,
|
|
678
|
+
encryption_file: str,
|
|
679
|
+
encryption_key: str,
|
|
680
|
+
no_executable: bool,
|
|
681
|
+
executable_as_dir: bool,
|
|
682
|
+
obfuscate: bool,
|
|
683
|
+
offline_package: bool,
|
|
684
|
+
exclude_python: bool,
|
|
685
|
+
python_version: str,
|
|
686
|
+
force_python_from_source: bool,
|
|
687
|
+
solution_entry_point: str,
|
|
688
|
+
github_token: str,
|
|
689
|
+
solution_ui_framework: str,
|
|
690
|
+
no_glow: bool,
|
|
691
|
+
env_file: Path | None = None,
|
|
692
|
+
):
|
|
693
|
+
"""Package the solution for distribution.
|
|
694
|
+
|
|
695
|
+
This command will package the solution into a single executable file for distribution. The solution is
|
|
696
|
+
packaged as a standalone executable that can be run on any machine without needing to install Python or any
|
|
697
|
+
dependencies. The solution is packaged with all its dependencies and the Python interpreter.
|
|
698
|
+
|
|
699
|
+
The solution is packaged using PyInstaller. The packaged solution is saved in the dist directory of the solution.
|
|
700
|
+
|
|
701
|
+
SOLUTION: The name of a registered Solution, or the path to a solution directory (relative or absolute).
|
|
702
|
+
When specifying a path, it must point to the root dir of the solution, which contains the src directory. If left
|
|
703
|
+
empty, the current working directory is assumed to be the root dir of a solution. When using a path or left empty,
|
|
704
|
+
if the resolved directory is a valid solution directory, the solution is registered in the database.
|
|
705
|
+
"""
|
|
706
|
+
try:
|
|
707
|
+
stored_solution, main_module = _resolve_input_solution(solution)
|
|
708
|
+
env_file = _resolve_effective_env_file(stored_solution.root_dir, env_file)
|
|
709
|
+
|
|
710
|
+
executable = get_exec_from_solution_venv(stored_solution.root_dir, "python")
|
|
711
|
+
cmd: list[str] = [
|
|
712
|
+
executable.as_posix(),
|
|
713
|
+
"-m",
|
|
714
|
+
"ansys.saf.desktop.installer",
|
|
715
|
+
]
|
|
716
|
+
cmd += ["--display-console-window"] if display_console_window else []
|
|
717
|
+
cmd += ["--encrypt"] if encrypt else []
|
|
718
|
+
cmd += ["--encryption-file", encryption_file] if encryption_file else []
|
|
719
|
+
cmd += ["--encryption-key", encryption_key] if encryption_key else []
|
|
720
|
+
cmd += ["--no-executable"] if no_executable else []
|
|
721
|
+
cmd += ["--executable-as-dir"] if executable_as_dir else []
|
|
722
|
+
cmd += ["--obfuscate"] if obfuscate else []
|
|
723
|
+
cmd += ["--offline-package"] if offline_package else []
|
|
724
|
+
cmd += ["--exclude-python"] if exclude_python else []
|
|
725
|
+
cmd += ["--python-version", python_version] if python_version else []
|
|
726
|
+
cmd += ["--force-python-from-source"] if force_python_from_source else []
|
|
727
|
+
cmd += ["--solution-entry-point", solution_entry_point] if solution_entry_point else []
|
|
728
|
+
cmd += ["--github-token", github_token] if github_token else []
|
|
729
|
+
cmd += ["--solution-ui-framework", solution_ui_framework] if solution_ui_framework else []
|
|
730
|
+
cmd += ["--no-glow"] if no_glow else []
|
|
731
|
+
cmd += ["--env-file", env_file.resolve().as_posix()] if env_file else []
|
|
732
|
+
|
|
733
|
+
solution_env = create_solution_environment(stored_solution.root_dir)
|
|
734
|
+
_set_solution_runtime_env_vars(solution_env, main_module, env_file)
|
|
735
|
+
subprocess.run(cmd, cwd=stored_solution.root_dir, env=solution_env, check=True)
|
|
736
|
+
except Exception as e:
|
|
737
|
+
_generic_exception_handler(e)
|
|
738
|
+
|
|
739
|
+
|
|
740
|
+
@saf.command(short_help="Add a new step to the solution, with or without associated UI.")
|
|
741
|
+
@click.argument("solution", type=str, default="")
|
|
742
|
+
@click.option(
|
|
743
|
+
"--step-name",
|
|
744
|
+
prompt="What is the Step name?",
|
|
745
|
+
default=DEFAULT_STEP_NAME,
|
|
746
|
+
help="The name of the step to add to the solution. It is assumed that the step name is provided in snake_case.",
|
|
747
|
+
type=str,
|
|
748
|
+
)
|
|
749
|
+
@click.option(
|
|
750
|
+
"--ui-framework",
|
|
751
|
+
type=click.Choice(UI_FRAMEWORKS),
|
|
752
|
+
prompt=UI_PROMPT,
|
|
753
|
+
show_choices=False,
|
|
754
|
+
default=DEFAULT_UI_FRAMEWORK,
|
|
755
|
+
help="Type of solution UI.",
|
|
756
|
+
)
|
|
757
|
+
@click.option(
|
|
758
|
+
"--template",
|
|
759
|
+
prompt="What is the Step template name?",
|
|
760
|
+
default=DEFAULT_TEMPLATE_NAME,
|
|
761
|
+
help="The name of the step template to be used.",
|
|
762
|
+
type=str,
|
|
763
|
+
)
|
|
764
|
+
def add_step(
|
|
765
|
+
solution: str,
|
|
766
|
+
step_name: str,
|
|
767
|
+
ui_framework: str,
|
|
768
|
+
template: str,
|
|
769
|
+
):
|
|
770
|
+
"""Add a new step to the solution, with or without associated UI.
|
|
771
|
+
|
|
772
|
+
SOLUTION: The name of a registered Solution, or the path to a solution directory (relative or absolute).
|
|
773
|
+
When specifying a path, it must point to the root dir of the solution, which contains the src directory. If left
|
|
774
|
+
empty, the current working directory is assumed to be the root dir of a solution. When using a path or left empty,
|
|
775
|
+
if the resolved directory is a valid solution directory, the solution is registered in the database.
|
|
776
|
+
"""
|
|
777
|
+
try:
|
|
778
|
+
stored_solution, solution_main_module = _resolve_input_solution(solution)
|
|
779
|
+
# expected format: <namespace>.<solution_name>.main
|
|
780
|
+
# e.g., saf.solutions.my_solution.main or org.apps.my_solution.main
|
|
781
|
+
solution_package_name = solution_main_module.split(".")[-2]
|
|
782
|
+
namespace_root = ".".join(solution_main_module.split(".")[:-2])
|
|
783
|
+
saf_step_template = resolve_template(template)
|
|
784
|
+
if saf_step_template.type != "step":
|
|
785
|
+
raise ValueError(f"Template '{template}' is of type '{saf_step_template.type}', expected 'step'")
|
|
786
|
+
|
|
787
|
+
ctx = click.get_current_context()
|
|
788
|
+
step_name_source = ctx.get_parameter_source("step_name")
|
|
789
|
+
|
|
790
|
+
if step_name_source == click.core.ParameterSource.PROMPT:
|
|
791
|
+
while step_name_exists(stored_solution.root_dir, solution_package_name, step_name):
|
|
792
|
+
click.secho(
|
|
793
|
+
f"A step with name '{step_name}' already exists in solution '{stored_solution.name}'.",
|
|
794
|
+
fg="yellow",
|
|
795
|
+
)
|
|
796
|
+
step_name = click.prompt("Please enter a different Step name", default=DEFAULT_STEP_NAME)
|
|
797
|
+
elif step_name_exists(stored_solution.root_dir, solution_package_name, step_name):
|
|
798
|
+
raise ValueError(f"Step name '{step_name}' is already taken in solution '{stored_solution.name}'.")
|
|
799
|
+
|
|
800
|
+
check_template_cli_compatibility(stored_solution.root_dir, saf_step_template)
|
|
801
|
+
|
|
802
|
+
with BackupManager(stored_solution.root_dir, solution_package_name, namespace_root):
|
|
803
|
+
add_step_to_solution(
|
|
804
|
+
stored_solution.name,
|
|
805
|
+
stored_solution.root_dir,
|
|
806
|
+
solution_package_name,
|
|
807
|
+
step_name,
|
|
808
|
+
ui_framework,
|
|
809
|
+
saf_step_template,
|
|
810
|
+
)
|
|
811
|
+
except Exception as e:
|
|
812
|
+
_generic_exception_handler(e)
|
|
813
|
+
|
|
814
|
+
|
|
815
|
+
def entry_point():
|
|
816
|
+
saf(obj={})
|