ansys-saf-cli 4.1.dev0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. ansys/saf/cli/__init__.py +15 -0
  2. ansys/saf/cli/_cli/__init__.py +15 -0
  3. ansys/saf/cli/_cli/main.py +816 -0
  4. ansys/saf/cli/_cli/py.typed +0 -0
  5. ansys/saf/cli/_config/__init__.py +15 -0
  6. ansys/saf/cli/_config/const.py +45 -0
  7. ansys/saf/cli/_config/py.typed +0 -0
  8. ansys/saf/cli/_database/__init__.py +15 -0
  9. ansys/saf/cli/_database/manager.py +94 -0
  10. ansys/saf/cli/_database/models.py +53 -0
  11. ansys/saf/cli/_database/py.typed +0 -0
  12. ansys/saf/cli/_solutions/__init__.py +15 -0
  13. ansys/saf/cli/_solutions/add_step.py +256 -0
  14. ansys/saf/cli/_solutions/dependencies.py +162 -0
  15. ansys/saf/cli/_solutions/environment.py +1145 -0
  16. ansys/saf/cli/_solutions/plugins.py +254 -0
  17. ansys/saf/cli/_solutions/py.typed +0 -0
  18. ansys/saf/cli/_solutions/scaffolding.py +73 -0
  19. ansys/saf/cli/_solutions/templates/__init__.py +15 -0
  20. ansys/saf/cli/_solutions/templates/py.typed +0 -0
  21. ansys/saf/cli/_solutions/templates/solution/cookiecutter.json +18 -0
  22. ansys/saf/cli/_solutions/templates/solution/hooks/post_gen_project.py +54 -0
  23. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/.codespell.ignore +2 -0
  24. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/.devcontainer/devcontainer.json +26 -0
  25. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/.env +5 -0
  26. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/.flake8 +8 -0
  27. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/.github/labeler.yml +9 -0
  28. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/.github/labels.yml +27 -0
  29. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/.github/workflows/build-release.yml +175 -0
  30. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/.github/workflows/label.yml +89 -0
  31. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/.gitignore +137 -0
  32. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/.pre-commit-config.yaml +48 -0
  33. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/.release-please-manifest.json +3 -0
  34. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/.reuse/templates/copyright.jinja2 +1 -0
  35. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/.vscode/extensions.json +8 -0
  36. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/.vscode/launch.json +28 -0
  37. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/AUTHORS +12 -0
  38. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/CHANGELOG.md +1 -0
  39. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/CODEOWNERS +3 -0
  40. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/CODE_OF_CONDUCT.md +65 -0
  41. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/CONTRIBUTING.md +2 -0
  42. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/CONTRIBUTORS.md +9 -0
  43. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/LICENSE.rst +41 -0
  44. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/README.md +69 -0
  45. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/deployments/Dockerfile +43 -0
  46. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/deployments/README.md +13 -0
  47. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/deployments/distributed-deployment-template/.env +27 -0
  48. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/deployments/distributed-deployment-template/compose.yaml +132 -0
  49. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/deployments/standalone/.env +20 -0
  50. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/deployments/standalone/compose.yaml +118 -0
  51. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/deployments/standalone-with-hps/.env +28 -0
  52. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/deployments/standalone-with-hps/compose.yaml +146 -0
  53. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/deployments/standalone-with-minerva/.env +25 -0
  54. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/deployments/standalone-with-minerva/compose.yaml +122 -0
  55. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/doc/.vale.ini +28 -0
  56. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/doc/Makefile +30 -0
  57. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/doc/changelog.d/changelog_template.jinja +17 -0
  58. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/doc/make.bat +46 -0
  59. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/doc/source/_static/css/custom.css +8 -0
  60. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/doc/source/_static/images/repository-banner.png +0 -0
  61. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/doc/source/_templates/README.md +1 -0
  62. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/doc/source/changelog.rst +12 -0
  63. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/doc/source/conf.py +253 -0
  64. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/doc/source/examples.rst +8 -0
  65. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/doc/source/getting_started/desktop_installation.rst +165 -0
  66. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/doc/source/getting_started/docker_installation.rst +68 -0
  67. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/doc/source/index.rst +51 -0
  68. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/doc/source/user_guide/index.rst +6 -0
  69. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/doc/styles/config/vocabularies/ANSYS/accept.txt +5 -0
  70. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/doc/styles/config/vocabularies/ANSYS/reject.txt +0 -0
  71. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/examples/README.md +1 -0
  72. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/lock_files/dash/poetry.lock +7621 -0
  73. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/lock_files/no_ui/poetry.lock +7173 -0
  74. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/minerva/cli/README.md +3 -0
  75. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/minerva/oauth/App_Data/Certificates/README.md +3 -0
  76. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/minerva/oauth/oauth.config +9 -0
  77. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/pyproject.toml +192 -0
  78. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/release-please-config.json +9 -0
  79. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/sonar-project.properties +12 -0
  80. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/__init__.py +3 -0
  81. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/main.py +19 -0
  82. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/portal_assets/application.svg +36 -0
  83. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/portal_assets/description.json +3 -0
  84. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/portal_assets/project.svg +1 -0
  85. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/solution/definition.py +24 -0
  86. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/solution/first_step.py +34 -0
  87. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/solution/method_assets/README.md +1 -0
  88. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/solution/scripts/README.md +1 -0
  89. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/solution/scripts/assets/README.md +1 -0
  90. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/solution/second_step.py +12 -0
  91. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/app.py +30 -0
  92. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/assets/css/all.css +9 -0
  93. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/assets/css/bootstrap.min.css +6 -0
  94. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/assets/css/style.css +47 -0
  95. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/assets/icons/dark/carbon--ibm-engineering-workflow-mgmt.svg +1 -0
  96. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/assets/icons/dark/carbon--return.svg +1 -0
  97. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/assets/icons/dark/game-icons--crossed-air-flows.svg +1 -0
  98. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/assets/icons/dark/material-symbols--home.svg +1 -0
  99. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/assets/icons/dark/radix-icons--moon.svg +1 -0
  100. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/assets/icons/dark/radix-icons--sun.svg +1 -0
  101. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/assets/icons/dark/streamline--startup-solid.svg +1 -0
  102. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/assets/icons/dark/teenyicons--doc-solid.svg +1 -0
  103. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/assets/icons/light/carbon--ibm-engineering-workflow-mgmt.svg +1 -0
  104. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/assets/icons/light/carbon--return.svg +1 -0
  105. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/assets/icons/light/game-icons--crossed-air-flows.svg +1 -0
  106. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/assets/icons/light/material-symbols--home.svg +1 -0
  107. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/assets/icons/light/radix-icons--moon.svg +1 -0
  108. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/assets/icons/light/radix-icons--sun.svg +1 -0
  109. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/assets/icons/light/streamline--startup-solid.svg +1 -0
  110. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/assets/icons/light/teenyicons--doc-solid.svg +1 -0
  111. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/assets/images/README.md +1 -0
  112. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/assets/images/workflow-placeholder.png +0 -0
  113. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/assets/installer/README.md +5 -0
  114. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/assets/logos/dark/placeholder_logo.png +0 -0
  115. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/assets/logos/light/placeholder_logo.png +0 -0
  116. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/assets/orchestrator/README.md +3 -0
  117. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/assets/pywebview/README.md +3 -0
  118. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/assets/scripts/README.md +1 -0
  119. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/components/README.md +1 -0
  120. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/pages/about_page.py +71 -0
  121. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/pages/first_page.py +147 -0
  122. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/pages/page.py +367 -0
  123. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/pages/second_page.py +32 -0
  124. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/tests/common_test_files/README.md +1 -0
  125. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/tests/conftest.py +37 -0
  126. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/tests/unit/test_solution_api.py +42 -0
  127. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/tests/unit/test_solution_ui.py +30 -0
  128. ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/tox.ini +56 -0
  129. ansys/saf/cli/_utilities/__init__.py +15 -0
  130. ansys/saf/cli/_utilities/archiver.py +67 -0
  131. ansys/saf/cli/_utilities/backup.py +181 -0
  132. ansys/saf/cli/_utilities/conversion.py +64 -0
  133. ansys/saf/cli/_utilities/platform.py +26 -0
  134. ansys/saf/cli/_utilities/py.typed +0 -0
  135. ansys/saf/cli/_utilities/solution_modules.py +173 -0
  136. ansys/saf/cli/integrations/solutions_manager.py +64 -0
  137. ansys/saf/cli/py.typed +0 -0
  138. ansys_saf_cli-4.1.dev0.dist-info/METADATA +201 -0
  139. ansys_saf_cli-4.1.dev0.dist-info/RECORD +143 -0
  140. ansys_saf_cli-4.1.dev0.dist-info/WHEEL +4 -0
  141. ansys_saf_cli-4.1.dev0.dist-info/entry_points.txt +3 -0
  142. ansys_saf_cli-4.1.dev0.dist-info/licenses/AUTHORS +12 -0
  143. ansys_saf_cli-4.1.dev0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,181 @@
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 shutil
19
+ import tempfile
20
+
21
+ import click
22
+
23
+ from ansys.saf.cli._utilities.conversion import namespace_to_path
24
+
25
+
26
+ class BackupManager:
27
+ """
28
+ Handles snapshot, backup, restore and cleanup operations for a solution add-step workflow.
29
+ Implements context manager protocol for automatic rollback on exceptions.
30
+
31
+ When used as a context manager, ``__enter__`` snapshots the solution module directory
32
+ and backs up all provided files upfront (non-existent files are silently skipped).
33
+ On exception, ``__exit__`` removes newly created files and restores all backed-up files;
34
+ the backup directory is always removed on exit.
35
+
36
+ Attributes:
37
+ solution_root_dir: The root directory of the solution.
38
+ solution_package_name: The name of the solution package module.
39
+ files_to_backup: List of (file_path, base_dir) tuples to back up.
40
+ """
41
+
42
+ def __init__(
43
+ self,
44
+ solution_root_dir: Path,
45
+ solution_package_name: str,
46
+ namespace_root: str,
47
+ files_to_backup: list[tuple[Path, Path]] | None = None,
48
+ ):
49
+ """Initialize BackupManager.
50
+
51
+ Args:
52
+ solution_root_dir: Root directory of the solution.
53
+ solution_package_name: Name of the solution package (used to compute base_solution_dir).
54
+ namespace_root: Root namespace of the solution.
55
+ files_to_backup: List of (file_path, base_dir) tuples to back up. If None, uses default files
56
+ (page.py, definition.py, pyproject.toml, poetry.lock).
57
+ """
58
+ self._solution_root_dir = solution_root_dir
59
+ # Compute base_solution_dir from solution_root_dir and solution_package_name
60
+ self._base_solution_dir = solution_root_dir / "src" / namespace_to_path(namespace_root) / solution_package_name
61
+
62
+ # If files_to_backup is not provided, use default files
63
+ if files_to_backup is None:
64
+ files_to_backup = [
65
+ # even if some of these files are not modified explicitly by the saf-cli,
66
+ # they can be modified by the post-gen hooks in the step templates themselves
67
+ (self._base_solution_dir / "ui" / "pages" / "page.py", self._base_solution_dir),
68
+ (self._base_solution_dir / "solution" / "definition.py", self._base_solution_dir),
69
+ (solution_root_dir / "pyproject.toml", solution_root_dir),
70
+ (solution_root_dir / "poetry.lock", solution_root_dir),
71
+ ]
72
+
73
+ self._files_to_backup = files_to_backup
74
+ self._backup_dir = Path(tempfile.mkdtemp())
75
+ self._preexisting_paths: set[str] = set()
76
+ self._backup_mapping: dict[Path, Path] = {}
77
+ self._missing_files_to_remove_on_restore: set[Path] = set()
78
+ self._snapshot_taken = False
79
+
80
+ def _snapshot_directory_paths(self) -> set[str]:
81
+ """Return relative paths of all files and directories under the solution module directory."""
82
+ if not self._base_solution_dir.exists():
83
+ return set()
84
+ return {str(p.relative_to(self._base_solution_dir)) for p in self._base_solution_dir.rglob("*")}
85
+
86
+ def _rollback_newly_created_files_and_empty_dirs(self) -> None:
87
+ """Remove newly created files and empty directories."""
88
+ if not self._snapshot_taken:
89
+ raise RuntimeError("Cannot rollback before taking a snapshot.")
90
+
91
+ post_paths = self._snapshot_directory_paths()
92
+ created_paths = post_paths - self._preexisting_paths
93
+
94
+ for rel in sorted(created_paths, reverse=True):
95
+ target = self._base_solution_dir / rel
96
+ try:
97
+ if target.is_file() or target.is_symlink():
98
+ target.unlink()
99
+ elif target.is_dir() and not any(target.iterdir()):
100
+ target.rmdir()
101
+ except OSError as e:
102
+ click.secho(
103
+ f"Warning: Failed cleanup: {target} ({e})",
104
+ fg="yellow",
105
+ )
106
+
107
+ for target in sorted(self._missing_files_to_remove_on_restore, reverse=True):
108
+ if not target.exists():
109
+ continue
110
+
111
+ try:
112
+ if target.is_file() or target.is_symlink():
113
+ target.unlink()
114
+ except OSError as e:
115
+ click.secho(
116
+ f"Warning: Failed cleanup: {target} ({e})",
117
+ fg="yellow",
118
+ )
119
+
120
+ def _backup_files(self) -> None:
121
+ """Backup files preserving relative structure."""
122
+ for src in (file for file, _ in self._files_to_backup):
123
+ if not src.is_file():
124
+ continue
125
+
126
+ # Find the base_dir for this file from _files_to_backup
127
+ base_dir = next((bd for f, bd in self._files_to_backup if f == src), self._base_solution_dir)
128
+ relative_path = src.relative_to(base_dir) if src.is_relative_to(base_dir) else Path(src.name)
129
+ backup_file = self._backup_dir / relative_path
130
+
131
+ try:
132
+ backup_file.parent.mkdir(parents=True, exist_ok=True)
133
+ shutil.copy2(src, backup_file)
134
+ self._backup_mapping[src] = backup_file
135
+ except OSError as e:
136
+ click.secho(
137
+ f"Warning: Failed to backup '{src}' -> '{backup_file}' ({e})",
138
+ fg="yellow",
139
+ )
140
+
141
+ def _restore_files(self) -> None:
142
+ """Restore files from backup."""
143
+ for original, backup in self._backup_mapping.items():
144
+ try:
145
+ original.parent.mkdir(parents=True, exist_ok=True)
146
+ shutil.copy2(backup, original)
147
+ except OSError as e:
148
+ click.secho(
149
+ f"Restore failed: {original} ({e})",
150
+ fg="yellow",
151
+ )
152
+
153
+ def __enter__(self) -> "BackupManager":
154
+ """Enter context manager.
155
+
156
+ Snapshots ``base_solution_dir`` and backs up every file supplied via ``files_to_backup``.
157
+ Files that do not yet exist (e.g. an optional ``poetry.lock``) are silently skipped.
158
+ """
159
+ # Take snapshot of the solution module directory
160
+ self._preexisting_paths = self._snapshot_directory_paths()
161
+ self._missing_files_to_remove_on_restore = {
162
+ file_path for file_path, _ in self._files_to_backup if not file_path.exists()
163
+ }
164
+ self._snapshot_taken = True
165
+
166
+ # Back up specified files
167
+ self._backup_files()
168
+ return self
169
+
170
+ def __exit__(self, exc_type, exc_val, exc_tb) -> bool: # type: ignore
171
+ """Exit context manager: restore state if exception occurred, always cleanup."""
172
+ try:
173
+ if exc_type is not None:
174
+ # Remove newly created files and empty directories
175
+ self._rollback_newly_created_files_and_empty_dirs()
176
+ # Restore all backed-up files
177
+ self._restore_files()
178
+ finally:
179
+ # Always cleanup backup directory
180
+ shutil.rmtree(self._backup_dir, ignore_errors=True)
181
+ return False
@@ -0,0 +1,64 @@
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
+
18
+ def to_display_name(name: str) -> str:
19
+ # input: "my-name" or "my_name" or "my name" or "My name"
20
+ # output: "My Name"
21
+ return name.replace("-", " ").replace("_", " ").title()
22
+
23
+
24
+ def to_module_name(name: str, module_type: str = "") -> str:
25
+ # input: "my-name" or "my_name" or "my name" or "My name"
26
+ # output: "my_name{_module-type}" (module_type: _step or _page)
27
+ module_name = name.lower().replace("-", "_").replace(" ", "_")
28
+ if module_type and not module_name.endswith(module_type):
29
+ module_name += f"_{module_type}"
30
+ return module_name
31
+
32
+
33
+ def to_class_name(name: str, class_type: str) -> str:
34
+ # input: "my-name" or "my_name" or "my name" or "My name"
35
+ # output: "MyName{ClassType}" (class_type: Solution or Step)
36
+ class_name = name.title().replace("-", "").replace("_", "").replace(" ", "")
37
+ if not class_name.endswith(class_type):
38
+ class_name += class_type
39
+ return class_name
40
+
41
+
42
+ def to_package_name(name: str) -> str:
43
+ # input: "my-name" or "my_name" or "my name" or "My name"
44
+ # output: "my-name"
45
+ return name.lower().replace("_", "-").replace(" ", "-")
46
+
47
+
48
+ def namespace_to_pkg_name(namespace: str) -> str:
49
+ # input: "my_org.my_project"
50
+ # output: "my-org-my-project"
51
+ namespace_parts = [to_package_name(part) for part in namespace.split(".")]
52
+ return "-".join(namespace_parts)
53
+
54
+
55
+ def to_docker_name(name: str, version: str) -> str:
56
+ # input: "my-name" or "my_name" or "my name" or "My name" and version = "0.1.dev0"
57
+ # output: "my-name-0-1-dev0"
58
+ return f"{to_package_name(name)}_{version.replace('.', '-')}"
59
+
60
+
61
+ def namespace_to_path(namespace: str) -> str:
62
+ # input: "my_org.my_project"
63
+ # output: "my_org/my_project"
64
+ return namespace.replace(".", "/")
@@ -0,0 +1,26 @@
1
+ # Copyright (C) 2026 ANSYS, Inc. and/or its affiliates.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ #
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ import os
18
+ from pathlib import Path
19
+ import platform
20
+
21
+
22
+ def appdata_directory() -> str:
23
+ if platform.system() == "Windows":
24
+ return Path(os.environ["APPDATA"]).as_posix()
25
+ else:
26
+ return str(Path(os.getenv("XDG_DATA_HOME", "~/.local/share")).expanduser())
File without changes
@@ -0,0 +1,173 @@
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 ast
18
+ import os
19
+ from pathlib import Path
20
+ import platform
21
+
22
+ from ansys.saf.cli._database.manager import SolutionDatabaseManager
23
+
24
+
25
+ def find_solution_root_dir(db_manager: SolutionDatabaseManager, solution: str) -> Path:
26
+ """Find the source directory of the solution given a str that can be:
27
+ - the name of a valid registered solution
28
+ - the path to a solution directory
29
+ - empty string, thus pointing to the current working directory.
30
+ """
31
+ if not solution:
32
+ solution_root_dir = Path.cwd()
33
+ registered_solutions = db_manager.get_solutions_by_name(solution)
34
+ if not registered_solutions:
35
+ solution_root_dir = Path(solution).expanduser().resolve()
36
+ if not solution_root_dir.is_dir():
37
+ raise NotADirectoryError(f"Solution not found at {solution_root_dir}")
38
+ elif len(registered_solutions) > 1:
39
+ error_message = f"Multiple solutions found with the name {solution}."
40
+ for registered_solution in registered_solutions:
41
+ error_message += f"\n- Found {registered_solution.name} at {registered_solution.root_dir}"
42
+ error_message += (
43
+ "\nHint: You can specify the correct one by using the full path to the solution as follows:\n"
44
+ "saf <command> /full/path/to/your-solution/"
45
+ )
46
+ raise ValueError(error_message)
47
+ else:
48
+ solution_root_dir = registered_solutions[0].root_dir
49
+
50
+ return solution_root_dir
51
+
52
+
53
+ def resolve_solution_main_file(solution_root_dir: Path) -> Path:
54
+ """Find the main file of the solution given the solution directory.
55
+
56
+ Searches for a main.py file under src/ at any namespace depth.
57
+ If not found, falls back to searching for main.pyc.
58
+ Expected structure: src/<namespace_path>/<solution_name>/main.py
59
+ """
60
+ solution_src_dir = solution_root_dir / "src"
61
+ if not solution_src_dir.is_dir():
62
+ raise NotADirectoryError(f"Solution source directory not found at {solution_src_dir}")
63
+
64
+ main_files = sorted(solution_src_dir.glob("**/main.py"))
65
+ if not main_files:
66
+ main_files = sorted(solution_src_dir.glob("**/main.pyc"))
67
+
68
+ if not main_files:
69
+ raise FileNotFoundError(
70
+ f"Solution main file not found under {solution_src_dir}. "
71
+ f"Expected structure: src/<namespace_path>/<solution_name>/main.py",
72
+ )
73
+ if len(main_files) > 1:
74
+ error_message = "Multiple main files found:\n"
75
+ for main_file in main_files:
76
+ error_message += f"- Found {main_file}\n"
77
+ error_message += "Only one solution is expected in the src directory."
78
+ raise ValueError(error_message)
79
+
80
+ return main_files[0]
81
+
82
+
83
+ def _get_solution_module_parts(solution_main_file: Path) -> list[str]:
84
+ """Return the dotted-module path parts after the src directory."""
85
+ resolved_path = solution_main_file.resolve()
86
+
87
+ try:
88
+ src_index = resolved_path.parts.index("src")
89
+ except ValueError:
90
+ raise RuntimeError(f"Could not find 'src' in path: {resolved_path}") from None
91
+
92
+ module_parts = list(resolved_path.parts[src_index + 1 :])
93
+ if not module_parts:
94
+ raise RuntimeError(f"Could not build module path from: {resolved_path}")
95
+
96
+ module_parts[-1] = Path(module_parts[-1]).stem
97
+ return module_parts
98
+
99
+
100
+ def get_main_module(solution_main_file: Path) -> str:
101
+ """Get the main module for a Solution and, optionally, validate that it is correct. Raises an exception
102
+ if a Solution main module is not found.
103
+
104
+ Parameters
105
+ ----------
106
+ solution_main_file : Path
107
+ The path of the Solution main file (e.g., <org>/solutions/<solution_name>/main.py).
108
+
109
+ Returns
110
+ -------
111
+ str
112
+ The main module str (e.g., my_company.solutions.my_solution.main)
113
+ """
114
+ return ".".join(_get_solution_module_parts(solution_main_file))
115
+
116
+
117
+ def find_solution_display_name(solution_main_file: Path) -> str | None:
118
+ # we cannot import the solution, since the CLI env doesn't have the solution dependencies
119
+ # We fallback then to parsing the definition.py file, which doesn't work for compiled solutions
120
+ # shouldn't be an issue since src directories are not typically compiled.
121
+ definition_file = solution_main_file.parent / "solution" / "definition.py"
122
+ if not definition_file.is_file():
123
+ return None
124
+
125
+ display_name: str | None = None
126
+ tree = ast.parse(definition_file.read_text())
127
+ class_definitions = [node for node in tree.body if isinstance(node, ast.ClassDef)]
128
+ for class_def in class_definitions:
129
+ if any(base.id == "Solution" for base in class_def.bases): # pyright: ignore
130
+ assignments = [node for node in class_def.body if isinstance(node, ast.AnnAssign)]
131
+ for assignment in assignments:
132
+ if assignment.target.id == "display_name": # pyright: ignore
133
+ display_name = str(assignment.value.value) # pyright: ignore
134
+ break
135
+ break
136
+ return display_name
137
+
138
+
139
+ def get_solution_venv_bin_dir(solution_root_dir: Path) -> Path:
140
+ if platform.system() == "Windows":
141
+ solution_venv_bin_dir = solution_root_dir / ".venv" / "Scripts"
142
+ elif platform.system() == "Linux":
143
+ solution_venv_bin_dir = solution_root_dir / ".venv" / "bin"
144
+ else:
145
+ raise ValueError("Unsupported operating system")
146
+ return solution_venv_bin_dir
147
+
148
+
149
+ def get_exec_from_solution_venv(solution_root_dir: Path, exec_name: str) -> Path:
150
+ solution_venv_bin_dir = get_solution_venv_bin_dir(solution_root_dir)
151
+ if platform.system() == "Windows":
152
+ exec_path = solution_venv_bin_dir / f"{exec_name}.exe"
153
+ elif platform.system() == "Linux":
154
+ exec_path = solution_venv_bin_dir / exec_name
155
+ else:
156
+ raise ValueError("Unsupported operating system")
157
+ if not exec_path.is_file():
158
+ raise FileNotFoundError(f"Executable not found at {exec_path}")
159
+ return exec_path
160
+
161
+
162
+ def create_solution_environment(solution_root_dir: Path) -> dict[str, str]:
163
+ solution_env = os.environ.copy()
164
+ # Poetry will always pick the active python environment (the environment in variable VIRTUAL_ENV) when managing
165
+ # dependencies. Removing this environment variable lets poetry use the solution's environment.
166
+ solution_env.pop("VIRTUAL_ENV", None)
167
+ # so internal calls to tools (e.g., poetry and sphinx in installer) resolve first to the binaries in the
168
+ # solution's venv instead of the ones in the SAF-CLI's venv or global ones. This doesn't seem to work with others
169
+ # such as python/pip. See _get_executable_absolute_path.
170
+ solution_env["PATH"] = os.pathsep.join(
171
+ [str(get_solution_venv_bin_dir(solution_root_dir)), solution_env.get("PATH", "")],
172
+ )
173
+ return solution_env
@@ -0,0 +1,64 @@
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 sys
19
+
20
+ import click
21
+
22
+ from ansys.saf.cli._solutions.environment import (
23
+ set_user_level_environment_variable,
24
+ validate_credentials,
25
+ )
26
+
27
+
28
+ @click.group()
29
+ def main():
30
+ """Solutions Manager Integration CLI"""
31
+
32
+
33
+ @main.command("validate-credentials")
34
+ @click.option("--source", required=True, help="Source configuration as JSON")
35
+ @click.option("--username", required=True, help="The username for authentication")
36
+ @click.option("--password", required=True, help="The password for authentication")
37
+ def validate_credentials_cmd(source: str, username: str, password: str):
38
+ """Validate credentials by testing PyPI access"""
39
+ try:
40
+ source_data = json.loads(source)
41
+ is_valid = validate_credentials(source_data, username, password)
42
+ click.echo("Credentials are valid!" if is_valid else "Invalid credentials.")
43
+ sys.exit(0)
44
+ except Exception as e:
45
+ click.echo(f"Error: {e}")
46
+ sys.exit(1)
47
+
48
+
49
+ @main.command("set-env-var")
50
+ @click.option("--key", required=True, help="The name of the environment variable")
51
+ @click.option("--value", required=True, help="The value to set for the environment variable")
52
+ def set_env_var_cmd(key: str, value: str):
53
+ """Set a user-level environment variable"""
54
+ try:
55
+ set_user_level_environment_variable(key, value)
56
+ click.echo(f"Environment variable '{key}' set successfully!")
57
+ sys.exit(0)
58
+ except Exception as e:
59
+ click.echo(f"Error: {e}")
60
+ sys.exit(1)
61
+
62
+
63
+ if __name__ == "__main__":
64
+ main()
ansys/saf/cli/py.typed ADDED
File without changes