truefoundry 0.2.10__py3-none-any.whl → 0.3.0rc1__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.

Files changed (99) hide show
  1. truefoundry/__init__.py +1 -0
  2. truefoundry/autodeploy/cli.py +31 -18
  3. truefoundry/deploy/__init__.py +118 -1
  4. truefoundry/deploy/auto_gen/models.py +1675 -0
  5. truefoundry/deploy/builder/__init__.py +116 -0
  6. truefoundry/deploy/builder/builders/__init__.py +22 -0
  7. truefoundry/deploy/builder/builders/dockerfile.py +57 -0
  8. truefoundry/deploy/builder/builders/tfy_notebook_buildpack/__init__.py +44 -0
  9. truefoundry/deploy/builder/builders/tfy_notebook_buildpack/dockerfile_template.py +51 -0
  10. truefoundry/deploy/builder/builders/tfy_python_buildpack/__init__.py +44 -0
  11. truefoundry/deploy/builder/builders/tfy_python_buildpack/dockerfile_template.py +158 -0
  12. truefoundry/deploy/builder/docker_service.py +168 -0
  13. truefoundry/deploy/cli/cli.py +19 -26
  14. truefoundry/deploy/cli/commands/__init__.py +18 -0
  15. truefoundry/deploy/cli/commands/apply_command.py +52 -0
  16. truefoundry/deploy/cli/commands/build_command.py +45 -0
  17. truefoundry/deploy/cli/commands/build_logs_command.py +89 -0
  18. truefoundry/deploy/cli/commands/create_command.py +75 -0
  19. truefoundry/deploy/cli/commands/delete_command.py +77 -0
  20. truefoundry/deploy/cli/commands/deploy_command.py +99 -0
  21. truefoundry/deploy/cli/commands/get_command.py +216 -0
  22. truefoundry/deploy/cli/commands/list_command.py +171 -0
  23. truefoundry/deploy/cli/commands/login_command.py +33 -0
  24. truefoundry/deploy/cli/commands/logout_command.py +20 -0
  25. truefoundry/deploy/cli/commands/logs_command.py +134 -0
  26. truefoundry/deploy/cli/commands/patch_application_command.py +79 -0
  27. truefoundry/deploy/cli/commands/patch_command.py +70 -0
  28. truefoundry/deploy/cli/commands/redeploy_command.py +41 -0
  29. truefoundry/deploy/cli/commands/terminate_comand.py +44 -0
  30. truefoundry/deploy/cli/commands/trigger_command.py +87 -0
  31. truefoundry/deploy/cli/config.py +10 -0
  32. truefoundry/deploy/cli/console.py +5 -0
  33. truefoundry/deploy/cli/const.py +12 -0
  34. truefoundry/deploy/cli/display_util.py +118 -0
  35. truefoundry/deploy/cli/util.py +92 -0
  36. truefoundry/deploy/core/__init__.py +7 -0
  37. truefoundry/deploy/core/login.py +9 -0
  38. truefoundry/deploy/core/logout.py +5 -0
  39. truefoundry/deploy/function_service/__init__.py +3 -0
  40. truefoundry/deploy/function_service/__main__.py +27 -0
  41. truefoundry/deploy/function_service/app.py +92 -0
  42. truefoundry/deploy/function_service/build.py +45 -0
  43. truefoundry/deploy/function_service/remote/__init__.py +6 -0
  44. truefoundry/deploy/function_service/remote/context.py +3 -0
  45. truefoundry/deploy/function_service/remote/method.py +67 -0
  46. truefoundry/deploy/function_service/remote/remote.py +144 -0
  47. truefoundry/deploy/function_service/route.py +137 -0
  48. truefoundry/deploy/function_service/service.py +113 -0
  49. truefoundry/deploy/function_service/utils.py +53 -0
  50. truefoundry/deploy/io/__init__.py +0 -0
  51. truefoundry/deploy/io/output_callback.py +23 -0
  52. truefoundry/deploy/io/rich_output_callback.py +27 -0
  53. truefoundry/deploy/json_util.py +7 -0
  54. truefoundry/deploy/lib/__init__.py +0 -0
  55. truefoundry/deploy/lib/auth/auth_service_client.py +81 -0
  56. truefoundry/deploy/lib/auth/credential_file_manager.py +115 -0
  57. truefoundry/deploy/lib/auth/credential_provider.py +131 -0
  58. truefoundry/deploy/lib/auth/servicefoundry_session.py +59 -0
  59. truefoundry/deploy/lib/clients/__init__.py +0 -0
  60. truefoundry/deploy/lib/clients/servicefoundry_client.py +723 -0
  61. truefoundry/deploy/lib/clients/shell_client.py +13 -0
  62. truefoundry/deploy/lib/clients/utils.py +41 -0
  63. truefoundry/deploy/lib/const.py +43 -0
  64. truefoundry/deploy/lib/dao/__init__.py +0 -0
  65. truefoundry/deploy/lib/dao/application.py +246 -0
  66. truefoundry/deploy/lib/dao/apply.py +80 -0
  67. truefoundry/deploy/lib/dao/version.py +33 -0
  68. truefoundry/deploy/lib/dao/workspace.py +71 -0
  69. truefoundry/deploy/lib/exceptions.py +23 -0
  70. truefoundry/deploy/lib/logs_utils.py +43 -0
  71. truefoundry/deploy/lib/messages.py +12 -0
  72. truefoundry/deploy/lib/model/__init__.py +0 -0
  73. truefoundry/deploy/lib/model/entity.py +382 -0
  74. truefoundry/deploy/lib/session.py +146 -0
  75. truefoundry/deploy/lib/util.py +70 -0
  76. truefoundry/deploy/lib/win32.py +129 -0
  77. truefoundry/deploy/v2/__init__.py +0 -0
  78. truefoundry/deploy/v2/lib/__init__.py +3 -0
  79. truefoundry/deploy/v2/lib/deploy.py +232 -0
  80. truefoundry/deploy/v2/lib/deployable_patched_models.py +68 -0
  81. truefoundry/deploy/v2/lib/models.py +53 -0
  82. truefoundry/deploy/v2/lib/patched_models.py +497 -0
  83. truefoundry/deploy/v2/lib/source.py +267 -0
  84. truefoundry/langchain/__init__.py +12 -1
  85. truefoundry/langchain/deprecated.py +302 -0
  86. truefoundry/langchain/truefoundry_chat.py +130 -0
  87. truefoundry/langchain/truefoundry_embeddings.py +171 -0
  88. truefoundry/langchain/truefoundry_llm.py +106 -0
  89. truefoundry/langchain/utils.py +85 -0
  90. truefoundry/logger.py +17 -0
  91. truefoundry/pydantic_v1.py +5 -0
  92. truefoundry/python_deploy_codegen.py +132 -0
  93. {truefoundry-0.2.10.dist-info → truefoundry-0.3.0rc1.dist-info}/METADATA +22 -5
  94. truefoundry-0.3.0rc1.dist-info/RECORD +124 -0
  95. truefoundry/deploy/cli/deploy.py +0 -165
  96. truefoundry-0.2.10.dist-info/RECORD +0 -38
  97. /truefoundry/{deploy/cli/version.py → version.py} +0 -0
  98. {truefoundry-0.2.10.dist-info → truefoundry-0.3.0rc1.dist-info}/WHEEL +0 -0
  99. {truefoundry-0.2.10.dist-info → truefoundry-0.3.0rc1.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,246 @@
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
+ component_name: str = None,
119
+ command: Optional[Union[str, Sequence[str]]] = None,
120
+ params: Optional[Dict[str, str]] = None,
121
+ ) -> TriggerJobResult:
122
+ """
123
+ Trigger a Job on TrueFoundry platform
124
+
125
+ Args:
126
+ application_fqn: Fully Qualified Name of the Deployed Job (without the version number)
127
+ component_name: Name of the component to trigger the job on. Required in case of type `application`, defaults to `None`.
128
+ command: command to run the job with, defaults to `None`. Can be a `str` or `List[str]`
129
+ When `None`, the job is triggered with configured command at the time of deployment.
130
+ When passed as a list, the command will be joined using `shlex.join`
131
+ params: A dict mapping from parameter names (as defined in the job spec) to string values
132
+
133
+ Returns:
134
+ TriggerJobResult: metadata returning status of job trigger
135
+ """
136
+ if params and command:
137
+ raise ValueError(
138
+ "`command` and `params` arguments are mutually exclusive. Please pass only one of them"
139
+ )
140
+
141
+ try:
142
+ # If user is passing in deployment fqn copied from UI, till we change the fqns on UI
143
+ application_fqn = get_application_fqn_from_deployment_fqn(application_fqn)
144
+ logger.warning(
145
+ "Detected version number in `application_fqn`. "
146
+ "Automatically discarding the version number. "
147
+ f"This automatic conversion will be removed in future. "
148
+ f"Please pass {application_fqn!r} as the value."
149
+ )
150
+ except ValueError:
151
+ pass
152
+ client = ServiceFoundryServiceClient()
153
+ _application_info = client.get_application_info_by_fqn(
154
+ application_fqn=application_fqn
155
+ )
156
+ application_info = client.get_application_info(
157
+ application_id=_application_info.applicationId
158
+ )
159
+ command_str = ""
160
+ message = ""
161
+ if not params and not command:
162
+ message = "Job triggered with pre-configured command"
163
+ elif command:
164
+ if not isinstance(command, str):
165
+ command_str = shlex.join(command).strip()
166
+ else:
167
+ command_str = command.strip()
168
+ message = f"Job triggered with command {command_str!r}"
169
+ elif params:
170
+ # Check if params has any non string values
171
+ for key, value in params.items():
172
+ if not isinstance(value, str):
173
+ raise ValueError(
174
+ f"Invalid value {value!r} for key {key!r}. "
175
+ "Only string values are allowed for `params`"
176
+ )
177
+ command_str = ""
178
+ message = f"Job triggered with params {params!r}"
179
+ result = client.trigger_job(
180
+ deployment_id=application_info.activeDeploymentId,
181
+ component_name=component_name,
182
+ command=command_str if command_str else None,
183
+ params=params if params else None,
184
+ )
185
+ previous_runs_url = f"{client.base_url.strip('/')}/deployments/{application_info.id}?tab=previousRuns"
186
+ logger.info(
187
+ f"{message}.\n"
188
+ f"You can check the status of your job run at {previous_runs_url}"
189
+ )
190
+ return result
191
+
192
+
193
+ def get_patched_application_definition(
194
+ application: Application,
195
+ manifest_patch: Dict[str, Any],
196
+ ) -> Dict[str, Any]:
197
+ # TODO: define ApplicationPatch type but for now
198
+ # I am adding a manual check for name since name patch can
199
+ # create a new application
200
+ if (
201
+ manifest_patch.get("name")
202
+ and manifest_patch.get("name") != application.deployment.manifest.name
203
+ ):
204
+ raise Exception(
205
+ f"Cannot change name of application from `{application.deployment.manifest.name}` to `{manifest_patch.get('name')}`"
206
+ )
207
+
208
+ patch_list_paths = find_list_paths(manifest_patch)
209
+
210
+ for path in patch_list_paths:
211
+ logger.warn(
212
+ f"You are patching the value at {path}. Note that updating array-type objects will replace the entire object."
213
+ )
214
+
215
+ return pydantic_v1_utils.deep_update(
216
+ application.deployment.manifest.dict(), manifest_patch
217
+ )
218
+
219
+
220
+ def terminate_job_run(
221
+ application_fqn: str,
222
+ job_run_name: str,
223
+ ):
224
+ try:
225
+ application_fqn = get_application_fqn_from_deployment_fqn(application_fqn)
226
+ logger.warning(
227
+ "Detected version number in `application_fqn`. "
228
+ "Automatically discarding the version number. "
229
+ f"This automatic conversion will be removed in future. "
230
+ f"Please pass {application_fqn!r} as the value."
231
+ )
232
+ except ValueError:
233
+ pass
234
+ client = ServiceFoundryServiceClient()
235
+ _application_info = client.get_application_info_by_fqn(
236
+ application_fqn=application_fqn
237
+ )
238
+ application_info = client.get_application_info(
239
+ application_id=_application_info.applicationId
240
+ )
241
+
242
+ response = client.terminate_job_run(
243
+ deployment_id=application_info.activeDeploymentId,
244
+ job_run_name=job_run_name,
245
+ )
246
+ return response
@@ -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,23 @@
1
+ class BadRequestException(Exception):
2
+ def __init__(self, status_code, message=None):
3
+ super(BadRequestException, self).__init__()
4
+ self.status_code = status_code
5
+ self.message = message
6
+
7
+ def __str__(self):
8
+ return self.message
9
+
10
+ def __repr__(self):
11
+ return self.message
12
+
13
+
14
+ class ConfigurationException(Exception):
15
+ def __init__(self, message=None):
16
+ super(ConfigurationException, self).__init__()
17
+ self.message = message
18
+
19
+ def __str__(self):
20
+ return self.message
21
+
22
+ def __repr__(self):
23
+ 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