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.
- v8x/__init__.py +509 -0
- v8x/apps/__init__.py +15 -0
- v8x/apps/lxd/__init__.py +12 -0
- v8x/apps/lxd/slurm/__init__.py +17 -0
- v8x/apps/lxd/slurm/app.py +1281 -0
- v8x/apps/lxd/slurm/constants.py +23 -0
- v8x/apps/lxd/slurm/render.py +51 -0
- v8x/apps/lxd/slurm/templates.py +168 -0
- v8x/apps/lxd/slurm/utils.py +12 -0
- v8x/apps/on_prem/__init__.py +12 -0
- v8x/apps/on_prem/slurm_multipass/__init__.py +16 -0
- v8x/apps/on_prem/slurm_multipass/app.py +352 -0
- v8x/apps/on_prem/slurm_multipass/constants.py +304 -0
- v8x/apps/on_prem/slurm_multipass/render.py +134 -0
- v8x/apps/on_prem/slurm_multipass/templates.py +319 -0
- v8x/apps/on_prem/slurm_multipass/utils.py +129 -0
- v8x/auth.py +589 -0
- v8x/cache.py +170 -0
- v8x/client.py +96 -0
- v8x/commands/__init__.py +12 -0
- v8x/commands/alias/__init__.py +34 -0
- v8x/commands/alias/apps.py +25 -0
- v8x/commands/alias/cloud_accounts.py +31 -0
- v8x/commands/alias/clouds.py +31 -0
- v8x/commands/alias/clusters.py +25 -0
- v8x/commands/alias/deployments.py +34 -0
- v8x/commands/alias/federations.py +25 -0
- v8x/commands/alias/networks.py +25 -0
- v8x/commands/alias/profiles.py +26 -0
- v8x/commands/alias/support_tickets.py +25 -0
- v8x/commands/alias/teams.py +26 -0
- v8x/commands/app/__init__.py +53 -0
- v8x/commands/app/deployment/__init__.py +90 -0
- v8x/commands/app/deployment/cleanup.py +225 -0
- v8x/commands/app/deployment/create.py +15 -0
- v8x/commands/app/deployment/delete.py +150 -0
- v8x/commands/app/deployment/get.py +54 -0
- v8x/commands/app/deployment/list.py +59 -0
- v8x/commands/app/deployment/render.py +298 -0
- v8x/commands/app/list.py +58 -0
- v8x/commands/cloud/__init__.py +31 -0
- v8x/commands/cloud/account/__init__.py +37 -0
- v8x/commands/cloud/account/create.py +311 -0
- v8x/commands/cloud/account/delete.py +101 -0
- v8x/commands/cloud/account/get.py +109 -0
- v8x/commands/cloud/account/list.py +92 -0
- v8x/commands/cloud/get.py +61 -0
- v8x/commands/cloud/list.py +57 -0
- v8x/commands/cluster/__init__.py +63 -0
- v8x/commands/cluster/compute_pool/__init__.py +31 -0
- v8x/commands/cluster/compute_pool/_helpers.py +53 -0
- v8x/commands/cluster/compute_pool/create.py +203 -0
- v8x/commands/cluster/compute_pool/delete.py +92 -0
- v8x/commands/cluster/compute_pool/get.py +107 -0
- v8x/commands/cluster/compute_pool/list.py +111 -0
- v8x/commands/cluster/create.py +738 -0
- v8x/commands/cluster/delete.py +301 -0
- v8x/commands/cluster/extend.py +216 -0
- v8x/commands/cluster/federation/__init__.py +35 -0
- v8x/commands/cluster/federation/create.py +72 -0
- v8x/commands/cluster/federation/delete.py +82 -0
- v8x/commands/cluster/federation/get.py +65 -0
- v8x/commands/cluster/federation/list.py +61 -0
- v8x/commands/cluster/federation/update.py +88 -0
- v8x/commands/cluster/get.py +80 -0
- v8x/commands/cluster/inference_endpoint/__init__.py +29 -0
- v8x/commands/cluster/inference_endpoint/_helpers.py +34 -0
- v8x/commands/cluster/inference_endpoint/create.py +174 -0
- v8x/commands/cluster/inference_endpoint/delete.py +61 -0
- v8x/commands/cluster/inference_endpoint/get.py +90 -0
- v8x/commands/cluster/inference_endpoint/list.py +89 -0
- v8x/commands/cluster/inference_endpoint/logs.py +67 -0
- v8x/commands/cluster/inference_endpoint/runtimes.py +80 -0
- v8x/commands/cluster/inference_endpoint/start_stop.py +85 -0
- v8x/commands/cluster/inference_preset/__init__.py +34 -0
- v8x/commands/cluster/inference_preset/_helpers.py +53 -0
- v8x/commands/cluster/inference_preset/create.py +142 -0
- v8x/commands/cluster/inference_preset/delete.py +88 -0
- v8x/commands/cluster/inference_preset/get.py +95 -0
- v8x/commands/cluster/inference_preset/list.py +93 -0
- v8x/commands/cluster/kubeflow/__init__.py +39 -0
- v8x/commands/cluster/kubeflow/_helpers.py +68 -0
- v8x/commands/cluster/kubeflow/create.py +125 -0
- v8x/commands/cluster/kubeflow/delete.py +123 -0
- v8x/commands/cluster/kubeflow/get.py +88 -0
- v8x/commands/cluster/list.py +74 -0
- v8x/commands/cluster/model_registry/__init__.py +39 -0
- v8x/commands/cluster/model_registry/_helpers.py +43 -0
- v8x/commands/cluster/model_registry/create.py +125 -0
- v8x/commands/cluster/model_registry/delete.py +61 -0
- v8x/commands/cluster/model_registry/get.py +78 -0
- v8x/commands/cluster/model_registry/job.py +74 -0
- v8x/commands/cluster/model_registry/list.py +92 -0
- v8x/commands/cluster/model_registry/search.py +84 -0
- v8x/commands/cluster/model_registry/update.py +74 -0
- v8x/commands/cluster/model_registry/versions.py +83 -0
- v8x/commands/cluster/namespace/__init__.py +31 -0
- v8x/commands/cluster/namespace/_helpers.py +48 -0
- v8x/commands/cluster/namespace/create.py +81 -0
- v8x/commands/cluster/namespace/delete.py +71 -0
- v8x/commands/cluster/namespace/get.py +89 -0
- v8x/commands/cluster/namespace/list.py +82 -0
- v8x/commands/cluster/network/__init__.py +36 -0
- v8x/commands/cluster/network/_helpers.py +267 -0
- v8x/commands/cluster/network/create.py +191 -0
- v8x/commands/cluster/network/delete.py +71 -0
- v8x/commands/cluster/network/get.py +80 -0
- v8x/commands/cluster/network/list.py +74 -0
- v8x/commands/cluster/network/update.py +134 -0
- v8x/commands/cluster/node_group/_helpers.py +56 -0
- v8x/commands/cluster/node_group/create.py +202 -0
- v8x/commands/cluster/render.py +214 -0
- v8x/commands/cluster/secret/__init__.py +24 -0
- v8x/commands/cluster/secret/_helpers.py +56 -0
- v8x/commands/cluster/secret/create.py +116 -0
- v8x/commands/cluster/secret/delete.py +62 -0
- v8x/commands/cluster/secret/get.py +86 -0
- v8x/commands/cluster/secret/list.py +92 -0
- v8x/commands/cluster/secret/test.py +68 -0
- v8x/commands/cluster/service/__init__.py +41 -0
- v8x/commands/cluster/service/create.py +171 -0
- v8x/commands/cluster/service/delete.py +114 -0
- v8x/commands/cluster/service/disable.py +403 -0
- v8x/commands/cluster/service/enable.py +155 -0
- v8x/commands/cluster/service/get.py +111 -0
- v8x/commands/cluster/service/list.py +157 -0
- v8x/commands/cluster/service/update.py +124 -0
- v8x/commands/cluster/slurm/__init__.py +45 -0
- v8x/commands/cluster/slurm/_helpers.py +68 -0
- v8x/commands/cluster/slurm/create.py +254 -0
- v8x/commands/cluster/slurm/delete.py +146 -0
- v8x/commands/cluster/slurm/deploy.py +201 -0
- v8x/commands/cluster/slurm/get.py +101 -0
- v8x/commands/cluster/slurm/list.py +97 -0
- v8x/commands/cluster/slurm/update.py +123 -0
- v8x/commands/cluster/update.py +335 -0
- v8x/commands/cluster/user_service/_crud.py +325 -0
- v8x/commands/cluster/user_service/_helpers.py +66 -0
- v8x/commands/cluster/utils.py +137 -0
- v8x/commands/cluster/workspace_preset/__init__.py +31 -0
- v8x/commands/cluster/workspace_preset/_helpers.py +53 -0
- v8x/commands/cluster/workspace_preset/create.py +152 -0
- v8x/commands/cluster/workspace_preset/delete.py +88 -0
- v8x/commands/cluster/workspace_preset/get.py +114 -0
- v8x/commands/cluster/workspace_preset/list.py +91 -0
- v8x/commands/config/__init__.py +27 -0
- v8x/commands/config/clear.py +85 -0
- v8x/commands/federation/__init__.py +40 -0
- v8x/commands/federation/create.py +58 -0
- v8x/commands/federation/delete.py +60 -0
- v8x/commands/federation/get.py +54 -0
- v8x/commands/federation/list.py +50 -0
- v8x/commands/federation/update.py +64 -0
- v8x/commands/get_kubeconfig.py +113 -0
- v8x/commands/job/__init__.py +39 -0
- v8x/commands/job/client.py +28 -0
- v8x/commands/job/script/__init__.py +35 -0
- v8x/commands/job/script/create.py +50 -0
- v8x/commands/job/script/delete.py +61 -0
- v8x/commands/job/script/get.py +44 -0
- v8x/commands/job/script/list.py +63 -0
- v8x/commands/job/script/update.py +79 -0
- v8x/commands/job/submission/__init__.py +30 -0
- v8x/commands/job/submission/create.py +89 -0
- v8x/commands/job/submission/delete.py +61 -0
- v8x/commands/job/submission/get.py +40 -0
- v8x/commands/job/submission/list.py +73 -0
- v8x/commands/job/submission/update.py +88 -0
- v8x/commands/job/template/__init__.py +28 -0
- v8x/commands/job/template/create.py +70 -0
- v8x/commands/job/template/delete.py +63 -0
- v8x/commands/job/template/get.py +46 -0
- v8x/commands/job/template/list.py +59 -0
- v8x/commands/job/template/update.py +85 -0
- v8x/commands/license/__init__.py +51 -0
- v8x/commands/license/booking/__init__.py +29 -0
- v8x/commands/license/booking/create.py +73 -0
- v8x/commands/license/booking/delete.py +48 -0
- v8x/commands/license/booking/get.py +40 -0
- v8x/commands/license/booking/list.py +60 -0
- v8x/commands/license/booking/main.py +160 -0
- v8x/commands/license/client.py +28 -0
- v8x/commands/license/configuration/__init__.py +35 -0
- v8x/commands/license/configuration/create.py +62 -0
- v8x/commands/license/configuration/delete.py +53 -0
- v8x/commands/license/configuration/get.py +40 -0
- v8x/commands/license/configuration/list.py +56 -0
- v8x/commands/license/configuration/update.py +65 -0
- v8x/commands/license/deployment/__init__.py +35 -0
- v8x/commands/license/deployment/create.py +63 -0
- v8x/commands/license/deployment/delete.py +51 -0
- v8x/commands/license/deployment/get.py +52 -0
- v8x/commands/license/deployment/list.py +79 -0
- v8x/commands/license/deployment/update.py +74 -0
- v8x/commands/license/feature/__init__.py +37 -0
- v8x/commands/license/feature/create.py +64 -0
- v8x/commands/license/feature/delete.py +54 -0
- v8x/commands/license/feature/get.py +40 -0
- v8x/commands/license/feature/list.py +53 -0
- v8x/commands/license/feature/update.py +68 -0
- v8x/commands/license/product/__init__.py +35 -0
- v8x/commands/license/product/create.py +61 -0
- v8x/commands/license/product/delete.py +54 -0
- v8x/commands/license/product/get.py +40 -0
- v8x/commands/license/product/list.py +53 -0
- v8x/commands/license/product/update.py +64 -0
- v8x/commands/license/server/__init__.py +35 -0
- v8x/commands/license/server/create.py +60 -0
- v8x/commands/license/server/delete.py +54 -0
- v8x/commands/license/server/get.py +49 -0
- v8x/commands/license/server/list.py +53 -0
- v8x/commands/license/server/update.py +64 -0
- v8x/commands/network/__init__.py +39 -0
- v8x/commands/network/attach.py +81 -0
- v8x/commands/network/create.py +97 -0
- v8x/commands/network/delete.py +42 -0
- v8x/commands/network/detach.py +85 -0
- v8x/commands/network/get.py +92 -0
- v8x/commands/network/list.py +85 -0
- v8x/commands/network/update.py +60 -0
- v8x/commands/profile/__init__.py +31 -0
- v8x/commands/profile/crud.py +240 -0
- v8x/commands/profile/render.py +65 -0
- v8x/commands/storage/__init__.py +57 -0
- v8x/commands/storage/_helpers.py +80 -0
- v8x/commands/storage/create.py +109 -0
- v8x/commands/storage/delete.py +97 -0
- v8x/commands/storage/external_expose/__init__.py +27 -0
- v8x/commands/storage/external_expose/cephfs.py +448 -0
- v8x/commands/storage/external_expose/nfs.py +216 -0
- v8x/commands/storage/get.py +89 -0
- v8x/commands/storage/list.py +95 -0
- v8x/commands/storage/list_available.py +78 -0
- v8x/commands/storage/namespace_import/__init__.py +663 -0
- v8x/commands/storage/namespace_import/cephfs.py +236 -0
- v8x/commands/storage/namespace_import/internal.py +318 -0
- v8x/commands/storage/namespace_import/nfs.py +221 -0
- v8x/commands/storage/system.py +376 -0
- v8x/commands/support_ticket/__init__.py +30 -0
- v8x/commands/support_ticket/create.py +88 -0
- v8x/commands/support_ticket/delete.py +77 -0
- v8x/commands/support_ticket/get.py +77 -0
- v8x/commands/support_ticket/list.py +108 -0
- v8x/commands/support_ticket/update.py +107 -0
- v8x/commands/team/__init__.py +40 -0
- v8x/commands/team/add_member.py +48 -0
- v8x/commands/team/add_resource.py +49 -0
- v8x/commands/team/create.py +86 -0
- v8x/commands/team/delete.py +34 -0
- v8x/commands/team/get.py +72 -0
- v8x/commands/team/list.py +72 -0
- v8x/commands/team/list_members.py +42 -0
- v8x/commands/team/remove_member.py +34 -0
- v8x/commands/team/set_role.py +39 -0
- v8x/commands/team/set_roles.py +55 -0
- v8x/commands/team/update.py +46 -0
- v8x/commands/vdeployer_web/__init__.py +30 -0
- v8x/commands/vdeployer_web/deploy.py +424 -0
- v8x/commands/vdeployer_web/destroy.py +160 -0
- v8x/commands/vdeployer_web/status.py +107 -0
- v8x/config.py +275 -0
- v8x/constants.py +70 -0
- v8x/deployment_apps/__init__.py +45 -0
- v8x/deployment_apps/common.py +164 -0
- v8x/deployment_apps/constants.py +21 -0
- v8x/deployment_apps/crud.py +279 -0
- v8x/deployment_apps/schema.py +34 -0
- v8x/deployments/__init__.py +21 -0
- v8x/deployments/crud.py +408 -0
- v8x/deployments/schema.py +170 -0
- v8x/exceptions.py +139 -0
- v8x/gql_client.py +710 -0
- v8x/libjuju/__init__.py +1097 -0
- v8x/main.py +499 -0
- v8x/profiles/__init__.py +16 -0
- v8x/profiles/crud.py +303 -0
- v8x/profiles/schema.py +108 -0
- v8x/render.py +2409 -0
- v8x/schemas.py +102 -0
- v8x/time_loop.py +134 -0
- v8x/utils.py +22 -0
- v8x/vantage_rest_api_client.py +343 -0
- v8x-0.1.2.dist-info/METADATA +127 -0
- v8x-0.1.2.dist-info/RECORD +287 -0
- v8x-0.1.2.dist-info/WHEEL +4 -0
- v8x-0.1.2.dist-info/entry_points.txt +2 -0
- 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."""
|
v8x/apps/lxd/__init__.py
ADDED
|
@@ -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__ = []
|