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.
- truefoundry/__init__.py +1 -0
- truefoundry/autodeploy/cli.py +31 -18
- truefoundry/deploy/__init__.py +118 -1
- truefoundry/deploy/auto_gen/models.py +1675 -0
- truefoundry/deploy/builder/__init__.py +116 -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 +44 -0
- truefoundry/deploy/builder/builders/tfy_notebook_buildpack/dockerfile_template.py +51 -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 +19 -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 +99 -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 +79 -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 +87 -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 +92 -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 +81 -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 +723 -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 +246 -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 +23 -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 +382 -0
- truefoundry/deploy/lib/session.py +146 -0
- truefoundry/deploy/lib/util.py +70 -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 +232 -0
- truefoundry/deploy/v2/lib/deployable_patched_models.py +68 -0
- truefoundry/deploy/v2/lib/models.py +53 -0
- truefoundry/deploy/v2/lib/patched_models.py +497 -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-0.2.10.dist-info → truefoundry-0.3.0rc1.dist-info}/METADATA +22 -5
- truefoundry-0.3.0rc1.dist-info/RECORD +124 -0
- truefoundry/deploy/cli/deploy.py +0 -165
- truefoundry-0.2.10.dist-info/RECORD +0 -38
- /truefoundry/{deploy/cli/version.py → version.py} +0 -0
- {truefoundry-0.2.10.dist-info → truefoundry-0.3.0rc1.dist-info}/WHEEL +0 -0
- {truefoundry-0.2.10.dist-info → truefoundry-0.3.0rc1.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import datetime
|
|
4
|
+
import time
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from typing import Any, Dict, List, Optional, Union
|
|
7
|
+
|
|
8
|
+
import jwt
|
|
9
|
+
|
|
10
|
+
from truefoundry.deploy.lib.util import get_application_fqn_from_deployment_fqn
|
|
11
|
+
from truefoundry.pydantic_v1 import BaseModel, Extra, Field, constr, validator
|
|
12
|
+
|
|
13
|
+
# TODO: switch to Enums for str literals
|
|
14
|
+
# TODO: Need a better approach to keep fields in sync with server
|
|
15
|
+
# most fields should have a default in case server adds/removes a field
|
|
16
|
+
# TODO: Implement NotImplementedError sections
|
|
17
|
+
UNDEFINED_STRING = "<Undefined>"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Base(BaseModel):
|
|
21
|
+
class Config:
|
|
22
|
+
validate_assignment = True
|
|
23
|
+
use_enum_values = True
|
|
24
|
+
extra = Extra.allow
|
|
25
|
+
|
|
26
|
+
def __repr_args__(self):
|
|
27
|
+
return [
|
|
28
|
+
(key, value)
|
|
29
|
+
for key, value in self.__dict__.items()
|
|
30
|
+
if key in self.__fields__
|
|
31
|
+
and self.__fields__[key].field_info.extra.get("repr", True)
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class Entity(Base):
|
|
36
|
+
createdAt: datetime.datetime = Field(repr=False)
|
|
37
|
+
updatedAt: datetime.datetime = Field(repr=False)
|
|
38
|
+
createdBy: str = Field(repr=False)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class Workspace(Entity):
|
|
42
|
+
id: str = Field(repr=False)
|
|
43
|
+
name: str
|
|
44
|
+
fqn: str
|
|
45
|
+
clusterId: str = Field(repr=False)
|
|
46
|
+
|
|
47
|
+
def list_row_data(self) -> Dict[str, Any]:
|
|
48
|
+
return {
|
|
49
|
+
"name": self.name,
|
|
50
|
+
"fqn": self.fqn,
|
|
51
|
+
"cluster_fqn": self.clusterId,
|
|
52
|
+
"created_at": self.createdAt,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
def get_data(self) -> Dict[str, Any]:
|
|
56
|
+
return {
|
|
57
|
+
"name": self.name,
|
|
58
|
+
"fqn": self.fqn,
|
|
59
|
+
"cluster_fqn": self.clusterId,
|
|
60
|
+
"created_at": self.createdAt,
|
|
61
|
+
"updated_at": self.updatedAt,
|
|
62
|
+
"created_by": self.createdBy,
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class WorkspaceResources(BaseModel):
|
|
67
|
+
cpu_limit: Optional[float]
|
|
68
|
+
memory_limit: Optional[int]
|
|
69
|
+
ephemeral_storage_limit: Optional[int]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class PortMetadata(BaseModel):
|
|
73
|
+
port: int
|
|
74
|
+
# TODO: done because of a bug, needs to reverted to be mandatory
|
|
75
|
+
host: Optional[str]
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class DeploymentTransitionStatus(str, Enum):
|
|
79
|
+
INITIALIZED: str = "INITIALIZED"
|
|
80
|
+
BUILDING: str = "BUILDING"
|
|
81
|
+
DEPLOYING: str = "DEPLOYING"
|
|
82
|
+
BUILD_SUCCESS: str = "BUILD_SUCCESS"
|
|
83
|
+
DEPLOY_SUCCESS: str = "DEPLOY_SUCCESS"
|
|
84
|
+
DEPLOY_FAILED: str = "DEPLOY_FAILED"
|
|
85
|
+
BUILD_FAILED: str = "BUILD_FAILED"
|
|
86
|
+
CANCELLED: str = "CANCELLED"
|
|
87
|
+
FAILED: str = "FAILED"
|
|
88
|
+
REUSING_EXISTING_BUILD: str = "REUSING_EXISTING_BUILD"
|
|
89
|
+
REDEPLOY_STARTED: str = "REDEPLOY_STARTED"
|
|
90
|
+
PAUSED: str = "PAUSED"
|
|
91
|
+
ROLLOUT_STARTED: str = "ROLLOUT_STARTED"
|
|
92
|
+
SET_TRAFFIC: str = "SET_TRAFFIC"
|
|
93
|
+
DEPLOY_FAILED_WITH_RETRY: str = "DEPLOY_FAILED_WITH_RETRY"
|
|
94
|
+
WAITING: str = "WAITING"
|
|
95
|
+
|
|
96
|
+
_: str = ""
|
|
97
|
+
|
|
98
|
+
@classmethod
|
|
99
|
+
def is_failure_state(cls, state: DeploymentTransitionStatus) -> bool:
|
|
100
|
+
return state in (cls.DEPLOY_FAILED, cls.BUILD_FAILED, cls.FAILED, cls.CANCELLED)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class DeploymentState(BaseModel):
|
|
104
|
+
isTerminalState: bool
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class DeploymentStatus(BaseModel):
|
|
108
|
+
state: DeploymentState
|
|
109
|
+
status: DeploymentTransitionStatus
|
|
110
|
+
transition: Optional[DeploymentTransitionStatus]
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class DeploymentManifest(Base):
|
|
114
|
+
name: Optional[str] = Field(default=UNDEFINED_STRING)
|
|
115
|
+
type: str
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class Deployment(Entity):
|
|
119
|
+
id: str = Field(repr=False)
|
|
120
|
+
fqn: str
|
|
121
|
+
version: int
|
|
122
|
+
currentStatusId: str = Field(repr=False)
|
|
123
|
+
applicationId: str = Field(repr=False)
|
|
124
|
+
manifest: DeploymentManifest = Field(repr=False)
|
|
125
|
+
failureReason: Optional[str]
|
|
126
|
+
deploymentStatuses: Optional[List[DeploymentStatus]]
|
|
127
|
+
metadata: Optional[
|
|
128
|
+
Union[Dict[str, Any], List[Dict[str, Any]]]
|
|
129
|
+
] # TODO (chiragjn): revisit the type of this field
|
|
130
|
+
currentStatus: Optional[DeploymentStatus] # Server was not returning CurrentStatus
|
|
131
|
+
application_fqn: str
|
|
132
|
+
|
|
133
|
+
def __init__(self, **kwargs) -> None:
|
|
134
|
+
deployment_fqn = kwargs.get("fqn")
|
|
135
|
+
if not deployment_fqn:
|
|
136
|
+
raise ValueError("'fqn' field is required")
|
|
137
|
+
application_fqn = ":".join(deployment_fqn.split(":")[:3])
|
|
138
|
+
kwargs["application_fqn"] = application_fqn
|
|
139
|
+
super().__init__(**kwargs)
|
|
140
|
+
|
|
141
|
+
@property
|
|
142
|
+
def application_fqn(self) -> str:
|
|
143
|
+
return get_application_fqn_from_deployment_fqn(self.fqn)
|
|
144
|
+
|
|
145
|
+
def list_row_data(self) -> Dict[str, Any]:
|
|
146
|
+
return {
|
|
147
|
+
"fqn": self.fqn,
|
|
148
|
+
"application_name": self.manifest.name,
|
|
149
|
+
"version": self.version,
|
|
150
|
+
"created_at": self.createdAt,
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
def get_data(self) -> Dict[str, Any]:
|
|
154
|
+
# TODO: Remove this splitting logic
|
|
155
|
+
cluster_fqn, workspace_name, *_ = self.fqn.split(":")
|
|
156
|
+
workspace_fqn = ":".join([cluster_fqn, workspace_name])
|
|
157
|
+
return {
|
|
158
|
+
"fqn": self.fqn,
|
|
159
|
+
"application_name": self.manifest.name,
|
|
160
|
+
"application_type": self.manifest.type,
|
|
161
|
+
"version": self.version,
|
|
162
|
+
"workspace_fqn": workspace_fqn,
|
|
163
|
+
"cluster_fqn": cluster_fqn,
|
|
164
|
+
"created_at": self.createdAt,
|
|
165
|
+
"updated_at": self.updatedAt,
|
|
166
|
+
"created_by": self.createdBy,
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
class Config:
|
|
170
|
+
extra = Extra.allow
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class ApplicationWorkspace(Base):
|
|
174
|
+
name: str
|
|
175
|
+
fqn: str
|
|
176
|
+
clusterId: str
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
class Application(Entity):
|
|
180
|
+
id: str = Field(repr=False)
|
|
181
|
+
name: str
|
|
182
|
+
fqn: str
|
|
183
|
+
tenantName: str
|
|
184
|
+
workspaceId: str = Field(repr=False)
|
|
185
|
+
lastVersion: int
|
|
186
|
+
activeVersion: int
|
|
187
|
+
workspace: ApplicationWorkspace
|
|
188
|
+
deployment: Deployment
|
|
189
|
+
activeDeploymentId: Optional[str] = Field(repr=False)
|
|
190
|
+
lastDeploymentId: str = Field(repr=False)
|
|
191
|
+
|
|
192
|
+
def list_row_data(self) -> Dict[str, Any]:
|
|
193
|
+
return {
|
|
194
|
+
"name": self.name,
|
|
195
|
+
"type": self.deployment.manifest.type,
|
|
196
|
+
"fqn": self.fqn,
|
|
197
|
+
"active_version": self.activeVersion,
|
|
198
|
+
"workspace_name": self.workspace.name,
|
|
199
|
+
"created_at": self.createdAt,
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
def get_data(self) -> Dict[str, Any]:
|
|
203
|
+
return {
|
|
204
|
+
"name": self.name,
|
|
205
|
+
"type": self.deployment.manifest.type,
|
|
206
|
+
"fqn": self.fqn,
|
|
207
|
+
"active_version": self.activeVersion,
|
|
208
|
+
"last_version": self.lastVersion,
|
|
209
|
+
"workspace_fqn": self.workspace.fqn,
|
|
210
|
+
"cluster_fqn": self.workspace.clusterId,
|
|
211
|
+
"created_at": self.createdAt,
|
|
212
|
+
"updated_at": self.updatedAt,
|
|
213
|
+
"created_by": self.createdBy,
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
class UserType(Enum):
|
|
218
|
+
user = "user"
|
|
219
|
+
serviceaccount = "serviceaccount"
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
class UserInfo(BaseModel):
|
|
223
|
+
user_id: constr(min_length=1)
|
|
224
|
+
user_type: UserType = UserType.user
|
|
225
|
+
email: Optional[str] = None
|
|
226
|
+
tenant_name: constr(min_length=1) = Field(alias="tenantName")
|
|
227
|
+
|
|
228
|
+
class Config:
|
|
229
|
+
allow_population_by_field_name = True
|
|
230
|
+
allow_mutation = False
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
class TenantInfo(BaseModel):
|
|
234
|
+
tenant_name: constr(min_length=1) = Field(alias="tenantName")
|
|
235
|
+
auth_server_url: str
|
|
236
|
+
|
|
237
|
+
class Config:
|
|
238
|
+
allow_population_by_field_name = True
|
|
239
|
+
allow_mutation = False
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
class Token(BaseModel):
|
|
243
|
+
access_token: constr(min_length=1) = Field(alias="accessToken", repr=False)
|
|
244
|
+
refresh_token: Optional[constr(min_length=1)] = Field(
|
|
245
|
+
alias="refreshToken", repr=False
|
|
246
|
+
)
|
|
247
|
+
decoded_value: Optional[Dict] = Field(exclude=True, repr=False)
|
|
248
|
+
|
|
249
|
+
class Config:
|
|
250
|
+
allow_population_by_field_name = True
|
|
251
|
+
allow_mutation = False
|
|
252
|
+
|
|
253
|
+
@validator("decoded_value", always=True, pre=True)
|
|
254
|
+
def _decode_jwt(cls, v, values, **kwargs):
|
|
255
|
+
access_token = values["access_token"]
|
|
256
|
+
return jwt.decode(
|
|
257
|
+
access_token,
|
|
258
|
+
options={
|
|
259
|
+
"verify_signature": False,
|
|
260
|
+
"verify_aud": False,
|
|
261
|
+
"verify_exp": False,
|
|
262
|
+
},
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
@property
|
|
266
|
+
def tenant_name(self) -> str:
|
|
267
|
+
return self.decoded_value["tenantName"]
|
|
268
|
+
|
|
269
|
+
def is_going_to_be_expired(self, buffer_in_seconds: int = 120) -> bool:
|
|
270
|
+
exp = int(self.decoded_value["exp"])
|
|
271
|
+
return (exp - time.time()) < buffer_in_seconds
|
|
272
|
+
|
|
273
|
+
def to_user_info(self) -> UserInfo:
|
|
274
|
+
return UserInfo(
|
|
275
|
+
user_id=self.decoded_value["username"],
|
|
276
|
+
email=self.decoded_value["email"]
|
|
277
|
+
if "email" in self.decoded_value
|
|
278
|
+
else None,
|
|
279
|
+
user_type=UserType(self.decoded_value.get("userType", UserType.user.value)),
|
|
280
|
+
tenant_name=self.tenant_name,
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
class CredentialsFileContent(BaseModel):
|
|
285
|
+
access_token: constr(min_length=1) = Field(repr=False)
|
|
286
|
+
refresh_token: Optional[constr(min_length=1)] = Field(repr=False)
|
|
287
|
+
host: constr(min_length=1)
|
|
288
|
+
|
|
289
|
+
class Config:
|
|
290
|
+
allow_mutation = False
|
|
291
|
+
|
|
292
|
+
def to_token(self) -> Token:
|
|
293
|
+
return Token(access_token=self.access_token, refresh_token=self.refresh_token)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
class DeviceCode(BaseModel):
|
|
297
|
+
user_code: str = Field(alias="userCode")
|
|
298
|
+
device_code: str = Field(alias="deviceCode")
|
|
299
|
+
|
|
300
|
+
class Config:
|
|
301
|
+
allow_population_by_field_name = True
|
|
302
|
+
allow_mutation = False
|
|
303
|
+
|
|
304
|
+
def get_user_clickable_url(self, auth_host: str) -> str:
|
|
305
|
+
return f"{auth_host}/authorize/device?userCode={self.user_code}"
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
class JobRun(Base):
|
|
309
|
+
name: str
|
|
310
|
+
applicationName: str
|
|
311
|
+
deploymentVersion: int
|
|
312
|
+
createdAt: int
|
|
313
|
+
endTime: Optional[int] = None
|
|
314
|
+
duration: Optional[str] = None
|
|
315
|
+
command: str
|
|
316
|
+
totalRetries: Optional[int] = 0
|
|
317
|
+
status: str
|
|
318
|
+
|
|
319
|
+
def list_row_data(self) -> Dict[str, Any]:
|
|
320
|
+
from truefoundry.deploy.cli.display_util import display_time_passed
|
|
321
|
+
|
|
322
|
+
triggered_at = (
|
|
323
|
+
(datetime.datetime.now().timestamp() * 1000) - self.createdAt
|
|
324
|
+
) // 1000
|
|
325
|
+
triggered_at = f"{display_time_passed(triggered_at)} ago"
|
|
326
|
+
duration = ""
|
|
327
|
+
if self.duration:
|
|
328
|
+
duration = display_time_passed(int(float(self.duration)))
|
|
329
|
+
return {
|
|
330
|
+
"name": self.name,
|
|
331
|
+
"deployment_version": self.deploymentVersion,
|
|
332
|
+
"status": self.status,
|
|
333
|
+
"triggered_at": triggered_at,
|
|
334
|
+
"duration": duration,
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
def get_data(self) -> Dict[str, Any]:
|
|
338
|
+
from truefoundry.deploy.cli.display_util import display_time_passed
|
|
339
|
+
|
|
340
|
+
created_at = datetime.datetime.fromtimestamp(self.createdAt // 1000)
|
|
341
|
+
end_time = ""
|
|
342
|
+
if self.endTime:
|
|
343
|
+
end_time = datetime.datetime.fromtimestamp(self.endTime // 1000)
|
|
344
|
+
duration = ""
|
|
345
|
+
if self.duration:
|
|
346
|
+
duration = display_time_passed(int(float(self.duration)))
|
|
347
|
+
return {
|
|
348
|
+
"name": self.name,
|
|
349
|
+
"application_name": self.applicationName,
|
|
350
|
+
"deployment_version": self.deploymentVersion,
|
|
351
|
+
"created_at": created_at,
|
|
352
|
+
"end_time": end_time,
|
|
353
|
+
"duration": duration,
|
|
354
|
+
"command": self.command,
|
|
355
|
+
"status": self.status,
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
class TriggerJobResult(Base):
|
|
360
|
+
message: str = Field(default="Unknown")
|
|
361
|
+
jobRunName: str = Field(default=None)
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
class DockerRegistryCredentials(Base):
|
|
365
|
+
fqn: str
|
|
366
|
+
registryUrl: str
|
|
367
|
+
username: str
|
|
368
|
+
password: str
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
class CreateDockerRepositoryResponse(Base):
|
|
372
|
+
repoName: str
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
class ApplyResult(BaseModel):
|
|
376
|
+
success: bool
|
|
377
|
+
message: str
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
class Manifest(Base):
|
|
381
|
+
type: str
|
|
382
|
+
name: str
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
import rich_click as click
|
|
5
|
+
|
|
6
|
+
from truefoundry.deploy.io.output_callback import OutputCallBack
|
|
7
|
+
from truefoundry.deploy.lib.auth.auth_service_client import AuthServiceClient
|
|
8
|
+
from truefoundry.deploy.lib.auth.credential_file_manager import CredentialsFileManager
|
|
9
|
+
from truefoundry.deploy.lib.auth.credential_provider import EnvCredentialProvider
|
|
10
|
+
from truefoundry.deploy.lib.clients.servicefoundry_client import (
|
|
11
|
+
ServiceFoundryServiceClient,
|
|
12
|
+
)
|
|
13
|
+
from truefoundry.deploy.lib.clients.utils import resolve_base_url
|
|
14
|
+
from truefoundry.deploy.lib.const import (
|
|
15
|
+
API_KEY_ENV_NAME,
|
|
16
|
+
HOST_ENV_NAME,
|
|
17
|
+
OLD_SFY_PROFILES_FILEPATH,
|
|
18
|
+
OLD_SFY_SESSIONS_FILEPATH,
|
|
19
|
+
RICH_OUTPUT_CALLBACK,
|
|
20
|
+
)
|
|
21
|
+
from truefoundry.deploy.lib.messages import (
|
|
22
|
+
PROMPT_ALREADY_LOGGED_OUT,
|
|
23
|
+
PROMPT_LOGOUT_SUCCESSFUL,
|
|
24
|
+
)
|
|
25
|
+
from truefoundry.deploy.lib.model.entity import CredentialsFileContent, Token
|
|
26
|
+
from truefoundry.logger import logger
|
|
27
|
+
|
|
28
|
+
if OLD_SFY_PROFILES_FILEPATH.exists():
|
|
29
|
+
logger.warning(
|
|
30
|
+
"%s file is deprecated. You can delete this file now.",
|
|
31
|
+
OLD_SFY_PROFILES_FILEPATH,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
if OLD_SFY_SESSIONS_FILEPATH.exists():
|
|
36
|
+
logger.warning(
|
|
37
|
+
"%s file is deprecated. You can delete this file now.",
|
|
38
|
+
OLD_SFY_SESSIONS_FILEPATH,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def login(
|
|
43
|
+
api_key: Optional[str] = None,
|
|
44
|
+
host: Optional[str] = None,
|
|
45
|
+
relogin: bool = False,
|
|
46
|
+
output_hook: OutputCallBack = RICH_OUTPUT_CALLBACK,
|
|
47
|
+
) -> bool:
|
|
48
|
+
if API_KEY_ENV_NAME in os.environ and HOST_ENV_NAME in os.environ:
|
|
49
|
+
logger.warning(
|
|
50
|
+
"Skipping login because environment variables %s and "
|
|
51
|
+
"%s are set and will be used when running truefoundry. "
|
|
52
|
+
"If you want to relogin then unset these environment keys.",
|
|
53
|
+
HOST_ENV_NAME,
|
|
54
|
+
API_KEY_ENV_NAME,
|
|
55
|
+
)
|
|
56
|
+
return False
|
|
57
|
+
|
|
58
|
+
if EnvCredentialProvider.can_provide():
|
|
59
|
+
logger.warning(
|
|
60
|
+
"TFY_API_KEY env var is already set. "
|
|
61
|
+
"When running truefoundry, it will use the api key to authorize.\n"
|
|
62
|
+
"Login will just save the credentials on disk."
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
host = resolve_base_url(host).strip("/")
|
|
66
|
+
|
|
67
|
+
with CredentialsFileManager() as cred_file:
|
|
68
|
+
if not relogin and cred_file.exists():
|
|
69
|
+
cred_file_content = cred_file.read()
|
|
70
|
+
if host != cred_file_content.host:
|
|
71
|
+
if click.confirm(
|
|
72
|
+
f"Already logged in to {cred_file_content.host!r}\n"
|
|
73
|
+
f"Do you want to relogin to {host!r}?"
|
|
74
|
+
):
|
|
75
|
+
return login(api_key=api_key, host=host, relogin=True)
|
|
76
|
+
|
|
77
|
+
user_info = cred_file_content.to_token().to_user_info()
|
|
78
|
+
user_name_display_info = user_info.email or user_info.user_type.value
|
|
79
|
+
output_hook.print_line(
|
|
80
|
+
f"Already logged in to {cred_file_content.host!r} as "
|
|
81
|
+
f"{user_info.user_id!r} ({user_name_display_info})\n"
|
|
82
|
+
"Please use `tfy login --relogin` or `tfy.login(relogin=True)` "
|
|
83
|
+
"to force relogin"
|
|
84
|
+
)
|
|
85
|
+
return False
|
|
86
|
+
|
|
87
|
+
if api_key:
|
|
88
|
+
servicefoundry_client = ServiceFoundryServiceClient(
|
|
89
|
+
init_session=False, base_url=host
|
|
90
|
+
)
|
|
91
|
+
token = _login_with_api_key(
|
|
92
|
+
api_key=api_key, servicefoundry_client=servicefoundry_client
|
|
93
|
+
)
|
|
94
|
+
else:
|
|
95
|
+
auth_service = AuthServiceClient(base_url=host)
|
|
96
|
+
# interactive login
|
|
97
|
+
token = _login_with_device_code(base_url=host, auth_service=auth_service)
|
|
98
|
+
|
|
99
|
+
cred_file_content = CredentialsFileContent(
|
|
100
|
+
access_token=token.access_token,
|
|
101
|
+
refresh_token=token.refresh_token,
|
|
102
|
+
host=host,
|
|
103
|
+
)
|
|
104
|
+
cred_file.write(cred_file_content)
|
|
105
|
+
|
|
106
|
+
user_info = token.to_user_info()
|
|
107
|
+
user_name_display_info = user_info.email or user_info.user_type.value
|
|
108
|
+
output_hook.print_line(
|
|
109
|
+
f"Successfully logged in to {cred_file_content.host!r} as "
|
|
110
|
+
f"{user_info.user_id!r} ({user_name_display_info})"
|
|
111
|
+
)
|
|
112
|
+
return True
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def logout(
|
|
116
|
+
output_hook: OutputCallBack = RICH_OUTPUT_CALLBACK,
|
|
117
|
+
) -> None:
|
|
118
|
+
with CredentialsFileManager() as cred_file:
|
|
119
|
+
if cred_file.delete():
|
|
120
|
+
output_hook.print_line(PROMPT_LOGOUT_SUCCESSFUL)
|
|
121
|
+
else:
|
|
122
|
+
output_hook.print_line(PROMPT_ALREADY_LOGGED_OUT)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _login_with_api_key(
|
|
126
|
+
api_key: str, servicefoundry_client: ServiceFoundryServiceClient
|
|
127
|
+
) -> Token:
|
|
128
|
+
logger.debug("Logging in with api key")
|
|
129
|
+
return servicefoundry_client.get_token_from_api_key(api_key=api_key)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _login_with_device_code(
|
|
133
|
+
base_url: str,
|
|
134
|
+
auth_service: AuthServiceClient,
|
|
135
|
+
output_hook: OutputCallBack = RICH_OUTPUT_CALLBACK,
|
|
136
|
+
) -> Token:
|
|
137
|
+
logger.debug("Logging in with device code")
|
|
138
|
+
device_code = auth_service.get_device_code()
|
|
139
|
+
url_to_go = device_code.get_user_clickable_url(auth_host=base_url)
|
|
140
|
+
output_hook.print_line(f"Opening:- {url_to_go}")
|
|
141
|
+
output_hook.print(
|
|
142
|
+
"Please click on the above link if it is not "
|
|
143
|
+
"automatically opened in a browser window."
|
|
144
|
+
)
|
|
145
|
+
click.launch(url_to_go)
|
|
146
|
+
return auth_service.get_token_from_device_code(device_code=device_code.device_code)
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import re
|
|
3
|
+
from typing import Union
|
|
4
|
+
|
|
5
|
+
from truefoundry.deploy.lib.const import (
|
|
6
|
+
SFY_DEBUG_ENV_KEY,
|
|
7
|
+
SFY_INTERNAL_ENV_KEY,
|
|
8
|
+
TFY_DEBUG_ENV_KEY,
|
|
9
|
+
TFY_INTERNAL_ENV_KEY,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def is_debug_env_set() -> bool:
|
|
14
|
+
return (
|
|
15
|
+
True if os.getenv(TFY_DEBUG_ENV_KEY) or os.getenv(SFY_DEBUG_ENV_KEY) else False
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def is_internal_env_set() -> bool:
|
|
20
|
+
return (
|
|
21
|
+
True
|
|
22
|
+
if os.getenv(TFY_INTERNAL_ENV_KEY) or os.getenv(SFY_INTERNAL_ENV_KEY)
|
|
23
|
+
else False
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def get_application_fqn_from_deployment_fqn(deployment_fqn: str) -> str:
|
|
28
|
+
if not re.search(r":\d+$", deployment_fqn):
|
|
29
|
+
raise ValueError(
|
|
30
|
+
"Invalid `deployment_fqn` format. A deployment fqn is supposed to end with a version number"
|
|
31
|
+
)
|
|
32
|
+
application_fqn, _ = deployment_fqn.rsplit(":", 1)
|
|
33
|
+
return application_fqn
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def get_deployment_fqn_from_application_fqn(
|
|
37
|
+
application_fqn: str, version: Union[str, int]
|
|
38
|
+
) -> str:
|
|
39
|
+
return f"{application_fqn}:{version}"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def is_notebook():
|
|
43
|
+
# https://stackoverflow.com/questions/15411967/how-can-i-check-if-code-is-executed-in-the-ipython-notebook
|
|
44
|
+
try:
|
|
45
|
+
# noinspection PyUnresolvedReferences
|
|
46
|
+
shell = get_ipython().__class__
|
|
47
|
+
if shell.__name__ == "ZMQInteractiveShell":
|
|
48
|
+
return True # Jupyter notebook or qtconsole
|
|
49
|
+
elif shell.__name__ == "TerminalInteractiveShell":
|
|
50
|
+
return False # Terminal running IPython
|
|
51
|
+
elif "google.colab" in str(shell):
|
|
52
|
+
return True # google colab notebook
|
|
53
|
+
else:
|
|
54
|
+
return False # Other type (?)
|
|
55
|
+
except NameError:
|
|
56
|
+
return False # Probably standard Python interpreter
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def find_list_paths(data, parent_key="", sep="."):
|
|
60
|
+
list_paths = []
|
|
61
|
+
if isinstance(data, dict):
|
|
62
|
+
for key, value in data.items():
|
|
63
|
+
new_key = f"{parent_key}{sep}{key}" if parent_key else key
|
|
64
|
+
list_paths.extend(find_list_paths(value, new_key, sep))
|
|
65
|
+
elif isinstance(data, list):
|
|
66
|
+
list_paths.append(parent_key)
|
|
67
|
+
for i, value in enumerate(data):
|
|
68
|
+
new_key = f"{parent_key}[{i}]"
|
|
69
|
+
list_paths.extend(find_list_paths(value, new_key, sep))
|
|
70
|
+
return list_paths
|