truefoundry 0.5.7rc1__py3-none-any.whl → 0.5.8__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/cli/__main__.py +2 -2
- truefoundry/common/credential_provider.py +1 -1
- truefoundry/common/entities.py +15 -15
- truefoundry/common/servicefoundry_client.py +7 -2
- truefoundry/common/session.py +1 -2
- truefoundry/common/types.py +7 -0
- truefoundry/common/utils.py +1 -1
- truefoundry/deploy/auto_gen/models.py +11 -3
- truefoundry/deploy/builder/docker_service.py +1 -1
- truefoundry/deploy/cli/commands/delete_command.py +40 -8
- truefoundry/deploy/cli/commands/deploy_command.py +3 -5
- truefoundry/deploy/lib/clients/servicefoundry_client.py +14 -7
- truefoundry/deploy/lib/dao/delete.py +88 -0
- truefoundry/deploy/lib/messages.py +1 -0
- truefoundry/deploy/lib/model/entity.py +5 -0
- truefoundry/deploy/lib/session.py +2 -5
- truefoundry/deploy/python_deploy_codegen.py +1 -1
- truefoundry/deploy/v2/lib/deploy.py +5 -5
- truefoundry/deploy/v2/lib/deploy_workflow.py +8 -5
- truefoundry/deploy/v2/lib/deployable_patched_models.py +13 -9
- truefoundry/deploy/v2/lib/models.py +1 -1
- truefoundry/deploy/v2/lib/patched_models.py +1 -1
- truefoundry/deploy/v2/lib/source.py +2 -1
- truefoundry/ml/autogen/client/api/health_api.py +1 -2
- truefoundry/ml/autogen/client/exceptions.py +1 -1
- truefoundry/ml/autogen/models/schema.py +1 -1
- truefoundry/ml/log_types/plot.py +2 -2
- truefoundry/ml/mlfoundry_run.py +1 -1
- truefoundry/ml/session.py +1 -2
- truefoundry-0.5.8.dist-info/METADATA +72 -0
- {truefoundry-0.5.7rc1.dist-info → truefoundry-0.5.8.dist-info}/RECORD +67 -70
- {truefoundry-0.5.7rc1.dist-info → truefoundry-0.5.8.dist-info}/WHEEL +1 -1
- truefoundry-0.5.8.dist-info/entry_points.txt +3 -0
- truefoundry/workflow/example/deploy.sh +0 -1
- truefoundry/workflow/example/hello_world_package/workflow.py +0 -20
- truefoundry/workflow/example/package/test_workflow.py +0 -151
- truefoundry/workflow/example/truefoundry.yaml +0 -9
- truefoundry/workflow/example/workflow.yaml +0 -116
- truefoundry-0.5.7rc1.dist-info/METADATA +0 -79
- truefoundry-0.5.7rc1.dist-info/entry_points.txt +0 -4
truefoundry/cli/__main__.py
CHANGED
|
@@ -71,7 +71,7 @@ def truefoundry_cli(ctx, json, debug):
|
|
|
71
71
|
logger.add_cli_handler(level=log_level)
|
|
72
72
|
|
|
73
73
|
|
|
74
|
-
def create_truefoundry_cli() -> click.
|
|
74
|
+
def create_truefoundry_cli() -> click.Group:
|
|
75
75
|
"""Generates CLI by combining all subcommands into a main CLI and returns in
|
|
76
76
|
|
|
77
77
|
Returns:
|
|
@@ -105,7 +105,7 @@ def main():
|
|
|
105
105
|
try:
|
|
106
106
|
cli = create_truefoundry_cli()
|
|
107
107
|
except Exception as e:
|
|
108
|
-
raise click.
|
|
108
|
+
raise click.UsageError(message=str(e)) from e
|
|
109
109
|
sys.exit(cli())
|
|
110
110
|
|
|
111
111
|
|
|
@@ -38,7 +38,7 @@ class EnvCredentialProvider(CredentialProvider):
|
|
|
38
38
|
)
|
|
39
39
|
self._host = resolve_tfy_host()
|
|
40
40
|
self._auth_service = AuthServiceClient.from_tfy_host(tfy_host=self._host)
|
|
41
|
-
self._token: Token = Token(access_token=api_key, refresh_token=None)
|
|
41
|
+
self._token: Token = Token(access_token=api_key, refresh_token=None)
|
|
42
42
|
|
|
43
43
|
@staticmethod
|
|
44
44
|
def can_provide() -> bool:
|
truefoundry/common/entities.py
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import time
|
|
2
|
-
from enum import Enum
|
|
3
2
|
from typing import Optional
|
|
4
3
|
|
|
5
4
|
import jwt
|
|
@@ -8,18 +7,12 @@ from typing_extensions import NotRequired, TypedDict
|
|
|
8
7
|
from truefoundry.pydantic_v1 import BaseModel, Field, NonEmptyStr, validator
|
|
9
8
|
|
|
10
9
|
|
|
11
|
-
class UserType(Enum):
|
|
12
|
-
user = "user"
|
|
13
|
-
serviceaccount = "serviceaccount"
|
|
14
|
-
|
|
15
|
-
|
|
16
10
|
class UserInfo(BaseModel):
|
|
17
11
|
class Config:
|
|
18
12
|
allow_population_by_field_name = True
|
|
19
13
|
allow_mutation = False
|
|
20
14
|
|
|
21
15
|
user_id: NonEmptyStr
|
|
22
|
-
user_type: UserType = UserType.user
|
|
23
16
|
email: Optional[str] = None
|
|
24
17
|
tenant_name: NonEmptyStr = Field(alias="tenantName")
|
|
25
18
|
|
|
@@ -27,9 +20,16 @@ class UserInfo(BaseModel):
|
|
|
27
20
|
class _DecodedToken(TypedDict):
|
|
28
21
|
tenantName: str
|
|
29
22
|
exp: int
|
|
30
|
-
username: str
|
|
23
|
+
username: NotRequired[str]
|
|
31
24
|
email: NotRequired[str]
|
|
32
|
-
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _user_slug(decoded_token: _DecodedToken) -> str:
|
|
28
|
+
return (
|
|
29
|
+
decoded_token.get("username")
|
|
30
|
+
or decoded_token.get("email")
|
|
31
|
+
or "--user-slug-missing--"
|
|
32
|
+
)
|
|
33
33
|
|
|
34
34
|
|
|
35
35
|
class Token(BaseModel):
|
|
@@ -55,19 +55,21 @@ class Token(BaseModel):
|
|
|
55
55
|
|
|
56
56
|
@property
|
|
57
57
|
def tenant_name(self) -> str:
|
|
58
|
+
assert self.decoded_value is not None
|
|
58
59
|
return self.decoded_value["tenantName"]
|
|
59
60
|
|
|
60
61
|
def is_going_to_be_expired(self, buffer_in_seconds: int = 120) -> bool:
|
|
62
|
+
assert self.decoded_value is not None
|
|
61
63
|
exp = int(self.decoded_value["exp"])
|
|
62
64
|
return (exp - time.time()) < buffer_in_seconds
|
|
63
65
|
|
|
64
66
|
def to_user_info(self) -> UserInfo:
|
|
65
|
-
|
|
66
|
-
|
|
67
|
+
assert self.decoded_value is not None
|
|
68
|
+
return UserInfo(
|
|
69
|
+
user_id=_user_slug(self.decoded_value),
|
|
67
70
|
email=self.decoded_value["email"]
|
|
68
71
|
if "email" in self.decoded_value
|
|
69
72
|
else None,
|
|
70
|
-
user_type=UserType(self.decoded_value.get("userType", UserType.user.value)),
|
|
71
73
|
tenant_name=self.tenant_name,
|
|
72
74
|
)
|
|
73
75
|
|
|
@@ -81,9 +83,7 @@ class CredentialsFileContent(BaseModel):
|
|
|
81
83
|
host: NonEmptyStr
|
|
82
84
|
|
|
83
85
|
def to_token(self) -> Token:
|
|
84
|
-
return Token(
|
|
85
|
-
access_token=self.access_token, refresh_token=self.refresh_token
|
|
86
|
-
)
|
|
86
|
+
return Token(access_token=self.access_token, refresh_token=self.refresh_token)
|
|
87
87
|
|
|
88
88
|
|
|
89
89
|
class TenantInfo(BaseModel):
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import functools
|
|
4
|
+
from typing import Callable, TypeVar
|
|
4
5
|
|
|
5
6
|
import requests
|
|
6
7
|
from packaging import version
|
|
8
|
+
from typing_extensions import ParamSpec
|
|
7
9
|
|
|
8
10
|
from truefoundry.common.constants import (
|
|
9
11
|
SERVICEFOUNDRY_CLIENT_MAX_RETRIES,
|
|
@@ -24,10 +26,13 @@ from truefoundry.common.utils import (
|
|
|
24
26
|
from truefoundry.logger import logger
|
|
25
27
|
from truefoundry.version import __version__
|
|
26
28
|
|
|
29
|
+
P = ParamSpec("P")
|
|
30
|
+
R = TypeVar("R")
|
|
27
31
|
|
|
28
|
-
|
|
32
|
+
|
|
33
|
+
def check_min_cli_version(fn: Callable[P, R]) -> Callable[P, R]:
|
|
29
34
|
@functools.wraps(fn)
|
|
30
|
-
def inner(*args, **kwargs):
|
|
35
|
+
def inner(*args: P.args, **kwargs: P.kwargs) -> R:
|
|
31
36
|
client: "ServiceFoundryServiceClient" = args[0]
|
|
32
37
|
client.check_min_cli_version()
|
|
33
38
|
return fn(*args, **kwargs)
|
truefoundry/common/session.py
CHANGED
|
@@ -50,10 +50,9 @@ class Session:
|
|
|
50
50
|
|
|
51
51
|
ACTIVE_SESSION = new_session
|
|
52
52
|
logger.info(
|
|
53
|
-
"Logged in to %r as %r
|
|
53
|
+
"Logged in to %r as %r",
|
|
54
54
|
new_session.tfy_host,
|
|
55
55
|
new_session.user_info.user_id,
|
|
56
|
-
new_session.user_info.email or new_session.user_info.user_type.value,
|
|
57
56
|
)
|
|
58
57
|
|
|
59
58
|
return ACTIVE_SESSION
|
truefoundry/common/utils.py
CHANGED
|
@@ -53,7 +53,7 @@ def get_tfy_servers_config(tfy_host: str) -> _TFYServersConfig:
|
|
|
53
53
|
global _tfy_servers_config
|
|
54
54
|
if _tfy_servers_config is None:
|
|
55
55
|
if ENV_VARS.TFY_CLI_LOCAL_DEV_MODE:
|
|
56
|
-
_tfy_servers_config = _TFYServersConfig()
|
|
56
|
+
_tfy_servers_config = _TFYServersConfig()
|
|
57
57
|
else:
|
|
58
58
|
_tfy_servers_config = _TFYServersConfig.from_tfy_host(tfy_host)
|
|
59
59
|
return _tfy_servers_config
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# generated by datamodel-codegen:
|
|
2
2
|
# filename: application.json
|
|
3
|
-
# timestamp: 2025-
|
|
3
|
+
# timestamp: 2025-02-19T09:48:05+00:00
|
|
4
4
|
|
|
5
5
|
from __future__ import annotations
|
|
6
6
|
|
|
@@ -1565,7 +1565,7 @@ class Notebook(BaseWorkbenchInput):
|
|
|
1565
1565
|
image: WorkbenchImage
|
|
1566
1566
|
cull_timeout: conint(ge=5) = Field(
|
|
1567
1567
|
30,
|
|
1568
|
-
description=
|
|
1568
|
+
description='+label=Stop after (minutes of inactivity)\n+usage=Stop the notebook instance after this much time in minutes of inactivity.\nThe notebook instance will be stopped even if the notebook is open in your browser, but nothing is running on the notebook.\n+sort=5\n+uiProps={"descriptionInline":true}',
|
|
1569
1569
|
)
|
|
1570
1570
|
|
|
1571
1571
|
|
|
@@ -1656,7 +1656,7 @@ class SSHServer(BaseWorkbenchInput):
|
|
|
1656
1656
|
)
|
|
1657
1657
|
cull_timeout: Optional[conint(ge=5)] = Field(
|
|
1658
1658
|
None,
|
|
1659
|
-
description=
|
|
1659
|
+
description='+label=Stop after (minutes of inactivity)\n+usage=Stop the SSH Server instance after this much time in minutes of inactivity. The instance is considered active if there is at least one active SSH connection (a client connected to the SSH server), or if a background job is running using tmux or screen, or if the pod has restarted.\n+sort=5\n+uiProps={"descriptionInline":true, "warningMessage":"Please note that stop after inactivity is only available for images with tag(including custom images) >= v0.3.10"}',
|
|
1660
1660
|
)
|
|
1661
1661
|
|
|
1662
1662
|
|
|
@@ -1696,6 +1696,10 @@ class SparkJob(BaseModel):
|
|
|
1696
1696
|
None,
|
|
1697
1697
|
description="+label=Environment Variables\n+usage=Configure environment variables to be injected in the service either as plain text. [Docs](https://docs.truefoundry.com/docs/env-variables)\n+icon=fa-globe\n+sort=21000",
|
|
1698
1698
|
)
|
|
1699
|
+
conf: Optional[Dict[str, Any]] = Field(
|
|
1700
|
+
None,
|
|
1701
|
+
description="+label=Spark Config Properties\n+usage=Extra configuration properties to be passed to the spark job. [Docs](https://spark.apache.org/docs/latest/configuration.html)\n+icon=fa-gear:#68BBE3\n+sort=21500",
|
|
1702
|
+
)
|
|
1699
1703
|
mounts: Optional[List[VolumeMount]] = Field(
|
|
1700
1704
|
None,
|
|
1701
1705
|
description="+label=Mounts\n+usage=Configure volumes to be mounted to driver and executors. [Docs](https://docs.truefoundry.com/docs/mounting-volumes-job)\n+sort=22000\n+uiType=Mounts",
|
|
@@ -1888,6 +1892,10 @@ class ApplicationSet(BaseModel):
|
|
|
1888
1892
|
None,
|
|
1889
1893
|
description="+label=Workspace FQN\n+docs=Fully qualified name of the workspace\n+uiType=Hidden",
|
|
1890
1894
|
)
|
|
1895
|
+
convert_template_manifest: Optional[bool] = Field(
|
|
1896
|
+
None,
|
|
1897
|
+
description="+label=Convert Template Manifest\n+docs=Flag to indicate if the template manifest should be converted to TrueFoundry manifest\n+uiType=Hidden",
|
|
1898
|
+
)
|
|
1891
1899
|
|
|
1892
1900
|
|
|
1893
1901
|
class Application(BaseModel):
|
|
@@ -38,7 +38,7 @@ def _catch_error_in_push(response: List[dict]):
|
|
|
38
38
|
for line in response:
|
|
39
39
|
if line.get("error") is not None:
|
|
40
40
|
raise Exception(
|
|
41
|
-
f
|
|
41
|
+
f"Failed to push to registry with message '{line.get('error')}'"
|
|
42
42
|
)
|
|
43
43
|
|
|
44
44
|
|
|
@@ -1,31 +1,63 @@
|
|
|
1
|
+
from typing import List, Tuple
|
|
2
|
+
|
|
1
3
|
import rich_click as click
|
|
2
4
|
|
|
3
5
|
from truefoundry.cli.config import CliConfig
|
|
6
|
+
from truefoundry.cli.console import console
|
|
4
7
|
from truefoundry.cli.const import COMMAND_CLS, GROUP_CLS
|
|
5
8
|
from truefoundry.cli.display_util import print_json
|
|
6
9
|
from truefoundry.cli.util import handle_exception_wrapper
|
|
7
10
|
from truefoundry.deploy.io.rich_output_callback import RichOutputCallBack
|
|
11
|
+
from truefoundry.deploy.lib.clients.servicefoundry_client import (
|
|
12
|
+
ServiceFoundryServiceClient,
|
|
13
|
+
)
|
|
8
14
|
from truefoundry.deploy.lib.dao import application as application_lib
|
|
15
|
+
from truefoundry.deploy.lib.dao import delete as delete_lib
|
|
9
16
|
from truefoundry.deploy.lib.dao import workspace as workspace_lib
|
|
10
17
|
from truefoundry.deploy.lib.messages import (
|
|
11
18
|
PROMPT_DELETED_APPLICATION,
|
|
12
19
|
PROMPT_DELETED_WORKSPACE,
|
|
20
|
+
PROMPT_DELETING_MANIFEST,
|
|
13
21
|
)
|
|
22
|
+
from truefoundry.deploy.lib.model.entity import DeleteResult
|
|
14
23
|
|
|
15
24
|
# TODO (chiragjn): --json should disable all non json console prints
|
|
16
25
|
|
|
17
26
|
|
|
18
|
-
@click.group(name="delete", cls=GROUP_CLS)
|
|
19
|
-
|
|
27
|
+
@click.group(name="delete", cls=GROUP_CLS, invoke_without_command=True)
|
|
28
|
+
@click.pass_context
|
|
29
|
+
@click.option(
|
|
30
|
+
"-f",
|
|
31
|
+
"--file",
|
|
32
|
+
"files",
|
|
33
|
+
type=click.Path(exists=True, dir_okay=False, resolve_path=True),
|
|
34
|
+
help="Path to yaml manifest file (You can pass multiple files at once by providing multiple -f options)",
|
|
35
|
+
show_default=True,
|
|
36
|
+
required=False,
|
|
37
|
+
multiple=True,
|
|
38
|
+
)
|
|
39
|
+
@handle_exception_wrapper
|
|
40
|
+
def delete_command(ctx, files: Tuple[str, ...]):
|
|
20
41
|
"""
|
|
21
42
|
Delete TrueFoundry resources
|
|
22
|
-
|
|
23
|
-
\b
|
|
24
|
-
Supported resources:
|
|
25
|
-
- Workspace
|
|
26
|
-
- Application
|
|
27
43
|
"""
|
|
28
|
-
|
|
44
|
+
if ctx.invoked_subcommand is None:
|
|
45
|
+
if not files:
|
|
46
|
+
raise click.UsageError("Missing option '-f' / '--file'")
|
|
47
|
+
delete_results: List[DeleteResult] = []
|
|
48
|
+
client = ServiceFoundryServiceClient()
|
|
49
|
+
for file in files:
|
|
50
|
+
with console.status(PROMPT_DELETING_MANIFEST.format(file), spinner="dots"):
|
|
51
|
+
for delete_result in delete_lib.delete_manifest_file(file, client):
|
|
52
|
+
if delete_result.success:
|
|
53
|
+
console.print(f"[green]\u2714 {delete_result.message}[/]")
|
|
54
|
+
else:
|
|
55
|
+
console.print(f"[red]\u2718 {delete_result.message}[/]")
|
|
56
|
+
|
|
57
|
+
delete_results.append(delete_result)
|
|
58
|
+
|
|
59
|
+
if not all(delete_result.success for delete_result in delete_results):
|
|
60
|
+
raise Exception("Failed to delete one or more resource manifests")
|
|
29
61
|
|
|
30
62
|
|
|
31
63
|
@click.command(name="workspace", cls=COMMAND_CLS, help="Delete a Workspace")
|
|
@@ -55,7 +55,7 @@ def _get_default_spec_file():
|
|
|
55
55
|
help="Wait and tail the deployment progress",
|
|
56
56
|
)
|
|
57
57
|
@click.option(
|
|
58
|
-
"--force
|
|
58
|
+
"--force/--no-force",
|
|
59
59
|
is_flag=True,
|
|
60
60
|
show_default=True,
|
|
61
61
|
default=False,
|
|
@@ -68,7 +68,7 @@ def deploy_command(
|
|
|
68
68
|
file: str,
|
|
69
69
|
workspace_fqn: Optional[str],
|
|
70
70
|
wait: bool,
|
|
71
|
-
|
|
71
|
+
force: bool = False,
|
|
72
72
|
):
|
|
73
73
|
if ctx.invoked_subcommand is not None:
|
|
74
74
|
return
|
|
@@ -85,9 +85,7 @@ def deploy_command(
|
|
|
85
85
|
application_definition = yaml.safe_load(f)
|
|
86
86
|
|
|
87
87
|
application = Application.parse_obj(application_definition)
|
|
88
|
-
application.deploy(
|
|
89
|
-
workspace_fqn=workspace_fqn, wait=wait, force_deploy=force_deploy
|
|
90
|
-
)
|
|
88
|
+
application.deploy(workspace_fqn=workspace_fqn, wait=wait, force=force)
|
|
91
89
|
sys.exit(0)
|
|
92
90
|
|
|
93
91
|
click.echo(
|
|
@@ -4,7 +4,7 @@ import json
|
|
|
4
4
|
import os
|
|
5
5
|
import time
|
|
6
6
|
from datetime import datetime, timezone
|
|
7
|
-
from typing import
|
|
7
|
+
from typing import Any, Dict, List, Optional
|
|
8
8
|
from urllib.parse import urljoin
|
|
9
9
|
|
|
10
10
|
import requests
|
|
@@ -50,9 +50,6 @@ DEPLOYMENT_LOGS_SUBSCRIBE_MESSAGE = "DEPLOYMENT_LOGS"
|
|
|
50
50
|
BUILD_LOGS_SUBSCRIBE_MESSAGE = "BUILD_LOGS"
|
|
51
51
|
MAX_RETRIES_WORKFLOW_TRIGGER = 3
|
|
52
52
|
|
|
53
|
-
if TYPE_CHECKING:
|
|
54
|
-
from truefoundry.deploy.auto_gen.models import Application
|
|
55
|
-
|
|
56
53
|
|
|
57
54
|
def _upload_packaged_code(metadata, package_file):
|
|
58
55
|
file_size = os.stat(package_file).st_size
|
|
@@ -242,13 +239,13 @@ class ServiceFoundryServiceClient(BaseServiceFoundryServiceClient):
|
|
|
242
239
|
self,
|
|
243
240
|
workspace_id: str,
|
|
244
241
|
application: auto_gen_models.Workflow,
|
|
245
|
-
|
|
242
|
+
force: bool = False,
|
|
246
243
|
) -> Deployment:
|
|
247
244
|
data = {
|
|
248
245
|
"workspaceId": workspace_id,
|
|
249
246
|
"name": application.name,
|
|
250
247
|
"manifest": application.dict(exclude_none=True),
|
|
251
|
-
"forceDeploy":
|
|
248
|
+
"forceDeploy": force,
|
|
252
249
|
}
|
|
253
250
|
logger.debug(json.dumps(data))
|
|
254
251
|
url = f"{self._api_server_url}/{VERSION_PREFIX}/deployment"
|
|
@@ -265,7 +262,7 @@ class ServiceFoundryServiceClient(BaseServiceFoundryServiceClient):
|
|
|
265
262
|
time_obj.replace(tzinfo=timezone.utc)
|
|
266
263
|
local_time = time_obj.astimezone(tzlocal())
|
|
267
264
|
local_time_str = local_time.isoformat()
|
|
268
|
-
return f
|
|
265
|
+
return f"[{local_time_str}] {log['log'].strip()}"
|
|
269
266
|
|
|
270
267
|
def _tail_logs(
|
|
271
268
|
self,
|
|
@@ -696,6 +693,16 @@ class ServiceFoundryServiceClient(BaseServiceFoundryServiceClient):
|
|
|
696
693
|
response_data = request_handling(response)
|
|
697
694
|
return response_data
|
|
698
695
|
|
|
696
|
+
@check_min_cli_version
|
|
697
|
+
def delete(self, manifest: Dict[str, Any]):
|
|
698
|
+
url = f"{self._api_server_url}/{VERSION_PREFIX}/delete"
|
|
699
|
+
body = {"manifest": manifest}
|
|
700
|
+
response = session_with_retries().post(
|
|
701
|
+
url, headers=self._get_header(), json=body
|
|
702
|
+
)
|
|
703
|
+
response_data = request_handling(response)
|
|
704
|
+
return response_data
|
|
705
|
+
|
|
699
706
|
def terminate_job_run(
|
|
700
707
|
self,
|
|
701
708
|
deployment_id: str,
|
|
@@ -0,0 +1,88 @@
|
|
|
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 DeleteResult, Manifest
|
|
10
|
+
from truefoundry.pydantic_v1 import ValidationError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _delete_manifest(
|
|
14
|
+
manifest: Dict[str, Any],
|
|
15
|
+
client: Optional[ServiceFoundryServiceClient] = None,
|
|
16
|
+
filename: Optional[str] = None,
|
|
17
|
+
index: Optional[int] = None,
|
|
18
|
+
) -> DeleteResult:
|
|
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 DeleteResult(
|
|
31
|
+
success=False,
|
|
32
|
+
message=f"Failed to parse manifest{file_metadata}. Error: {ex}",
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
try:
|
|
36
|
+
client.delete(manifest.dict())
|
|
37
|
+
|
|
38
|
+
return DeleteResult(
|
|
39
|
+
success=True,
|
|
40
|
+
message=(
|
|
41
|
+
f"Successfully deleted resource manifest {manifest.name} of type {manifest.type}."
|
|
42
|
+
),
|
|
43
|
+
)
|
|
44
|
+
except Exception as ex:
|
|
45
|
+
return DeleteResult(
|
|
46
|
+
success=False,
|
|
47
|
+
message=(
|
|
48
|
+
f"Failed to delete resource manifest {manifest.name} of type {manifest.type}. Error: {ex}."
|
|
49
|
+
),
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def delete_manifest(
|
|
54
|
+
manifest: Dict[str, Any],
|
|
55
|
+
client: Optional[ServiceFoundryServiceClient] = None,
|
|
56
|
+
) -> DeleteResult:
|
|
57
|
+
return _delete_manifest(manifest=manifest, client=client)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def delete_manifest_file(
|
|
61
|
+
filepath: str,
|
|
62
|
+
client: Optional[ServiceFoundryServiceClient] = None,
|
|
63
|
+
) -> Iterator[DeleteResult]:
|
|
64
|
+
client = client or ServiceFoundryServiceClient()
|
|
65
|
+
filename = Path(filepath).name
|
|
66
|
+
try:
|
|
67
|
+
with open(filepath, "r") as f:
|
|
68
|
+
manifests_it = list(yaml.safe_load_all(f))
|
|
69
|
+
except Exception as ex:
|
|
70
|
+
yield DeleteResult(
|
|
71
|
+
success=False,
|
|
72
|
+
message=f"Failed to read file {filepath} as a valid YAML file. Error: {ex}",
|
|
73
|
+
)
|
|
74
|
+
else:
|
|
75
|
+
for index, manifest in enumerate(manifests_it):
|
|
76
|
+
if not isinstance(manifest, dict):
|
|
77
|
+
yield DeleteResult(
|
|
78
|
+
success=False,
|
|
79
|
+
message=f"Failed to delete resource manifest at index {index} from file {filename}. Error: A manifest must be a dict, got type {type(manifest)}",
|
|
80
|
+
)
|
|
81
|
+
continue
|
|
82
|
+
|
|
83
|
+
yield _delete_manifest(
|
|
84
|
+
manifest=manifest,
|
|
85
|
+
client=client,
|
|
86
|
+
filename=filename,
|
|
87
|
+
index=index,
|
|
88
|
+
)
|
|
@@ -10,3 +10,4 @@ PROMPT_NO_WORKSPACES = f"""[yellow]No workspaces found. Either cluster name is w
|
|
|
10
10
|
PROMPT_NO_APPLICATIONS = f"""[yellow]No applications found. You can create one with [bold]{TFY} deploy[/] from within your application folder"""
|
|
11
11
|
PROMPT_NO_VERSIONS = """[yellow]No application versions found."""
|
|
12
12
|
PROMPT_APPLYING_MANIFEST = """[yellow]Applying manifest for file {!r}[/]"""
|
|
13
|
+
PROMPT_DELETING_MANIFEST = """[yellow]Deleting manifest for file {!r}[/]"""
|
|
@@ -57,10 +57,9 @@ def login(
|
|
|
57
57
|
return login(api_key=api_key, host=host, relogin=True)
|
|
58
58
|
|
|
59
59
|
user_info = cred_file_content.to_token().to_user_info()
|
|
60
|
-
user_name_display_info = user_info.email or user_info.user_type.value
|
|
61
60
|
output_hook.print_line(
|
|
62
61
|
relogin_error_message(
|
|
63
|
-
f"Already logged in to {cred_file_content.host!r} as {user_info.user_id!r}
|
|
62
|
+
f"Already logged in to {cred_file_content.host!r} as {user_info.user_id!r}",
|
|
64
63
|
host=host,
|
|
65
64
|
)
|
|
66
65
|
)
|
|
@@ -81,10 +80,8 @@ def login(
|
|
|
81
80
|
cred_file.write(cred_file_content)
|
|
82
81
|
|
|
83
82
|
user_info = token.to_user_info()
|
|
84
|
-
user_name_display_info = user_info.email or user_info.user_type.value
|
|
85
83
|
output_hook.print_line(
|
|
86
|
-
f"Successfully logged in to {cred_file_content.host!r} as "
|
|
87
|
-
f"{user_info.user_id!r} ({user_name_display_info})"
|
|
84
|
+
f"Successfully logged in to {cred_file_content.host!r} as {user_info.user_id!r}"
|
|
88
85
|
)
|
|
89
86
|
return True
|
|
90
87
|
|
|
@@ -214,8 +214,8 @@ No Workspace FQN was provided or mentioned in the spec.
|
|
|
214
214
|
Either add a `workspace_fqn` to your yaml spec as
|
|
215
215
|
|
|
216
216
|
```
|
|
217
|
-
name: {getattr(component,
|
|
218
|
-
type: {getattr(component,
|
|
217
|
+
name: {getattr(component, "name", "my-app")}
|
|
218
|
+
type: {getattr(component, "type", "undefined")}
|
|
219
219
|
...
|
|
220
220
|
workspace_fqn: <your workspace fqn>
|
|
221
221
|
```
|
|
@@ -224,7 +224,7 @@ or Python deployment spec as
|
|
|
224
224
|
|
|
225
225
|
```
|
|
226
226
|
app = {component.__class__.__name__}(
|
|
227
|
-
name='{getattr(component,
|
|
227
|
+
name='{getattr(component, "name", "my-app")}',
|
|
228
228
|
...
|
|
229
229
|
workspace_fqn='<your workspace fqn>'
|
|
230
230
|
)
|
|
@@ -252,7 +252,7 @@ def deploy_component(
|
|
|
252
252
|
component: Component,
|
|
253
253
|
workspace_fqn: Optional[str] = None,
|
|
254
254
|
wait: bool = True,
|
|
255
|
-
|
|
255
|
+
force: bool = False,
|
|
256
256
|
) -> Deployment:
|
|
257
257
|
_warn_when_gpu_selected_without_cuda(component=component)
|
|
258
258
|
workspace_fqn = _resolve_workspace_fqn(
|
|
@@ -274,7 +274,7 @@ def deploy_component(
|
|
|
274
274
|
response = client.deploy_application(
|
|
275
275
|
workspace_id=workspace_id,
|
|
276
276
|
application=updated_component,
|
|
277
|
-
|
|
277
|
+
force=force,
|
|
278
278
|
)
|
|
279
279
|
logger.info(
|
|
280
280
|
"🚀 Deployment started for application '%s'. Deployment FQN is '%s'.",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import sys
|
|
3
3
|
from pathlib import Path
|
|
4
|
-
from typing import
|
|
4
|
+
from typing import Dict, List, Optional, Union
|
|
5
5
|
|
|
6
6
|
import requirements
|
|
7
7
|
from flytekit.configuration import (
|
|
@@ -19,6 +19,7 @@ from flytekit.tools.translator import TaskSpec as FlyteTaskSpec
|
|
|
19
19
|
from flytekit.tools.translator import WorkflowSpec as FlyteWorkflowSpec
|
|
20
20
|
from google.protobuf.json_format import MessageToDict
|
|
21
21
|
|
|
22
|
+
from truefoundry.common.types import UploadCodePackageCallable
|
|
22
23
|
from truefoundry.deploy.auto_gen import models as auto_gen_models
|
|
23
24
|
from truefoundry.deploy.lib.clients.servicefoundry_client import (
|
|
24
25
|
ServiceFoundryServiceClient,
|
|
@@ -39,7 +40,7 @@ from truefoundry.workflow.workflow import (
|
|
|
39
40
|
def _handle_code_upload_for_workflow(
|
|
40
41
|
workflow: auto_gen_models.Workflow,
|
|
41
42
|
workspace_fqn: str,
|
|
42
|
-
upload_code_package:
|
|
43
|
+
upload_code_package: UploadCodePackageCallable,
|
|
43
44
|
) -> auto_gen_models.Workflow:
|
|
44
45
|
new_workflow = workflow.copy(deep=True)
|
|
45
46
|
new_workflow.source = local_source_to_remote_source(
|
|
@@ -92,7 +93,7 @@ def _is_tfy_wf_present_in_task_python_build(
|
|
|
92
93
|
|
|
93
94
|
|
|
94
95
|
def _is_dynamic_task(flyte_task: FlyteTaskSpec) -> bool:
|
|
95
|
-
envs: Dict[str
|
|
96
|
+
envs: Dict[str, str] = flyte_task.template.container.env or {}
|
|
96
97
|
return SERIALIZED_CONTEXT_ENV_VAR in envs.keys()
|
|
97
98
|
|
|
98
99
|
|
|
@@ -273,7 +274,7 @@ def deploy_workflow(
|
|
|
273
274
|
workflow: auto_gen_models.Workflow,
|
|
274
275
|
workspace_fqn: str,
|
|
275
276
|
wait: bool = True,
|
|
276
|
-
|
|
277
|
+
force: bool = False,
|
|
277
278
|
) -> Deployment:
|
|
278
279
|
_generate_manifest_for_workflow(workflow)
|
|
279
280
|
_validate_workspace_fqn(workflow, workspace_fqn)
|
|
@@ -296,7 +297,9 @@ def deploy_workflow(
|
|
|
296
297
|
)
|
|
297
298
|
|
|
298
299
|
deployment = client.deploy_application(
|
|
299
|
-
workspace_id=workspace_id,
|
|
300
|
+
workspace_id=workspace_id,
|
|
301
|
+
application=workflow,
|
|
302
|
+
force=force,
|
|
300
303
|
)
|
|
301
304
|
logger.info(
|
|
302
305
|
"🚀 Deployment started for application '%s'. Deployment FQN is '%s'.",
|