truefoundry 0.10.7__py3-none-any.whl → 0.11.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/deploy/builder/builders/tfy_spark_buildpack/dockerfile_template.py +6 -1
- truefoundry/deploy/builder/builders/tfy_spark_buildpack/tfy_execute_notebook.py +100 -7
- truefoundry/ml/artifact/truefoundry_artifact_repo.py +59 -60
- truefoundry/ml/log_types/artifacts/artifact.py +5 -9
- truefoundry/ml/log_types/artifacts/dataset.py +3 -1
- truefoundry/ml/log_types/artifacts/model.py +5 -9
- truefoundry/ml/mlfoundry_api.py +50 -44
- truefoundry/ml/mlfoundry_run.py +3 -7
- {truefoundry-0.10.7.dist-info → truefoundry-0.11.0.dist-info}/METADATA +2 -2
- {truefoundry-0.10.7.dist-info → truefoundry-0.11.0.dist-info}/RECORD +12 -12
- {truefoundry-0.10.7.dist-info → truefoundry-0.11.0.dist-info}/WHEEL +0 -0
- {truefoundry-0.10.7.dist-info → truefoundry-0.11.0.dist-info}/entry_points.txt +0 -0
|
@@ -47,7 +47,12 @@ RUN apt update && \
|
|
|
47
47
|
+ _POST_USER_TEMPLATE
|
|
48
48
|
)
|
|
49
49
|
|
|
50
|
-
ADDITIONAL_PIP_PACKAGES = [
|
|
50
|
+
ADDITIONAL_PIP_PACKAGES = [
|
|
51
|
+
"papermill>=2.6.0,<2.7.0",
|
|
52
|
+
"ipykernel>=6.0.0,<7.0.0",
|
|
53
|
+
"nbconvert>=7.16.6,<7.17.0",
|
|
54
|
+
"boto3>=1.38.43,<1.40.0",
|
|
55
|
+
]
|
|
51
56
|
|
|
52
57
|
|
|
53
58
|
def generate_pip_install_command(
|
|
@@ -4,9 +4,77 @@
|
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
import argparse
|
|
7
|
+
import os
|
|
7
8
|
import sys
|
|
8
9
|
|
|
10
|
+
import boto3
|
|
11
|
+
import nbformat
|
|
9
12
|
import papermill as pm
|
|
13
|
+
from botocore.client import Config
|
|
14
|
+
from nbconvert import HTMLExporter
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def convert_notebook_to_html(notebook_path, output_html_path):
|
|
18
|
+
"""
|
|
19
|
+
Convert a Jupyter notebook to an HTML file.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
notebook_path: Path to the input notebook (.ipynb)
|
|
23
|
+
output_html_path: Path for the output HTML file (.html)
|
|
24
|
+
"""
|
|
25
|
+
print(f"Converting notebook {notebook_path} to HTML...")
|
|
26
|
+
try:
|
|
27
|
+
with open(notebook_path, "r", encoding="utf-8") as f:
|
|
28
|
+
notebook_content = nbformat.read(f, as_version=4)
|
|
29
|
+
|
|
30
|
+
html_exporter = HTMLExporter()
|
|
31
|
+
# Use lab for https://nbconvert.readthedocs.io/en/latest/customizing.html#where-are-nbconvert-templates-installed
|
|
32
|
+
html_exporter.template_name = "lab"
|
|
33
|
+
(body, _) = html_exporter.from_notebook_node(notebook_content)
|
|
34
|
+
|
|
35
|
+
with open(output_html_path, "w", encoding="utf-8") as f:
|
|
36
|
+
f.write(body)
|
|
37
|
+
print(f"Successfully converted notebook to HTML: {output_html_path}")
|
|
38
|
+
except Exception as e:
|
|
39
|
+
print(f"Error converting notebook to HTML: {e}")
|
|
40
|
+
raise
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def upload_file_to_s3(file_path, bucket_name, s3_key):
|
|
44
|
+
print(f"Uploading {file_path} to s3://{bucket_name}/{s3_key}...")
|
|
45
|
+
# Use s3proxy for pushing data to s3
|
|
46
|
+
# The JWT token is already available in the pod
|
|
47
|
+
aws_access_key_id = os.environ.get("SPARK_APPLICATION_EVENT_LOG_JWT_TOKEN")
|
|
48
|
+
aws_secret_access_key = "__token__"
|
|
49
|
+
s3_endpoint_url = os.environ.get("S3_PROXY_URL")
|
|
50
|
+
|
|
51
|
+
if not aws_access_key_id:
|
|
52
|
+
raise ValueError(
|
|
53
|
+
"SPARK_APPLICATION_EVENT_LOG_JWT_TOKEN environment variable is not set"
|
|
54
|
+
)
|
|
55
|
+
if not s3_endpoint_url:
|
|
56
|
+
raise ValueError("S3_PROXY_URL environment variable is not set")
|
|
57
|
+
|
|
58
|
+
# Needed for the issue https://github.com/gaul/s3proxy/issues/765
|
|
59
|
+
s3_config = Config(
|
|
60
|
+
request_checksum_calculation="when_required",
|
|
61
|
+
response_checksum_validation="when_required",
|
|
62
|
+
)
|
|
63
|
+
try:
|
|
64
|
+
client = boto3.client(
|
|
65
|
+
"s3",
|
|
66
|
+
aws_access_key_id=aws_access_key_id,
|
|
67
|
+
aws_secret_access_key=aws_secret_access_key,
|
|
68
|
+
endpoint_url=s3_endpoint_url,
|
|
69
|
+
config=s3_config,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
with open(file_path, "rb") as data:
|
|
73
|
+
client.put_object(Bucket=bucket_name, Key=s3_key, Body=data)
|
|
74
|
+
print(f"Successfully uploaded {file_path} to s3://{bucket_name}/{s3_key}")
|
|
75
|
+
except Exception as e:
|
|
76
|
+
print(f"Error uploading file to S3: {e}")
|
|
77
|
+
raise
|
|
10
78
|
|
|
11
79
|
|
|
12
80
|
def execute_notebook(notebook_path, output_path="/tmp/output.ipynb", parameters=None):
|
|
@@ -26,8 +94,8 @@ def execute_notebook(notebook_path, output_path="/tmp/output.ipynb", parameters=
|
|
|
26
94
|
|
|
27
95
|
print(f"Starting execution of notebook: {notebook_path}")
|
|
28
96
|
pm.execute_notebook(
|
|
29
|
-
notebook_path,
|
|
30
|
-
output_path,
|
|
97
|
+
input_path=notebook_path,
|
|
98
|
+
output_path=output_path,
|
|
31
99
|
parameters=parameters,
|
|
32
100
|
# TODO(gw): Replace with kernel name for venv
|
|
33
101
|
kernel_name="python3",
|
|
@@ -38,6 +106,7 @@ def execute_notebook(notebook_path, output_path="/tmp/output.ipynb", parameters=
|
|
|
38
106
|
stderr_file=sys.stderr,
|
|
39
107
|
)
|
|
40
108
|
print(f"Successfully executed notebook: {notebook_path}")
|
|
109
|
+
return output_path
|
|
41
110
|
|
|
42
111
|
|
|
43
112
|
if __name__ == "__main__":
|
|
@@ -45,17 +114,41 @@ if __name__ == "__main__":
|
|
|
45
114
|
description="Execute a Jupyter notebook using papermill for Spark applications"
|
|
46
115
|
)
|
|
47
116
|
parser.add_argument("notebook_path", help="Path to the notebook file to execute")
|
|
48
|
-
|
|
49
117
|
args = parser.parse_args()
|
|
50
118
|
|
|
51
|
-
|
|
119
|
+
output_notebook_path = "/tmp/output.ipynb"
|
|
120
|
+
|
|
121
|
+
# This would be the same as the default bucket used by servicefoundry-server
|
|
122
|
+
s3_bucket = os.environ.get("TFY_NOTEBOOK_OUTPUT_S3_BUCKET")
|
|
123
|
+
# This would be something like sparkjob-events/<tenant-id>
|
|
124
|
+
s3_key_prefix = os.environ.get("TFY_NOTEBOOK_OUTPUT_S3_KEY_PREFIX")
|
|
125
|
+
|
|
52
126
|
try:
|
|
53
|
-
execute_notebook(
|
|
127
|
+
executed_notebook_path = execute_notebook(
|
|
128
|
+
args.notebook_path, output_path=output_notebook_path
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
# The following may also be modeled as an entrypoint
|
|
132
|
+
# https://papermill.readthedocs.io/en/latest/extending-entry-points.html
|
|
133
|
+
# Will take that up with next iteration where we save the executed notebook periodically
|
|
134
|
+
if s3_bucket and s3_key_prefix:
|
|
135
|
+
print("Converting notebook to HTML and uploading to S3...")
|
|
136
|
+
html_output_path = "/tmp/output.html"
|
|
137
|
+
convert_notebook_to_html(
|
|
138
|
+
notebook_path=executed_notebook_path, output_html_path=html_output_path
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
# Construct S3 key: use the original notebook name for the HTML file
|
|
142
|
+
notebook_name = os.path.basename(args.notebook_path)
|
|
143
|
+
s3_html_key = f"{s3_key_prefix}/output.html"
|
|
144
|
+
upload_file_to_s3(
|
|
145
|
+
file_path=html_output_path, bucket_name=s3_bucket, s3_key=s3_html_key
|
|
146
|
+
)
|
|
147
|
+
print(f"Successfully uploaded HTML to s3://{s3_bucket}/{s3_html_key}")
|
|
148
|
+
|
|
54
149
|
except Exception as e:
|
|
55
150
|
print(f"Error executing notebook {args.notebook_path}: {e}")
|
|
56
151
|
print(
|
|
57
152
|
"Exiting with status code 1 to signal failure to parent process/orchestrator"
|
|
58
153
|
)
|
|
59
154
|
sys.exit(1)
|
|
60
|
-
|
|
61
|
-
# TODO(gw): Publish the output notebook to blob storage from where it could be rendered
|
|
@@ -30,7 +30,14 @@ from rich.progress import (
|
|
|
30
30
|
TransferSpeedColumn,
|
|
31
31
|
)
|
|
32
32
|
from tqdm.utils import CallbackIOWrapper
|
|
33
|
+
from truefoundry_sdk import (
|
|
34
|
+
MultiPartUploadResponse,
|
|
35
|
+
MultiPartUploadStorageProvider,
|
|
36
|
+
Operation,
|
|
37
|
+
SignedUrl,
|
|
38
|
+
)
|
|
33
39
|
|
|
40
|
+
from truefoundry import client
|
|
34
41
|
from truefoundry.common.constants import ENV_VARS
|
|
35
42
|
from truefoundry.common.request_utils import (
|
|
36
43
|
augmented_raise_for_status,
|
|
@@ -45,23 +52,13 @@ from truefoundry.common.storage_provider_utils import (
|
|
|
45
52
|
)
|
|
46
53
|
from truefoundry.ml._autogen.client import ( # type: ignore[attr-defined]
|
|
47
54
|
ApiClient,
|
|
48
|
-
CreateMultiPartUploadForDatasetRequestDto,
|
|
49
|
-
CreateMultiPartUploadRequestDto,
|
|
50
55
|
FileInfoDto,
|
|
51
|
-
GetSignedURLForDatasetWriteRequestDto,
|
|
52
|
-
GetSignedURLsForArtifactVersionReadRequestDto,
|
|
53
|
-
GetSignedURLsForArtifactVersionWriteRequestDto,
|
|
54
|
-
GetSignedURLsForDatasetReadRequestDto,
|
|
55
56
|
ListFilesForArtifactVersionRequestDto,
|
|
56
57
|
ListFilesForArtifactVersionsResponseDto,
|
|
57
58
|
ListFilesForDatasetRequestDto,
|
|
58
59
|
ListFilesForDatasetResponseDto,
|
|
59
60
|
MlfoundryArtifactsApi,
|
|
60
|
-
MultiPartUploadDto,
|
|
61
|
-
MultiPartUploadResponseDto,
|
|
62
|
-
MultiPartUploadStorageProvider,
|
|
63
61
|
RunArtifactsApi,
|
|
64
|
-
SignedURLDto,
|
|
65
62
|
)
|
|
66
63
|
from truefoundry.ml.exceptions import MlFoundryException
|
|
67
64
|
from truefoundry.ml.logger import logger
|
|
@@ -127,7 +124,7 @@ def verify_artifact_path(artifact_path):
|
|
|
127
124
|
|
|
128
125
|
|
|
129
126
|
def _signed_url_upload_file(
|
|
130
|
-
signed_url:
|
|
127
|
+
signed_url: SignedUrl,
|
|
131
128
|
local_file: str,
|
|
132
129
|
progress_bar: Progress,
|
|
133
130
|
abort_event: Optional[Event] = None,
|
|
@@ -206,21 +203,20 @@ def _any_future_has_failed(futures) -> bool:
|
|
|
206
203
|
|
|
207
204
|
class ArtifactIdentifier(BaseModel):
|
|
208
205
|
artifact_version_id: Optional[uuid.UUID] = None
|
|
206
|
+
dataset_id: Optional[str] = None
|
|
209
207
|
dataset_fqn: Optional[str] = None
|
|
210
208
|
|
|
211
209
|
@root_validator
|
|
212
210
|
def _check_identifier_type(cls, values: Dict[str, Any]):
|
|
213
211
|
if not values.get("artifact_version_id", False) and not values.get(
|
|
214
|
-
"
|
|
212
|
+
"dataset_id", False
|
|
215
213
|
):
|
|
216
214
|
raise MlFoundryException(
|
|
217
|
-
"One of the version_id or
|
|
215
|
+
"One of the version_id or dataset_id should be passed"
|
|
218
216
|
)
|
|
219
|
-
if values.get("artifact_version_id", False) and values.get(
|
|
220
|
-
"dataset_fqn", False
|
|
221
|
-
):
|
|
217
|
+
if values.get("artifact_version_id", False) and values.get("dataset_id", False):
|
|
222
218
|
raise MlFoundryException(
|
|
223
|
-
"Exactly one of version_id or
|
|
219
|
+
"Exactly one of version_id or dataset_id should be passed"
|
|
224
220
|
)
|
|
225
221
|
return values
|
|
226
222
|
|
|
@@ -243,24 +239,25 @@ class MlFoundryArtifactsRepository:
|
|
|
243
239
|
self,
|
|
244
240
|
artifact_identifier: ArtifactIdentifier,
|
|
245
241
|
paths,
|
|
246
|
-
) -> List[
|
|
242
|
+
) -> List[SignedUrl]:
|
|
247
243
|
if artifact_identifier.artifact_version_id:
|
|
248
|
-
signed_urls_response =
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
244
|
+
signed_urls_response = client.artifact_versions.get_signed_urls(
|
|
245
|
+
id=str(artifact_identifier.artifact_version_id),
|
|
246
|
+
paths=paths,
|
|
247
|
+
operation=Operation.READ,
|
|
252
248
|
)
|
|
253
|
-
signed_urls = signed_urls_response.
|
|
254
|
-
elif artifact_identifier.
|
|
255
|
-
signed_urls_dataset_response =
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
249
|
+
signed_urls = signed_urls_response.data
|
|
250
|
+
elif artifact_identifier.dataset_id:
|
|
251
|
+
signed_urls_dataset_response = client.data_directories.get_signed_urls(
|
|
252
|
+
id=str(artifact_identifier.dataset_id),
|
|
253
|
+
paths=paths,
|
|
254
|
+
operation=Operation.READ,
|
|
259
255
|
)
|
|
260
|
-
signed_urls = signed_urls_dataset_response.
|
|
256
|
+
signed_urls = signed_urls_dataset_response.data
|
|
257
|
+
|
|
261
258
|
else:
|
|
262
259
|
raise ValueError(
|
|
263
|
-
"Invalid artifact type - both `artifact_version_id` and `
|
|
260
|
+
"Invalid artifact type - both `artifact_version_id` and `dataset_id` both are None"
|
|
264
261
|
)
|
|
265
262
|
return signed_urls
|
|
266
263
|
|
|
@@ -268,24 +265,24 @@ class MlFoundryArtifactsRepository:
|
|
|
268
265
|
self,
|
|
269
266
|
artifact_identifier: ArtifactIdentifier,
|
|
270
267
|
paths: List[str],
|
|
271
|
-
) -> List[
|
|
268
|
+
) -> List[SignedUrl]:
|
|
272
269
|
if artifact_identifier.artifact_version_id:
|
|
273
|
-
signed_urls_response =
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
270
|
+
signed_urls_response = client.artifact_versions.get_signed_urls(
|
|
271
|
+
id=str(artifact_identifier.artifact_version_id),
|
|
272
|
+
paths=paths,
|
|
273
|
+
operation=Operation.WRITE,
|
|
277
274
|
)
|
|
278
|
-
signed_urls = signed_urls_response.
|
|
279
|
-
elif artifact_identifier.
|
|
280
|
-
signed_urls_dataset_response =
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
275
|
+
signed_urls = signed_urls_response.data
|
|
276
|
+
elif artifact_identifier.dataset_id:
|
|
277
|
+
signed_urls_dataset_response = client.data_directories.get_signed_urls(
|
|
278
|
+
id=str(artifact_identifier.dataset_id),
|
|
279
|
+
paths=paths,
|
|
280
|
+
operation=Operation.WRITE,
|
|
284
281
|
)
|
|
285
|
-
signed_urls = signed_urls_dataset_response.
|
|
282
|
+
signed_urls = signed_urls_dataset_response.data
|
|
286
283
|
else:
|
|
287
284
|
raise ValueError(
|
|
288
|
-
"Invalid artifact type - both `artifact_version_id` and `
|
|
285
|
+
"Invalid artifact type - both `artifact_version_id` and `dataset_id` both are None"
|
|
289
286
|
)
|
|
290
287
|
return signed_urls
|
|
291
288
|
|
|
@@ -293,7 +290,7 @@ class MlFoundryArtifactsRepository:
|
|
|
293
290
|
self,
|
|
294
291
|
local_file: str,
|
|
295
292
|
artifact_path: str,
|
|
296
|
-
signed_url: Optional[
|
|
293
|
+
signed_url: Optional[SignedUrl],
|
|
297
294
|
progress_bar: Progress,
|
|
298
295
|
abort_event: Optional[Event] = None,
|
|
299
296
|
):
|
|
@@ -322,28 +319,30 @@ class MlFoundryArtifactsRepository:
|
|
|
322
319
|
artifact_identifier: ArtifactIdentifier,
|
|
323
320
|
path,
|
|
324
321
|
num_parts,
|
|
325
|
-
) ->
|
|
322
|
+
) -> MultiPartUpload:
|
|
326
323
|
if artifact_identifier.artifact_version_id:
|
|
327
|
-
create_multipart_response:
|
|
328
|
-
|
|
329
|
-
|
|
324
|
+
create_multipart_response: MultiPartUploadResponse = (
|
|
325
|
+
client.artifact_versions.create_multi_part_upload(
|
|
326
|
+
id=str(artifact_identifier.artifact_version_id),
|
|
330
327
|
path=path,
|
|
331
328
|
num_parts=num_parts,
|
|
332
329
|
)
|
|
333
330
|
)
|
|
334
|
-
multipart_upload = create_multipart_response.
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
331
|
+
multipart_upload = create_multipart_response.data
|
|
332
|
+
|
|
333
|
+
elif artifact_identifier.dataset_id:
|
|
334
|
+
create_multipart_for_dataset_response: MultiPartUploadResponse = (
|
|
335
|
+
client.data_directories.create_multipart_upload(
|
|
336
|
+
id=str(artifact_identifier.dataset_id),
|
|
339
337
|
path=path,
|
|
340
338
|
num_parts=num_parts,
|
|
341
339
|
)
|
|
342
340
|
)
|
|
343
|
-
multipart_upload = create_multipart_for_dataset_response.
|
|
341
|
+
multipart_upload = create_multipart_for_dataset_response.data
|
|
342
|
+
|
|
344
343
|
else:
|
|
345
344
|
raise ValueError(
|
|
346
|
-
"Invalid artifact type - both `artifact_version_id` and `
|
|
345
|
+
"Invalid artifact type - both `artifact_version_id` and `dataset_id` both are None"
|
|
347
346
|
)
|
|
348
347
|
return multipart_upload
|
|
349
348
|
|
|
@@ -370,10 +369,10 @@ class MlFoundryArtifactsRepository:
|
|
|
370
369
|
)
|
|
371
370
|
if (
|
|
372
371
|
multipart_upload.storage_provider
|
|
373
|
-
is MultiPartUploadStorageProvider.
|
|
372
|
+
is MultiPartUploadStorageProvider.S3COMPATIBLE
|
|
374
373
|
):
|
|
375
374
|
s3_compatible_multipart_upload(
|
|
376
|
-
multipart_upload=MultiPartUpload.parse_obj(multipart_upload.
|
|
375
|
+
multipart_upload=MultiPartUpload.parse_obj(multipart_upload.dict()),
|
|
377
376
|
local_file=local_file,
|
|
378
377
|
executor=executor,
|
|
379
378
|
multipart_info=multipart_info,
|
|
@@ -403,7 +402,7 @@ class MlFoundryArtifactsRepository:
|
|
|
403
402
|
artifact_path: str,
|
|
404
403
|
multipart_info: _FileMultiPartInfo,
|
|
405
404
|
progress_bar: Progress,
|
|
406
|
-
signed_url: Optional[
|
|
405
|
+
signed_url: Optional[SignedUrl] = None,
|
|
407
406
|
abort_event: Optional[Event] = None,
|
|
408
407
|
executor_for_multipart_upload: Optional[ThreadPoolExecutor] = None,
|
|
409
408
|
):
|
|
@@ -678,7 +677,7 @@ class MlFoundryArtifactsRepository:
|
|
|
678
677
|
remote_file_path: str,
|
|
679
678
|
local_path: str,
|
|
680
679
|
progress_bar: Optional[Progress],
|
|
681
|
-
signed_url: Optional[
|
|
680
|
+
signed_url: Optional[SignedUrl],
|
|
682
681
|
abort_event: Optional[Event] = None,
|
|
683
682
|
):
|
|
684
683
|
if not remote_file_path:
|
|
@@ -719,7 +718,7 @@ class MlFoundryArtifactsRepository:
|
|
|
719
718
|
self,
|
|
720
719
|
src_artifact_path: str,
|
|
721
720
|
dst_local_dir_path: str,
|
|
722
|
-
signed_url: Optional[
|
|
721
|
+
signed_url: Optional[SignedUrl],
|
|
723
722
|
progress_bar: Optional[Progress] = None,
|
|
724
723
|
abort_event=None,
|
|
725
724
|
) -> str:
|
|
@@ -8,6 +8,7 @@ import warnings
|
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
from typing import TYPE_CHECKING, Any, Dict, NamedTuple, Optional, Union
|
|
10
10
|
|
|
11
|
+
from truefoundry import client
|
|
11
12
|
from truefoundry.common.warnings import TrueFoundryDeprecationWarning
|
|
12
13
|
from truefoundry.ml._autogen.client import ( # type: ignore[attr-defined]
|
|
13
14
|
ArtifactDto,
|
|
@@ -16,7 +17,6 @@ from truefoundry.ml._autogen.client import ( # type: ignore[attr-defined]
|
|
|
16
17
|
ArtifactVersionDto,
|
|
17
18
|
CreateArtifactVersionRequestDto,
|
|
18
19
|
DeleteArtifactVersionsRequestDto,
|
|
19
|
-
ExperimentsApi,
|
|
20
20
|
ExternalBlobStorageSource,
|
|
21
21
|
FinalizeArtifactVersionRequestDto,
|
|
22
22
|
MlfoundryArtifactsApi,
|
|
@@ -444,19 +444,15 @@ def _log_artifact_version_helper(
|
|
|
444
444
|
|
|
445
445
|
if run:
|
|
446
446
|
mlfoundry_artifacts_api = run._mlfoundry_artifacts_api
|
|
447
|
-
|
|
448
|
-
ml_repo =
|
|
449
|
-
experiment_id=str(run._experiment_id)
|
|
450
|
-
).experiment.name
|
|
447
|
+
repos = client.ml_repos.get(id=run._experiment_id)
|
|
448
|
+
ml_repo = repos.data.manifest.name
|
|
451
449
|
ml_repo_id = run._experiment_id
|
|
452
450
|
else:
|
|
453
451
|
assert ml_repo is not None
|
|
454
452
|
api_client = _get_api_client()
|
|
455
453
|
mlfoundry_artifacts_api = MlfoundryArtifactsApi(api_client=api_client)
|
|
456
|
-
|
|
457
|
-
ml_repo_id =
|
|
458
|
-
experiment_name=ml_repo
|
|
459
|
-
).experiment.experiment_id
|
|
454
|
+
result = list(client.ml_repos.list(name=ml_repo, limit=1))[0]
|
|
455
|
+
ml_repo_id = result.id
|
|
460
456
|
|
|
461
457
|
metadata = metadata or {}
|
|
462
458
|
if ARTIFACT_METADATA_TRUEFOUNDRY_KEY not in metadata:
|
|
@@ -84,7 +84,9 @@ class DataDirectory:
|
|
|
84
84
|
|
|
85
85
|
def _get_artifacts_repo(self):
|
|
86
86
|
return MlFoundryArtifactsRepository(
|
|
87
|
-
artifact_identifier=ArtifactIdentifier(
|
|
87
|
+
artifact_identifier=ArtifactIdentifier(
|
|
88
|
+
dataset_id=self._dataset.id, dataset_fqn=self._dataset.fqn
|
|
89
|
+
),
|
|
88
90
|
api_client=self._api_client,
|
|
89
91
|
)
|
|
90
92
|
|
|
@@ -9,12 +9,12 @@ import warnings
|
|
|
9
9
|
from pathlib import Path
|
|
10
10
|
from typing import TYPE_CHECKING, Any, Dict, Optional, Union
|
|
11
11
|
|
|
12
|
+
from truefoundry import client
|
|
12
13
|
from truefoundry.common.warnings import TrueFoundryDeprecationWarning
|
|
13
14
|
from truefoundry.ml._autogen.client import ( # type: ignore[attr-defined]
|
|
14
15
|
ArtifactType,
|
|
15
16
|
CreateArtifactVersionRequestDto,
|
|
16
17
|
DeleteArtifactVersionsRequestDto,
|
|
17
|
-
ExperimentsApi,
|
|
18
18
|
ExternalBlobStorageSource,
|
|
19
19
|
FinalizeArtifactVersionRequestDto,
|
|
20
20
|
Framework,
|
|
@@ -531,19 +531,15 @@ def _log_model_version( # noqa: C901
|
|
|
531
531
|
|
|
532
532
|
if run:
|
|
533
533
|
mlfoundry_artifacts_api = run._mlfoundry_artifacts_api
|
|
534
|
-
|
|
535
|
-
ml_repo =
|
|
536
|
-
experiment_id=str(run._experiment_id)
|
|
537
|
-
).experiment.name
|
|
534
|
+
repos = client.ml_repos.get(id=run._experiment_id)
|
|
535
|
+
ml_repo = repos.data.manifest.name
|
|
538
536
|
ml_repo_id = run._experiment_id
|
|
539
537
|
else:
|
|
540
538
|
assert ml_repo is not None
|
|
541
539
|
api_client = _get_api_client()
|
|
542
540
|
mlfoundry_artifacts_api = MlfoundryArtifactsApi(api_client=api_client)
|
|
543
|
-
|
|
544
|
-
ml_repo_id =
|
|
545
|
-
experiment_name=ml_repo
|
|
546
|
-
).experiment.experiment_id
|
|
541
|
+
result = list(client.ml_repos.list(name=ml_repo, limit=1))[0]
|
|
542
|
+
ml_repo_id = result.id
|
|
547
543
|
|
|
548
544
|
step = step or 0
|
|
549
545
|
total_size = None
|
truefoundry/ml/mlfoundry_api.py
CHANGED
|
@@ -16,14 +16,16 @@ from typing import (
|
|
|
16
16
|
)
|
|
17
17
|
|
|
18
18
|
import coolname
|
|
19
|
+
from truefoundry_sdk import Collaborator, MlRepoManifest, NotFoundError
|
|
20
|
+
from truefoundry_sdk.core import ApiError
|
|
19
21
|
|
|
22
|
+
from truefoundry import client
|
|
20
23
|
from truefoundry.common.utils import ContextualDirectoryManager
|
|
21
24
|
from truefoundry.ml import constants
|
|
22
25
|
from truefoundry.ml._autogen.client import ( # type: ignore[attr-defined]
|
|
23
26
|
ArtifactDto,
|
|
24
27
|
ArtifactType,
|
|
25
28
|
CreateDatasetRequestDto,
|
|
26
|
-
CreateExperimentRequestDto,
|
|
27
29
|
CreateRunRequestDto,
|
|
28
30
|
DatasetDto,
|
|
29
31
|
ExperimentsApi,
|
|
@@ -132,24 +134,21 @@ class MlFoundry:
|
|
|
132
134
|
str: The id of the ML Repo.
|
|
133
135
|
"""
|
|
134
136
|
try:
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
ml_repo_instance = _ml_repo_instance.experiment
|
|
139
|
-
except NotFoundException:
|
|
137
|
+
result = list(client.ml_repos.list(name=ml_repo, limit=1))
|
|
138
|
+
ml_repo_instance = result[0]
|
|
139
|
+
except (IndexError, NotFoundError):
|
|
140
140
|
err_msg = (
|
|
141
141
|
f"ML Repo Does Not Exist for name: {ml_repo}. You may either "
|
|
142
142
|
"create it from the dashboard or using client.create_ml_repo('<ml_repo_name>')"
|
|
143
143
|
)
|
|
144
144
|
raise MlFoundryException(err_msg) from None
|
|
145
|
-
except
|
|
145
|
+
except ApiError as e:
|
|
146
146
|
err_msg = (
|
|
147
147
|
f"Error happened in getting ML Repo based on name: "
|
|
148
148
|
f"{ml_repo}. Error details: {e}"
|
|
149
149
|
)
|
|
150
150
|
raise MlFoundryException(err_msg) from e
|
|
151
|
-
|
|
152
|
-
return ml_repo_instance.experiment_id
|
|
151
|
+
return ml_repo_instance.id
|
|
153
152
|
|
|
154
153
|
def list_ml_repos(self) -> List[str]:
|
|
155
154
|
"""Returns a list of names of ML Repos accessible by the current user.
|
|
@@ -160,26 +159,14 @@ class MlFoundry:
|
|
|
160
159
|
# TODO (chiragjn): This API should yield ML Repo Entities instead of just names
|
|
161
160
|
# Kinda useless without it
|
|
162
161
|
ml_repos_names = []
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
except ApiException as e:
|
|
172
|
-
err_msg = f"Error happened in fetching ML Repos. Error details: {e}"
|
|
173
|
-
raise MlFoundryException(err_msg) from e
|
|
174
|
-
else:
|
|
175
|
-
ml_repos = _ml_repos.experiments
|
|
176
|
-
page_token = _ml_repos.next_page_token
|
|
177
|
-
for ml_repo in ml_repos:
|
|
178
|
-
# ML Repo with experiment_id 0 represents default ML Repo which we are removing.
|
|
179
|
-
if ml_repo.experiment_id != "0":
|
|
180
|
-
ml_repos_names.append(ml_repo.name)
|
|
181
|
-
if not ml_repos or page_token is None:
|
|
182
|
-
done = True
|
|
162
|
+
max_results = 25
|
|
163
|
+
try:
|
|
164
|
+
for ml_repo_instance in client.ml_repos.list(limit=max_results):
|
|
165
|
+
if ml_repo_instance.id != "0":
|
|
166
|
+
ml_repos_names.append(ml_repo_instance.manifest.name)
|
|
167
|
+
except ApiError as e:
|
|
168
|
+
err_msg = f"Error happened in fetching ML Repos. Error details: {e}"
|
|
169
|
+
raise MlFoundryException(err_msg) from e
|
|
183
170
|
return ml_repos_names
|
|
184
171
|
|
|
185
172
|
def create_ml_repo(
|
|
@@ -211,23 +198,39 @@ class MlFoundry:
|
|
|
211
198
|
if description:
|
|
212
199
|
_validate_ml_repo_description(description=description)
|
|
213
200
|
try:
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
existing_ml_repo = _ml_repo_instance.experiment
|
|
218
|
-
except NotFoundException:
|
|
201
|
+
result = list(client.ml_repos.list(name=name, limit=1))
|
|
202
|
+
existing_ml_repo = result[0]
|
|
203
|
+
except (IndexError, NotFoundError):
|
|
219
204
|
existing_ml_repo = None
|
|
205
|
+
except ApiError as e:
|
|
206
|
+
err_msg = (
|
|
207
|
+
f"Error happened in getting ML Repo based on name: "
|
|
208
|
+
f"{name}. Error details: {e}"
|
|
209
|
+
)
|
|
210
|
+
raise MlFoundryException(err_msg) from e
|
|
220
211
|
|
|
221
212
|
if not existing_ml_repo:
|
|
213
|
+
session = client._get_session()
|
|
214
|
+
user_info = session.user_info
|
|
215
|
+
if not user_info.email:
|
|
216
|
+
raise MlFoundryException(
|
|
217
|
+
"Virtual accounts cannot be used to create new ML Repos"
|
|
218
|
+
)
|
|
222
219
|
try:
|
|
223
|
-
|
|
224
|
-
|
|
220
|
+
client.ml_repos.create_or_update(
|
|
221
|
+
manifest=MlRepoManifest(
|
|
225
222
|
name=name,
|
|
226
223
|
description=description,
|
|
227
224
|
storage_integration_fqn=storage_integration_fqn,
|
|
228
|
-
|
|
225
|
+
collaborators=[
|
|
226
|
+
Collaborator(
|
|
227
|
+
subject=f"user:{user_info.email}",
|
|
228
|
+
role_id="mlf-project-admin",
|
|
229
|
+
)
|
|
230
|
+
],
|
|
231
|
+
),
|
|
229
232
|
)
|
|
230
|
-
except
|
|
233
|
+
except ApiError as e:
|
|
231
234
|
err_msg = f"Error happened in creating ML Repo with name: {name}. Error details: {e}"
|
|
232
235
|
raise MlFoundryException(err_msg) from e
|
|
233
236
|
return
|
|
@@ -554,16 +557,19 @@ class MlFoundry:
|
|
|
554
557
|
"""
|
|
555
558
|
_validate_ml_repo_name(ml_repo_name=ml_repo)
|
|
556
559
|
try:
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
560
|
+
result = list(client.ml_repos.list(name=ml_repo, limit=1))
|
|
561
|
+
ml_repo_instance = result[0]
|
|
562
|
+
except (IndexError, NotFoundError):
|
|
563
|
+
raise MlFoundryException(
|
|
564
|
+
f"ML Repo with name {ml_repo} does not exist. "
|
|
565
|
+
"You may either create it from the dashboard or using client.create_ml_repo('<ml_repo_name>')"
|
|
566
|
+
) from None
|
|
567
|
+
except ApiError as e:
|
|
562
568
|
raise MlFoundryException(
|
|
563
569
|
f"ML Repo with name {ml_repo} does not exist or your user does not have permission to access it: {e}"
|
|
564
570
|
) from e
|
|
565
571
|
|
|
566
|
-
ml_repo_id =
|
|
572
|
+
ml_repo_id = ml_repo_instance.id
|
|
567
573
|
|
|
568
574
|
page_token = None
|
|
569
575
|
done = False
|
truefoundry/ml/mlfoundry_run.py
CHANGED
|
@@ -16,12 +16,11 @@ from typing import (
|
|
|
16
16
|
)
|
|
17
17
|
from urllib.parse import urljoin, urlsplit
|
|
18
18
|
|
|
19
|
-
from truefoundry import version
|
|
19
|
+
from truefoundry import client, version
|
|
20
20
|
from truefoundry.ml import constants
|
|
21
21
|
from truefoundry.ml._autogen.client import ( # type: ignore[attr-defined]
|
|
22
22
|
ArtifactType,
|
|
23
23
|
DeleteRunRequest,
|
|
24
|
-
ExperimentsApi,
|
|
25
24
|
ListArtifactVersionsRequestDto,
|
|
26
25
|
ListModelVersionsRequestDto,
|
|
27
26
|
LogBatchRequestDto,
|
|
@@ -110,7 +109,6 @@ class MlFoundryRun:
|
|
|
110
109
|
ACTIVE_RUNS.add_run(self)
|
|
111
110
|
|
|
112
111
|
self._api_client = _get_api_client()
|
|
113
|
-
self._experiments_api = ExperimentsApi(api_client=self._api_client)
|
|
114
112
|
self._runs_api = RunsApi(api_client=self._api_client)
|
|
115
113
|
self._metrics_api = MetricsApi(api_client=self._api_client)
|
|
116
114
|
self._mlfoundry_artifacts_api = MlfoundryArtifactsApi(
|
|
@@ -165,10 +163,8 @@ class MlFoundryRun:
|
|
|
165
163
|
@_ensure_not_deleted
|
|
166
164
|
def ml_repo(self) -> str:
|
|
167
165
|
"""Get ml_repo name of which the current `run` is part of"""
|
|
168
|
-
_experiment =
|
|
169
|
-
|
|
170
|
-
)
|
|
171
|
-
return _experiment.experiment.name
|
|
166
|
+
_experiment = client.ml_repos.get(id=self._experiment_id)
|
|
167
|
+
return _experiment.data.manifest.name
|
|
172
168
|
|
|
173
169
|
@property
|
|
174
170
|
@_ensure_not_deleted
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: truefoundry
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.11.0
|
|
4
4
|
Summary: TrueFoundry CLI
|
|
5
5
|
Author-email: TrueFoundry Team <abhishek@truefoundry.com>
|
|
6
6
|
Requires-Python: <3.14,>=3.8.1
|
|
@@ -30,7 +30,7 @@ Requires-Dist: requirements-parser<0.12.0,>=0.11.0
|
|
|
30
30
|
Requires-Dist: rich-click<2.0.0,>=1.2.1
|
|
31
31
|
Requires-Dist: rich<14.0.0,>=13.7.1
|
|
32
32
|
Requires-Dist: tqdm<5.0.0,>=4.0.0
|
|
33
|
-
Requires-Dist: truefoundry-sdk<0.2.0,>=0.1.
|
|
33
|
+
Requires-Dist: truefoundry-sdk<0.2.0,>=0.1.5
|
|
34
34
|
Requires-Dist: typing-extensions>=4.0
|
|
35
35
|
Requires-Dist: urllib3<3,>=1.26.18
|
|
36
36
|
Requires-Dist: yq<4.0.0,>=3.1.0
|
|
@@ -66,8 +66,8 @@ truefoundry/deploy/builder/builders/tfy_notebook_buildpack/dockerfile_template.p
|
|
|
66
66
|
truefoundry/deploy/builder/builders/tfy_python_buildpack/__init__.py,sha256=_fjqHKn80qKi68SAMMALge7_A6e1sTsQWichw8uoGIw,2025
|
|
67
67
|
truefoundry/deploy/builder/builders/tfy_python_buildpack/dockerfile_template.py,sha256=f4l3fH21E2b8W3-JotMKc0AdPcCxV7LRPxxYJa7z_UQ,9134
|
|
68
68
|
truefoundry/deploy/builder/builders/tfy_spark_buildpack/__init__.py,sha256=NEPlM6_vTVxp4ITa18B8DBbgYCn1q5d8be21lbgu5oY,2888
|
|
69
|
-
truefoundry/deploy/builder/builders/tfy_spark_buildpack/dockerfile_template.py,sha256=
|
|
70
|
-
truefoundry/deploy/builder/builders/tfy_spark_buildpack/tfy_execute_notebook.py,sha256=
|
|
69
|
+
truefoundry/deploy/builder/builders/tfy_spark_buildpack/dockerfile_template.py,sha256=2zohUaW8Yw_QREHlpRW7Pooomt19HJh44fHjlsiDmwM,6064
|
|
70
|
+
truefoundry/deploy/builder/builders/tfy_spark_buildpack/tfy_execute_notebook.py,sha256=tTx-GZDVf5iB1Pyz2z5c2LH1yrb7lErFbJcr-giAIuI,5734
|
|
71
71
|
truefoundry/deploy/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
72
72
|
truefoundry/deploy/cli/commands/__init__.py,sha256=qv818jxqSAygJ3h-6Ul8t-5VOgR_UrSgsVtNCl3e5G0,1408
|
|
73
73
|
truefoundry/deploy/cli/commands/apply_command.py,sha256=DmXmKVokkauyKIiJDtErTwbJ5_LvQeJbTQsG5BjyKpo,2427
|
|
@@ -127,8 +127,8 @@ truefoundry/ml/exceptions.py,sha256=QpDJSWmF7dIsByS0qOQbQZ_jytdNTzkHDDO3BxhTSo0,
|
|
|
127
127
|
truefoundry/ml/git_info.py,sha256=jvAVm9ilqivnGq8qJdUvYdd8Siv0PLtqurB-PXsS5ho,2023
|
|
128
128
|
truefoundry/ml/internal_namespace.py,sha256=QcqMHp6-C2im2H_02hlhi01EIcr1HhNaZprszs13EMU,1790
|
|
129
129
|
truefoundry/ml/logger.py,sha256=VT-BF3BnBYTWVq87O58F0c8uXMu94gYzsiFlGY3_7Ao,458
|
|
130
|
-
truefoundry/ml/mlfoundry_api.py,sha256=
|
|
131
|
-
truefoundry/ml/mlfoundry_run.py,sha256=
|
|
130
|
+
truefoundry/ml/mlfoundry_api.py,sha256=hA6XjaONGEUS4beewZd5HEA0mGRew_wMvUJaQW7CThc,60168
|
|
131
|
+
truefoundry/ml/mlfoundry_run.py,sha256=b0wm08OUAiO1UpzdDNBeJh-Gsm_O5kEdbY86Ugv-26Y,44181
|
|
132
132
|
truefoundry/ml/model_framework.py,sha256=nVbKTtKDRBdLzt7Wrg5_vJKZ-awHbISGvL73s-V9egU,18975
|
|
133
133
|
truefoundry/ml/prompt_utils.py,sha256=8FueyElVTXLnLtC3O6hKsW_snocArr_B8KG3Qv6eFIQ,2651
|
|
134
134
|
truefoundry/ml/run_utils.py,sha256=0W208wSLUrbdfk2pjNcZlkUi9bNxG2JORqoe-5rVqHI,2423
|
|
@@ -347,7 +347,7 @@ truefoundry/ml/_autogen/models/schema.py,sha256=a_bp42MMPUbwO3407m0UW2W8EOhnxZXf
|
|
|
347
347
|
truefoundry/ml/_autogen/models/signature.py,sha256=rBjpxUIsEeWM0sIyYG5uCJB18DKHR4k5yZw8TzuoP48,4987
|
|
348
348
|
truefoundry/ml/_autogen/models/utils.py,sha256=c7RtSLXhOLcP8rjuUtfnMdaKVTZvvbsmw98gPAkAFrs,24371
|
|
349
349
|
truefoundry/ml/artifact/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
350
|
-
truefoundry/ml/artifact/truefoundry_artifact_repo.py,sha256=
|
|
350
|
+
truefoundry/ml/artifact/truefoundry_artifact_repo.py,sha256=ocX5EIcLQa8Uc_C3NxxgNorpxc-z1Yp4TLvmzSORPpw,36862
|
|
351
351
|
truefoundry/ml/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
352
352
|
truefoundry/ml/cli/cli.py,sha256=MwpY7z_NEeJE_XIP7XbZELjNeu2vpMmohttHCKDRk54,335
|
|
353
353
|
truefoundry/ml/cli/utils.py,sha256=j6_mZ4Spn114mz3P4QQ8jx0tmorXIuyQnHXVUSDvZi4,1035
|
|
@@ -360,11 +360,11 @@ truefoundry/ml/log_types/__init__.py,sha256=g4u4D4Jaj0aBK5GtrLV88-qThKZR9pSZ17vF
|
|
|
360
360
|
truefoundry/ml/log_types/plot.py,sha256=LDh4uy6z2P_a2oPM2lc85c0lt8utVvunohzeMawFjZw,7572
|
|
361
361
|
truefoundry/ml/log_types/pydantic_base.py,sha256=eBlw_AEyAz4iJKDP4zgJOCFWcldwQqpf7FADW1jzIQY,272
|
|
362
362
|
truefoundry/ml/log_types/utils.py,sha256=xjJ21jdPScvFmw3TbVh5NCzbzJwaqiXJyiiT4xxX1EI,335
|
|
363
|
-
truefoundry/ml/log_types/artifacts/artifact.py,sha256=
|
|
363
|
+
truefoundry/ml/log_types/artifacts/artifact.py,sha256=dZj9UkRvvU5EN-HUiuLCPsmosSS0yyHM5JNWzaaIxMA,19887
|
|
364
364
|
truefoundry/ml/log_types/artifacts/constants.py,sha256=uB2JPEqwTbqevkQv2QcEMROsm_4cVAl6s0QU1MLa8SQ,1088
|
|
365
|
-
truefoundry/ml/log_types/artifacts/dataset.py,sha256=
|
|
365
|
+
truefoundry/ml/log_types/artifacts/dataset.py,sha256=OgWIoT59AhMw8P01FfvUKbJ3EL6HQf_Xw8X4E3Ff5Sg,13172
|
|
366
366
|
truefoundry/ml/log_types/artifacts/general_artifact.py,sha256=yr-SQ2fhUR_sE1MB5zoHHYpGC8tizH_-t3lhsxCAULU,2747
|
|
367
|
-
truefoundry/ml/log_types/artifacts/model.py,sha256=
|
|
367
|
+
truefoundry/ml/log_types/artifacts/model.py,sha256=kAiq7TDJ8RHG1Z4IN9mQD1dzKgmLCP1p0v_Yta9GHlM,24864
|
|
368
368
|
truefoundry/ml/log_types/artifacts/utils.py,sha256=q_atcGzn3wfxItt3RABxjdris8b3njEFNuC8ihWqUSI,8088
|
|
369
369
|
truefoundry/ml/log_types/image/__init__.py,sha256=fcOq8yQnNj1rkLcPeIjLXBpdA1WIeiPsXOlAAvMxx7M,76
|
|
370
370
|
truefoundry/ml/log_types/image/constants.py,sha256=wLtGEOA4T5fZHSlOXPuNDLX3lpbCtwlvGKPFk_1fah0,255
|
|
@@ -381,7 +381,7 @@ truefoundry/workflow/remote_filesystem/__init__.py,sha256=LQ95ViEjJ7Ts4JcCGOxMPs
|
|
|
381
381
|
truefoundry/workflow/remote_filesystem/logger.py,sha256=em2l7D6sw7xTLDP0kQSLpgfRRCLpN14Qw85TN7ujQcE,1022
|
|
382
382
|
truefoundry/workflow/remote_filesystem/tfy_signed_url_client.py,sha256=xcT0wQmQlgzcj0nP3tJopyFSVWT1uv3nhiTIuwfXYeg,12342
|
|
383
383
|
truefoundry/workflow/remote_filesystem/tfy_signed_url_fs.py,sha256=nSGPZu0Gyd_jz0KsEE-7w_BmnTD8CVF1S8cUJoxaCbc,13305
|
|
384
|
-
truefoundry-0.
|
|
385
|
-
truefoundry-0.
|
|
386
|
-
truefoundry-0.
|
|
387
|
-
truefoundry-0.
|
|
384
|
+
truefoundry-0.11.0.dist-info/METADATA,sha256=nxfBh-eMI7_N9k5Baa3I_H3UwKMNOUH2jWPuE5-l4TE,2505
|
|
385
|
+
truefoundry-0.11.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
386
|
+
truefoundry-0.11.0.dist-info/entry_points.txt,sha256=xVjn7RMN-MW2-9f7YU-bBdlZSvvrwzhpX1zmmRmsNPU,98
|
|
387
|
+
truefoundry-0.11.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|