v8x 0.1.2__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 (287) hide show
  1. v8x/__init__.py +509 -0
  2. v8x/apps/__init__.py +15 -0
  3. v8x/apps/lxd/__init__.py +12 -0
  4. v8x/apps/lxd/slurm/__init__.py +17 -0
  5. v8x/apps/lxd/slurm/app.py +1281 -0
  6. v8x/apps/lxd/slurm/constants.py +23 -0
  7. v8x/apps/lxd/slurm/render.py +51 -0
  8. v8x/apps/lxd/slurm/templates.py +168 -0
  9. v8x/apps/lxd/slurm/utils.py +12 -0
  10. v8x/apps/on_prem/__init__.py +12 -0
  11. v8x/apps/on_prem/slurm_multipass/__init__.py +16 -0
  12. v8x/apps/on_prem/slurm_multipass/app.py +352 -0
  13. v8x/apps/on_prem/slurm_multipass/constants.py +304 -0
  14. v8x/apps/on_prem/slurm_multipass/render.py +134 -0
  15. v8x/apps/on_prem/slurm_multipass/templates.py +319 -0
  16. v8x/apps/on_prem/slurm_multipass/utils.py +129 -0
  17. v8x/auth.py +589 -0
  18. v8x/cache.py +170 -0
  19. v8x/client.py +96 -0
  20. v8x/commands/__init__.py +12 -0
  21. v8x/commands/alias/__init__.py +34 -0
  22. v8x/commands/alias/apps.py +25 -0
  23. v8x/commands/alias/cloud_accounts.py +31 -0
  24. v8x/commands/alias/clouds.py +31 -0
  25. v8x/commands/alias/clusters.py +25 -0
  26. v8x/commands/alias/deployments.py +34 -0
  27. v8x/commands/alias/federations.py +25 -0
  28. v8x/commands/alias/networks.py +25 -0
  29. v8x/commands/alias/profiles.py +26 -0
  30. v8x/commands/alias/support_tickets.py +25 -0
  31. v8x/commands/alias/teams.py +26 -0
  32. v8x/commands/app/__init__.py +53 -0
  33. v8x/commands/app/deployment/__init__.py +90 -0
  34. v8x/commands/app/deployment/cleanup.py +225 -0
  35. v8x/commands/app/deployment/create.py +15 -0
  36. v8x/commands/app/deployment/delete.py +150 -0
  37. v8x/commands/app/deployment/get.py +54 -0
  38. v8x/commands/app/deployment/list.py +59 -0
  39. v8x/commands/app/deployment/render.py +298 -0
  40. v8x/commands/app/list.py +58 -0
  41. v8x/commands/cloud/__init__.py +31 -0
  42. v8x/commands/cloud/account/__init__.py +37 -0
  43. v8x/commands/cloud/account/create.py +311 -0
  44. v8x/commands/cloud/account/delete.py +101 -0
  45. v8x/commands/cloud/account/get.py +109 -0
  46. v8x/commands/cloud/account/list.py +92 -0
  47. v8x/commands/cloud/get.py +61 -0
  48. v8x/commands/cloud/list.py +57 -0
  49. v8x/commands/cluster/__init__.py +63 -0
  50. v8x/commands/cluster/compute_pool/__init__.py +31 -0
  51. v8x/commands/cluster/compute_pool/_helpers.py +53 -0
  52. v8x/commands/cluster/compute_pool/create.py +203 -0
  53. v8x/commands/cluster/compute_pool/delete.py +92 -0
  54. v8x/commands/cluster/compute_pool/get.py +107 -0
  55. v8x/commands/cluster/compute_pool/list.py +111 -0
  56. v8x/commands/cluster/create.py +738 -0
  57. v8x/commands/cluster/delete.py +301 -0
  58. v8x/commands/cluster/extend.py +216 -0
  59. v8x/commands/cluster/federation/__init__.py +35 -0
  60. v8x/commands/cluster/federation/create.py +72 -0
  61. v8x/commands/cluster/federation/delete.py +82 -0
  62. v8x/commands/cluster/federation/get.py +65 -0
  63. v8x/commands/cluster/federation/list.py +61 -0
  64. v8x/commands/cluster/federation/update.py +88 -0
  65. v8x/commands/cluster/get.py +80 -0
  66. v8x/commands/cluster/inference_endpoint/__init__.py +29 -0
  67. v8x/commands/cluster/inference_endpoint/_helpers.py +34 -0
  68. v8x/commands/cluster/inference_endpoint/create.py +174 -0
  69. v8x/commands/cluster/inference_endpoint/delete.py +61 -0
  70. v8x/commands/cluster/inference_endpoint/get.py +90 -0
  71. v8x/commands/cluster/inference_endpoint/list.py +89 -0
  72. v8x/commands/cluster/inference_endpoint/logs.py +67 -0
  73. v8x/commands/cluster/inference_endpoint/runtimes.py +80 -0
  74. v8x/commands/cluster/inference_endpoint/start_stop.py +85 -0
  75. v8x/commands/cluster/inference_preset/__init__.py +34 -0
  76. v8x/commands/cluster/inference_preset/_helpers.py +53 -0
  77. v8x/commands/cluster/inference_preset/create.py +142 -0
  78. v8x/commands/cluster/inference_preset/delete.py +88 -0
  79. v8x/commands/cluster/inference_preset/get.py +95 -0
  80. v8x/commands/cluster/inference_preset/list.py +93 -0
  81. v8x/commands/cluster/kubeflow/__init__.py +39 -0
  82. v8x/commands/cluster/kubeflow/_helpers.py +68 -0
  83. v8x/commands/cluster/kubeflow/create.py +125 -0
  84. v8x/commands/cluster/kubeflow/delete.py +123 -0
  85. v8x/commands/cluster/kubeflow/get.py +88 -0
  86. v8x/commands/cluster/list.py +74 -0
  87. v8x/commands/cluster/model_registry/__init__.py +39 -0
  88. v8x/commands/cluster/model_registry/_helpers.py +43 -0
  89. v8x/commands/cluster/model_registry/create.py +125 -0
  90. v8x/commands/cluster/model_registry/delete.py +61 -0
  91. v8x/commands/cluster/model_registry/get.py +78 -0
  92. v8x/commands/cluster/model_registry/job.py +74 -0
  93. v8x/commands/cluster/model_registry/list.py +92 -0
  94. v8x/commands/cluster/model_registry/search.py +84 -0
  95. v8x/commands/cluster/model_registry/update.py +74 -0
  96. v8x/commands/cluster/model_registry/versions.py +83 -0
  97. v8x/commands/cluster/namespace/__init__.py +31 -0
  98. v8x/commands/cluster/namespace/_helpers.py +48 -0
  99. v8x/commands/cluster/namespace/create.py +81 -0
  100. v8x/commands/cluster/namespace/delete.py +71 -0
  101. v8x/commands/cluster/namespace/get.py +89 -0
  102. v8x/commands/cluster/namespace/list.py +82 -0
  103. v8x/commands/cluster/network/__init__.py +36 -0
  104. v8x/commands/cluster/network/_helpers.py +267 -0
  105. v8x/commands/cluster/network/create.py +191 -0
  106. v8x/commands/cluster/network/delete.py +71 -0
  107. v8x/commands/cluster/network/get.py +80 -0
  108. v8x/commands/cluster/network/list.py +74 -0
  109. v8x/commands/cluster/network/update.py +134 -0
  110. v8x/commands/cluster/node_group/_helpers.py +56 -0
  111. v8x/commands/cluster/node_group/create.py +202 -0
  112. v8x/commands/cluster/render.py +214 -0
  113. v8x/commands/cluster/secret/__init__.py +24 -0
  114. v8x/commands/cluster/secret/_helpers.py +56 -0
  115. v8x/commands/cluster/secret/create.py +116 -0
  116. v8x/commands/cluster/secret/delete.py +62 -0
  117. v8x/commands/cluster/secret/get.py +86 -0
  118. v8x/commands/cluster/secret/list.py +92 -0
  119. v8x/commands/cluster/secret/test.py +68 -0
  120. v8x/commands/cluster/service/__init__.py +41 -0
  121. v8x/commands/cluster/service/create.py +171 -0
  122. v8x/commands/cluster/service/delete.py +114 -0
  123. v8x/commands/cluster/service/disable.py +403 -0
  124. v8x/commands/cluster/service/enable.py +155 -0
  125. v8x/commands/cluster/service/get.py +111 -0
  126. v8x/commands/cluster/service/list.py +157 -0
  127. v8x/commands/cluster/service/update.py +124 -0
  128. v8x/commands/cluster/slurm/__init__.py +45 -0
  129. v8x/commands/cluster/slurm/_helpers.py +68 -0
  130. v8x/commands/cluster/slurm/create.py +254 -0
  131. v8x/commands/cluster/slurm/delete.py +146 -0
  132. v8x/commands/cluster/slurm/deploy.py +201 -0
  133. v8x/commands/cluster/slurm/get.py +101 -0
  134. v8x/commands/cluster/slurm/list.py +97 -0
  135. v8x/commands/cluster/slurm/update.py +123 -0
  136. v8x/commands/cluster/update.py +335 -0
  137. v8x/commands/cluster/user_service/_crud.py +325 -0
  138. v8x/commands/cluster/user_service/_helpers.py +66 -0
  139. v8x/commands/cluster/utils.py +137 -0
  140. v8x/commands/cluster/workspace_preset/__init__.py +31 -0
  141. v8x/commands/cluster/workspace_preset/_helpers.py +53 -0
  142. v8x/commands/cluster/workspace_preset/create.py +152 -0
  143. v8x/commands/cluster/workspace_preset/delete.py +88 -0
  144. v8x/commands/cluster/workspace_preset/get.py +114 -0
  145. v8x/commands/cluster/workspace_preset/list.py +91 -0
  146. v8x/commands/config/__init__.py +27 -0
  147. v8x/commands/config/clear.py +85 -0
  148. v8x/commands/federation/__init__.py +40 -0
  149. v8x/commands/federation/create.py +58 -0
  150. v8x/commands/federation/delete.py +60 -0
  151. v8x/commands/federation/get.py +54 -0
  152. v8x/commands/federation/list.py +50 -0
  153. v8x/commands/federation/update.py +64 -0
  154. v8x/commands/get_kubeconfig.py +113 -0
  155. v8x/commands/job/__init__.py +39 -0
  156. v8x/commands/job/client.py +28 -0
  157. v8x/commands/job/script/__init__.py +35 -0
  158. v8x/commands/job/script/create.py +50 -0
  159. v8x/commands/job/script/delete.py +61 -0
  160. v8x/commands/job/script/get.py +44 -0
  161. v8x/commands/job/script/list.py +63 -0
  162. v8x/commands/job/script/update.py +79 -0
  163. v8x/commands/job/submission/__init__.py +30 -0
  164. v8x/commands/job/submission/create.py +89 -0
  165. v8x/commands/job/submission/delete.py +61 -0
  166. v8x/commands/job/submission/get.py +40 -0
  167. v8x/commands/job/submission/list.py +73 -0
  168. v8x/commands/job/submission/update.py +88 -0
  169. v8x/commands/job/template/__init__.py +28 -0
  170. v8x/commands/job/template/create.py +70 -0
  171. v8x/commands/job/template/delete.py +63 -0
  172. v8x/commands/job/template/get.py +46 -0
  173. v8x/commands/job/template/list.py +59 -0
  174. v8x/commands/job/template/update.py +85 -0
  175. v8x/commands/license/__init__.py +51 -0
  176. v8x/commands/license/booking/__init__.py +29 -0
  177. v8x/commands/license/booking/create.py +73 -0
  178. v8x/commands/license/booking/delete.py +48 -0
  179. v8x/commands/license/booking/get.py +40 -0
  180. v8x/commands/license/booking/list.py +60 -0
  181. v8x/commands/license/booking/main.py +160 -0
  182. v8x/commands/license/client.py +28 -0
  183. v8x/commands/license/configuration/__init__.py +35 -0
  184. v8x/commands/license/configuration/create.py +62 -0
  185. v8x/commands/license/configuration/delete.py +53 -0
  186. v8x/commands/license/configuration/get.py +40 -0
  187. v8x/commands/license/configuration/list.py +56 -0
  188. v8x/commands/license/configuration/update.py +65 -0
  189. v8x/commands/license/deployment/__init__.py +35 -0
  190. v8x/commands/license/deployment/create.py +63 -0
  191. v8x/commands/license/deployment/delete.py +51 -0
  192. v8x/commands/license/deployment/get.py +52 -0
  193. v8x/commands/license/deployment/list.py +79 -0
  194. v8x/commands/license/deployment/update.py +74 -0
  195. v8x/commands/license/feature/__init__.py +37 -0
  196. v8x/commands/license/feature/create.py +64 -0
  197. v8x/commands/license/feature/delete.py +54 -0
  198. v8x/commands/license/feature/get.py +40 -0
  199. v8x/commands/license/feature/list.py +53 -0
  200. v8x/commands/license/feature/update.py +68 -0
  201. v8x/commands/license/product/__init__.py +35 -0
  202. v8x/commands/license/product/create.py +61 -0
  203. v8x/commands/license/product/delete.py +54 -0
  204. v8x/commands/license/product/get.py +40 -0
  205. v8x/commands/license/product/list.py +53 -0
  206. v8x/commands/license/product/update.py +64 -0
  207. v8x/commands/license/server/__init__.py +35 -0
  208. v8x/commands/license/server/create.py +60 -0
  209. v8x/commands/license/server/delete.py +54 -0
  210. v8x/commands/license/server/get.py +49 -0
  211. v8x/commands/license/server/list.py +53 -0
  212. v8x/commands/license/server/update.py +64 -0
  213. v8x/commands/network/__init__.py +39 -0
  214. v8x/commands/network/attach.py +81 -0
  215. v8x/commands/network/create.py +97 -0
  216. v8x/commands/network/delete.py +42 -0
  217. v8x/commands/network/detach.py +85 -0
  218. v8x/commands/network/get.py +92 -0
  219. v8x/commands/network/list.py +85 -0
  220. v8x/commands/network/update.py +60 -0
  221. v8x/commands/profile/__init__.py +31 -0
  222. v8x/commands/profile/crud.py +240 -0
  223. v8x/commands/profile/render.py +65 -0
  224. v8x/commands/storage/__init__.py +57 -0
  225. v8x/commands/storage/_helpers.py +80 -0
  226. v8x/commands/storage/create.py +109 -0
  227. v8x/commands/storage/delete.py +97 -0
  228. v8x/commands/storage/external_expose/__init__.py +27 -0
  229. v8x/commands/storage/external_expose/cephfs.py +448 -0
  230. v8x/commands/storage/external_expose/nfs.py +216 -0
  231. v8x/commands/storage/get.py +89 -0
  232. v8x/commands/storage/list.py +95 -0
  233. v8x/commands/storage/list_available.py +78 -0
  234. v8x/commands/storage/namespace_import/__init__.py +663 -0
  235. v8x/commands/storage/namespace_import/cephfs.py +236 -0
  236. v8x/commands/storage/namespace_import/internal.py +318 -0
  237. v8x/commands/storage/namespace_import/nfs.py +221 -0
  238. v8x/commands/storage/system.py +376 -0
  239. v8x/commands/support_ticket/__init__.py +30 -0
  240. v8x/commands/support_ticket/create.py +88 -0
  241. v8x/commands/support_ticket/delete.py +77 -0
  242. v8x/commands/support_ticket/get.py +77 -0
  243. v8x/commands/support_ticket/list.py +108 -0
  244. v8x/commands/support_ticket/update.py +107 -0
  245. v8x/commands/team/__init__.py +40 -0
  246. v8x/commands/team/add_member.py +48 -0
  247. v8x/commands/team/add_resource.py +49 -0
  248. v8x/commands/team/create.py +86 -0
  249. v8x/commands/team/delete.py +34 -0
  250. v8x/commands/team/get.py +72 -0
  251. v8x/commands/team/list.py +72 -0
  252. v8x/commands/team/list_members.py +42 -0
  253. v8x/commands/team/remove_member.py +34 -0
  254. v8x/commands/team/set_role.py +39 -0
  255. v8x/commands/team/set_roles.py +55 -0
  256. v8x/commands/team/update.py +46 -0
  257. v8x/commands/vdeployer_web/__init__.py +30 -0
  258. v8x/commands/vdeployer_web/deploy.py +424 -0
  259. v8x/commands/vdeployer_web/destroy.py +160 -0
  260. v8x/commands/vdeployer_web/status.py +107 -0
  261. v8x/config.py +275 -0
  262. v8x/constants.py +70 -0
  263. v8x/deployment_apps/__init__.py +45 -0
  264. v8x/deployment_apps/common.py +164 -0
  265. v8x/deployment_apps/constants.py +21 -0
  266. v8x/deployment_apps/crud.py +279 -0
  267. v8x/deployment_apps/schema.py +34 -0
  268. v8x/deployments/__init__.py +21 -0
  269. v8x/deployments/crud.py +408 -0
  270. v8x/deployments/schema.py +170 -0
  271. v8x/exceptions.py +139 -0
  272. v8x/gql_client.py +710 -0
  273. v8x/libjuju/__init__.py +1097 -0
  274. v8x/main.py +499 -0
  275. v8x/profiles/__init__.py +16 -0
  276. v8x/profiles/crud.py +303 -0
  277. v8x/profiles/schema.py +108 -0
  278. v8x/render.py +2409 -0
  279. v8x/schemas.py +102 -0
  280. v8x/time_loop.py +134 -0
  281. v8x/utils.py +22 -0
  282. v8x/vantage_rest_api_client.py +343 -0
  283. v8x-0.1.2.dist-info/METADATA +127 -0
  284. v8x-0.1.2.dist-info/RECORD +287 -0
  285. v8x-0.1.2.dist-info/WHEEL +4 -0
  286. v8x-0.1.2.dist-info/entry_points.txt +2 -0
  287. v8x-0.1.2.dist-info/licenses/LICENSE +674 -0
v8x/__init__.py ADDED
@@ -0,0 +1,509 @@
1
+ # Copyright (C) 2025 Vantage Compute Corporation
2
+ # This program is free software: you can redistribute it and/or modify it under
3
+ # the terms of the GNU General Public License as published by the Free Software
4
+ # Foundation, version 3.
5
+ #
6
+ # This program is distributed in the hope that it will be useful, but WITHOUT
7
+ # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
8
+ # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
9
+ #
10
+ # You should have received a copy of the GNU General Public License along with
11
+ # this program. If not, see <https://www.gnu.org/licenses/>.
12
+ """v8x package for managing cloud computing resources."""
13
+
14
+ import asyncio
15
+ import importlib.metadata
16
+ import inspect
17
+ import logging
18
+ import sys
19
+ import time
20
+ from functools import wraps
21
+ from typing import Any, Callable, List, Optional, get_type_hints # noqa: F401
22
+
23
+ import typer
24
+ from pydantic import BaseModel, ConfigDict
25
+ from typing_extensions import Annotated
26
+
27
+ from v8x.constants import V8X_DEBUG_LOG_PATH
28
+
29
+ __version__ = importlib.metadata.version("v8x")
30
+
31
+ # Global variable to track the file logging handler
32
+ _file_handler: Optional[logging.Handler] = None
33
+ _logging_initialized: bool = False
34
+
35
+ # Add a null handler at import time to prevent logs from being lost
36
+ # This handler will be replaced by setup_logging()
37
+ logging.getLogger().addHandler(logging.NullHandler())
38
+
39
+
40
+ def setup_logging(verbose: bool = False) -> None:
41
+ """Configure logging based on verbosity flag.
42
+
43
+ File logging to ~/.v8x/debug.log is always enabled.
44
+
45
+ Args:
46
+ verbose: If True, enable DEBUG level logging to console
47
+ """
48
+ global _file_handler, _logging_initialized
49
+
50
+ # Get the root logger
51
+ root_logger = logging.getLogger()
52
+
53
+ # Only remove handlers if we've already configured logging before
54
+ # On first call, there shouldn't be any handlers
55
+ if _logging_initialized:
56
+ # Remove existing handlers except file handler
57
+ handlers_to_remove = [h for h in root_logger.handlers if h != _file_handler]
58
+ for handler in handlers_to_remove:
59
+ root_logger.removeHandler(handler)
60
+
61
+ if verbose:
62
+ console_level = logging.DEBUG
63
+ # Enable rich tracebacks only in verbose mode
64
+ from rich import traceback
65
+
66
+ traceback.install()
67
+ logging.getLogger("httpx").disabled = False
68
+ logging.getLogger("httpcore").disabled = False
69
+ else:
70
+ console_level = logging.ERROR
71
+ # Disable rich tracebacks in normal mode
72
+ sys.excepthook = sys.__excepthook__
73
+
74
+ # Add console handler
75
+ console_handler = logging.StreamHandler(sys.stdout)
76
+ console_handler.setLevel(console_level)
77
+ console_formatter = logging.Formatter(
78
+ "%(asctime)s | %(levelname)-8s | %(name)s:%(funcName)s:%(lineno)d - %(message)s",
79
+ datefmt="%Y-%m-%d %H:%M:%S",
80
+ )
81
+ console_handler.setFormatter(console_formatter)
82
+ root_logger.addHandler(console_handler)
83
+
84
+ # Set root logger level to DEBUG to capture all logs for file handler
85
+ # Console handler will filter based on its own level
86
+ root_logger.setLevel(logging.DEBUG)
87
+
88
+ # IMPORTANT: Reset all existing loggers to ensure they pick up the new level
89
+ # This is necessary because loggers created before setup_logging() may have
90
+ # cached their effective level
91
+ for logger_name in list(logging.Logger.manager.loggerDict.keys()):
92
+ logger_instance = logging.getLogger(logger_name)
93
+ # Only reset level for loggers in our namespace
94
+ if logger_name.startswith("v8x"):
95
+ logger_instance.setLevel(logging.NOTSET) # Inherit from root
96
+
97
+ _logging_initialized = True
98
+
99
+ if _file_handler is None:
100
+ from logging.handlers import RotatingFileHandler
101
+
102
+ V8X_DEBUG_LOG_PATH.parent.mkdir(parents=True, exist_ok=True)
103
+
104
+ _file_handler = RotatingFileHandler(
105
+ V8X_DEBUG_LOG_PATH,
106
+ maxBytes=10 * 1024 * 1024, # 10 MB
107
+ backupCount=7,
108
+ )
109
+ _file_handler.setLevel(logging.DEBUG)
110
+ file_formatter = logging.Formatter(
111
+ "%(asctime)s | %(levelname)-8s | %(name)s:%(funcName)s:%(lineno)d - %(message)s",
112
+ datefmt="%Y-%m-%d %H:%M:%S",
113
+ )
114
+ _file_handler.setFormatter(file_formatter)
115
+ root_logger.addHandler(_file_handler)
116
+
117
+ logger = logging.getLogger(__name__)
118
+ logger.debug(
119
+ "Logging configured (verbose=%s, file_logging=always_enabled)",
120
+ verbose,
121
+ )
122
+
123
+
124
+ def maybe_run_async(func: Callable) -> Callable:
125
+ """Wrap async functions for use in Typer commands.
126
+
127
+ This wraps an async function so it can be used as a Typer command.
128
+ When the command is invoked, it will run the async function in an event loop.
129
+
130
+ Args:
131
+ func: The async function to wrap
132
+
133
+ Returns:
134
+ A wrapper function that runs the async function in an event loop
135
+ """
136
+ if not inspect.iscoroutinefunction(func):
137
+ # Function is not async, return as-is
138
+ return func
139
+
140
+ @wraps(func)
141
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
142
+ """Run the async function in an event loop."""
143
+ try:
144
+ # Check if we're already in an event loop
145
+ asyncio.get_running_loop()
146
+ # We're in an event loop, cannot use asyncio.run()
147
+ # Return the coroutine directly (for tests)
148
+ return func(*args, **kwargs)
149
+ except RuntimeError:
150
+ # No event loop running, safe to use asyncio.run()
151
+ return asyncio.run(func(*args, **kwargs))
152
+
153
+ return wrapper
154
+
155
+
156
+ class TyperCommandParameter(BaseModel):
157
+ """Represents a command parameter that can be automatically injected."""
158
+
159
+ name: str
160
+ type: Any # Will hold inspect.Parameter.KEYWORD_ONLY
161
+ default: Any
162
+ annotation: Any
163
+
164
+ model_config = ConfigDict(arbitrary_types_allowed=True)
165
+
166
+
167
+ # Define common parameters that will be injected into all commands
168
+ inherited_command_parameters = [
169
+ TyperCommandParameter(
170
+ name="json",
171
+ type=inspect.Parameter.KEYWORD_ONLY,
172
+ default=False,
173
+ annotation=Annotated[bool, typer.Option("--json", "-j", help="Output in JSON format")],
174
+ ),
175
+ TyperCommandParameter(
176
+ name="verbose",
177
+ type=inspect.Parameter.KEYWORD_ONLY,
178
+ default=False,
179
+ annotation=Annotated[
180
+ bool, typer.Option("--verbose", "-v", help="Enable verbose terminal output")
181
+ ],
182
+ ),
183
+ TyperCommandParameter(
184
+ name="profile",
185
+ type=inspect.Parameter.KEYWORD_ONLY,
186
+ default=None,
187
+ annotation=Annotated[
188
+ Optional[str], typer.Option("--profile", "-p", help="Profile name to use")
189
+ ],
190
+ ),
191
+ ]
192
+
193
+
194
+ class AsyncTyper(typer.Typer):
195
+ """A Typer subclass that automatically wraps async functions with asyncio.run()."""
196
+
197
+ @staticmethod
198
+ def format_elapsed_time(start_time: float) -> str:
199
+ """Format elapsed time from start_time to current time with high granularity.
200
+
201
+ Args:
202
+ start_time: Start time from time.time()
203
+
204
+ Returns:
205
+ Formatted time string like "0:05.123", "1:23:45.678", or "0.123s"
206
+ """
207
+ elapsed = time.time() - start_time
208
+
209
+ # For very short times (< 1 second), show milliseconds only
210
+ if elapsed < 1.0:
211
+ return f"{elapsed:.3f}s"
212
+
213
+ # For times >= 1 second, show with millisecond precision
214
+ hours = int(elapsed // 3600)
215
+ minutes = int((elapsed % 3600) // 60)
216
+ seconds = elapsed % 60 # Keep as float for milliseconds
217
+
218
+ if hours > 0:
219
+ return f"{hours}:{minutes:02d}:{seconds:06.3f}"
220
+ else:
221
+ return f"{minutes}:{seconds:06.3f}"
222
+
223
+ @staticmethod
224
+ def get_elapsed_time(ctx: typer.Context) -> str:
225
+ """Get formatted elapsed time from context.
226
+
227
+ Args:
228
+ ctx: Typer context with command_start_time attribute
229
+
230
+ Returns:
231
+ Formatted elapsed time or "0.000s" if no timing available
232
+ """
233
+ if hasattr(ctx, "obj") and ctx.obj and hasattr(ctx.obj, "command_start_time"):
234
+ return AsyncTyper.format_elapsed_time(ctx.obj.command_start_time)
235
+ return "0.000s"
236
+
237
+ @staticmethod
238
+ def get_command_start_time(ctx: typer.Context) -> Optional[float]:
239
+ """Get command start time from context.
240
+
241
+ Args:
242
+ ctx: Typer context with command_start_time attribute
243
+
244
+ Returns:
245
+ Command start time or None if not available
246
+ """
247
+ if hasattr(ctx, "obj") and ctx.obj and hasattr(ctx.obj, "command_start_time"):
248
+ return ctx.obj.command_start_time
249
+ return None
250
+
251
+ @staticmethod
252
+ def maybe_run_async(func: Callable, *args: Any, **kwargs: Any) -> Any:
253
+ """Run function asynchronously if it's a coroutine, otherwise run normally."""
254
+ if inspect.iscoroutinefunction(func):
255
+ # Check if we're already in an event loop
256
+ try:
257
+ asyncio.get_running_loop()
258
+ # We're in an event loop, cannot use asyncio.run()
259
+ # This typically happens in tests, return the coroutine
260
+ return func(*args, **kwargs)
261
+ except RuntimeError:
262
+ # No event loop running, safe to use asyncio.run()
263
+ return asyncio.run(func(*args, **kwargs))
264
+ else:
265
+ # Check if the function call returns a coroutine
266
+ result = func(*args, **kwargs)
267
+ if inspect.iscoroutine(result):
268
+ # Function returned a coroutine, need to run it
269
+ try:
270
+ asyncio.get_running_loop()
271
+ # We're in an event loop, return the coroutine
272
+ return result
273
+ except RuntimeError:
274
+ # No event loop running, safe to use asyncio.run()
275
+ return asyncio.run(result)
276
+ return result
277
+
278
+ def command(
279
+ self,
280
+ name: Optional[str] = None,
281
+ *,
282
+ cls: Optional[type] = None,
283
+ context_settings: Optional[dict] = None,
284
+ help: Optional[str] = None,
285
+ epilog: Optional[str] = None,
286
+ short_help: Optional[str] = None,
287
+ options_metavar: Optional[str] = None,
288
+ add_help_option: bool = True,
289
+ no_args_is_help: bool = False,
290
+ hidden: bool = False,
291
+ deprecated: bool = False,
292
+ rich_help_panel: Optional[str] = None,
293
+ ):
294
+ """Override command decorator to handle async functions and auto-inject common options."""
295
+
296
+ def decorator(func: Callable) -> Callable:
297
+ import functools
298
+
299
+ # Get the original function's signature
300
+ original_sig = inspect.signature(func)
301
+ resolved_hints = get_type_hints(func)
302
+ new_params = []
303
+
304
+ for param in original_sig.parameters.values():
305
+ annotation = resolved_hints.get(param.name, param.annotation)
306
+ new_params.append(param.replace(annotation=annotation))
307
+
308
+ # Inject inherited command parameters if they don't already exist
309
+ for cmd_param in inherited_command_parameters:
310
+ if cmd_param.name not in original_sig.parameters:
311
+ # Create the parameter with the correct attributes
312
+ param = inspect.Parameter(
313
+ name=cmd_param.name,
314
+ kind=inspect.Parameter.KEYWORD_ONLY,
315
+ default=cmd_param.default,
316
+ annotation=cmd_param.annotation,
317
+ )
318
+ new_params.append(param)
319
+
320
+ # Create new signature with all injected parameters
321
+ new_sig = original_sig.replace(parameters=new_params)
322
+
323
+ # Create a wrapper that handles the injected parameters
324
+ def command_wrapper(ctx: typer.Context, *args: Any, **kwargs: Any) -> Any:
325
+ # Start timing the command execution
326
+ command_start_time = time.time()
327
+
328
+ # Extract and store injected parameters in context
329
+ if hasattr(ctx, "obj") and ctx.obj is not None:
330
+ # Store the start time in the context for later use
331
+ ctx.obj.command_start_time = command_start_time
332
+
333
+ # Handle json parameter
334
+ json_flag = kwargs.pop("json", False)
335
+ ctx.obj.json_output = json_flag or getattr(ctx.obj, "json_output", False)
336
+
337
+ # Update the formatter's json_output flag if formatter exists
338
+ if hasattr(ctx.obj, "formatter") and ctx.obj.formatter is not None:
339
+ ctx.obj.formatter.json_output = ctx.obj.json_output
340
+
341
+ # Handle verbose parameter
342
+ verbose_flag = kwargs.pop("verbose", False)
343
+ ctx.obj.verbose = verbose_flag or getattr(ctx.obj, "verbose", False)
344
+
345
+ setup_logging(verbose=ctx.obj.verbose)
346
+
347
+ # Handle profile parameter
348
+ # Explicit --profile flag always takes precedence over context default
349
+ profile_value = kwargs.pop("profile", None)
350
+ if profile_value and profile_value != "default":
351
+ # User explicitly passed --profile with a non-default value
352
+ ctx.obj.profile = profile_value
353
+ elif not hasattr(ctx.obj, "profile") or not ctx.obj.profile:
354
+ # No profile set yet, use default
355
+ ctx.obj.profile = profile_value or "default"
356
+
357
+ # Call the original function without the injected parameters
358
+ return func(ctx, *args, **kwargs)
359
+
360
+ # Set the new signature on the wrapper
361
+ command_wrapper.__signature__ = new_sig # type: ignore[misc]
362
+ command_wrapper.__name__ = func.__name__
363
+ command_wrapper.__doc__ = func.__doc__
364
+ command_wrapper.__module__ = func.__module__
365
+ command_wrapper.__qualname__ = func.__qualname__
366
+ command_wrapper.__annotations__ = {
367
+ param.name: param.annotation
368
+ for param in new_sig.parameters.values()
369
+ if param.annotation is not inspect.Parameter.empty
370
+ }
371
+ return_annotation = resolved_hints.get("return", new_sig.return_annotation)
372
+ if return_annotation is not inspect.Signature.empty:
373
+ command_wrapper.__annotations__["return"] = return_annotation
374
+
375
+ # Handle async functions
376
+ if inspect.iscoroutinefunction(func):
377
+
378
+ @functools.wraps(command_wrapper)
379
+ def sync_wrapper(*args, **kwargs):
380
+ return self.maybe_run_async(command_wrapper, *args, **kwargs)
381
+
382
+ wrapped_func = sync_wrapper
383
+ # Copy signature to sync wrapper too
384
+ wrapped_func.__signature__ = new_sig # type: ignore[misc]
385
+ else:
386
+ wrapped_func = command_wrapper
387
+
388
+ # Build kwargs for parent method, filtering out None values
389
+ command_kwargs = {
390
+ "name": name,
391
+ "cls": cls,
392
+ "context_settings": context_settings,
393
+ "help": help,
394
+ "epilog": epilog,
395
+ "short_help": short_help,
396
+ "add_help_option": add_help_option,
397
+ "no_args_is_help": no_args_is_help,
398
+ "hidden": hidden,
399
+ "deprecated": deprecated,
400
+ "rich_help_panel": rich_help_panel,
401
+ }
402
+ if options_metavar is not None:
403
+ command_kwargs["options_metavar"] = options_metavar
404
+
405
+ return super(AsyncTyper, self).command(**command_kwargs)(wrapped_func)
406
+
407
+ return decorator
408
+
409
+ def app_command(
410
+ self,
411
+ name: Optional[str] = None,
412
+ *,
413
+ cls: Optional[type] = None,
414
+ context_settings: Optional[dict] = None,
415
+ help: Optional[str] = None,
416
+ epilog: Optional[str] = None,
417
+ short_help: Optional[str] = None,
418
+ options_metavar: Optional[str] = None,
419
+ add_help_option: bool = True,
420
+ no_args_is_help: bool = False,
421
+ hidden: bool = False,
422
+ deprecated: bool = False,
423
+ rich_help_panel: Optional[str] = None,
424
+ ):
425
+ """Command decorator that automatically handles async functions and provides a consistent pattern.
426
+
427
+ This decorator can be extended to automatically inject common options in the future.
428
+ For now, it provides the same functionality as the standard command decorator.
429
+
430
+ Usage:
431
+ @app.app_command()
432
+ def my_command(ctx: typer.Context, name: str, json_output: JsonOption = False):
433
+ if should_use_json(ctx):
434
+ # JSON output logic
435
+ else:
436
+ # Rich/interactive output logic
437
+ """
438
+ return self.command(
439
+ name=name,
440
+ cls=cls,
441
+ context_settings=context_settings,
442
+ help=help,
443
+ epilog=epilog,
444
+ short_help=short_help,
445
+ options_metavar=options_metavar,
446
+ add_help_option=add_help_option,
447
+ no_args_is_help=no_args_is_help,
448
+ hidden=hidden,
449
+ deprecated=deprecated,
450
+ rich_help_panel=rich_help_panel,
451
+ )
452
+
453
+ def callback(
454
+ self,
455
+ *,
456
+ cls: Optional[type] = None,
457
+ invoke_without_command: bool = False,
458
+ no_args_is_help: bool = False,
459
+ subcommand_metavar: Optional[str] = None,
460
+ chain: bool = False,
461
+ result_callback: Optional[Callable] = None,
462
+ context_settings: Optional[dict] = None,
463
+ help: Optional[str] = None,
464
+ epilog: Optional[str] = None,
465
+ short_help: Optional[str] = None,
466
+ options_metavar: Optional[str] = None,
467
+ add_help_option: bool = True,
468
+ hidden: bool = False,
469
+ deprecated: bool = False,
470
+ rich_help_panel: Optional[str] = None,
471
+ ):
472
+ """Override callback decorator to handle async functions."""
473
+
474
+ def decorator(func: Callable) -> Callable:
475
+ if inspect.iscoroutinefunction(func):
476
+ # Create a sync wrapper that preserves the original function signature
477
+ import functools
478
+
479
+ @functools.wraps(func)
480
+ def sync_wrapper(*args, **kwargs):
481
+ return self.maybe_run_async(func, *args, **kwargs)
482
+
483
+ wrapped_func = sync_wrapper
484
+ else:
485
+ wrapped_func = func
486
+
487
+ # Build kwargs for parent method, filtering out None values
488
+ kwargs = {
489
+ "cls": cls,
490
+ "invoke_without_command": invoke_without_command,
491
+ "no_args_is_help": no_args_is_help,
492
+ "subcommand_metavar": subcommand_metavar,
493
+ "chain": chain,
494
+ "result_callback": result_callback,
495
+ "context_settings": context_settings,
496
+ "help": help,
497
+ "epilog": epilog,
498
+ "short_help": short_help,
499
+ "add_help_option": add_help_option,
500
+ "hidden": hidden,
501
+ "deprecated": deprecated,
502
+ "rich_help_panel": rich_help_panel,
503
+ }
504
+ if options_metavar is not None:
505
+ kwargs["options_metavar"] = options_metavar
506
+
507
+ return super(AsyncTyper, self).callback(**kwargs)(wrapped_func)
508
+
509
+ return decorator
v8x/apps/__init__.py ADDED
@@ -0,0 +1,15 @@
1
+ # Copyright 2025 Vantage Compute Corporation
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Built-in deployment applications."""
@@ -0,0 +1,12 @@
1
+ # Copyright (C) 2025 Vantage Compute Corporation
2
+ # This program is free software: you can redistribute it and/or modify it under
3
+ # the terms of the GNU General Public License as published by the Free Software
4
+ # Foundation, version 3.
5
+ #
6
+ # This program is distributed in the hope that it will be useful, but WITHOUT
7
+ # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
8
+ # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
9
+ #
10
+ # You should have received a copy of the GNU General Public License along with
11
+ # this program. If not, see <https://www.gnu.org/licenses/>.
12
+ """LXD deployment apps."""
@@ -0,0 +1,17 @@
1
+ # Copyright (C) 2025 Vantage Compute Corporation
2
+ # This program is free software: you can redistribute it and/or modify it under
3
+ # the terms of the GNU General Public License as published by the Free Software
4
+ # Foundation, version 3.
5
+ #
6
+ # This program is distributed in the hope that it will be useful, but WITHOUT
7
+ # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
8
+ # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
9
+ #
10
+ # You should have received a copy of the GNU General Public License along with
11
+ # this program. If not, see <https://www.gnu.org/licenses/>.
12
+ """Vantage System deployment applications package.
13
+
14
+ This package contains deployment instructions to deploy Vantage System on LXD.
15
+ """
16
+
17
+ __all__ = []