truefoundry 0.3.0rc10__py3-none-any.whl → 0.3.0rc12__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.

@@ -1,4 +1,5 @@
1
1
  import time
2
+ from abc import ABC, abstractmethod
2
3
 
3
4
  import requests
4
5
 
@@ -9,17 +10,114 @@ from truefoundry.deploy.lib.model.entity import DeviceCode, Token
9
10
  from truefoundry.logger import logger
10
11
 
11
12
 
12
- class AuthServiceClient:
13
+ class AuthServiceClient(ABC):
13
14
  def __init__(self, base_url):
14
15
  from truefoundry.deploy.lib.clients.servicefoundry_client import (
15
16
  ServiceFoundryServiceClient,
16
17
  )
17
18
 
18
19
  client = ServiceFoundryServiceClient(init_session=False, base_url=base_url)
19
- tenant_info = client.get_tenant_info()
20
20
 
21
- self._auth_server_url = tenant_info.auth_server_url
22
- self._tenant_name = tenant_info.tenant_name
21
+ self._api_server_url = client._api_server_url
22
+ self._auth_server_url = client.tenant_info.auth_server_url
23
+ self._tenant_name = client.tenant_info.tenant_name
24
+
25
+ @classmethod
26
+ def from_base_url(cls, base_url: str) -> "AuthServiceClient":
27
+ from truefoundry.deploy.lib.clients.servicefoundry_client import (
28
+ ServiceFoundryServiceClient,
29
+ )
30
+
31
+ client = ServiceFoundryServiceClient(init_session=False, base_url=base_url)
32
+ if client.python_sdk_config.use_sfy_server_auth_apis:
33
+ return ServiceFoundryServerAuthServiceClient(base_url)
34
+ return AuthServerServiceClient(base_url)
35
+
36
+ @abstractmethod
37
+ def refresh_token(self, token: Token, host: str = None) -> Token: ...
38
+
39
+ @abstractmethod
40
+ def get_device_code(self) -> DeviceCode: ...
41
+
42
+ @abstractmethod
43
+ def get_token_from_device_code(
44
+ self, device_code: str, timeout: float = 60, poll_interval_seconds: int = 1
45
+ ) -> Token: ...
46
+
47
+
48
+ class ServiceFoundryServerAuthServiceClient(AuthServiceClient):
49
+ def __init__(self, base_url):
50
+ super().__init__(base_url)
51
+
52
+ def refresh_token(self, token: Token, host: str = None) -> Token:
53
+ host_arg_str = f"--host {host}" if host else "--host HOST"
54
+ if not token.refresh_token:
55
+ # TODO: Add a way to propagate error messages without traceback to the output interface side
56
+ raise Exception(
57
+ f"Unable to resume login session. Please log in again using `tfy login {host_arg_str} --relogin`"
58
+ )
59
+ url = f"{self._api_server_url}/{VERSION_PREFIX}/oauth2/token"
60
+ data = {
61
+ "tenantName": token.tenant_name,
62
+ "refreshToken": token.refresh_token,
63
+ "grantType": "refresh_token",
64
+ "returnJWT": True,
65
+ }
66
+ res = requests.post(url, json=data)
67
+ try:
68
+ res = request_handling(res)
69
+ return Token.parse_obj(res)
70
+ except BadRequestException as ex:
71
+ raise Exception(
72
+ f"Unable to resume login session. Please log in again using `tfy login {host_arg_str} --relogin`"
73
+ ) from ex
74
+
75
+ def get_device_code(self) -> DeviceCode:
76
+ url = f"{self._api_server_url}/{VERSION_PREFIX}/oauth2/device-authorize"
77
+ data = {"tenantName": self._tenant_name}
78
+ res = requests.post(url, json=data)
79
+ res = request_handling(res)
80
+ return DeviceCode.parse_obj(res)
81
+
82
+ def get_token_from_device_code(
83
+ self, device_code: str, timeout: float = 60, poll_interval_seconds: int = 1
84
+ ) -> Token:
85
+ timeout = timeout or 60
86
+ poll_interval_seconds = poll_interval_seconds or 1
87
+ url = f"{self._api_server_url}/{VERSION_PREFIX}/oauth2/token"
88
+ data = {
89
+ "tenantName": self._tenant_name,
90
+ "deviceCode": device_code,
91
+ "grantType": "device_code",
92
+ "returnJWT": True,
93
+ }
94
+ response = requests.post(url=url, json=data)
95
+ start_time = time.monotonic()
96
+
97
+ for response in poll_for_function(
98
+ requests.post, poll_after_secs=poll_interval_seconds, url=url, json=data
99
+ ):
100
+ if response.status_code == 201:
101
+ response = response.json()
102
+ return Token.parse_obj(response)
103
+ elif response.status_code == 202:
104
+ logger.debug("User has not authorized yet. Checking again.")
105
+ else:
106
+ raise Exception(
107
+ "Failed to get token using device code. "
108
+ f"status_code {response.status_code},\n {response.text}"
109
+ )
110
+ time_elapsed = time.monotonic() - start_time
111
+ if time_elapsed > timeout:
112
+ logger.warning("Polled server for %s secs.", int(time_elapsed))
113
+ break
114
+
115
+ raise Exception(f"Did not get authorized within {timeout} seconds.")
116
+
117
+
118
+ class AuthServerServiceClient(AuthServiceClient):
119
+ def __init__(self, base_url):
120
+ super().__init__(base_url)
23
121
 
24
122
  def refresh_token(self, token: Token, host: str = None) -> Token:
25
123
  host_arg_str = f"--host {host}" if host else "--host HOST"
@@ -33,7 +131,7 @@ class AuthServiceClient:
33
131
  "tenantName": token.tenant_name,
34
132
  "refreshToken": token.refresh_token,
35
133
  }
36
- res = requests.post(url, data=data)
134
+ res = requests.post(url, json=data)
37
135
  try:
38
136
  res = request_handling(res)
39
137
  return Token.parse_obj(res)
@@ -45,12 +143,14 @@ class AuthServiceClient:
45
143
  def get_device_code(self) -> DeviceCode:
46
144
  url = f"{self._auth_server_url}/api/{VERSION_PREFIX}/oauth/device"
47
145
  data = {"tenantName": self._tenant_name}
48
- res = requests.post(url, data=data)
146
+ res = requests.post(url, json=data)
49
147
  res = request_handling(res)
148
+ # TODO: temporary cleanup of incorrect attributes
149
+ res = {"userCode": res.get("userCode"), "deviceCode": res.get("deviceCode")}
50
150
  return DeviceCode.parse_obj(res)
51
151
 
52
152
  def get_token_from_device_code(
53
- self, device_code: str, timeout: float = 60
153
+ self, device_code: str, timeout: float = 60, poll_interval_seconds: int = 1
54
154
  ) -> Token:
55
155
  url = f"{self._auth_server_url}/api/{VERSION_PREFIX}/oauth/device/token"
56
156
  data = {
@@ -61,7 +161,7 @@ class AuthServiceClient:
61
161
  poll_interval_seconds = 1
62
162
 
63
163
  for response in poll_for_function(
64
- requests.post, poll_after_secs=poll_interval_seconds, url=url, data=data
164
+ requests.post, poll_after_secs=poll_interval_seconds, url=url, json=data
65
165
  ):
66
166
  if response.status_code == 201:
67
167
  response = response.json()
@@ -44,7 +44,7 @@ class EnvCredentialProvider(CredentialProvider):
44
44
  # TODO: Read host from cred file as well.
45
45
  base_url = resolve_base_url().strip("/")
46
46
  self._host = base_url
47
- self._auth_service = AuthServiceClient(base_url=base_url)
47
+ self._auth_service = AuthServiceClient.from_base_url(base_url=base_url)
48
48
 
49
49
  servicefoundry_client = ServiceFoundryServiceClient(
50
50
  init_session=False, base_url=base_url
@@ -82,7 +82,7 @@ class FileCredentialProvider(CredentialProvider):
82
82
  self._token = self._last_cred_file_content.to_token()
83
83
  self._host = self._last_cred_file_content.host
84
84
 
85
- self._auth_service = AuthServiceClient(base_url=self._host)
85
+ self._auth_service = AuthServiceClient.from_base_url(base_url=self._host)
86
86
 
87
87
  @staticmethod
88
88
  def can_provide() -> bool:
@@ -26,12 +26,14 @@ from truefoundry.deploy.lib.model.entity import (
26
26
  Deployment,
27
27
  DockerRegistryCredentials,
28
28
  JobRun,
29
+ PythonSDKConfig,
29
30
  TenantInfo,
30
31
  Token,
31
32
  TriggerJobResult,
32
33
  Workspace,
33
34
  WorkspaceResources,
34
35
  )
36
+ from truefoundry.deploy.lib.util import timed_lru_cache
35
37
  from truefoundry.deploy.lib.win32 import allow_interrupt
36
38
  from truefoundry.deploy.v2.lib.models import (
37
39
  AppDeploymentStatusResponse,
@@ -93,6 +95,24 @@ def check_min_cli_version(fn):
93
95
  return inner
94
96
 
95
97
 
98
+ @timed_lru_cache(seconds=30 * 60)
99
+ def _cached_get_tenant_info(api_server_url: str) -> TenantInfo:
100
+ res = requests.get(
101
+ url=f"{api_server_url}/v1/tenant-id",
102
+ params={"hostName": urlparse(api_server_url).netloc},
103
+ )
104
+ res = request_handling(res)
105
+ return TenantInfo.parse_obj(res)
106
+
107
+
108
+ @timed_lru_cache(seconds=30 * 60)
109
+ def _cached_get_python_sdk_config(api_server_url: str) -> PythonSDKConfig:
110
+ url = f"{api_server_url}/v1/min-cli-version"
111
+ res = requests.get(url)
112
+ res = request_handling(res)
113
+ return PythonSDKConfig.parse_obj(res)
114
+
115
+
96
116
  class ServiceFoundryServiceClient:
97
117
  def __init__(self, init_session: bool = True, base_url: Optional[str] = None):
98
118
  self._session: Optional[ServiceFoundrySession] = None
@@ -111,20 +131,19 @@ class ServiceFoundryServiceClient:
111
131
  def base_url(self) -> str:
112
132
  return self._base_url
113
133
 
114
- def get_tenant_info(self) -> TenantInfo:
115
- res = requests.get(
116
- url=f"{self._api_server_url}/v1/tenant-id",
117
- params={"hostName": urlparse(self._api_server_url).netloc},
118
- )
119
- res = request_handling(res)
120
- return TenantInfo.parse_obj(res)
134
+ @property
135
+ def tenant_info(self) -> TenantInfo:
136
+ return _cached_get_tenant_info(self._api_server_url)
137
+
138
+ @property
139
+ def python_sdk_config(self) -> PythonSDKConfig:
140
+ return _cached_get_python_sdk_config(self._api_server_url)
121
141
 
122
142
  @functools.cached_property
123
143
  def _min_cli_version_required(self) -> str:
124
- url = f"{self._api_server_url}/v1/min-cli-version"
125
- res = requests.get(url)
126
- res = request_handling(res)
127
- return res["truefoundryCliMinVersion"]
144
+ return _cached_get_python_sdk_config(
145
+ self._api_server_url
146
+ ).truefoundry_cli_min_version
128
147
 
129
148
  def _get_header(self):
130
149
  if not self._session:
@@ -297,6 +297,11 @@ class CredentialsFileContent(BaseModel):
297
297
  class DeviceCode(BaseModel):
298
298
  user_code: str = Field(alias="userCode")
299
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")
300
305
 
301
306
  class Config:
302
307
  allow_population_by_field_name = True
@@ -306,6 +311,18 @@ class DeviceCode(BaseModel):
306
311
  return f"{auth_host}/authorize/device?userCode={self.user_code}"
307
312
 
308
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
+
309
326
  class JobRun(Base):
310
327
  name: str
311
328
  applicationName: str
@@ -92,7 +92,7 @@ def login(
92
92
  api_key=api_key, servicefoundry_client=servicefoundry_client
93
93
  )
94
94
  else:
95
- auth_service = AuthServiceClient(base_url=host)
95
+ auth_service = AuthServiceClient.from_base_url(base_url=host)
96
96
  # interactive login
97
97
  token = _login_with_device_code(base_url=host, auth_service=auth_service)
98
98
 
@@ -136,11 +136,23 @@ def _login_with_device_code(
136
136
  ) -> Token:
137
137
  logger.debug("Logging in with device code")
138
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."
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
+ output_hook.print_line(message)
151
+ if auto_open_url:
152
+ output_hook.print_line(f"Opening:- {auto_open_url}")
153
+ click.launch(auto_open_url)
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,
144
158
  )
145
- click.launch(url_to_go)
146
- return auth_service.get_token_from_device_code(device_code=device_code.device_code)
@@ -1,5 +1,7 @@
1
1
  import os
2
2
  import re
3
+ from functools import lru_cache, wraps
4
+ from time import monotonic_ns
3
5
  from typing import Union
4
6
 
5
7
  from truefoundry.deploy.lib.const import (
@@ -68,3 +70,21 @@ def find_list_paths(data, parent_key="", sep="."):
68
70
  new_key = f"{parent_key}[{i}]"
69
71
  list_paths.extend(find_list_paths(value, new_key, sep))
70
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: truefoundry
3
- Version: 0.3.0rc10
3
+ Version: 0.3.0rc12
4
4
  Summary: Truefoundry CLI
5
5
  Author: Abhishek Choudhary
6
6
  Author-email: abhishek@truefoundry.com
@@ -24,7 +24,7 @@ Requires-Dist: flytekit (==1.12.2) ; extra == "workflow"
24
24
  Requires-Dist: gitignorefile (>=1.1.2,<1.2.0)
25
25
  Requires-Dist: importlib-metadata (>=6.0.1,<8.0.0)
26
26
  Requires-Dist: importlib-resources (>=5.2.0,<6.0.0)
27
- Requires-Dist: mlfoundry (==0.11.3) ; extra == "ml"
27
+ Requires-Dist: mlfoundry (==0.11.4) ; extra == "ml"
28
28
  Requires-Dist: openai (>=1.16.2,<2.0.0)
29
29
  Requires-Dist: packaging (>=20.0,<25.0)
30
30
  Requires-Dist: pydantic (>=1.10.0,<3)
@@ -78,12 +78,12 @@ truefoundry/deploy/io/output_callback.py,sha256=V2YwUFec4G4a67lM4r-x_64AqdOVNo_9
78
78
  truefoundry/deploy/io/rich_output_callback.py,sha256=TJLiRD-EnFVwgcepxR7WN0koKqW1X2DevETPhNPi_nU,829
79
79
  truefoundry/deploy/json_util.py,sha256=x_-7YYQ4_HUIJ8ofOcclAp9JWhgTWjR9Th6Q0FuRqGk,175
80
80
  truefoundry/deploy/lib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
81
- truefoundry/deploy/lib/auth/auth_service_client.py,sha256=rrz2t6lkNgVBoWsIrdyaK9OHN5XZJtA4AAqaDx3PQyc,3299
81
+ truefoundry/deploy/lib/auth/auth_service_client.py,sha256=C3sgoDZ2RUijJY_nStqt-BmSwUAdjVa8yAhazOGz710,7386
82
82
  truefoundry/deploy/lib/auth/credential_file_manager.py,sha256=DXeXWoVakfZI2Geu8Futwef_eVMHrZ1et7d9MIXOhhY,4233
83
- truefoundry/deploy/lib/auth/credential_provider.py,sha256=MwTN8170TXi7g9m2Fw4VRReZqk1DzBFG1bMVgd7jYZk,4375
83
+ truefoundry/deploy/lib/auth/credential_provider.py,sha256=eXgfA2-q20XxslOmlocmi-rKQ5AtBxVMGuftfrLVlb4,4403
84
84
  truefoundry/deploy/lib/auth/servicefoundry_session.py,sha256=2OahwRg-8l3QUwpC2iLA8lHavkeTxKXUP_0AN27HPS8,1859
85
85
  truefoundry/deploy/lib/clients/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
86
- truefoundry/deploy/lib/clients/servicefoundry_client.py,sha256=NASbj67lrQKZwhu7mVu6ngE9A8C76FqqUpfFYQKjVAU,26394
86
+ truefoundry/deploy/lib/clients/servicefoundry_client.py,sha256=yqeaF8nFeqR01q9d7E2s7sDQVpgF1ES9CECXpKNMT5g,26945
87
87
  truefoundry/deploy/lib/clients/shell_client.py,sha256=tMrc0Ha1DmGtUCJrZD8eusOzfe8R_WIe6AAH7nxL0xA,461
88
88
  truefoundry/deploy/lib/clients/utils.py,sha256=rK7DrvA71kSTjy23-Bk4LTQjgViBWeHtcV_SlBLZw6M,1282
89
89
  truefoundry/deploy/lib/const.py,sha256=Yk_nXeZWzwKs-6hEXDjVDyjwEGh35T5TWaBxyJgP-Zw,1395
@@ -96,9 +96,9 @@ truefoundry/deploy/lib/exceptions.py,sha256=ZW44bSwmlcr3GcB10E8Jz-zlgu9wyi8tE7xA
96
96
  truefoundry/deploy/lib/logs_utils.py,sha256=SQxRv3jDDmgHdOUMhlMaAPGYskybnBUMpst7QU_i_sc,1469
97
97
  truefoundry/deploy/lib/messages.py,sha256=nhp0bCYf_XpUM68hTq5lBY-__vtEyV2uP7NgnJXJ_Vg,925
98
98
  truefoundry/deploy/lib/model/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
99
- truefoundry/deploy/lib/model/entity.py,sha256=gFlSG6RVuaWQQ2v3YtONhnTbIRzKUIryMgcJN6HmZV4,11309
100
- truefoundry/deploy/lib/session.py,sha256=0S9foO78BXYSwCh6nPaWG-QuwgxaK4VZqzshjNkyXls,5229
101
- truefoundry/deploy/lib/util.py,sha256=3Pc0gQgomapTSjMQo743STShBn1B6PNdBpw3bTpdxOw,2200
99
+ truefoundry/deploy/lib/model/entity.py,sha256=aNiQ7kK_V1kH7C_VC6HP7ibas_9Ohkoqzs-WdTfrrf4,12037
100
+ truefoundry/deploy/lib/session.py,sha256=WumH3cfVr8DqcuY5ngdnPBI_oeMUxG_JqyONQYKlM6k,5844
101
+ truefoundry/deploy/lib/util.py,sha256=sNMv9peDU7RTCDaAfyWPpPpCs_WcZGqOvc13Jqx9y6w,2809
102
102
  truefoundry/deploy/lib/win32.py,sha256=1RcvPTdlOAJ48rt8rCbE2Ufha2ztRqBAE9dueNXArrY,5009
103
103
  truefoundry/deploy/v2/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
104
104
  truefoundry/deploy/v2/lib/__init__.py,sha256=WEiVMZXOVljzEE3tpGJil14liIn_PCDoACJ6b3tZ6sI,188
@@ -130,7 +130,7 @@ truefoundry/workflow/map_task.py,sha256=2m3qGXQ90k9LdS45q8dqCCECc3qr8t2m_LMCVd1m
130
130
  truefoundry/workflow/python_task.py,sha256=SRXRLC4vdBqGjhkwuaY39LEWN6iPCpJAuW17URRdWTY,1128
131
131
  truefoundry/workflow/task.py,sha256=ToitYiKcNzFCtOVQwz1W8sRjbR97eVS7vQBdbgUQtKg,1779
132
132
  truefoundry/workflow/workflow.py,sha256=WaTqUjhwfAXDWu4E5ehuwAxrCbDJkoAf1oWmR2E9Qy0,4575
133
- truefoundry-0.3.0rc10.dist-info/METADATA,sha256=ycIiBcPg1h3rNv8TSK3OHc4PHWDH6DGVP9f0MC5acSo,2694
134
- truefoundry-0.3.0rc10.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
135
- truefoundry-0.3.0rc10.dist-info/entry_points.txt,sha256=TXvUxQkI6zmqJuycPsyxEIMr3oqfDjgrWj0m_9X12x4,95
136
- truefoundry-0.3.0rc10.dist-info/RECORD,,
133
+ truefoundry-0.3.0rc12.dist-info/METADATA,sha256=UHVlgtge2QopJNqTeUfoy51XbmFNbTrCKSMC5HUieug,2694
134
+ truefoundry-0.3.0rc12.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
135
+ truefoundry-0.3.0rc12.dist-info/entry_points.txt,sha256=TXvUxQkI6zmqJuycPsyxEIMr3oqfDjgrWj0m_9X12x4,95
136
+ truefoundry-0.3.0rc12.dist-info/RECORD,,