truefoundry 0.4.4rc3__py3-none-any.whl → 0.4.4rc5__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/common/constants.py +21 -0
- truefoundry/common/tfy_signed_url_client.py +251 -0
- truefoundry/common/tfy_signed_url_fs.py +232 -0
- truefoundry/deploy/python_deploy_codegen.py +12 -11
- truefoundry/workflow/__init__.py +5 -0
- {truefoundry-0.4.4rc3.dist-info → truefoundry-0.4.4rc5.dist-info}/METADATA +2 -1
- {truefoundry-0.4.4rc3.dist-info → truefoundry-0.4.4rc5.dist-info}/RECORD +9 -7
- {truefoundry-0.4.4rc3.dist-info → truefoundry-0.4.4rc5.dist-info}/WHEEL +0 -0
- {truefoundry-0.4.4rc3.dist-info → truefoundry-0.4.4rc5.dist-info}/entry_points.txt +0 -0
truefoundry/common/constants.py
CHANGED
|
@@ -11,12 +11,33 @@ CREDENTIAL_FILEPATH = TFY_CONFIG_DIR / "credentials.json"
|
|
|
11
11
|
TFY_HOST_ENV_KEY = "TFY_HOST"
|
|
12
12
|
TFY_API_KEY_ENV_KEY = "TFY_API_KEY"
|
|
13
13
|
|
|
14
|
+
TFY_INTERNAL_SIGNED_URL_SERVER_HOST_ENV_KEY = "TFY_INTERNAL_SIGNED_URL_SERVER_HOST"
|
|
15
|
+
TFY_INTERNAL_SIGNED_URL_SERVER_TOKEN_ENV_KEY = "TFY_INTERNAL_SIGNED_URL_SERVER_TOKEN"
|
|
16
|
+
TFY_INTERNAL_SIGNED_URL_SERVER_DEFAULT_TTL_ENV_KEY = (
|
|
17
|
+
"TFY_INTERNAL_SIGNED_URL_SERVER_DEFAULT_TTL"
|
|
18
|
+
)
|
|
19
|
+
TFY_INTERNAL_SIGNED_URL_SERVER_MAX_TIMEOUT_ENV_KEY = (
|
|
20
|
+
"TFY_INTERNAL_SIGNED_URL_SERVER_MAX_TIMEOUT"
|
|
21
|
+
)
|
|
22
|
+
|
|
14
23
|
|
|
15
24
|
class TrueFoundrySdkEnv(BaseSettings):
|
|
16
25
|
# Note: Every field in this class should have a default value
|
|
17
26
|
# Never expect the user to set these values
|
|
18
27
|
TFY_HOST: Optional[str] = Field(default=None, env=TFY_HOST_ENV_KEY)
|
|
19
28
|
TFY_API_KEY: Optional[SecretStr] = Field(default=None, env=TFY_API_KEY_ENV_KEY)
|
|
29
|
+
TFY_INTERNAL_SIGNED_URL_SERVER_HOST: Optional[str] = Field(
|
|
30
|
+
default=None, env=TFY_INTERNAL_SIGNED_URL_SERVER_HOST_ENV_KEY
|
|
31
|
+
)
|
|
32
|
+
TFY_INTERNAL_SIGNED_URL_SERVER_TOKEN: Optional[str] = Field(
|
|
33
|
+
default=None, env=TFY_INTERNAL_SIGNED_URL_SERVER_TOKEN_ENV_KEY
|
|
34
|
+
)
|
|
35
|
+
TFY_INTERNAL_SIGNED_URL_SERVER_DEFAULT_TTL: int = Field(
|
|
36
|
+
default=3600, env=TFY_INTERNAL_SIGNED_URL_SERVER_DEFAULT_TTL_ENV_KEY
|
|
37
|
+
) # 1 hour
|
|
38
|
+
TFY_INTERNAL_SIGNED_URL_SERVER_MAX_TIMEOUT: int = Field(
|
|
39
|
+
default=5, env=TFY_INTERNAL_SIGNED_URL_SERVER_MAX_TIMEOUT_ENV_KEY
|
|
40
|
+
) # 5 seconds
|
|
20
41
|
TFY_ARTIFACTS_DOWNLOAD_CHUNK_SIZE_BYTES: int = 100 * 1000 * 1000
|
|
21
42
|
TFY_ARTIFACTS_DOWNLOAD_MAX_WORKERS: int = max(min(32, (os.cpu_count() or 2) * 2), 4)
|
|
22
43
|
TFY_ARTIFACTS_UPLOAD_MAX_WORKERS: int = max(min(32, (os.cpu_count() or 2) * 2), 4)
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
# file: client.py
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from typing import Any, Dict, List, Optional
|
|
4
|
+
from urllib.parse import urlencode, urljoin
|
|
5
|
+
|
|
6
|
+
import requests
|
|
7
|
+
from requests.exceptions import RequestException
|
|
8
|
+
|
|
9
|
+
from truefoundry.common.constants import ENV_VARS
|
|
10
|
+
from truefoundry.pydantic_v1 import BaseModel, Field
|
|
11
|
+
|
|
12
|
+
DEFAULT_TTL = ENV_VARS.TFY_INTERNAL_SIGNED_URL_SERVER_DEFAULT_TTL
|
|
13
|
+
MAX_TIMEOUT = ENV_VARS.TFY_INTERNAL_SIGNED_URL_SERVER_MAX_TIMEOUT
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SignedURLAPIResponseDto(BaseModel):
|
|
17
|
+
uri: str
|
|
18
|
+
signed_url: str
|
|
19
|
+
headers: Optional[Dict[str, Any]] = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class SignedURLIsDirectoryAPIResponseDto(BaseModel):
|
|
23
|
+
is_directory: bool = Field(..., alias="isDirectory")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class SignedURLExistsAPIResponseDto(BaseModel):
|
|
27
|
+
exists: bool
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class FileInfo(BaseModel):
|
|
31
|
+
path: str
|
|
32
|
+
is_directoy: bool = Field(..., alias="isDirectory")
|
|
33
|
+
bytes: Optional[int] = None
|
|
34
|
+
signedUrl: Optional[str] = None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class PagedList(BaseModel):
|
|
38
|
+
items: List[FileInfo]
|
|
39
|
+
token: Optional[str] = None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class SignedURLServerEndpoint(str, Enum):
|
|
43
|
+
"""Enumeration for Signed URL Server endpoints."""
|
|
44
|
+
|
|
45
|
+
READ = "/v1/signed-uri/read"
|
|
46
|
+
WRITE = "/v1/signed-uri/write"
|
|
47
|
+
EXISTS = "/v1/exists"
|
|
48
|
+
IS_DIRECTORY = "/v1/is-dir"
|
|
49
|
+
LIST_FILES = "/v1/list-files"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class SignedURLClient:
|
|
53
|
+
"""Client to interact with the Signed URL Server for file operations."""
|
|
54
|
+
|
|
55
|
+
def __init__(
|
|
56
|
+
self,
|
|
57
|
+
base_url: Optional[str] = None,
|
|
58
|
+
token: Optional[str] = None,
|
|
59
|
+
ttl: int = DEFAULT_TTL,
|
|
60
|
+
):
|
|
61
|
+
"""
|
|
62
|
+
Initialize the SignedURLClient.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
base_url (str): Base URL of the signed URL server (optional).
|
|
66
|
+
token (str): Token for authentication (optional).
|
|
67
|
+
ttl (int): Default time-to-live for signed URLs in seconds.
|
|
68
|
+
"""
|
|
69
|
+
self.base_url = base_url or ENV_VARS.TFY_INTERNAL_SIGNED_URL_SERVER_HOST
|
|
70
|
+
self.token = token or ENV_VARS.TFY_INTERNAL_SIGNED_URL_SERVER_TOKEN
|
|
71
|
+
self.ttl = ttl
|
|
72
|
+
|
|
73
|
+
self.headers = {
|
|
74
|
+
"Authorization": f"Bearer {self.token}",
|
|
75
|
+
"Content-Type": "application/json",
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
def _make_request(
|
|
79
|
+
self, endpoint: str, method: str = "GET", payload: Optional[Dict] = None
|
|
80
|
+
) -> Dict:
|
|
81
|
+
"""
|
|
82
|
+
Internal method to handle requests to the signed URL server.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
endpoint (str): The endpoint for the request.
|
|
86
|
+
method (str): HTTP method (GET, POST, etc.).
|
|
87
|
+
payload (Dict, optional): JSON payload for the request.
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
Dict: JSON response from the server.
|
|
91
|
+
|
|
92
|
+
Raises:
|
|
93
|
+
RuntimeError: For network errors or invalid responses.
|
|
94
|
+
"""
|
|
95
|
+
url = urljoin(self.base_url, endpoint)
|
|
96
|
+
try:
|
|
97
|
+
response = requests.request(
|
|
98
|
+
method, url, headers=self.headers, json=payload, timeout=MAX_TIMEOUT
|
|
99
|
+
)
|
|
100
|
+
response.raise_for_status()
|
|
101
|
+
return response.json()
|
|
102
|
+
except RequestException as e:
|
|
103
|
+
raise RuntimeError(f"Error during request to {url}: {e}") from e
|
|
104
|
+
|
|
105
|
+
def _make_server_api_call(
|
|
106
|
+
self, endpoint: SignedURLServerEndpoint, params: Optional[Dict] = None
|
|
107
|
+
) -> Dict:
|
|
108
|
+
"""Get a signed URL for the specified operation and URI."""
|
|
109
|
+
query_string = urlencode(params or {})
|
|
110
|
+
endpoint_with_params = f"{endpoint.value}?{query_string}"
|
|
111
|
+
return self._make_request(endpoint_with_params)
|
|
112
|
+
|
|
113
|
+
def _upload_data(self, signed_url: str, data: Any) -> None:
|
|
114
|
+
"""Helper method to upload data to the signed URL."""
|
|
115
|
+
try:
|
|
116
|
+
# TODO: Add TIMEOUT to this request
|
|
117
|
+
response = requests.put(
|
|
118
|
+
signed_url,
|
|
119
|
+
headers={"Content-Type": "application/octet-stream"},
|
|
120
|
+
data=data,
|
|
121
|
+
)
|
|
122
|
+
response.raise_for_status()
|
|
123
|
+
except Exception as e:
|
|
124
|
+
raise RuntimeError(f"Failed to upload data: {e}") from e
|
|
125
|
+
|
|
126
|
+
def upload_from_bytes(self, data: bytes, storage_uri: str) -> str:
|
|
127
|
+
"""Upload bytes to the specified storage path using a signed URL."""
|
|
128
|
+
signed_object = self._make_server_api_call(
|
|
129
|
+
endpoint=SignedURLServerEndpoint.WRITE,
|
|
130
|
+
params={"uri": storage_uri, "expiryInSeconds": self.ttl},
|
|
131
|
+
)
|
|
132
|
+
pre_signed_object_dto = SignedURLAPIResponseDto.parse_obj(signed_object)
|
|
133
|
+
self._upload_data(pre_signed_object_dto.signed_url, data)
|
|
134
|
+
return storage_uri
|
|
135
|
+
|
|
136
|
+
def upload(self, file_path: str, storage_uri: str) -> str:
|
|
137
|
+
"""Upload a file to the specified storage path using a signed URL."""
|
|
138
|
+
print(f"Uploading {file_path} to {storage_uri}")
|
|
139
|
+
response = self._make_server_api_call(
|
|
140
|
+
endpoint=SignedURLServerEndpoint.WRITE,
|
|
141
|
+
params={"uri": storage_uri, "expiryInSeconds": self.ttl},
|
|
142
|
+
)
|
|
143
|
+
pre_signed_object_dto = SignedURLAPIResponseDto.parse_obj(response)
|
|
144
|
+
with open(file_path, "rb") as file:
|
|
145
|
+
self._upload_data(pre_signed_object_dto.signed_url, file)
|
|
146
|
+
return storage_uri
|
|
147
|
+
|
|
148
|
+
def _download_file(
|
|
149
|
+
self, signed_url: str, local_path: Optional[str] = None
|
|
150
|
+
) -> Optional[bytes]:
|
|
151
|
+
"""Common method to download a file using a signed URL.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
signed_url (str): The signed URL to download from.
|
|
155
|
+
local_path (Optional[str]): The local path to save the file. If None, the file will not be saved.
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
Optional[bytes]: The content of the downloaded file if local_path is None; otherwise, None.
|
|
159
|
+
"""
|
|
160
|
+
try:
|
|
161
|
+
# TODO: Add TIMEOUT to this request
|
|
162
|
+
response = requests.get(
|
|
163
|
+
signed_url,
|
|
164
|
+
stream=True,
|
|
165
|
+
headers=self.headers,
|
|
166
|
+
)
|
|
167
|
+
response.raise_for_status()
|
|
168
|
+
|
|
169
|
+
if local_path:
|
|
170
|
+
with open(local_path, "wb") as file:
|
|
171
|
+
for chunk in response.iter_content(chunk_size=8192):
|
|
172
|
+
file.write(chunk)
|
|
173
|
+
return None # File saved successfully; no need to return content.
|
|
174
|
+
|
|
175
|
+
return response.content # Return the content if not saving to a local path.
|
|
176
|
+
|
|
177
|
+
except requests.RequestException as e:
|
|
178
|
+
raise RuntimeError(f"Failed to download file from {signed_url}: {e}") from e
|
|
179
|
+
|
|
180
|
+
def download(self, storage_uri: str, local_path: str) -> Optional[str]:
|
|
181
|
+
"""Download a file from the specified storage path to a local path using a signed URL.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
storage_uri (str): The storage URI to download the file from.
|
|
185
|
+
local_path (str): The local path to save the downloaded file.
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
Optional[str]: The local path if successful; None if the file is saved.
|
|
189
|
+
"""
|
|
190
|
+
print(f"Dowloading {storage_uri} to {local_path}")
|
|
191
|
+
response = self._make_server_api_call(
|
|
192
|
+
endpoint=SignedURLServerEndpoint.READ,
|
|
193
|
+
params={"uri": storage_uri, "expiryInSeconds": self.ttl},
|
|
194
|
+
)
|
|
195
|
+
presigned_object = SignedURLAPIResponseDto.parse_obj(response)
|
|
196
|
+
self._download_file(presigned_object.signed_url, local_path)
|
|
197
|
+
return local_path # Indicate successful download.
|
|
198
|
+
|
|
199
|
+
def download_to_bytes(self, storage_uri: str) -> bytes:
|
|
200
|
+
"""Download a file from the specified storage path and return it as bytes.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
storage_uri (str): The storage URI to download the file from.
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
bytes: The content of the downloaded file.
|
|
207
|
+
"""
|
|
208
|
+
response = self._make_server_api_call(
|
|
209
|
+
endpoint=SignedURLServerEndpoint.READ,
|
|
210
|
+
params={"uri": storage_uri, "expiryInSeconds": self.ttl},
|
|
211
|
+
)
|
|
212
|
+
presigned_object = SignedURLAPIResponseDto.parse_obj(response)
|
|
213
|
+
return self._download_file(presigned_object.signed_url)
|
|
214
|
+
|
|
215
|
+
def exists(self, uri: str) -> bool:
|
|
216
|
+
"""Check if a file exists at the specified path."""
|
|
217
|
+
response = self._make_server_api_call(
|
|
218
|
+
endpoint=SignedURLServerEndpoint.EXISTS, params={"uri": uri}
|
|
219
|
+
)
|
|
220
|
+
response_obj = SignedURLExistsAPIResponseDto.parse_obj(response)
|
|
221
|
+
return response_obj.exists
|
|
222
|
+
|
|
223
|
+
def is_directory(self, uri: str) -> bool:
|
|
224
|
+
"""Check if the specified URI is a directory."""
|
|
225
|
+
response = self._make_server_api_call(
|
|
226
|
+
endpoint=SignedURLServerEndpoint.IS_DIRECTORY, params={"path": uri}
|
|
227
|
+
)
|
|
228
|
+
response_obj = SignedURLIsDirectoryAPIResponseDto.parse_obj(response)
|
|
229
|
+
print(f"Path {uri} is_directory: {response_obj.is_directory}")
|
|
230
|
+
return response_obj.is_directory
|
|
231
|
+
|
|
232
|
+
def list_files(self, path: str, detail: bool = False, max_results: int = 1000):
|
|
233
|
+
"""List files in the specified directory."""
|
|
234
|
+
token = ""
|
|
235
|
+
items: List[FileInfo] = []
|
|
236
|
+
# Fetch all files in the specified path, in pages of max_results
|
|
237
|
+
while True:
|
|
238
|
+
response = self._make_server_api_call(
|
|
239
|
+
endpoint=SignedURLServerEndpoint.LIST_FILES,
|
|
240
|
+
params={"path": path, "maxResults": max_results, "pageToken": token},
|
|
241
|
+
)
|
|
242
|
+
response_obj = PagedList.parse_obj(response)
|
|
243
|
+
items.extend(response_obj.items)
|
|
244
|
+
token = response_obj.token
|
|
245
|
+
if not token:
|
|
246
|
+
break
|
|
247
|
+
|
|
248
|
+
# Return the items or paths based on the detail flag
|
|
249
|
+
if detail:
|
|
250
|
+
return items
|
|
251
|
+
return [item.path for item in items]
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
# file: tfy_signed_url_fs.py
|
|
2
|
+
# pylint: disable=W0223
|
|
3
|
+
import io
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
from fsspec.spec import DEFAULT_CALLBACK, AbstractBufferedFile, AbstractFileSystem
|
|
9
|
+
|
|
10
|
+
from truefoundry.common.constants import ENV_VARS
|
|
11
|
+
from truefoundry.common.tfy_signed_url_client import SignedURLClient
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SignedURLFileSystem(AbstractFileSystem):
|
|
15
|
+
def __init__(self, base_url: Optional[str] = None, token: Optional[str] = None):
|
|
16
|
+
super().__init__()
|
|
17
|
+
base_url = base_url or ENV_VARS.TFY_INTERNAL_SIGNED_URL_SERVER_HOST
|
|
18
|
+
token = token or ENV_VARS.TFY_INTERNAL_SIGNED_URL_SERVER_TOKEN
|
|
19
|
+
self.client = SignedURLClient(base_url, token)
|
|
20
|
+
|
|
21
|
+
def exists(self, path, **kwargs):
|
|
22
|
+
"""Is there a file at the given path"""
|
|
23
|
+
return self.client.exists(path)
|
|
24
|
+
|
|
25
|
+
def get(
|
|
26
|
+
self,
|
|
27
|
+
rpath,
|
|
28
|
+
lpath,
|
|
29
|
+
recursive=False,
|
|
30
|
+
callback=DEFAULT_CALLBACK,
|
|
31
|
+
maxdepth=None,
|
|
32
|
+
**kwargs,
|
|
33
|
+
):
|
|
34
|
+
"""Get file(s) to local"""
|
|
35
|
+
# TODO: Add support for ThreadPoolExecutor here
|
|
36
|
+
# TODO: Do a proper error handling here
|
|
37
|
+
# TODO: Add support for async download
|
|
38
|
+
if self.isdir(rpath):
|
|
39
|
+
if not recursive:
|
|
40
|
+
raise ValueError(
|
|
41
|
+
f"{rpath} is a directory, but recursive is not enabled."
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
# Handle recursive download
|
|
45
|
+
files = self.ls(rpath, detail=True)
|
|
46
|
+
|
|
47
|
+
for file_info in files:
|
|
48
|
+
file_path = file_info.path.rstrip("/").rsplit("/")[-1]
|
|
49
|
+
|
|
50
|
+
is_directory = file_info.is_directoy
|
|
51
|
+
# Construct the relative path for local download
|
|
52
|
+
relative_path = rpath.rstrip("/") + "/" + file_path
|
|
53
|
+
target_local_path = lpath.rstrip("/") + "/" + file_path
|
|
54
|
+
|
|
55
|
+
if is_directory:
|
|
56
|
+
# If it's a directory, create the directory locally
|
|
57
|
+
Path(target_local_path).mkdir(parents=True, exist_ok=True)
|
|
58
|
+
relative_path = relative_path + "/"
|
|
59
|
+
if recursive:
|
|
60
|
+
# Recursively download the contents of the directory
|
|
61
|
+
self.get(
|
|
62
|
+
relative_path,
|
|
63
|
+
target_local_path,
|
|
64
|
+
recursive=True,
|
|
65
|
+
maxdepth=maxdepth,
|
|
66
|
+
**kwargs,
|
|
67
|
+
)
|
|
68
|
+
else:
|
|
69
|
+
# Download the file
|
|
70
|
+
self.client.download(
|
|
71
|
+
storage_uri=relative_path, local_path=target_local_path
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
else:
|
|
75
|
+
target_local_path = lpath
|
|
76
|
+
if target_local_path.endswith("/"):
|
|
77
|
+
Path(target_local_path).mkdir(parents=True, exist_ok=True)
|
|
78
|
+
target_local_path = target_local_path + rpath.rsplit("/")[-1]
|
|
79
|
+
self.client.download(storage_uri=rpath, local_path=target_local_path)
|
|
80
|
+
|
|
81
|
+
def put(
|
|
82
|
+
self,
|
|
83
|
+
lpath,
|
|
84
|
+
rpath,
|
|
85
|
+
recursive=False,
|
|
86
|
+
callback=DEFAULT_CALLBACK,
|
|
87
|
+
maxdepth=None,
|
|
88
|
+
**kwargs,
|
|
89
|
+
):
|
|
90
|
+
local_path = Path(lpath)
|
|
91
|
+
if local_path.is_dir():
|
|
92
|
+
if not recursive:
|
|
93
|
+
raise ValueError(
|
|
94
|
+
f"{lpath} is a directory, but recursive is set to False."
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
# Optionally limit recursion depth
|
|
98
|
+
max_depth = maxdepth if maxdepth is not None else float("inf")
|
|
99
|
+
|
|
100
|
+
# Walk through the directory structure
|
|
101
|
+
for root, _, files in os.walk(lpath):
|
|
102
|
+
current_depth = Path(root).relative_to(local_path).parts
|
|
103
|
+
if len(current_depth) > max_depth:
|
|
104
|
+
continue # Skip files deeper than the max depth
|
|
105
|
+
|
|
106
|
+
rel_dir = Path(root).relative_to(local_path)
|
|
107
|
+
remote_dir = (
|
|
108
|
+
rpath.rstrip("/")
|
|
109
|
+
if rel_dir == Path(".")
|
|
110
|
+
else rpath.rstrip("/") + "/" + str(rel_dir)
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
# Upload each file
|
|
114
|
+
for file in files:
|
|
115
|
+
local_file_path = Path(root) / file
|
|
116
|
+
remote_file_path = f"{remote_dir}/{file}"
|
|
117
|
+
self.client.upload(
|
|
118
|
+
file_path=str(local_file_path),
|
|
119
|
+
storage_uri=str(remote_file_path),
|
|
120
|
+
)
|
|
121
|
+
return None
|
|
122
|
+
else:
|
|
123
|
+
# Handle single file upload
|
|
124
|
+
return self.client.upload(
|
|
125
|
+
file_path=str(local_path), storage_uri=str(Path(rpath))
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
def isdir(self, path):
|
|
129
|
+
"""Is this entry directory-like?"""
|
|
130
|
+
return self.client.is_directory(path)
|
|
131
|
+
|
|
132
|
+
def open(
|
|
133
|
+
self,
|
|
134
|
+
path,
|
|
135
|
+
mode="rb",
|
|
136
|
+
block_size=None,
|
|
137
|
+
cache_options=None,
|
|
138
|
+
compression=None,
|
|
139
|
+
**kwargs,
|
|
140
|
+
):
|
|
141
|
+
"""
|
|
142
|
+
Open a file for reading or writing.
|
|
143
|
+
"""
|
|
144
|
+
if "r" in mode:
|
|
145
|
+
# Reading mode
|
|
146
|
+
file_content = self.client.download_to_bytes(path)
|
|
147
|
+
return io.BytesIO(file_content)
|
|
148
|
+
elif "w" in mode or "a" in mode:
|
|
149
|
+
# Writing mode (appending treated as writing)
|
|
150
|
+
buffer = io.BytesIO()
|
|
151
|
+
buffer.seek(0)
|
|
152
|
+
|
|
153
|
+
def on_close(buffer=buffer, path=path):
|
|
154
|
+
"""
|
|
155
|
+
Callback when file is closed, automatically upload the content.
|
|
156
|
+
"""
|
|
157
|
+
buffer.seek(0)
|
|
158
|
+
self.client.upload_from_bytes(buffer.read(), storage_uri=path)
|
|
159
|
+
|
|
160
|
+
# Wrapping the buffer to automatically upload on close
|
|
161
|
+
return io.BufferedWriter(buffer, on_close)
|
|
162
|
+
|
|
163
|
+
def write(self, path, data, **kwargs):
|
|
164
|
+
"""
|
|
165
|
+
Write data to the file at the specified path (this could be tied to open's close).
|
|
166
|
+
"""
|
|
167
|
+
if isinstance(data, io.BytesIO):
|
|
168
|
+
data.seek(0)
|
|
169
|
+
content = data.read()
|
|
170
|
+
elif isinstance(data, str):
|
|
171
|
+
content = data.encode() # Encode to bytes
|
|
172
|
+
else:
|
|
173
|
+
raise ValueError("Unsupported data type for writing")
|
|
174
|
+
|
|
175
|
+
# Upload the content to the remote file system
|
|
176
|
+
self.client.upload_from_bytes(content, storage_uri=path)
|
|
177
|
+
|
|
178
|
+
def ls(self, path, detail=True, **kwargs):
|
|
179
|
+
"""List objects at path."""
|
|
180
|
+
return self.client.list_files(path, detail=detail)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
class SignedURLBufferedFile(AbstractBufferedFile):
|
|
184
|
+
"""
|
|
185
|
+
Buffered file implementation for Signed URL-based file system.
|
|
186
|
+
"""
|
|
187
|
+
|
|
188
|
+
def __init__(
|
|
189
|
+
self, fs: SignedURLFileSystem, path: str, mode: str, block_size: int, **kwargs
|
|
190
|
+
):
|
|
191
|
+
"""
|
|
192
|
+
Initialize the buffered file, determining the mode (read/write).
|
|
193
|
+
"""
|
|
194
|
+
super().__init__(fs, path, mode, block_size, **kwargs)
|
|
195
|
+
self.buffer = io.BytesIO()
|
|
196
|
+
self.client = fs.client
|
|
197
|
+
|
|
198
|
+
if "r" in mode:
|
|
199
|
+
# Download the file content for reading
|
|
200
|
+
file_content = fs.client.download_to_bytes(path)
|
|
201
|
+
self.buffer.write(file_content)
|
|
202
|
+
self.buffer.seek(0) # Reset buffer after writing content
|
|
203
|
+
|
|
204
|
+
def _upload_on_close(self):
|
|
205
|
+
"""
|
|
206
|
+
Upload content back to the remote store when the file is closed.
|
|
207
|
+
"""
|
|
208
|
+
self.buffer.seek(0)
|
|
209
|
+
self.client.upload_from_bytes(self.buffer.read(), storage_uri=self.path)
|
|
210
|
+
|
|
211
|
+
def close(self):
|
|
212
|
+
"""
|
|
213
|
+
Close the file, ensuring the content is uploaded for write/append modes.
|
|
214
|
+
"""
|
|
215
|
+
if self.writable():
|
|
216
|
+
self._upload_on_close()
|
|
217
|
+
self.buffer.close()
|
|
218
|
+
super().close()
|
|
219
|
+
|
|
220
|
+
def _fetch_range(self, start, end):
|
|
221
|
+
"""
|
|
222
|
+
Fetch a specific byte range from the file. Useful for large files and range reads.
|
|
223
|
+
"""
|
|
224
|
+
self.buffer.seek(start)
|
|
225
|
+
return self.buffer.read(end - start)
|
|
226
|
+
|
|
227
|
+
def _upload_chunk(self, final=False):
|
|
228
|
+
"""
|
|
229
|
+
Upload a chunk of the file. For larger files, data may be uploaded in chunks.
|
|
230
|
+
"""
|
|
231
|
+
if final:
|
|
232
|
+
self._upload_on_close()
|
|
@@ -107,16 +107,16 @@ def convert_deployment_config_to_python(workspace_fqn: str, application_spec: di
|
|
|
107
107
|
Convert a deployment config to a python file that can be used to deploy to a workspace
|
|
108
108
|
"""
|
|
109
109
|
application = Application.parse_obj(application_spec)
|
|
110
|
-
|
|
111
|
-
|
|
110
|
+
application_obj = application.__root__
|
|
111
|
+
application_type = application_obj.type
|
|
112
112
|
if (
|
|
113
|
-
hasattr(
|
|
114
|
-
and
|
|
115
|
-
and
|
|
113
|
+
hasattr(application_obj, "image")
|
|
114
|
+
and application_obj.image.type == "build"
|
|
115
|
+
and application_obj.image.build_source.type == "remote"
|
|
116
116
|
):
|
|
117
|
-
|
|
117
|
+
application_obj.image.build_source = LocalSource(local_build=False)
|
|
118
118
|
|
|
119
|
-
spec_repr = get_python_repr(
|
|
119
|
+
spec_repr = get_python_repr(application_obj)
|
|
120
120
|
spec_repr = replace_enums_with_values(spec_repr)
|
|
121
121
|
spec_repr = remove_none_type_fields(spec_repr)
|
|
122
122
|
spec_repr = remove_type_field(spec_repr)
|
|
@@ -144,7 +144,8 @@ def convert_deployment_config_to_python(workspace_fqn: str, application_spec: di
|
|
|
144
144
|
def generate_python_snippet_for_trigger_job(
|
|
145
145
|
application_fqn: str, command: Optional[str], params: Optional[Dict[str, str]]
|
|
146
146
|
):
|
|
147
|
-
job_run_python_template = """
|
|
147
|
+
job_run_python_template = """\
|
|
148
|
+
from truefoundry.deploy import trigger_job
|
|
148
149
|
|
|
149
150
|
response = trigger_job(
|
|
150
151
|
application_fqn="{{application_fqn}}",
|
|
@@ -160,16 +161,16 @@ print(response.jobRunName)
|
|
|
160
161
|
)
|
|
161
162
|
|
|
162
163
|
if command is not None:
|
|
163
|
-
output_python_str = output_python_str.replace("{{command}}", repr(command))
|
|
164
164
|
output_python_str = output_python_str.replace("# command", "command")
|
|
165
|
+
output_python_str = output_python_str.replace("{{command}}", repr(command))
|
|
165
166
|
else:
|
|
166
167
|
output_python_str = output_python_str.replace(
|
|
167
168
|
"{{command}}", "<Enter Command Here>"
|
|
168
169
|
)
|
|
169
170
|
|
|
170
171
|
if params is not None:
|
|
171
|
-
output_python_str = output_python_str.replace("{{params}}", repr(params))
|
|
172
172
|
output_python_str = output_python_str.replace("# params", "params")
|
|
173
|
+
output_python_str = output_python_str.replace("{{params}}", repr(params))
|
|
173
174
|
else:
|
|
174
175
|
output_python_str = output_python_str.replace(
|
|
175
176
|
"{{params}}", "<Enter Params(key-value pairs) here as python dict>"
|
|
@@ -195,7 +196,7 @@ def generate_curl_snippet_for_trigger_job(
|
|
|
195
196
|
}'
|
|
196
197
|
"""
|
|
197
198
|
output_curl_str = job_run_curl_request_template.replace(
|
|
198
|
-
"{{control_plane_url}}", control_plane_url
|
|
199
|
+
"{{control_plane_url}}", control_plane_url.rstrip("/")
|
|
199
200
|
)
|
|
200
201
|
output_curl_str = output_curl_str.replace("{{application_id}}", application_id)
|
|
201
202
|
output_curl_str = output_curl_str.replace(
|
truefoundry/workflow/__init__.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
try:
|
|
2
|
+
import fsspec
|
|
2
3
|
from flytekit import task as _
|
|
3
4
|
except ImportError:
|
|
4
5
|
print("To use workflows, please run 'pip install truefoundry[workflow]'.")
|
|
@@ -6,6 +7,7 @@ except ImportError:
|
|
|
6
7
|
from flytekit import conditional
|
|
7
8
|
from flytekit.types.directory import FlyteDirectory
|
|
8
9
|
|
|
10
|
+
from truefoundry.common.tfy_signed_url_fs import SignedURLFileSystem
|
|
9
11
|
from truefoundry.deploy.v2.lib.patched_models import (
|
|
10
12
|
ContainerTaskConfig,
|
|
11
13
|
PythonTaskConfig,
|
|
@@ -32,3 +34,6 @@ __all__ = [
|
|
|
32
34
|
"PythonTaskConfig",
|
|
33
35
|
"ExecutionConfig",
|
|
34
36
|
]
|
|
37
|
+
print("SignedURL FS Spec registered for s3 and gs")
|
|
38
|
+
fsspec.register_implementation("s3", SignedURLFileSystem, clobber=True)
|
|
39
|
+
fsspec.register_implementation("gs", SignedURLFileSystem, clobber=True)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: truefoundry
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.4rc5
|
|
4
4
|
Summary: Truefoundry CLI
|
|
5
5
|
Author: Abhishek Choudhary
|
|
6
6
|
Author-email: abhishek@truefoundry.com
|
|
@@ -22,6 +22,7 @@ Requires-Dist: docker (>=6.1.2,<8.0.0)
|
|
|
22
22
|
Requires-Dist: fastapi (>=0.56.0,<0.200.0)
|
|
23
23
|
Requires-Dist: filelock (>=3.8.0,<4.0.0)
|
|
24
24
|
Requires-Dist: flytekit (==1.12.2) ; extra == "workflow"
|
|
25
|
+
Requires-Dist: fsspec (>=2024.9.0,<2025.0.0)
|
|
25
26
|
Requires-Dist: gitignorefile (>=1.1.2,<2.0.0)
|
|
26
27
|
Requires-Dist: importlib-metadata (>=4.11.3,<9.0.0)
|
|
27
28
|
Requires-Dist: importlib-resources (>=5.2.0,<7.0.0)
|
|
@@ -27,13 +27,15 @@ truefoundry/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
|
27
27
|
truefoundry/cli/__main__.py,sha256=-NkhYlT3mC5MhtekueKAvCw-sWvguj0LJRpXWzvvFjc,727
|
|
28
28
|
truefoundry/common/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
29
29
|
truefoundry/common/auth_service_client.py,sha256=tZOa0NdATnItsMeTnEnUeTZQIgUJtpU-nvLdWtB4Px8,7978
|
|
30
|
-
truefoundry/common/constants.py,sha256=
|
|
30
|
+
truefoundry/common/constants.py,sha256=0T3WbCqSPcqGj46es0fgo2skzCUhQ70hDbcwW2cel7E,2327
|
|
31
31
|
truefoundry/common/credential_file_manager.py,sha256=1yEk1Zm2xS4G0VDFwKSZ4w0VUrcPWQ1nJnoBaz9xyKA,4251
|
|
32
32
|
truefoundry/common/credential_provider.py,sha256=Aht7hFLsnyRgMR34dRbzln7dor0WYSeA8ej8ApNmnKM,4148
|
|
33
33
|
truefoundry/common/entities.py,sha256=8O-EGPk4PKqnyoFMKUTxISCU19rz0KBnfRDJU695DhY,3797
|
|
34
34
|
truefoundry/common/exceptions.py,sha256=ePpiQ_zmWe4e94gOgeMiyP_AZnKwjEBfyXsB5ScGYcI,329
|
|
35
35
|
truefoundry/common/request_utils.py,sha256=5xw4YGUcMf71Ncal3OfFCa-PoWDIvG3hYGCDa4Da4OI,2854
|
|
36
36
|
truefoundry/common/servicefoundry_client.py,sha256=2fxmgCM-ckFHpnm6n_mL-5Z8RWN_q-dYVvFC29bkYSg,3120
|
|
37
|
+
truefoundry/common/tfy_signed_url_client.py,sha256=zqGIG7L6roPdSSdZ5IVTIVjYsOJsWvMa8SMkYttkg70,9401
|
|
38
|
+
truefoundry/common/tfy_signed_url_fs.py,sha256=YoYRQwXBTIERkwgQAhbCb3jHZ3dRaSNEqaPyFMBvTDc,8045
|
|
37
39
|
truefoundry/common/utils.py,sha256=MYFjNtHGqauqhj9tmbdErCJR49AfXDwg-5kYbBh8HpI,3258
|
|
38
40
|
truefoundry/deploy/__init__.py,sha256=ugawKF2G02EmEXX35oZ2tec12d9oWN28Sf6mtGGIERY,2281
|
|
39
41
|
truefoundry/deploy/auto_gen/models.py,sha256=4MaxkG2_5Wg6avaZRlK0D4JiVEM5rk3NU0BCiTx8VyU,82477
|
|
@@ -107,7 +109,7 @@ truefoundry/deploy/lib/model/entity.py,sha256=fq8hvdJQgQn4uZqxpKrzmaoJhQG53_EbDo
|
|
|
107
109
|
truefoundry/deploy/lib/session.py,sha256=Vg6rCA315T0yS0xG4ayJ84Ia_9ZfibH8utOSwPBMAmw,4953
|
|
108
110
|
truefoundry/deploy/lib/util.py,sha256=3TapV7yczkheC1MMMfmJDGGzTl2l6e4jCYd_Rr5aoQ8,1330
|
|
109
111
|
truefoundry/deploy/lib/win32.py,sha256=1RcvPTdlOAJ48rt8rCbE2Ufha2ztRqBAE9dueNXArrY,5009
|
|
110
|
-
truefoundry/deploy/python_deploy_codegen.py,sha256=
|
|
112
|
+
truefoundry/deploy/python_deploy_codegen.py,sha256=z9VSETb3Lrqn7sUD75EksbmA1vRiEl0LNnz9PTqF8ZM,6462
|
|
111
113
|
truefoundry/deploy/v2/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
112
114
|
truefoundry/deploy/v2/lib/__init__.py,sha256=WEiVMZXOVljzEE3tpGJil14liIn_PCDoACJ6b3tZ6sI,188
|
|
113
115
|
truefoundry/deploy/v2/lib/deploy.py,sha256=HIcY3SzQ5lWl7avuuKi3J0Z-PBES6Sf4hgMK-m6_53U,11990
|
|
@@ -329,7 +331,7 @@ truefoundry/ml/session.py,sha256=F83GTC5WwGBjnJ69Ct8MqMnlutYc56JCc6YhEY1Wl-A,539
|
|
|
329
331
|
truefoundry/ml/validation_utils.py,sha256=XBSUd9OoyriWJpT3M5LKz17iWY3yVMr3hM5vdaVjtf0,12082
|
|
330
332
|
truefoundry/pydantic_v1.py,sha256=jSuhGtz0Mbk1qYu8jJ1AcnIDK4oxUsdhALc4spqstmM,345
|
|
331
333
|
truefoundry/version.py,sha256=bqiT4Q-VWrTC6P4qfK43mez-Ppf-smWfrl6DcwV7mrw,137
|
|
332
|
-
truefoundry/workflow/__init__.py,sha256=
|
|
334
|
+
truefoundry/workflow/__init__.py,sha256=MtbOKTkGTaM9AKqJTH93-76MJPtWAHy8UOGbS8jgbxI,1218
|
|
333
335
|
truefoundry/workflow/container_task.py,sha256=8arieePsX4__OnG337hOtCiNgJwtKJJCsZcmFmCBJtk,402
|
|
334
336
|
truefoundry/workflow/example/deploy.sh,sha256=wfbPRrCi04WYRqCf4g-Xo12uWbcqPD6G_Tz0lV0jU_U,60
|
|
335
337
|
truefoundry/workflow/example/hello_world_package/workflow.py,sha256=IkRKfPY5BcvLPo_PVuNbZKK9PPJ93LRkzb1a3RKQYOw,435
|
|
@@ -340,7 +342,7 @@ truefoundry/workflow/map_task.py,sha256=2m3qGXQ90k9LdS45q8dqCCECc3qr8t2m_LMCVd1m
|
|
|
340
342
|
truefoundry/workflow/python_task.py,sha256=SRXRLC4vdBqGjhkwuaY39LEWN6iPCpJAuW17URRdWTY,1128
|
|
341
343
|
truefoundry/workflow/task.py,sha256=ToitYiKcNzFCtOVQwz1W8sRjbR97eVS7vQBdbgUQtKg,1779
|
|
342
344
|
truefoundry/workflow/workflow.py,sha256=WaTqUjhwfAXDWu4E5ehuwAxrCbDJkoAf1oWmR2E9Qy0,4575
|
|
343
|
-
truefoundry-0.4.
|
|
344
|
-
truefoundry-0.4.
|
|
345
|
-
truefoundry-0.4.
|
|
346
|
-
truefoundry-0.4.
|
|
345
|
+
truefoundry-0.4.4rc5.dist-info/METADATA,sha256=RNyf_zTUd1RoEKNQ2xEqeNx4z3ICZCkFm17EsEsy_r4,3146
|
|
346
|
+
truefoundry-0.4.4rc5.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
|
|
347
|
+
truefoundry-0.4.4rc5.dist-info/entry_points.txt,sha256=TXvUxQkI6zmqJuycPsyxEIMr3oqfDjgrWj0m_9X12x4,95
|
|
348
|
+
truefoundry-0.4.4rc5.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|