truefoundry 0.2.10__py3-none-any.whl → 0.3.0__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.
Potentially problematic release.
This version of truefoundry might be problematic. Click here for more details.
- truefoundry/__init__.py +1 -0
- truefoundry/autodeploy/cli.py +31 -18
- truefoundry/deploy/__init__.py +112 -1
- truefoundry/deploy/auto_gen/models.py +1714 -0
- truefoundry/deploy/builder/__init__.py +134 -0
- truefoundry/deploy/builder/builders/__init__.py +22 -0
- truefoundry/deploy/builder/builders/dockerfile.py +57 -0
- truefoundry/deploy/builder/builders/tfy_notebook_buildpack/__init__.py +46 -0
- truefoundry/deploy/builder/builders/tfy_notebook_buildpack/dockerfile_template.py +66 -0
- truefoundry/deploy/builder/builders/tfy_python_buildpack/__init__.py +44 -0
- truefoundry/deploy/builder/builders/tfy_python_buildpack/dockerfile_template.py +158 -0
- truefoundry/deploy/builder/docker_service.py +168 -0
- truefoundry/deploy/cli/cli.py +21 -26
- truefoundry/deploy/cli/commands/__init__.py +18 -0
- truefoundry/deploy/cli/commands/apply_command.py +52 -0
- truefoundry/deploy/cli/commands/build_command.py +45 -0
- truefoundry/deploy/cli/commands/build_logs_command.py +89 -0
- truefoundry/deploy/cli/commands/create_command.py +75 -0
- truefoundry/deploy/cli/commands/delete_command.py +77 -0
- truefoundry/deploy/cli/commands/deploy_command.py +102 -0
- truefoundry/deploy/cli/commands/get_command.py +216 -0
- truefoundry/deploy/cli/commands/list_command.py +171 -0
- truefoundry/deploy/cli/commands/login_command.py +33 -0
- truefoundry/deploy/cli/commands/logout_command.py +20 -0
- truefoundry/deploy/cli/commands/logs_command.py +134 -0
- truefoundry/deploy/cli/commands/patch_application_command.py +81 -0
- truefoundry/deploy/cli/commands/patch_command.py +70 -0
- truefoundry/deploy/cli/commands/redeploy_command.py +41 -0
- truefoundry/deploy/cli/commands/terminate_comand.py +44 -0
- truefoundry/deploy/cli/commands/trigger_command.py +145 -0
- truefoundry/deploy/cli/config.py +10 -0
- truefoundry/deploy/cli/console.py +5 -0
- truefoundry/deploy/cli/const.py +12 -0
- truefoundry/deploy/cli/display_util.py +118 -0
- truefoundry/deploy/cli/util.py +129 -0
- truefoundry/deploy/core/__init__.py +7 -0
- truefoundry/deploy/core/login.py +9 -0
- truefoundry/deploy/core/logout.py +5 -0
- truefoundry/deploy/function_service/__init__.py +3 -0
- truefoundry/deploy/function_service/__main__.py +27 -0
- truefoundry/deploy/function_service/app.py +92 -0
- truefoundry/deploy/function_service/build.py +45 -0
- truefoundry/deploy/function_service/remote/__init__.py +6 -0
- truefoundry/deploy/function_service/remote/context.py +3 -0
- truefoundry/deploy/function_service/remote/method.py +67 -0
- truefoundry/deploy/function_service/remote/remote.py +144 -0
- truefoundry/deploy/function_service/route.py +137 -0
- truefoundry/deploy/function_service/service.py +113 -0
- truefoundry/deploy/function_service/utils.py +53 -0
- truefoundry/deploy/io/__init__.py +0 -0
- truefoundry/deploy/io/output_callback.py +23 -0
- truefoundry/deploy/io/rich_output_callback.py +27 -0
- truefoundry/deploy/json_util.py +7 -0
- truefoundry/deploy/lib/__init__.py +0 -0
- truefoundry/deploy/lib/auth/auth_service_client.py +181 -0
- truefoundry/deploy/lib/auth/credential_file_manager.py +115 -0
- truefoundry/deploy/lib/auth/credential_provider.py +131 -0
- truefoundry/deploy/lib/auth/servicefoundry_session.py +59 -0
- truefoundry/deploy/lib/clients/__init__.py +0 -0
- truefoundry/deploy/lib/clients/servicefoundry_client.py +746 -0
- truefoundry/deploy/lib/clients/shell_client.py +13 -0
- truefoundry/deploy/lib/clients/utils.py +41 -0
- truefoundry/deploy/lib/const.py +43 -0
- truefoundry/deploy/lib/dao/__init__.py +0 -0
- truefoundry/deploy/lib/dao/application.py +263 -0
- truefoundry/deploy/lib/dao/apply.py +80 -0
- truefoundry/deploy/lib/dao/version.py +33 -0
- truefoundry/deploy/lib/dao/workspace.py +71 -0
- truefoundry/deploy/lib/exceptions.py +26 -0
- truefoundry/deploy/lib/logs_utils.py +43 -0
- truefoundry/deploy/lib/messages.py +12 -0
- truefoundry/deploy/lib/model/__init__.py +0 -0
- truefoundry/deploy/lib/model/entity.py +400 -0
- truefoundry/deploy/lib/session.py +158 -0
- truefoundry/deploy/lib/util.py +90 -0
- truefoundry/deploy/lib/win32.py +129 -0
- truefoundry/deploy/v2/__init__.py +0 -0
- truefoundry/deploy/v2/lib/__init__.py +3 -0
- truefoundry/deploy/v2/lib/deploy.py +283 -0
- truefoundry/deploy/v2/lib/deploy_workflow.py +295 -0
- truefoundry/deploy/v2/lib/deployable_patched_models.py +86 -0
- truefoundry/deploy/v2/lib/models.py +53 -0
- truefoundry/deploy/v2/lib/patched_models.py +479 -0
- truefoundry/deploy/v2/lib/source.py +267 -0
- truefoundry/langchain/__init__.py +12 -1
- truefoundry/langchain/deprecated.py +302 -0
- truefoundry/langchain/truefoundry_chat.py +130 -0
- truefoundry/langchain/truefoundry_embeddings.py +171 -0
- truefoundry/langchain/truefoundry_llm.py +106 -0
- truefoundry/langchain/utils.py +85 -0
- truefoundry/logger.py +17 -0
- truefoundry/pydantic_v1.py +5 -0
- truefoundry/python_deploy_codegen.py +132 -0
- truefoundry/version.py +6 -0
- truefoundry/workflow/__init__.py +19 -0
- truefoundry/workflow/container_task.py +12 -0
- truefoundry/workflow/example/deploy.sh +1 -0
- truefoundry/workflow/example/hello_world_package/workflow.py +20 -0
- truefoundry/workflow/example/package/test_workflow.py +152 -0
- truefoundry/workflow/example/truefoundry.yaml +9 -0
- truefoundry/workflow/example/workflow.yaml +116 -0
- truefoundry/workflow/map_task.py +45 -0
- truefoundry/workflow/python_task.py +32 -0
- truefoundry/workflow/task.py +50 -0
- truefoundry/workflow/workflow.py +114 -0
- {truefoundry-0.2.10.dist-info → truefoundry-0.3.0.dist-info}/METADATA +27 -7
- truefoundry-0.3.0.dist-info/RECORD +136 -0
- truefoundry/deploy/cli/deploy.py +0 -165
- truefoundry/deploy/cli/version.py +0 -6
- truefoundry-0.2.10.dist-info/RECORD +0 -38
- {truefoundry-0.2.10.dist-info → truefoundry-0.3.0.dist-info}/WHEEL +0 -0
- {truefoundry-0.2.10.dist-info → truefoundry-0.3.0.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
|
|
3
|
+
from truefoundry.logger import logger
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Shell:
|
|
7
|
+
def execute_shell_command(self, command, ip=None):
|
|
8
|
+
logger.debug(f"executing command: {command}")
|
|
9
|
+
try:
|
|
10
|
+
p = subprocess.run(command, stdout=subprocess.PIPE, check=True, input=ip)
|
|
11
|
+
return p.stdout.decode("UTF-8")
|
|
12
|
+
except subprocess.CalledProcessError as cpe:
|
|
13
|
+
raise Exception(f"failed to execute: {command}, error: {cpe}") from cpe
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import time
|
|
3
|
+
from typing import Any, Callable, Generator, Optional
|
|
4
|
+
|
|
5
|
+
from truefoundry.deploy.lib.const import HOST_ENV_NAME
|
|
6
|
+
from truefoundry.deploy.lib.exceptions import BadRequestException
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def request_handling(res):
|
|
10
|
+
try:
|
|
11
|
+
status_code = res.status_code
|
|
12
|
+
except Exception as ex:
|
|
13
|
+
raise Exception("Unknown error occurred. Couldn't get status code.") from ex
|
|
14
|
+
if 200 <= status_code <= 299:
|
|
15
|
+
if res.content == b"":
|
|
16
|
+
return None
|
|
17
|
+
return res.json()
|
|
18
|
+
if 400 <= status_code <= 499:
|
|
19
|
+
try:
|
|
20
|
+
message = str(res.json()["message"])
|
|
21
|
+
except Exception:
|
|
22
|
+
message = res
|
|
23
|
+
raise BadRequestException(res.status_code, message)
|
|
24
|
+
if 500 <= status_code <= 599:
|
|
25
|
+
raise Exception(res.content)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def poll_for_function(
|
|
29
|
+
func: Callable, poll_after_secs: int = 5, *args, **kwargs
|
|
30
|
+
) -> Generator[Any, None, None]:
|
|
31
|
+
while True:
|
|
32
|
+
yield func(*args, **kwargs)
|
|
33
|
+
time.sleep(poll_after_secs)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def resolve_base_url(host: Optional[str] = None) -> str:
|
|
37
|
+
if not host and not os.getenv(HOST_ENV_NAME):
|
|
38
|
+
raise ValueError(
|
|
39
|
+
f"Either `host` should be provided by --host <value>, or `{HOST_ENV_NAME}` env must be set"
|
|
40
|
+
)
|
|
41
|
+
return host or os.getenv(HOST_ENV_NAME)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from truefoundry.deploy.io.rich_output_callback import RichOutputCallBack
|
|
4
|
+
|
|
5
|
+
DEFAULT_BASE_URL = "https://app.truefoundry.com"
|
|
6
|
+
API_SERVER_RELATIVE_PATH = "api/svc"
|
|
7
|
+
DEFAULT_API_SERVER = f"{DEFAULT_BASE_URL.rstrip('/')}/{API_SERVER_RELATIVE_PATH}"
|
|
8
|
+
DEFAULT_AUTH_UI = DEFAULT_BASE_URL
|
|
9
|
+
DEFAULT_AUTH_SERVER = (
|
|
10
|
+
"https://auth-server.tfy-ctl-euwe1-production.production.truefoundry.com"
|
|
11
|
+
)
|
|
12
|
+
DEFAULT_TENANT_NAME = "truefoundry"
|
|
13
|
+
DEFAULT_PROFILE_NAME = "default"
|
|
14
|
+
HOST_ENV_NAME = "TFY_HOST"
|
|
15
|
+
API_KEY_ENV_NAME = "TFY_API_KEY"
|
|
16
|
+
|
|
17
|
+
_SFY_CONFIG_DIR = Path.home() / ".truefoundry"
|
|
18
|
+
SFY_CONFIG_DIR = str(_SFY_CONFIG_DIR) # as a directory
|
|
19
|
+
CREDENTIAL_FILEPATH = _SFY_CONFIG_DIR / "credentials.json"
|
|
20
|
+
|
|
21
|
+
OLD_SFY_PROFILES_FILEPATH = _SFY_CONFIG_DIR / "profiles.json" # as a directory
|
|
22
|
+
OLD_SFY_SESSIONS_FILEPATH = _SFY_CONFIG_DIR / "sessions.json" # as a directory
|
|
23
|
+
OLD_SESSION_FILEPATH = str(
|
|
24
|
+
Path.home() / ".truefoundry"
|
|
25
|
+
) # as a filepath, to be removed in future versions
|
|
26
|
+
|
|
27
|
+
# Polling during login redirect
|
|
28
|
+
MAX_POLLING_RETRY = 100
|
|
29
|
+
POLLING_SLEEP_TIME_IN_SEC = 4
|
|
30
|
+
|
|
31
|
+
# Refresh access token cutoff
|
|
32
|
+
REFRESH_ACCESS_TOKEN_IN_SEC = 10 * 60
|
|
33
|
+
|
|
34
|
+
ENTITY_JSON_DATETIME_FORMAT = "%Y-%m-%dT%H:%M:%S.%fZ"
|
|
35
|
+
RICH_OUTPUT_CALLBACK = RichOutputCallBack()
|
|
36
|
+
|
|
37
|
+
VERSION_PREFIX = "v1"
|
|
38
|
+
|
|
39
|
+
SFY_DEBUG_ENV_KEY = "SFY_DEBUG"
|
|
40
|
+
SFY_INTERNAL_ENV_KEY = "SFY_INTERNAL"
|
|
41
|
+
|
|
42
|
+
TFY_DEBUG_ENV_KEY = "TFY_DEBUG"
|
|
43
|
+
TFY_INTERNAL_ENV_KEY = "TFY_INTERNAL"
|
|
File without changes
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import shlex
|
|
2
|
+
from typing import Any, Dict, Optional, Sequence, Union
|
|
3
|
+
|
|
4
|
+
import truefoundry.deploy.lib.dao.version as version_lib
|
|
5
|
+
from truefoundry.deploy.lib.clients.servicefoundry_client import (
|
|
6
|
+
ServiceFoundryServiceClient,
|
|
7
|
+
)
|
|
8
|
+
from truefoundry.deploy.lib.model.entity import (
|
|
9
|
+
Application,
|
|
10
|
+
Deployment,
|
|
11
|
+
TriggerJobResult,
|
|
12
|
+
)
|
|
13
|
+
from truefoundry.deploy.lib.util import (
|
|
14
|
+
find_list_paths,
|
|
15
|
+
get_application_fqn_from_deployment_fqn,
|
|
16
|
+
)
|
|
17
|
+
from truefoundry.logger import logger
|
|
18
|
+
from truefoundry.pydantic_v1 import utils as pydantic_v1_utils
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def list_applications(
|
|
22
|
+
application_type: str,
|
|
23
|
+
workspace_fqn: Optional[str] = None,
|
|
24
|
+
client: Optional[ServiceFoundryServiceClient] = None,
|
|
25
|
+
):
|
|
26
|
+
client = client or ServiceFoundryServiceClient()
|
|
27
|
+
if workspace_fqn:
|
|
28
|
+
workspace = client.get_id_from_fqn(fqn=workspace_fqn, fqn_type="workspace")
|
|
29
|
+
applications = client.list_applications(workspace_id=workspace["workspaceId"])
|
|
30
|
+
else:
|
|
31
|
+
applications = client.list_applications()
|
|
32
|
+
|
|
33
|
+
if application_type != "all":
|
|
34
|
+
applications = [
|
|
35
|
+
application
|
|
36
|
+
for application in applications
|
|
37
|
+
if application.deployment.manifest.type == application_type
|
|
38
|
+
]
|
|
39
|
+
return applications
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def get_application(
|
|
43
|
+
application_fqn: str,
|
|
44
|
+
client: Optional[ServiceFoundryServiceClient] = None,
|
|
45
|
+
):
|
|
46
|
+
client = client or ServiceFoundryServiceClient()
|
|
47
|
+
application = client.get_id_from_fqn(fqn=application_fqn, fqn_type="app")
|
|
48
|
+
application = client.get_application_info(
|
|
49
|
+
application_id=application["applicationId"]
|
|
50
|
+
)
|
|
51
|
+
return application
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def delete_application(
|
|
55
|
+
application_fqn: str,
|
|
56
|
+
client: Optional[ServiceFoundryServiceClient] = None,
|
|
57
|
+
) -> Dict[str, Any]:
|
|
58
|
+
client = client or ServiceFoundryServiceClient()
|
|
59
|
+
application = client.get_id_from_fqn(fqn=application_fqn, fqn_type="app")
|
|
60
|
+
response = client.remove_application(application_id=application["applicationId"])
|
|
61
|
+
return response
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def redeploy_application(
|
|
65
|
+
application_fqn: str,
|
|
66
|
+
version: int,
|
|
67
|
+
wait: bool,
|
|
68
|
+
client: Optional[ServiceFoundryServiceClient] = None,
|
|
69
|
+
) -> Deployment:
|
|
70
|
+
from truefoundry.deploy.v2.lib.deployable_patched_models import Application
|
|
71
|
+
|
|
72
|
+
client = client or ServiceFoundryServiceClient()
|
|
73
|
+
|
|
74
|
+
deployment_info = version_lib.get_version(
|
|
75
|
+
application_fqn=application_fqn, version=version
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
manifest = deployment_info.manifest.dict()
|
|
79
|
+
|
|
80
|
+
application_id = deployment_info.applicationId
|
|
81
|
+
application_info = client.get_application_info(application_id=application_id)
|
|
82
|
+
workspace_fqn = application_info.workspace.fqn
|
|
83
|
+
|
|
84
|
+
application = Application.parse_obj(manifest)
|
|
85
|
+
deployment = application.deploy(workspace_fqn=workspace_fqn, wait=wait)
|
|
86
|
+
return deployment
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def list_job_runs(
|
|
90
|
+
application_fqn: str,
|
|
91
|
+
max_results: Optional[int] = None,
|
|
92
|
+
offset: Optional[int] = None,
|
|
93
|
+
client: Optional[ServiceFoundryServiceClient] = None,
|
|
94
|
+
):
|
|
95
|
+
client = client or ServiceFoundryServiceClient()
|
|
96
|
+
application = client.get_id_from_fqn(fqn=application_fqn, fqn_type="app")
|
|
97
|
+
response = client.list_job_runs(
|
|
98
|
+
application_id=application["applicationId"], limit=max_results, offset=offset
|
|
99
|
+
)
|
|
100
|
+
return response
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def get_job_run(
|
|
104
|
+
application_fqn: str,
|
|
105
|
+
job_run_name: str,
|
|
106
|
+
client: Optional[ServiceFoundryServiceClient] = None,
|
|
107
|
+
):
|
|
108
|
+
client = client or ServiceFoundryServiceClient()
|
|
109
|
+
application = client.get_id_from_fqn(fqn=application_fqn, fqn_type="app")
|
|
110
|
+
response = client.get_job_run(
|
|
111
|
+
application_id=application["applicationId"], job_run_name=job_run_name
|
|
112
|
+
)
|
|
113
|
+
return response
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def trigger_job(
|
|
117
|
+
application_fqn: str,
|
|
118
|
+
command: Optional[Union[str, Sequence[str]]] = None,
|
|
119
|
+
params: Optional[Dict[str, str]] = None,
|
|
120
|
+
) -> TriggerJobResult:
|
|
121
|
+
"""
|
|
122
|
+
Trigger a Job on TrueFoundry platform
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
application_fqn: Fully Qualified Name of the Deployed Job (without the version number)
|
|
126
|
+
command: command to run the job with, defaults to `None`. Can be a `str` or `List[str]`
|
|
127
|
+
When `None`, the job is triggered with configured command at the time of deployment.
|
|
128
|
+
When passed as a list, the command will be joined using `shlex.join`
|
|
129
|
+
params: A dict mapping from parameter names (as defined in the job spec) to string values
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
TriggerJobResult: metadata returning status of job trigger
|
|
133
|
+
"""
|
|
134
|
+
if params and command:
|
|
135
|
+
raise ValueError(
|
|
136
|
+
"`command` and `params` arguments are mutually exclusive. Please pass only one of them"
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
try:
|
|
140
|
+
# If user is passing in deployment fqn copied from UI, till we change the fqns on UI
|
|
141
|
+
application_fqn = get_application_fqn_from_deployment_fqn(application_fqn)
|
|
142
|
+
logger.warning(
|
|
143
|
+
"Detected version number in `application_fqn`. "
|
|
144
|
+
"Automatically discarding the version number. "
|
|
145
|
+
f"This automatic conversion will be removed in future. "
|
|
146
|
+
f"Please pass {application_fqn!r} as the value."
|
|
147
|
+
)
|
|
148
|
+
except ValueError:
|
|
149
|
+
pass
|
|
150
|
+
client = ServiceFoundryServiceClient()
|
|
151
|
+
_application_info = client.get_application_info_by_fqn(
|
|
152
|
+
application_fqn=application_fqn
|
|
153
|
+
)
|
|
154
|
+
application_info = client.get_application_info(
|
|
155
|
+
application_id=_application_info.applicationId
|
|
156
|
+
)
|
|
157
|
+
command_str = ""
|
|
158
|
+
message = ""
|
|
159
|
+
if not params and not command:
|
|
160
|
+
message = "Job triggered with pre-configured command"
|
|
161
|
+
elif command:
|
|
162
|
+
if not isinstance(command, str):
|
|
163
|
+
command_str = shlex.join(command).strip()
|
|
164
|
+
else:
|
|
165
|
+
command_str = command.strip()
|
|
166
|
+
message = f"Job triggered with command {command_str!r}"
|
|
167
|
+
elif params:
|
|
168
|
+
# Check if params has any non string values
|
|
169
|
+
for key, value in params.items():
|
|
170
|
+
if not isinstance(value, str):
|
|
171
|
+
raise ValueError(
|
|
172
|
+
f"Invalid value {value!r} for key {key!r}. "
|
|
173
|
+
"Only string values are allowed for `params`"
|
|
174
|
+
)
|
|
175
|
+
command_str = ""
|
|
176
|
+
message = f"Job triggered with params {params!r}"
|
|
177
|
+
result = client.trigger_job(
|
|
178
|
+
deployment_id=application_info.activeDeploymentId,
|
|
179
|
+
command=command_str if command_str else None,
|
|
180
|
+
params=params if params else None,
|
|
181
|
+
)
|
|
182
|
+
previous_runs_url = f"{client.base_url.strip('/')}/deployments/{application_info.id}?tab=previousRuns"
|
|
183
|
+
logger.info(
|
|
184
|
+
f"{message}.\n"
|
|
185
|
+
f"You can check the status of your job run at {previous_runs_url}"
|
|
186
|
+
)
|
|
187
|
+
return result
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def get_patched_application_definition(
|
|
191
|
+
application: Application,
|
|
192
|
+
manifest_patch: Dict[str, Any],
|
|
193
|
+
) -> Dict[str, Any]:
|
|
194
|
+
# TODO: define ApplicationPatch type but for now
|
|
195
|
+
# I am adding a manual check for name since name patch can
|
|
196
|
+
# create a new application
|
|
197
|
+
if (
|
|
198
|
+
manifest_patch.get("name")
|
|
199
|
+
and manifest_patch.get("name") != application.deployment.manifest.name
|
|
200
|
+
):
|
|
201
|
+
raise Exception(
|
|
202
|
+
f"Cannot change name of application from `{application.deployment.manifest.name}` to `{manifest_patch.get('name')}`"
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
patch_list_paths = find_list_paths(manifest_patch)
|
|
206
|
+
|
|
207
|
+
for path in patch_list_paths:
|
|
208
|
+
logger.warn(
|
|
209
|
+
f"You are patching the value at {path}. Note that updating array-type objects will replace the entire object."
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
return pydantic_v1_utils.deep_update(
|
|
213
|
+
application.deployment.manifest.dict(), manifest_patch
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def terminate_job_run(
|
|
218
|
+
application_fqn: str,
|
|
219
|
+
job_run_name: str,
|
|
220
|
+
):
|
|
221
|
+
try:
|
|
222
|
+
application_fqn = get_application_fqn_from_deployment_fqn(application_fqn)
|
|
223
|
+
logger.warning(
|
|
224
|
+
"Detected version number in `application_fqn`. "
|
|
225
|
+
"Automatically discarding the version number. "
|
|
226
|
+
f"This automatic conversion will be removed in future. "
|
|
227
|
+
f"Please pass {application_fqn!r} as the value."
|
|
228
|
+
)
|
|
229
|
+
except ValueError:
|
|
230
|
+
pass
|
|
231
|
+
client = ServiceFoundryServiceClient()
|
|
232
|
+
_application_info = client.get_application_info_by_fqn(
|
|
233
|
+
application_fqn=application_fqn
|
|
234
|
+
)
|
|
235
|
+
application_info = client.get_application_info(
|
|
236
|
+
application_id=_application_info.applicationId
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
response = client.terminate_job_run(
|
|
240
|
+
deployment_id=application_info.activeDeploymentId,
|
|
241
|
+
job_run_name=job_run_name,
|
|
242
|
+
)
|
|
243
|
+
return response
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def trigger_workflow(application_fqn: str, inputs: Optional[Dict[str, Any]] = None):
|
|
247
|
+
inputs = inputs or {}
|
|
248
|
+
client = ServiceFoundryServiceClient()
|
|
249
|
+
_application_info = client.get_application_info_by_fqn(
|
|
250
|
+
application_fqn=application_fqn
|
|
251
|
+
)
|
|
252
|
+
application_info = client.get_application_info(
|
|
253
|
+
application_id=_application_info.applicationId
|
|
254
|
+
)
|
|
255
|
+
client.trigger_workflow(
|
|
256
|
+
application_id=application_info.id,
|
|
257
|
+
inputs=inputs,
|
|
258
|
+
)
|
|
259
|
+
logger.info(f"Started Execution for Workflow: {application_info.name}")
|
|
260
|
+
executions_page = (
|
|
261
|
+
f"{client.base_url.strip('/')}/deployments/{application_info.id}?tab=executions"
|
|
262
|
+
)
|
|
263
|
+
logger.info(f"You can check the executions at {executions_page}")
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Any, Dict, Iterator, Optional
|
|
3
|
+
|
|
4
|
+
import yaml
|
|
5
|
+
|
|
6
|
+
from truefoundry.deploy.lib.clients.servicefoundry_client import (
|
|
7
|
+
ServiceFoundryServiceClient,
|
|
8
|
+
)
|
|
9
|
+
from truefoundry.deploy.lib.model.entity import ApplyResult, Manifest
|
|
10
|
+
from truefoundry.pydantic_v1 import ValidationError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _apply_manifest(
|
|
14
|
+
manifest: Dict[str, Any],
|
|
15
|
+
client: Optional[ServiceFoundryServiceClient] = None,
|
|
16
|
+
filename: Optional[str] = None,
|
|
17
|
+
index: Optional[int] = None,
|
|
18
|
+
) -> ApplyResult:
|
|
19
|
+
client = client or ServiceFoundryServiceClient()
|
|
20
|
+
|
|
21
|
+
file_metadata = ""
|
|
22
|
+
if index is not None:
|
|
23
|
+
file_metadata += f" at index {index}"
|
|
24
|
+
if filename:
|
|
25
|
+
file_metadata += f" from file {filename}"
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
manifest = Manifest.parse_obj(manifest)
|
|
29
|
+
except ValidationError as ex:
|
|
30
|
+
return ApplyResult(
|
|
31
|
+
success=False,
|
|
32
|
+
message=f"Failed to apply manifest{file_metadata}. Error: {ex}",
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
try:
|
|
36
|
+
client.apply(manifest.dict())
|
|
37
|
+
return ApplyResult(
|
|
38
|
+
success=True,
|
|
39
|
+
message=f"Successfully configured manifest {manifest.name} of type {manifest.type}",
|
|
40
|
+
)
|
|
41
|
+
except Exception as ex:
|
|
42
|
+
return ApplyResult(
|
|
43
|
+
success=False,
|
|
44
|
+
message=f"Failed to apply manifest {manifest.name} of type {manifest.type}. Error: {ex}",
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def apply_manifest(
|
|
49
|
+
manifest: Dict[str, Any],
|
|
50
|
+
client: Optional[ServiceFoundryServiceClient] = None,
|
|
51
|
+
) -> ApplyResult:
|
|
52
|
+
return _apply_manifest(manifest=manifest, client=client)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def apply_manifest_file(
|
|
56
|
+
filepath: str,
|
|
57
|
+
client: Optional[ServiceFoundryServiceClient] = None,
|
|
58
|
+
) -> Iterator[ApplyResult]:
|
|
59
|
+
client = client or ServiceFoundryServiceClient()
|
|
60
|
+
filename = Path(filepath).name
|
|
61
|
+
try:
|
|
62
|
+
with open(filepath, "r") as f:
|
|
63
|
+
manifests_it = list(yaml.safe_load_all(f))
|
|
64
|
+
except Exception as ex:
|
|
65
|
+
yield ApplyResult(
|
|
66
|
+
success=False,
|
|
67
|
+
message=f"Failed to read file {filepath} as a valid YAML file. Error: {ex}",
|
|
68
|
+
)
|
|
69
|
+
else:
|
|
70
|
+
for index, manifest in enumerate(manifests_it):
|
|
71
|
+
if not isinstance(manifest, dict):
|
|
72
|
+
yield ApplyResult(
|
|
73
|
+
success=False,
|
|
74
|
+
message=f"Failed to apply manifest at index {index} from file {filename}. Error: A manifest must be a dict, got type {type(manifest)}",
|
|
75
|
+
)
|
|
76
|
+
continue
|
|
77
|
+
|
|
78
|
+
yield _apply_manifest(
|
|
79
|
+
manifest=manifest, client=client, filename=filename, index=index
|
|
80
|
+
)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from typing import List, Optional
|
|
2
|
+
|
|
3
|
+
from truefoundry.deploy.lib.clients.servicefoundry_client import (
|
|
4
|
+
ServiceFoundryServiceClient,
|
|
5
|
+
)
|
|
6
|
+
from truefoundry.deploy.lib.model.entity import Deployment
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def list_versions(
|
|
10
|
+
application_fqn: str,
|
|
11
|
+
client: Optional[ServiceFoundryServiceClient] = None,
|
|
12
|
+
) -> List[Deployment]:
|
|
13
|
+
client = client or ServiceFoundryServiceClient()
|
|
14
|
+
application = client.get_id_from_fqn(fqn=application_fqn, fqn_type="app")
|
|
15
|
+
versions = client.list_versions(application_id=application["applicationId"])
|
|
16
|
+
return versions
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def get_version(
|
|
20
|
+
application_fqn: str,
|
|
21
|
+
version: int,
|
|
22
|
+
client: Optional[ServiceFoundryServiceClient] = None,
|
|
23
|
+
) -> Deployment:
|
|
24
|
+
client = client or ServiceFoundryServiceClient()
|
|
25
|
+
application = client.get_id_from_fqn(fqn=application_fqn, fqn_type="app")
|
|
26
|
+
versions = client.list_versions(
|
|
27
|
+
application_id=application["applicationId"], deployment_version=version
|
|
28
|
+
)
|
|
29
|
+
if len(versions) == 0:
|
|
30
|
+
raise ValueError(
|
|
31
|
+
f"Version {version!r} for Application with FQN {application_fqn!r} does not exist."
|
|
32
|
+
)
|
|
33
|
+
return versions[0]
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
from truefoundry.deploy.cli.console import console
|
|
4
|
+
from truefoundry.deploy.lib.clients.servicefoundry_client import (
|
|
5
|
+
ServiceFoundryServiceClient,
|
|
6
|
+
)
|
|
7
|
+
from truefoundry.deploy.lib.messages import PROMPT_CREATING_NEW_WORKSPACE
|
|
8
|
+
from truefoundry.deploy.lib.model.entity import Workspace, WorkspaceResources
|
|
9
|
+
from truefoundry.logger import logger
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def create_workspace(
|
|
13
|
+
name: str,
|
|
14
|
+
cluster_name: str,
|
|
15
|
+
cpu_limit: Optional[float] = None,
|
|
16
|
+
memory_limit: Optional[int] = None,
|
|
17
|
+
ephemeral_storage_limit: Optional[int] = None,
|
|
18
|
+
client: Optional[ServiceFoundryServiceClient] = None,
|
|
19
|
+
) -> Workspace:
|
|
20
|
+
client = client or ServiceFoundryServiceClient()
|
|
21
|
+
|
|
22
|
+
workspace_resources = WorkspaceResources(
|
|
23
|
+
cpu_limit=cpu_limit,
|
|
24
|
+
memory_limit=memory_limit,
|
|
25
|
+
ephemeral_storage_limit=ephemeral_storage_limit,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
with console.status(PROMPT_CREATING_NEW_WORKSPACE.format(name), spinner="dots"):
|
|
29
|
+
workspace = client.create_workspace(
|
|
30
|
+
workspace_name=name,
|
|
31
|
+
cluster_name=cluster_name,
|
|
32
|
+
resources=workspace_resources,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
url = f"{client.base_url.strip('/')}/workspaces"
|
|
36
|
+
logger.info(
|
|
37
|
+
"You can find your workspace: '%s' on the dashboard: %s", workspace.name, url
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
return workspace
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def list_workspaces(
|
|
44
|
+
cluster_name: Optional[str] = None,
|
|
45
|
+
client: Optional[ServiceFoundryServiceClient] = None,
|
|
46
|
+
):
|
|
47
|
+
client = client or ServiceFoundryServiceClient()
|
|
48
|
+
workspaces = client.list_workspaces(cluster_id=cluster_name)
|
|
49
|
+
return workspaces
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def get_workspace_by_fqn(
|
|
53
|
+
workspace_fqn: str,
|
|
54
|
+
client: Optional[ServiceFoundryServiceClient] = None,
|
|
55
|
+
) -> Workspace:
|
|
56
|
+
client = client or ServiceFoundryServiceClient()
|
|
57
|
+
workspaces = client.get_workspace_by_fqn(workspace_fqn=workspace_fqn)
|
|
58
|
+
if len(workspaces) == 0:
|
|
59
|
+
raise ValueError(f"Workspace with FQN {workspace_fqn!r} does not exist.")
|
|
60
|
+
workspace = workspaces[0]
|
|
61
|
+
return workspace
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def delete_workspace(
|
|
65
|
+
workspace_fqn: str,
|
|
66
|
+
client: Optional[ServiceFoundryServiceClient] = None,
|
|
67
|
+
) -> Workspace:
|
|
68
|
+
client = client or ServiceFoundryServiceClient()
|
|
69
|
+
workspace = client.get_id_from_fqn(fqn=workspace_fqn, fqn_type="workspace")
|
|
70
|
+
deleted_workspace = client.remove_workspace(workspace_id=workspace["workspaceId"])
|
|
71
|
+
return deleted_workspace
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class BadRequestException(Exception):
|
|
5
|
+
def __init__(self, status_code: int, message: Optional[str] = None):
|
|
6
|
+
super().__init__()
|
|
7
|
+
self.status_code = status_code
|
|
8
|
+
self.message = message
|
|
9
|
+
|
|
10
|
+
def __str__(self):
|
|
11
|
+
return self.message
|
|
12
|
+
|
|
13
|
+
def __repr__(self):
|
|
14
|
+
return self.message
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ConfigurationException(Exception):
|
|
18
|
+
def __init__(self, message: Optional[str] = None):
|
|
19
|
+
super().__init__()
|
|
20
|
+
self.message = message
|
|
21
|
+
|
|
22
|
+
def __str__(self):
|
|
23
|
+
return self.message
|
|
24
|
+
|
|
25
|
+
def __repr__(self):
|
|
26
|
+
return self.message
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from datetime import datetime, timedelta, timezone
|
|
3
|
+
|
|
4
|
+
from dateutil import parser
|
|
5
|
+
from dateutil.tz import tzlocal
|
|
6
|
+
|
|
7
|
+
from truefoundry.logger import logger
|
|
8
|
+
|
|
9
|
+
time_duration_regex = re.compile("^[0-9]+[smhd]$")
|
|
10
|
+
|
|
11
|
+
granularity_timedelta = {
|
|
12
|
+
"s": timedelta(seconds=1),
|
|
13
|
+
"m": timedelta(minutes=1),
|
|
14
|
+
"h": timedelta(hours=1),
|
|
15
|
+
"d": timedelta(days=1),
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def get_timestamp_from_timestamp_or_duration(time_or_duration: str):
|
|
20
|
+
"""
|
|
21
|
+
Returns timestamp in milliseconds
|
|
22
|
+
"""
|
|
23
|
+
if time_duration_regex.match(time_or_duration):
|
|
24
|
+
number = int(time_or_duration[:-1])
|
|
25
|
+
granularity = time_or_duration[-1]
|
|
26
|
+
current_time = datetime.now(timezone.utc)
|
|
27
|
+
required_time = current_time - granularity_timedelta[granularity] * number
|
|
28
|
+
return required_time.timestamp() * 1000
|
|
29
|
+
else:
|
|
30
|
+
logger.debug(
|
|
31
|
+
f"Cannot parse: {time_or_duration} as duration, regex match failed"
|
|
32
|
+
)
|
|
33
|
+
try:
|
|
34
|
+
date_time_obj = parser.parse(time_or_duration)
|
|
35
|
+
except ValueError as ve:
|
|
36
|
+
logger.debug(f"Value not a valid timestamp or duration: {time_or_duration}")
|
|
37
|
+
raise ValueError(
|
|
38
|
+
f"Value not a valid timestamp or duration: {time_or_duration}"
|
|
39
|
+
) from ve
|
|
40
|
+
if not date_time_obj.tzinfo:
|
|
41
|
+
date_time_obj = date_time_obj.replace(tzinfo=tzlocal())
|
|
42
|
+
date_time_obj = date_time_obj.astimezone(timezone.utc)
|
|
43
|
+
return date_time_obj.timestamp() * 1000
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
TFY = "TFY"
|
|
2
|
+
|
|
3
|
+
# TODO: probably create another `rich_messages.py` and apply all formatting there
|
|
4
|
+
PROMPT_LOGOUT_SUCCESSFUL = """[green bold]Logged Out![/]"""
|
|
5
|
+
PROMPT_ALREADY_LOGGED_OUT = """[yellow]You are already logged out[/]"""
|
|
6
|
+
PROMPT_CREATING_NEW_WORKSPACE = """[yellow]Creating a new workspace {!r}[/]"""
|
|
7
|
+
PROMPT_DELETED_WORKSPACE = """[green]Deleted workspace {!r}[/]"""
|
|
8
|
+
PROMPT_DELETED_APPLICATION = """[green]Deleted Application {!r}[/]"""
|
|
9
|
+
PROMPT_NO_WORKSPACES = f"""[yellow]No workspaces found. Either cluster name is wrong, or your cluster doesn't contain any workspaces. You can create one with [bold]{TFY} create workspace[/][/]"""
|
|
10
|
+
PROMPT_NO_APPLICATIONS = f"""[yellow]No applications found. You can create one with [bold]{TFY} deploy[/] from within your application folder"""
|
|
11
|
+
PROMPT_NO_VERSIONS = """[yellow]No application versions found."""
|
|
12
|
+
PROMPT_APPLYING_MANIFEST = """[yellow]Applying manifest for file {!r}[/]"""
|
|
File without changes
|