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.

Files changed (112) hide show
  1. truefoundry/__init__.py +1 -0
  2. truefoundry/autodeploy/cli.py +31 -18
  3. truefoundry/deploy/__init__.py +112 -1
  4. truefoundry/deploy/auto_gen/models.py +1714 -0
  5. truefoundry/deploy/builder/__init__.py +134 -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 +46 -0
  9. truefoundry/deploy/builder/builders/tfy_notebook_buildpack/dockerfile_template.py +66 -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 +21 -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 +102 -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 +81 -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 +145 -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 +129 -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 +181 -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 +746 -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 +263 -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 +26 -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 +400 -0
  74. truefoundry/deploy/lib/session.py +158 -0
  75. truefoundry/deploy/lib/util.py +90 -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 +283 -0
  80. truefoundry/deploy/v2/lib/deploy_workflow.py +295 -0
  81. truefoundry/deploy/v2/lib/deployable_patched_models.py +86 -0
  82. truefoundry/deploy/v2/lib/models.py +53 -0
  83. truefoundry/deploy/v2/lib/patched_models.py +479 -0
  84. truefoundry/deploy/v2/lib/source.py +267 -0
  85. truefoundry/langchain/__init__.py +12 -1
  86. truefoundry/langchain/deprecated.py +302 -0
  87. truefoundry/langchain/truefoundry_chat.py +130 -0
  88. truefoundry/langchain/truefoundry_embeddings.py +171 -0
  89. truefoundry/langchain/truefoundry_llm.py +106 -0
  90. truefoundry/langchain/utils.py +85 -0
  91. truefoundry/logger.py +17 -0
  92. truefoundry/pydantic_v1.py +5 -0
  93. truefoundry/python_deploy_codegen.py +132 -0
  94. truefoundry/version.py +6 -0
  95. truefoundry/workflow/__init__.py +19 -0
  96. truefoundry/workflow/container_task.py +12 -0
  97. truefoundry/workflow/example/deploy.sh +1 -0
  98. truefoundry/workflow/example/hello_world_package/workflow.py +20 -0
  99. truefoundry/workflow/example/package/test_workflow.py +152 -0
  100. truefoundry/workflow/example/truefoundry.yaml +9 -0
  101. truefoundry/workflow/example/workflow.yaml +116 -0
  102. truefoundry/workflow/map_task.py +45 -0
  103. truefoundry/workflow/python_task.py +32 -0
  104. truefoundry/workflow/task.py +50 -0
  105. truefoundry/workflow/workflow.py +114 -0
  106. {truefoundry-0.2.10.dist-info → truefoundry-0.3.0.dist-info}/METADATA +27 -7
  107. truefoundry-0.3.0.dist-info/RECORD +136 -0
  108. truefoundry/deploy/cli/deploy.py +0 -165
  109. truefoundry/deploy/cli/version.py +0 -6
  110. truefoundry-0.2.10.dist-info/RECORD +0 -38
  111. {truefoundry-0.2.10.dist-info → truefoundry-0.3.0.dist-info}/WHEEL +0 -0
  112. {truefoundry-0.2.10.dist-info → truefoundry-0.3.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,400 @@
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
+ COMPONENTS_DEPLOYING: str = "COMPONENTS_DEPLOYING"
88
+ FAILED: str = "FAILED"
89
+ REUSING_EXISTING_BUILD: str = "REUSING_EXISTING_BUILD"
90
+ REDEPLOY_STARTED: str = "REDEPLOY_STARTED"
91
+ PAUSED: str = "PAUSED"
92
+ ROLLOUT_STARTED: str = "ROLLOUT_STARTED"
93
+ SET_TRAFFIC: str = "SET_TRAFFIC"
94
+ DEPLOY_FAILED_WITH_RETRY: str = "DEPLOY_FAILED_WITH_RETRY"
95
+ WAITING: str = "WAITING"
96
+
97
+ _: str = ""
98
+
99
+ @classmethod
100
+ def is_failure_state(cls, state: DeploymentTransitionStatus) -> bool:
101
+ return state in (cls.DEPLOY_FAILED, cls.BUILD_FAILED, cls.FAILED, cls.CANCELLED)
102
+
103
+
104
+ class DeploymentState(BaseModel):
105
+ isTerminalState: bool
106
+
107
+
108
+ class DeploymentStatus(BaseModel):
109
+ state: DeploymentState
110
+ status: DeploymentTransitionStatus
111
+ transition: Optional[DeploymentTransitionStatus]
112
+
113
+
114
+ class DeploymentManifest(Base):
115
+ name: Optional[str] = Field(default=UNDEFINED_STRING)
116
+ type: str
117
+
118
+
119
+ class Deployment(Entity):
120
+ id: str = Field(repr=False)
121
+ fqn: str
122
+ version: int
123
+ currentStatusId: str = Field(repr=False)
124
+ applicationId: str = Field(repr=False)
125
+ manifest: DeploymentManifest = Field(repr=False)
126
+ failureReason: Optional[str]
127
+ deploymentStatuses: Optional[List[DeploymentStatus]]
128
+ metadata: Optional[
129
+ Union[Dict[str, Any], List[Dict[str, Any]]]
130
+ ] # TODO (chiragjn): revisit the type of this field
131
+ currentStatus: Optional[DeploymentStatus] # Server was not returning CurrentStatus
132
+ application_fqn: str
133
+
134
+ def __init__(self, **kwargs) -> None:
135
+ deployment_fqn = kwargs.get("fqn")
136
+ if not deployment_fqn:
137
+ raise ValueError("'fqn' field is required")
138
+ application_fqn = ":".join(deployment_fqn.split(":")[:3])
139
+ kwargs["application_fqn"] = application_fqn
140
+ super().__init__(**kwargs)
141
+
142
+ @property
143
+ def application_fqn(self) -> str:
144
+ return get_application_fqn_from_deployment_fqn(self.fqn)
145
+
146
+ def list_row_data(self) -> Dict[str, Any]:
147
+ return {
148
+ "fqn": self.fqn,
149
+ "application_name": self.manifest.name,
150
+ "version": self.version,
151
+ "created_at": self.createdAt,
152
+ }
153
+
154
+ def get_data(self) -> Dict[str, Any]:
155
+ # TODO: Remove this splitting logic
156
+ cluster_fqn, workspace_name, *_ = self.fqn.split(":")
157
+ workspace_fqn = ":".join([cluster_fqn, workspace_name])
158
+ return {
159
+ "fqn": self.fqn,
160
+ "application_name": self.manifest.name,
161
+ "application_type": self.manifest.type,
162
+ "version": self.version,
163
+ "workspace_fqn": workspace_fqn,
164
+ "cluster_fqn": cluster_fqn,
165
+ "created_at": self.createdAt,
166
+ "updated_at": self.updatedAt,
167
+ "created_by": self.createdBy,
168
+ }
169
+
170
+ class Config:
171
+ extra = Extra.allow
172
+
173
+
174
+ class ApplicationWorkspace(Base):
175
+ name: str
176
+ fqn: str
177
+ clusterId: str
178
+
179
+
180
+ class Application(Entity):
181
+ id: str = Field(repr=False)
182
+ name: str
183
+ fqn: str
184
+ tenantName: str
185
+ workspaceId: str = Field(repr=False)
186
+ lastVersion: int
187
+ activeVersion: int
188
+ workspace: ApplicationWorkspace
189
+ deployment: Deployment
190
+ activeDeploymentId: Optional[str] = Field(repr=False)
191
+ lastDeploymentId: str = Field(repr=False)
192
+
193
+ def list_row_data(self) -> Dict[str, Any]:
194
+ return {
195
+ "name": self.name,
196
+ "type": self.deployment.manifest.type,
197
+ "fqn": self.fqn,
198
+ "active_version": self.activeVersion,
199
+ "workspace_name": self.workspace.name,
200
+ "created_at": self.createdAt,
201
+ }
202
+
203
+ def get_data(self) -> Dict[str, Any]:
204
+ return {
205
+ "name": self.name,
206
+ "type": self.deployment.manifest.type,
207
+ "fqn": self.fqn,
208
+ "active_version": self.activeVersion,
209
+ "last_version": self.lastVersion,
210
+ "workspace_fqn": self.workspace.fqn,
211
+ "cluster_fqn": self.workspace.clusterId,
212
+ "created_at": self.createdAt,
213
+ "updated_at": self.updatedAt,
214
+ "created_by": self.createdBy,
215
+ }
216
+
217
+
218
+ class UserType(Enum):
219
+ user = "user"
220
+ serviceaccount = "serviceaccount"
221
+
222
+
223
+ class UserInfo(BaseModel):
224
+ user_id: constr(min_length=1)
225
+ user_type: UserType = UserType.user
226
+ email: Optional[str] = None
227
+ tenant_name: constr(min_length=1) = Field(alias="tenantName")
228
+
229
+ class Config:
230
+ allow_population_by_field_name = True
231
+ allow_mutation = False
232
+
233
+
234
+ class TenantInfo(BaseModel):
235
+ tenant_name: constr(min_length=1) = Field(alias="tenantName")
236
+ auth_server_url: str
237
+
238
+ class Config:
239
+ allow_population_by_field_name = True
240
+ allow_mutation = False
241
+
242
+
243
+ class Token(BaseModel):
244
+ access_token: constr(min_length=1) = Field(alias="accessToken", repr=False)
245
+ refresh_token: Optional[constr(min_length=1)] = Field(
246
+ alias="refreshToken", repr=False
247
+ )
248
+ decoded_value: Optional[Dict] = Field(exclude=True, repr=False)
249
+
250
+ class Config:
251
+ allow_population_by_field_name = True
252
+ allow_mutation = False
253
+
254
+ @validator("decoded_value", always=True, pre=True)
255
+ def _decode_jwt(cls, v, values, **kwargs):
256
+ access_token = values["access_token"]
257
+ return jwt.decode(
258
+ access_token,
259
+ options={
260
+ "verify_signature": False,
261
+ "verify_aud": False,
262
+ "verify_exp": False,
263
+ },
264
+ )
265
+
266
+ @property
267
+ def tenant_name(self) -> str:
268
+ return self.decoded_value["tenantName"]
269
+
270
+ def is_going_to_be_expired(self, buffer_in_seconds: int = 120) -> bool:
271
+ exp = int(self.decoded_value["exp"])
272
+ return (exp - time.time()) < buffer_in_seconds
273
+
274
+ def to_user_info(self) -> UserInfo:
275
+ return UserInfo(
276
+ user_id=self.decoded_value["username"],
277
+ email=self.decoded_value["email"]
278
+ if "email" in self.decoded_value
279
+ else None,
280
+ user_type=UserType(self.decoded_value.get("userType", UserType.user.value)),
281
+ tenant_name=self.tenant_name,
282
+ )
283
+
284
+
285
+ class CredentialsFileContent(BaseModel):
286
+ access_token: constr(min_length=1) = Field(repr=False)
287
+ refresh_token: Optional[constr(min_length=1)] = Field(repr=False)
288
+ host: constr(min_length=1)
289
+
290
+ class Config:
291
+ allow_mutation = False
292
+
293
+ def to_token(self) -> Token:
294
+ return Token(access_token=self.access_token, refresh_token=self.refresh_token)
295
+
296
+
297
+ class DeviceCode(BaseModel):
298
+ user_code: str = Field(alias="userCode")
299
+ device_code: str = Field(alias="deviceCode")
300
+ verification_url: Optional[str] = Field(alias="verificationURI")
301
+ complete_verification_url: Optional[str] = Field(alias="verificationURIComplete")
302
+ expires_in_seconds: int = Field(alias="expiresInSeconds", default=60)
303
+ interval_in_seconds: int = Field(alias="intervalInSeconds", default=1)
304
+ message: Optional[str] = Field(alias="message")
305
+
306
+ class Config:
307
+ allow_population_by_field_name = True
308
+ allow_mutation = False
309
+
310
+ def get_user_clickable_url(self, auth_host: str) -> str:
311
+ return f"{auth_host}/authorize/device?userCode={self.user_code}"
312
+
313
+
314
+ class PythonSDKConfig(BaseModel):
315
+ min_version: str = Field(alias="minVersion")
316
+ truefoundry_cli_min_version: str = Field(alias="truefoundryCliMinVersion")
317
+ use_sfy_server_auth_apis: Optional[bool] = Field(
318
+ alias="useSFYServerAuthAPIs", default=False
319
+ )
320
+
321
+ class Config:
322
+ allow_population_by_field_name = True
323
+ allow_mutation = False
324
+
325
+
326
+ class JobRun(Base):
327
+ name: str
328
+ applicationName: str
329
+ deploymentVersion: int
330
+ createdAt: int
331
+ endTime: Optional[int] = None
332
+ duration: Optional[str] = None
333
+ command: str
334
+ totalRetries: Optional[int] = 0
335
+ status: str
336
+
337
+ def list_row_data(self) -> Dict[str, Any]:
338
+ from truefoundry.deploy.cli.display_util import display_time_passed
339
+
340
+ triggered_at = (
341
+ (datetime.datetime.now().timestamp() * 1000) - self.createdAt
342
+ ) // 1000
343
+ triggered_at = f"{display_time_passed(triggered_at)} ago"
344
+ duration = ""
345
+ if self.duration:
346
+ duration = display_time_passed(int(float(self.duration)))
347
+ return {
348
+ "name": self.name,
349
+ "deployment_version": self.deploymentVersion,
350
+ "status": self.status,
351
+ "triggered_at": triggered_at,
352
+ "duration": duration,
353
+ }
354
+
355
+ def get_data(self) -> Dict[str, Any]:
356
+ from truefoundry.deploy.cli.display_util import display_time_passed
357
+
358
+ created_at = datetime.datetime.fromtimestamp(self.createdAt // 1000)
359
+ end_time = ""
360
+ if self.endTime:
361
+ end_time = datetime.datetime.fromtimestamp(self.endTime // 1000)
362
+ duration = ""
363
+ if self.duration:
364
+ duration = display_time_passed(int(float(self.duration)))
365
+ return {
366
+ "name": self.name,
367
+ "application_name": self.applicationName,
368
+ "deployment_version": self.deploymentVersion,
369
+ "created_at": created_at,
370
+ "end_time": end_time,
371
+ "duration": duration,
372
+ "command": self.command,
373
+ "status": self.status,
374
+ }
375
+
376
+
377
+ class TriggerJobResult(Base):
378
+ message: str = Field(default="Unknown")
379
+ jobRunName: str = Field(default=None)
380
+
381
+
382
+ class DockerRegistryCredentials(Base):
383
+ fqn: str
384
+ registryUrl: str
385
+ username: str
386
+ password: str
387
+
388
+
389
+ class CreateDockerRepositoryResponse(Base):
390
+ repoName: str
391
+
392
+
393
+ class ApplyResult(BaseModel):
394
+ success: bool
395
+ message: str
396
+
397
+
398
+ class Manifest(Base):
399
+ type: str
400
+ name: str
@@ -0,0 +1,158 @@
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.from_base_url(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
+ auto_open_url = None
140
+ message = "Please click on the above link if it is not automatically opened in a browser window."
141
+ if device_code.complete_verification_url:
142
+ auto_open_url = device_code.complete_verification_url
143
+ elif device_code.verification_url:
144
+ if device_code.message:
145
+ message = device_code.message
146
+ else:
147
+ message = f"Please open the following URL in a browser and enter the code {device_code.user_code} when prompted: {device_code.verification_url}"
148
+ else:
149
+ auto_open_url = device_code.get_user_clickable_url(auth_host=base_url)
150
+ if auto_open_url:
151
+ output_hook.print_line(f"Opening:- {auto_open_url}")
152
+ click.launch(auto_open_url)
153
+ output_hook.print_line(message)
154
+ return auth_service.get_token_from_device_code(
155
+ device_code=device_code.device_code,
156
+ timeout=device_code.expires_in_seconds,
157
+ poll_interval_seconds=device_code.interval_in_seconds,
158
+ )
@@ -0,0 +1,90 @@
1
+ import os
2
+ import re
3
+ from functools import lru_cache, wraps
4
+ from time import monotonic_ns
5
+ from typing import Union
6
+
7
+ from truefoundry.deploy.lib.const import (
8
+ SFY_DEBUG_ENV_KEY,
9
+ SFY_INTERNAL_ENV_KEY,
10
+ TFY_DEBUG_ENV_KEY,
11
+ TFY_INTERNAL_ENV_KEY,
12
+ )
13
+
14
+
15
+ def is_debug_env_set() -> bool:
16
+ return (
17
+ True if os.getenv(TFY_DEBUG_ENV_KEY) or os.getenv(SFY_DEBUG_ENV_KEY) else False
18
+ )
19
+
20
+
21
+ def is_internal_env_set() -> bool:
22
+ return (
23
+ True
24
+ if os.getenv(TFY_INTERNAL_ENV_KEY) or os.getenv(SFY_INTERNAL_ENV_KEY)
25
+ else False
26
+ )
27
+
28
+
29
+ def get_application_fqn_from_deployment_fqn(deployment_fqn: str) -> str:
30
+ if not re.search(r":\d+$", deployment_fqn):
31
+ raise ValueError(
32
+ "Invalid `deployment_fqn` format. A deployment fqn is supposed to end with a version number"
33
+ )
34
+ application_fqn, _ = deployment_fqn.rsplit(":", 1)
35
+ return application_fqn
36
+
37
+
38
+ def get_deployment_fqn_from_application_fqn(
39
+ application_fqn: str, version: Union[str, int]
40
+ ) -> str:
41
+ return f"{application_fqn}:{version}"
42
+
43
+
44
+ def is_notebook():
45
+ # https://stackoverflow.com/questions/15411967/how-can-i-check-if-code-is-executed-in-the-ipython-notebook
46
+ try:
47
+ # noinspection PyUnresolvedReferences
48
+ shell = get_ipython().__class__
49
+ if shell.__name__ == "ZMQInteractiveShell":
50
+ return True # Jupyter notebook or qtconsole
51
+ elif shell.__name__ == "TerminalInteractiveShell":
52
+ return False # Terminal running IPython
53
+ elif "google.colab" in str(shell):
54
+ return True # google colab notebook
55
+ else:
56
+ return False # Other type (?)
57
+ except NameError:
58
+ return False # Probably standard Python interpreter
59
+
60
+
61
+ def find_list_paths(data, parent_key="", sep="."):
62
+ list_paths = []
63
+ if isinstance(data, dict):
64
+ for key, value in data.items():
65
+ new_key = f"{parent_key}{sep}{key}" if parent_key else key
66
+ list_paths.extend(find_list_paths(value, new_key, sep))
67
+ elif isinstance(data, list):
68
+ list_paths.append(parent_key)
69
+ for i, value in enumerate(data):
70
+ new_key = f"{parent_key}[{i}]"
71
+ list_paths.extend(find_list_paths(value, new_key, sep))
72
+ return list_paths
73
+
74
+
75
+ def timed_lru_cache(seconds: int = 300, maxsize: int = None):
76
+ def wrapper_cache(func):
77
+ func = lru_cache(maxsize=maxsize)(func)
78
+ func.delta = seconds * 10**9
79
+ func.expiration = monotonic_ns() + func.delta
80
+
81
+ @wraps(func)
82
+ def wrapped_func(*args, **kwargs):
83
+ if monotonic_ns() >= func.expiration:
84
+ func.cache_clear()
85
+ func.expiration = monotonic_ns() + func.delta
86
+ return func(*args, **kwargs)
87
+
88
+ return wrapped_func
89
+
90
+ return wrapper_cache