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.
- truefoundry/__init__.py +1 -0
- truefoundry/autodeploy/cli.py +31 -18
- truefoundry/deploy/__init__.py +112 -1
- truefoundry/deploy/auto_gen/models.py +1714 -0
- truefoundry/deploy/builder/__init__.py +134 -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 +46 -0
- truefoundry/deploy/builder/builders/tfy_notebook_buildpack/dockerfile_template.py +66 -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 +21 -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 +102 -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 +81 -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 +145 -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 +129 -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 +181 -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 +746 -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 +263 -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 +26 -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 +400 -0
- truefoundry/deploy/lib/session.py +158 -0
- truefoundry/deploy/lib/util.py +90 -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 +283 -0
- truefoundry/deploy/v2/lib/deploy_workflow.py +295 -0
- truefoundry/deploy/v2/lib/deployable_patched_models.py +86 -0
- truefoundry/deploy/v2/lib/models.py +53 -0
- truefoundry/deploy/v2/lib/patched_models.py +479 -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/version.py +6 -0
- truefoundry/workflow/__init__.py +19 -0
- truefoundry/workflow/container_task.py +12 -0
- truefoundry/workflow/example/deploy.sh +1 -0
- truefoundry/workflow/example/hello_world_package/workflow.py +20 -0
- truefoundry/workflow/example/package/test_workflow.py +152 -0
- truefoundry/workflow/example/truefoundry.yaml +9 -0
- truefoundry/workflow/example/workflow.yaml +116 -0
- truefoundry/workflow/map_task.py +45 -0
- truefoundry/workflow/python_task.py +32 -0
- truefoundry/workflow/task.py +50 -0
- truefoundry/workflow/workflow.py +114 -0
- {truefoundry-0.2.10.dist-info → truefoundry-0.3.0.dist-info}/METADATA +27 -7
- truefoundry-0.3.0.dist-info/RECORD +136 -0
- truefoundry/deploy/cli/deploy.py +0 -165
- truefoundry/deploy/cli/version.py +0 -6
- truefoundry-0.2.10.dist-info/RECORD +0 -38
- {truefoundry-0.2.10.dist-info → truefoundry-0.3.0.dist-info}/WHEEL +0 -0
- {truefoundry-0.2.10.dist-info → truefoundry-0.3.0.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from rich.console import Console
|
|
2
|
+
from rich.highlighter import ReprHighlighter
|
|
3
|
+
from rich.panel import Panel
|
|
4
|
+
from rich.text import Text
|
|
5
|
+
|
|
6
|
+
from truefoundry.deploy.io.output_callback import OutputCallBack
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _text(line):
|
|
10
|
+
return Text.from_ansi(str(line)) if "\x1b" in line else str(line)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class RichOutputCallBack(OutputCallBack):
|
|
14
|
+
console = Console(soft_wrap=True)
|
|
15
|
+
highlighter = ReprHighlighter()
|
|
16
|
+
|
|
17
|
+
def print_header(self, line):
|
|
18
|
+
self.console.rule(_text(line), style="cyan")
|
|
19
|
+
|
|
20
|
+
def print_line(self, line):
|
|
21
|
+
self.console.print(_text(line))
|
|
22
|
+
|
|
23
|
+
def print_lines_in_panel(self, lines, header=None):
|
|
24
|
+
self.console.print(Panel(self.highlighter("\n".join(lines)), title=header))
|
|
25
|
+
|
|
26
|
+
def print_code_in_panel(self, lines, header=None, lang="python"):
|
|
27
|
+
self.print_lines_in_panel(lines, header)
|
|
File without changes
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
|
+
|
|
4
|
+
import requests
|
|
5
|
+
|
|
6
|
+
from truefoundry.deploy.lib.clients.utils import poll_for_function, request_handling
|
|
7
|
+
from truefoundry.deploy.lib.const import VERSION_PREFIX
|
|
8
|
+
from truefoundry.deploy.lib.exceptions import BadRequestException
|
|
9
|
+
from truefoundry.deploy.lib.model.entity import DeviceCode, Token
|
|
10
|
+
from truefoundry.logger import logger
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AuthServiceClient(ABC):
|
|
14
|
+
def __init__(self, base_url):
|
|
15
|
+
from truefoundry.deploy.lib.clients.servicefoundry_client import (
|
|
16
|
+
ServiceFoundryServiceClient,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
client = ServiceFoundryServiceClient(init_session=False, base_url=base_url)
|
|
20
|
+
|
|
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)
|
|
121
|
+
|
|
122
|
+
def refresh_token(self, token: Token, host: str = None) -> Token:
|
|
123
|
+
host_arg_str = f"--host {host}" if host else "--host HOST"
|
|
124
|
+
if not token.refresh_token:
|
|
125
|
+
# TODO: Add a way to propagate error messages without traceback to the output interface side
|
|
126
|
+
raise Exception(
|
|
127
|
+
f"Unable to resume login session. Please log in again using `tfy login {host_arg_str} --relogin`"
|
|
128
|
+
)
|
|
129
|
+
url = f"{self._auth_server_url}/api/{VERSION_PREFIX}/oauth/token/refresh"
|
|
130
|
+
data = {
|
|
131
|
+
"tenantName": token.tenant_name,
|
|
132
|
+
"refreshToken": token.refresh_token,
|
|
133
|
+
}
|
|
134
|
+
res = requests.post(url, json=data)
|
|
135
|
+
try:
|
|
136
|
+
res = request_handling(res)
|
|
137
|
+
return Token.parse_obj(res)
|
|
138
|
+
except BadRequestException as ex:
|
|
139
|
+
raise Exception(
|
|
140
|
+
f"Unable to resume login session. Please log in again using `tfy login {host_arg_str} --relogin`"
|
|
141
|
+
) from ex
|
|
142
|
+
|
|
143
|
+
def get_device_code(self) -> DeviceCode:
|
|
144
|
+
url = f"{self._auth_server_url}/api/{VERSION_PREFIX}/oauth/device"
|
|
145
|
+
data = {"tenantName": self._tenant_name}
|
|
146
|
+
res = requests.post(url, json=data)
|
|
147
|
+
res = request_handling(res)
|
|
148
|
+
# TODO: temporary cleanup of incorrect attributes
|
|
149
|
+
res = {"userCode": res.get("userCode"), "deviceCode": res.get("deviceCode")}
|
|
150
|
+
return DeviceCode.parse_obj(res)
|
|
151
|
+
|
|
152
|
+
def get_token_from_device_code(
|
|
153
|
+
self, device_code: str, timeout: float = 60, poll_interval_seconds: int = 1
|
|
154
|
+
) -> Token:
|
|
155
|
+
url = f"{self._auth_server_url}/api/{VERSION_PREFIX}/oauth/device/token"
|
|
156
|
+
data = {
|
|
157
|
+
"tenantName": self._tenant_name,
|
|
158
|
+
"deviceCode": device_code,
|
|
159
|
+
}
|
|
160
|
+
start_time = time.monotonic()
|
|
161
|
+
poll_interval_seconds = 1
|
|
162
|
+
|
|
163
|
+
for response in poll_for_function(
|
|
164
|
+
requests.post, poll_after_secs=poll_interval_seconds, url=url, json=data
|
|
165
|
+
):
|
|
166
|
+
if response.status_code == 201:
|
|
167
|
+
response = response.json()
|
|
168
|
+
return Token.parse_obj(response)
|
|
169
|
+
elif response.status_code == 202:
|
|
170
|
+
logger.debug("User has not authorized yet. Checking again.")
|
|
171
|
+
else:
|
|
172
|
+
raise Exception(
|
|
173
|
+
"Failed to get token using device code. "
|
|
174
|
+
f"status_code {response.status_code},\n {response.text}"
|
|
175
|
+
)
|
|
176
|
+
time_elapsed = time.monotonic() - start_time
|
|
177
|
+
if time_elapsed > timeout:
|
|
178
|
+
logger.warning("Polled server for %s secs.", int(time_elapsed))
|
|
179
|
+
break
|
|
180
|
+
|
|
181
|
+
raise Exception(f"Did not get authorized within {timeout} seconds.")
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import threading
|
|
5
|
+
from functools import lru_cache, wraps
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
from filelock import FileLock, Timeout
|
|
10
|
+
|
|
11
|
+
from truefoundry.deploy.lib.const import CREDENTIAL_FILEPATH
|
|
12
|
+
from truefoundry.deploy.lib.model.entity import CredentialsFileContent
|
|
13
|
+
from truefoundry.logger import logger
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _ensure_lock_taken(method):
|
|
17
|
+
@wraps(method)
|
|
18
|
+
def lock_guard(self, *method_args, **method_kwargs):
|
|
19
|
+
if not self.lock_taken():
|
|
20
|
+
raise Exception(
|
|
21
|
+
"Trying to write to credential file without using with block"
|
|
22
|
+
)
|
|
23
|
+
return method(self, *method_args, **method_kwargs)
|
|
24
|
+
|
|
25
|
+
return lock_guard
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
CRED_FILE_THREAD_LOCK = threading.RLock()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@lru_cache(maxsize=None)
|
|
32
|
+
def get_file_lock(lock_file_path: str) -> FileLock:
|
|
33
|
+
return FileLock(lock_file_path)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class CredentialsFileManager:
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
credentials_file_path: Path = CREDENTIAL_FILEPATH,
|
|
40
|
+
lock_timeout: float = 60.0,
|
|
41
|
+
) -> None:
|
|
42
|
+
credentials_file_path = credentials_file_path.absolute()
|
|
43
|
+
logger.debug("credential file path %r", credentials_file_path)
|
|
44
|
+
|
|
45
|
+
credentials_lock_file_path = f"{credentials_file_path}.lock"
|
|
46
|
+
logger.debug("credential lock file path %r", credentials_lock_file_path)
|
|
47
|
+
|
|
48
|
+
self._credentials_file_path = credentials_file_path
|
|
49
|
+
cred_file_dir = credentials_file_path.parent
|
|
50
|
+
cred_file_dir.mkdir(exist_ok=True, parents=True)
|
|
51
|
+
|
|
52
|
+
self._file_lock = get_file_lock(credentials_lock_file_path)
|
|
53
|
+
self._lock_timeout = lock_timeout
|
|
54
|
+
self._lock_owner: Optional[int] = None
|
|
55
|
+
|
|
56
|
+
def __enter__(self) -> CredentialsFileManager:
|
|
57
|
+
# The lock objects are recursive locks, which means that once acquired, they will not block on successive lock requests:
|
|
58
|
+
lock_aquired = CRED_FILE_THREAD_LOCK.acquire(timeout=self._lock_timeout)
|
|
59
|
+
if not lock_aquired:
|
|
60
|
+
raise Exception(
|
|
61
|
+
"Could not aquire CRED_FILE_THREAD_LOCK"
|
|
62
|
+
f" in {self._lock_timeout} seconds"
|
|
63
|
+
)
|
|
64
|
+
try:
|
|
65
|
+
self._file_lock.acquire(timeout=self._lock_timeout)
|
|
66
|
+
except Timeout as ex:
|
|
67
|
+
raise Exception(
|
|
68
|
+
f"Failed to aquire lock on credential file within {self._lock_timeout} seconds.\n"
|
|
69
|
+
"Is any other process trying to login?"
|
|
70
|
+
) from ex
|
|
71
|
+
logger.debug("Acquired file and thread lock to access credential file")
|
|
72
|
+
self._lock_owner = threading.get_ident()
|
|
73
|
+
return self
|
|
74
|
+
|
|
75
|
+
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
76
|
+
self._file_lock.release()
|
|
77
|
+
CRED_FILE_THREAD_LOCK.release()
|
|
78
|
+
logger.debug("Released file and thread lock to access credential file")
|
|
79
|
+
self._lock_owner = None
|
|
80
|
+
|
|
81
|
+
def lock_taken(self) -> bool:
|
|
82
|
+
return self._lock_owner == threading.get_ident()
|
|
83
|
+
|
|
84
|
+
@_ensure_lock_taken
|
|
85
|
+
def read(self) -> CredentialsFileContent:
|
|
86
|
+
try:
|
|
87
|
+
return CredentialsFileContent.parse_file(self._credentials_file_path)
|
|
88
|
+
except Exception as ex:
|
|
89
|
+
raise Exception(
|
|
90
|
+
"Error while reading the credentials file "
|
|
91
|
+
f"{self._credentials_file_path}. Please login again "
|
|
92
|
+
"using `tfy login --relogin` or `tfy.login(relogin=True)` function"
|
|
93
|
+
) from ex
|
|
94
|
+
|
|
95
|
+
@_ensure_lock_taken
|
|
96
|
+
def write(self, credentials_file_content: CredentialsFileContent) -> None:
|
|
97
|
+
if not isinstance(credentials_file_content, CredentialsFileContent):
|
|
98
|
+
raise Exception(
|
|
99
|
+
"Only object of type `CredentialsFileContent` is allowed. "
|
|
100
|
+
f"Got {type(credentials_file_content)}"
|
|
101
|
+
)
|
|
102
|
+
logger.debug("Updating the credential file content")
|
|
103
|
+
with open(self._credentials_file_path, "w", encoding="utf8") as file:
|
|
104
|
+
file.write(credentials_file_content.json())
|
|
105
|
+
|
|
106
|
+
@_ensure_lock_taken
|
|
107
|
+
def delete(self) -> bool:
|
|
108
|
+
if not os.path.exists(self._credentials_file_path):
|
|
109
|
+
return False
|
|
110
|
+
os.remove(self._credentials_file_path)
|
|
111
|
+
return True
|
|
112
|
+
|
|
113
|
+
@_ensure_lock_taken
|
|
114
|
+
def exists(self) -> bool:
|
|
115
|
+
return self._credentials_file_path.exists()
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import threading
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
|
|
5
|
+
from truefoundry.deploy.lib.auth.auth_service_client import AuthServiceClient
|
|
6
|
+
from truefoundry.deploy.lib.auth.credential_file_manager import (
|
|
7
|
+
CredentialsFileContent,
|
|
8
|
+
CredentialsFileManager,
|
|
9
|
+
)
|
|
10
|
+
from truefoundry.deploy.lib.clients.utils import resolve_base_url
|
|
11
|
+
from truefoundry.deploy.lib.const import API_KEY_ENV_NAME
|
|
12
|
+
from truefoundry.deploy.lib.model.entity import Token
|
|
13
|
+
from truefoundry.logger import logger
|
|
14
|
+
|
|
15
|
+
TOKEN_REFRESH_LOCK = threading.RLock()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class CredentialProvider(ABC):
|
|
19
|
+
@property
|
|
20
|
+
@abstractmethod
|
|
21
|
+
def token(self) -> Token: ...
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
@abstractmethod
|
|
25
|
+
def base_url(self) -> str: ...
|
|
26
|
+
|
|
27
|
+
@staticmethod
|
|
28
|
+
@abstractmethod
|
|
29
|
+
def can_provide() -> bool: ...
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class EnvCredentialProvider(CredentialProvider):
|
|
33
|
+
def __init__(self) -> None:
|
|
34
|
+
from truefoundry.deploy.lib.clients.servicefoundry_client import (
|
|
35
|
+
ServiceFoundryServiceClient,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
logger.debug("Using env var credential provider")
|
|
39
|
+
api_key = os.getenv(API_KEY_ENV_NAME)
|
|
40
|
+
if not api_key:
|
|
41
|
+
raise Exception(
|
|
42
|
+
f"Value of {API_KEY_ENV_NAME} env var should be non-empty string"
|
|
43
|
+
)
|
|
44
|
+
# TODO: Read host from cred file as well.
|
|
45
|
+
base_url = resolve_base_url().strip("/")
|
|
46
|
+
self._host = base_url
|
|
47
|
+
self._auth_service = AuthServiceClient.from_base_url(base_url=base_url)
|
|
48
|
+
|
|
49
|
+
servicefoundry_client = ServiceFoundryServiceClient(
|
|
50
|
+
init_session=False, base_url=base_url
|
|
51
|
+
)
|
|
52
|
+
self._token: Token = servicefoundry_client.get_token_from_api_key(
|
|
53
|
+
api_key=api_key
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
@staticmethod
|
|
57
|
+
def can_provide() -> bool:
|
|
58
|
+
return API_KEY_ENV_NAME in os.environ
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def token(self) -> Token:
|
|
62
|
+
with TOKEN_REFRESH_LOCK:
|
|
63
|
+
if self._token.is_going_to_be_expired():
|
|
64
|
+
logger.info("Refreshing access token")
|
|
65
|
+
self._token = self._auth_service.refresh_token(
|
|
66
|
+
self._token, self.base_url
|
|
67
|
+
)
|
|
68
|
+
return self._token
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def base_url(self) -> str:
|
|
72
|
+
return self._host
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class FileCredentialProvider(CredentialProvider):
|
|
76
|
+
def __init__(self) -> None:
|
|
77
|
+
logger.debug("Using file credential provider")
|
|
78
|
+
self._cred_file = CredentialsFileManager()
|
|
79
|
+
|
|
80
|
+
with self._cred_file:
|
|
81
|
+
self._last_cred_file_content = self._cred_file.read()
|
|
82
|
+
self._token = self._last_cred_file_content.to_token()
|
|
83
|
+
self._host = self._last_cred_file_content.host
|
|
84
|
+
|
|
85
|
+
self._auth_service = AuthServiceClient.from_base_url(base_url=self._host)
|
|
86
|
+
|
|
87
|
+
@staticmethod
|
|
88
|
+
def can_provide() -> bool:
|
|
89
|
+
with CredentialsFileManager() as cred_file:
|
|
90
|
+
return cred_file.exists()
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def token(self) -> Token:
|
|
94
|
+
with TOKEN_REFRESH_LOCK:
|
|
95
|
+
if not self._token.is_going_to_be_expired():
|
|
96
|
+
return self._token
|
|
97
|
+
|
|
98
|
+
logger.info("Refreshing access token")
|
|
99
|
+
with self._cred_file:
|
|
100
|
+
new_cred_file_content = self._cred_file.read()
|
|
101
|
+
new_token = new_cred_file_content.to_token()
|
|
102
|
+
new_host = new_cred_file_content.host
|
|
103
|
+
|
|
104
|
+
if new_cred_file_content == self._last_cred_file_content:
|
|
105
|
+
self._token = self._auth_service.refresh_token(
|
|
106
|
+
self._token, self.base_url
|
|
107
|
+
)
|
|
108
|
+
self._last_cred_file_content = CredentialsFileContent(
|
|
109
|
+
host=self._host,
|
|
110
|
+
access_token=self._token.access_token,
|
|
111
|
+
refresh_token=self._token.refresh_token,
|
|
112
|
+
)
|
|
113
|
+
self._cred_file.write(self._last_cred_file_content)
|
|
114
|
+
return self._token
|
|
115
|
+
|
|
116
|
+
if (
|
|
117
|
+
new_host == self._host
|
|
118
|
+
and new_token.to_user_info() == self._token.to_user_info()
|
|
119
|
+
):
|
|
120
|
+
self._last_cred_file_content = new_cred_file_content
|
|
121
|
+
self._token = new_token
|
|
122
|
+
# recursive
|
|
123
|
+
return self.token
|
|
124
|
+
|
|
125
|
+
raise Exception(
|
|
126
|
+
"Credentials on disk changed while mlfoundry was running."
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
@property
|
|
130
|
+
def base_url(self) -> str:
|
|
131
|
+
return self._host
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from truefoundry.deploy.lib.auth.credential_provider import (
|
|
6
|
+
CredentialProvider,
|
|
7
|
+
EnvCredentialProvider,
|
|
8
|
+
FileCredentialProvider,
|
|
9
|
+
)
|
|
10
|
+
from truefoundry.deploy.lib.model.entity import UserInfo
|
|
11
|
+
from truefoundry.logger import logger
|
|
12
|
+
|
|
13
|
+
ACTIVE_SESSION: Optional[ServiceFoundrySession] = None
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ServiceFoundrySession:
|
|
17
|
+
def __init__(self) -> None:
|
|
18
|
+
self._cred_provider = self._get_cred_provider()
|
|
19
|
+
self._user_info: UserInfo = self._cred_provider.token.to_user_info()
|
|
20
|
+
|
|
21
|
+
global ACTIVE_SESSION
|
|
22
|
+
if (ACTIVE_SESSION is None) or (
|
|
23
|
+
ACTIVE_SESSION
|
|
24
|
+
and ACTIVE_SESSION.base_url != self.base_url
|
|
25
|
+
and ACTIVE_SESSION.user_info != self.user_info
|
|
26
|
+
):
|
|
27
|
+
logger.info(
|
|
28
|
+
"Logged in to %r as %r (%s)",
|
|
29
|
+
self.base_url,
|
|
30
|
+
self.user_info.user_id,
|
|
31
|
+
self.user_info.email or self.user_info.user_type.value,
|
|
32
|
+
)
|
|
33
|
+
ACTIVE_SESSION = self
|
|
34
|
+
|
|
35
|
+
@staticmethod
|
|
36
|
+
def _get_cred_provider() -> CredentialProvider:
|
|
37
|
+
final_cred_provider = None
|
|
38
|
+
for cred_provider in [EnvCredentialProvider, FileCredentialProvider]:
|
|
39
|
+
if cred_provider.can_provide():
|
|
40
|
+
final_cred_provider = cred_provider()
|
|
41
|
+
break
|
|
42
|
+
if final_cred_provider is None:
|
|
43
|
+
raise Exception(
|
|
44
|
+
"Please login again using `tfy login --relogin`"
|
|
45
|
+
"or `tfy.login(relogin=True)` function"
|
|
46
|
+
)
|
|
47
|
+
return final_cred_provider
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def access_token(self) -> str:
|
|
51
|
+
return self._cred_provider.token.access_token
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def base_url(self) -> str:
|
|
55
|
+
return self._cred_provider.base_url
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def user_info(self) -> UserInfo:
|
|
59
|
+
return self._user_info
|
|
File without changes
|