truefoundry 0.6.2__py3-none-any.whl → 0.6.3__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/utils.py +4 -1
- truefoundry/deploy/__init__.py +3 -0
- truefoundry/deploy/_autogen/models.py +138 -77
- truefoundry/deploy/lib/clients/servicefoundry_client.py +13 -4
- truefoundry/deploy/v2/lib/deploy.py +19 -7
- truefoundry/deploy/v2/lib/deploy_workflow.py +35 -5
- truefoundry/deploy/v2/lib/patched_models.py +8 -0
- truefoundry/workflow/workflow.py +10 -3
- {truefoundry-0.6.2.dist-info → truefoundry-0.6.3.dist-info}/METADATA +2 -2
- {truefoundry-0.6.2.dist-info → truefoundry-0.6.3.dist-info}/RECORD +12 -12
- {truefoundry-0.6.2.dist-info → truefoundry-0.6.3.dist-info}/WHEEL +0 -0
- {truefoundry-0.6.2.dist-info → truefoundry-0.6.3.dist-info}/entry_points.txt +0 -0
truefoundry/common/utils.py
CHANGED
|
@@ -93,7 +93,10 @@ def timed_lru_cache(
|
|
|
93
93
|
|
|
94
94
|
|
|
95
95
|
def poll_for_function(
|
|
96
|
-
func: Callable[..., T],
|
|
96
|
+
func: Callable[..., T],
|
|
97
|
+
poll_after_secs: int = 5,
|
|
98
|
+
*args,
|
|
99
|
+
**kwargs,
|
|
97
100
|
) -> Generator[T, None, None]:
|
|
98
101
|
while True:
|
|
99
102
|
yield func(*args, **kwargs)
|
truefoundry/deploy/__init__.py
CHANGED
|
@@ -14,6 +14,7 @@ from truefoundry.deploy._autogen.models import (
|
|
|
14
14
|
SparkExecutorDynamicScaling,
|
|
15
15
|
SparkExecutorFixedInstances,
|
|
16
16
|
WorkbenchImage,
|
|
17
|
+
WorkflowAlert,
|
|
17
18
|
)
|
|
18
19
|
from truefoundry.deploy.lib.dao.application import (
|
|
19
20
|
delete_application,
|
|
@@ -72,6 +73,7 @@ from truefoundry.deploy.v2.lib.patched_models import (
|
|
|
72
73
|
CUDAVersion,
|
|
73
74
|
DockerFileBuild,
|
|
74
75
|
DynamicVolumeConfig,
|
|
76
|
+
Email,
|
|
75
77
|
Endpoint,
|
|
76
78
|
GcpTPU,
|
|
77
79
|
GitHelmRepo,
|
|
@@ -109,6 +111,7 @@ from truefoundry.deploy.v2.lib.patched_models import (
|
|
|
109
111
|
Schedule,
|
|
110
112
|
SecretMount,
|
|
111
113
|
ServiceAutoscaling,
|
|
114
|
+
SlackWebhook,
|
|
112
115
|
SQSInputConfig,
|
|
113
116
|
SQSOutputConfig,
|
|
114
117
|
SQSQueueMetricConfig,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# generated by datamodel-codegen:
|
|
2
2
|
# filename: application.json
|
|
3
|
-
# timestamp: 2025-03-
|
|
3
|
+
# timestamp: 2025-03-27T11:40:57+00:00
|
|
4
4
|
|
|
5
5
|
from __future__ import annotations
|
|
6
6
|
|
|
@@ -212,6 +212,17 @@ class DynamicVolumeConfig(BaseModel):
|
|
|
212
212
|
size: conint(ge=1, le=64000) = Field(..., description="Size of volume in Gi")
|
|
213
213
|
|
|
214
214
|
|
|
215
|
+
class Email(BaseModel):
|
|
216
|
+
type: Literal["email"] = Field(..., description="")
|
|
217
|
+
notification_channel: constr(min_length=1) = Field(
|
|
218
|
+
..., description="Specify the notification channel to send alerts to"
|
|
219
|
+
)
|
|
220
|
+
to_emails: List[constr(min_length=1)] = Field(
|
|
221
|
+
...,
|
|
222
|
+
description="List of recipients' email addresses if the notification channel is Email.",
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
|
|
215
226
|
class Endpoint(BaseModel):
|
|
216
227
|
host: constr(
|
|
217
228
|
regex=r"^((([a-zA-Z0-9\-]{1,63}\.)([a-zA-Z0-9\-]{1,63}\.)*([A-Za-z]{1,63}))|(((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)))$"
|
|
@@ -349,25 +360,6 @@ class Image(BaseModel):
|
|
|
349
360
|
)
|
|
350
361
|
|
|
351
362
|
|
|
352
|
-
class JobAlert(BaseModel):
|
|
353
|
-
"""
|
|
354
|
-
Describes the configuration for the job alerts
|
|
355
|
-
"""
|
|
356
|
-
|
|
357
|
-
notification_channel: constr(min_length=1) = Field(
|
|
358
|
-
..., description="Specify the notification channel to send alerts to"
|
|
359
|
-
)
|
|
360
|
-
to_emails: Optional[List[constr(min_length=1)]] = Field(
|
|
361
|
-
None,
|
|
362
|
-
description="List of recipients' email addresses if the notification channel is Email.",
|
|
363
|
-
)
|
|
364
|
-
on_start: bool = Field(False, description="Send an alert when the job starts")
|
|
365
|
-
on_completion: bool = Field(
|
|
366
|
-
False, description="Send an alert when the job completes"
|
|
367
|
-
)
|
|
368
|
-
on_failure: bool = Field(True, description="Send an alert when the job fails")
|
|
369
|
-
|
|
370
|
-
|
|
371
363
|
class Claim(BaseModel):
|
|
372
364
|
key: str
|
|
373
365
|
values: List[str]
|
|
@@ -389,6 +381,10 @@ class JwtAuthConfig(BaseModel):
|
|
|
389
381
|
claims: Optional[List[Claim]] = Field(
|
|
390
382
|
None, description="List of key-value pairs of claims to verify in the JWT token"
|
|
391
383
|
)
|
|
384
|
+
bypass_auth_paths: Optional[List[constr(regex=r"^/[^*]*")]] = Field(
|
|
385
|
+
None,
|
|
386
|
+
description="List of paths that will bypass auth.\nneeds to start with a forward slash(/) and should not contain wildcards(*)",
|
|
387
|
+
)
|
|
392
388
|
|
|
393
389
|
|
|
394
390
|
class KafkaMetricConfig(BaseModel):
|
|
@@ -510,15 +506,36 @@ class NvidiaGPU(BaseModel):
|
|
|
510
506
|
|
|
511
507
|
class Profile(str, Enum):
|
|
512
508
|
"""
|
|
513
|
-
Name of the MIG profile to use. One of
|
|
509
|
+
Name of the MIG profile to use. One of the following based on gpu type
|
|
510
|
+
Please refer to https://docs.nvidia.com/datacenter/tesla/mig-user-guide/#supported-mig-profiles for more details
|
|
511
|
+
A100 40 GB - [1g.5gb, 1g.10gb, 2g.10gb, 3g.20gb, 4g.20gb]
|
|
512
|
+
A100 80 GB / H100 80 GB - [1g.10gb, 1g.20gb, 2g.20gb, 3g.40gb, 4g.40gb]
|
|
513
|
+
H100 94 GB - [1g.12gb, 1g.24gb, 2g.24gb, 3g.47gb, 4g.47gb]
|
|
514
|
+
H100 96 GB - [1g.12gb, 1g.24gb, 2g.24gb, 3g.48gb, 4g.48gb]
|
|
515
|
+
H200 141 GB - [1g.18gb, 1g.35gb, 2g.35gb, 3g.71gb, 4g.71gb]
|
|
514
516
|
"""
|
|
515
517
|
|
|
516
518
|
field_1g_5gb = "1g.5gb"
|
|
517
|
-
field_2g_10gb = "2g.10gb"
|
|
518
|
-
field_3g_20gb = "3g.20gb"
|
|
519
519
|
field_1g_10gb = "1g.10gb"
|
|
520
|
+
field_1g_12gb = "1g.12gb"
|
|
521
|
+
field_1g_18gb = "1g.18gb"
|
|
522
|
+
field_1g_20gb = "1g.20gb"
|
|
523
|
+
field_1g_24gb = "1g.24gb"
|
|
524
|
+
field_1g_35gb = "1g.35gb"
|
|
525
|
+
field_2g_10gb = "2g.10gb"
|
|
520
526
|
field_2g_20gb = "2g.20gb"
|
|
527
|
+
field_2g_24gb = "2g.24gb"
|
|
528
|
+
field_2g_35gb = "2g.35gb"
|
|
529
|
+
field_3g_20gb = "3g.20gb"
|
|
521
530
|
field_3g_40gb = "3g.40gb"
|
|
531
|
+
field_3g_47gb = "3g.47gb"
|
|
532
|
+
field_3g_48gb = "3g.48gb"
|
|
533
|
+
field_3g_71gb = "3g.71gb"
|
|
534
|
+
field_4g_20gb = "4g.20gb"
|
|
535
|
+
field_4g_40gb = "4g.40gb"
|
|
536
|
+
field_4g_47gb = "4g.47gb"
|
|
537
|
+
field_4g_48gb = "4g.48gb"
|
|
538
|
+
field_4g_71gb = "4g.71gb"
|
|
522
539
|
|
|
523
540
|
|
|
524
541
|
class NvidiaMIGGPU(BaseModel):
|
|
@@ -529,7 +546,7 @@ class NvidiaMIGGPU(BaseModel):
|
|
|
529
546
|
)
|
|
530
547
|
profile: Profile = Field(
|
|
531
548
|
...,
|
|
532
|
-
description="Name of the MIG profile to use. One of [1g.5gb, 2g.10gb, 3g.20gb, 1g.10gb, 2g.20gb, 3g.40gb]",
|
|
549
|
+
description="Name of the MIG profile to use. One of the following based on gpu type\nPlease refer to https://docs.nvidia.com/datacenter/tesla/mig-user-guide/#supported-mig-profiles for more details\nA100 40 GB - [1g.5gb, 1g.10gb, 2g.10gb, 3g.20gb, 4g.20gb]\nA100 80 GB / H100 80 GB - [1g.10gb, 1g.20gb, 2g.20gb, 3g.40gb, 4g.40gb]\nH100 94 GB - [1g.12gb, 1g.24gb, 2g.24gb, 3g.47gb, 4g.47gb]\nH100 96 GB - [1g.12gb, 1g.24gb, 2g.24gb, 3g.48gb, 4g.48gb]\nH200 141 GB - [1g.18gb, 1g.35gb, 2g.35gb, 3g.71gb, 4g.71gb]",
|
|
533
550
|
)
|
|
534
551
|
|
|
535
552
|
|
|
@@ -812,6 +829,13 @@ class ServiceAutoscaling(BaseAutoscaling):
|
|
|
812
829
|
)
|
|
813
830
|
|
|
814
831
|
|
|
832
|
+
class SlackWebhook(BaseModel):
|
|
833
|
+
type: Literal["slack-webhook"] = Field(..., description="")
|
|
834
|
+
notification_channel: constr(min_length=1) = Field(
|
|
835
|
+
..., description="Specify the notification channel to send alerts to"
|
|
836
|
+
)
|
|
837
|
+
|
|
838
|
+
|
|
815
839
|
class SparkDriverConfig(BaseModel):
|
|
816
840
|
ui_endpoint: Endpoint
|
|
817
841
|
resources: Optional[Resources] = None
|
|
@@ -918,6 +942,10 @@ class TrueFoundryArtifactSource(BaseModel):
|
|
|
918
942
|
|
|
919
943
|
class TrueFoundryInteractiveLogin(BaseModel):
|
|
920
944
|
type: Literal["truefoundry_oauth"] = Field(..., description="")
|
|
945
|
+
bypass_auth_paths: Optional[List[constr(regex=r"^/[^*]*")]] = Field(
|
|
946
|
+
None,
|
|
947
|
+
description="List of paths that will bypass auth.\nneeds to start with a forward slash(/) and should not contain wildcards(*)",
|
|
948
|
+
)
|
|
921
949
|
|
|
922
950
|
|
|
923
951
|
class VolumeBrowser(BaseModel):
|
|
@@ -1145,59 +1173,6 @@ class Helm(BaseModel):
|
|
|
1145
1173
|
)
|
|
1146
1174
|
|
|
1147
1175
|
|
|
1148
|
-
class Job(BaseModel):
|
|
1149
|
-
"""
|
|
1150
|
-
Describes the configuration for the job
|
|
1151
|
-
"""
|
|
1152
|
-
|
|
1153
|
-
type: Literal["job"] = Field(..., description="")
|
|
1154
|
-
name: constr(regex=r"^[a-z](?:[a-z0-9]|-(?!-)){1,30}[a-z0-9]$") = Field(
|
|
1155
|
-
..., description="Name of the job"
|
|
1156
|
-
)
|
|
1157
|
-
image: Union[Build, Image] = Field(
|
|
1158
|
-
...,
|
|
1159
|
-
description="Specify whether you want to deploy a Docker image or build and deploy from source code",
|
|
1160
|
-
)
|
|
1161
|
-
trigger: Union[Manual, Schedule] = Field(
|
|
1162
|
-
{"type": "manual"}, description="Specify the trigger"
|
|
1163
|
-
)
|
|
1164
|
-
trigger_on_deploy: bool = Field(
|
|
1165
|
-
False, description="Trigger the job after deploy immediately"
|
|
1166
|
-
)
|
|
1167
|
-
params: Optional[List[Param]] = Field(
|
|
1168
|
-
None, description="Configure params and pass it to create different job runs"
|
|
1169
|
-
)
|
|
1170
|
-
env: Optional[Dict[str, str]] = Field(
|
|
1171
|
-
None,
|
|
1172
|
-
description="Configure environment variables to be injected in the service either as plain text or secrets. [Docs](https://docs.truefoundry.com/docs/env-variables)",
|
|
1173
|
-
)
|
|
1174
|
-
resources: Optional[Resources] = None
|
|
1175
|
-
alerts: Optional[List[JobAlert]] = Field(
|
|
1176
|
-
None,
|
|
1177
|
-
description="Configure alerts to be sent when the job starts/fails/completes",
|
|
1178
|
-
)
|
|
1179
|
-
retries: conint(ge=0, le=10) = Field(
|
|
1180
|
-
0,
|
|
1181
|
-
description="Specify the maximum number of attempts to retry a job before it is marked as failed.",
|
|
1182
|
-
)
|
|
1183
|
-
timeout: Optional[conint(le=432000, gt=0)] = Field(
|
|
1184
|
-
None, description="Job timeout in seconds."
|
|
1185
|
-
)
|
|
1186
|
-
concurrency_limit: Optional[PositiveInt] = Field(
|
|
1187
|
-
None, description="Number of runs that can run concurrently"
|
|
1188
|
-
)
|
|
1189
|
-
service_account: Optional[str] = Field(None, description="")
|
|
1190
|
-
mounts: Optional[List[Union[SecretMount, StringDataMount, VolumeMount]]] = Field(
|
|
1191
|
-
None,
|
|
1192
|
-
description="Configure data to be mounted to job pod(s) as a string, secret or volume. [Docs](https://docs.truefoundry.com/docs/mounting-volumes-job)",
|
|
1193
|
-
)
|
|
1194
|
-
labels: Optional[Dict[str, str]] = Field(None, description="")
|
|
1195
|
-
kustomize: Optional[Kustomize] = None
|
|
1196
|
-
workspace_fqn: Optional[str] = Field(
|
|
1197
|
-
None, description="Fully qualified name of the workspace"
|
|
1198
|
-
)
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
1176
|
class KafkaInputConfig(BaseModel):
|
|
1202
1177
|
"""
|
|
1203
1178
|
Describes the configuration for the input Kafka worker
|
|
@@ -1439,6 +1414,18 @@ class WorkerConfig(BaseModel):
|
|
|
1439
1414
|
)
|
|
1440
1415
|
|
|
1441
1416
|
|
|
1417
|
+
class WorkflowAlert(BaseModel):
|
|
1418
|
+
"""
|
|
1419
|
+
Describes the configuration for the workflow alerts
|
|
1420
|
+
"""
|
|
1421
|
+
|
|
1422
|
+
notification_target: Optional[Union[Email, SlackWebhook]] = None
|
|
1423
|
+
on_completion: bool = Field(
|
|
1424
|
+
False, description="Send an alert when the job completes"
|
|
1425
|
+
)
|
|
1426
|
+
on_failure: bool = Field(True, description="Send an alert when the job fails")
|
|
1427
|
+
|
|
1428
|
+
|
|
1442
1429
|
class BaseService(BaseModel):
|
|
1443
1430
|
name: constr(regex=r"^[a-z](?:[a-z0-9]|-(?!-)){1,30}[a-z0-9]$") = Field(
|
|
1444
1431
|
...,
|
|
@@ -1487,6 +1474,26 @@ class FlyteTaskTemplate(BaseModel):
|
|
|
1487
1474
|
custom: FlyteTaskCustom
|
|
1488
1475
|
|
|
1489
1476
|
|
|
1477
|
+
class JobAlert(BaseModel):
|
|
1478
|
+
"""
|
|
1479
|
+
Describes the configuration for the job alerts
|
|
1480
|
+
"""
|
|
1481
|
+
|
|
1482
|
+
notification_channel: Optional[constr(min_length=1)] = Field(
|
|
1483
|
+
None, description="Specify the notification channel to send alerts to"
|
|
1484
|
+
)
|
|
1485
|
+
to_emails: Optional[List[constr(min_length=1)]] = Field(
|
|
1486
|
+
None,
|
|
1487
|
+
description="List of recipients' email addresses if the notification channel is Email.",
|
|
1488
|
+
)
|
|
1489
|
+
notification_target: Optional[Union[Email, SlackWebhook]] = None
|
|
1490
|
+
on_start: bool = Field(False, description="Send an alert when the job starts")
|
|
1491
|
+
on_completion: bool = Field(
|
|
1492
|
+
False, description="Send an alert when the job completes"
|
|
1493
|
+
)
|
|
1494
|
+
on_failure: bool = Field(True, description="Send an alert when the job fails")
|
|
1495
|
+
|
|
1496
|
+
|
|
1490
1497
|
class Service(BaseService):
|
|
1491
1498
|
"""
|
|
1492
1499
|
Describes the configuration for the service
|
|
@@ -1528,6 +1535,59 @@ class FlyteTask(BaseModel):
|
|
|
1528
1535
|
description: Optional[Any] = None
|
|
1529
1536
|
|
|
1530
1537
|
|
|
1538
|
+
class Job(BaseModel):
|
|
1539
|
+
"""
|
|
1540
|
+
Describes the configuration for the job
|
|
1541
|
+
"""
|
|
1542
|
+
|
|
1543
|
+
type: Literal["job"] = Field(..., description="")
|
|
1544
|
+
name: constr(regex=r"^[a-z](?:[a-z0-9]|-(?!-)){1,30}[a-z0-9]$") = Field(
|
|
1545
|
+
..., description="Name of the job"
|
|
1546
|
+
)
|
|
1547
|
+
image: Union[Build, Image] = Field(
|
|
1548
|
+
...,
|
|
1549
|
+
description="Specify whether you want to deploy a Docker image or build and deploy from source code",
|
|
1550
|
+
)
|
|
1551
|
+
trigger: Union[Manual, Schedule] = Field(
|
|
1552
|
+
{"type": "manual"}, description="Specify the trigger"
|
|
1553
|
+
)
|
|
1554
|
+
trigger_on_deploy: bool = Field(
|
|
1555
|
+
False, description="Trigger the job after deploy immediately"
|
|
1556
|
+
)
|
|
1557
|
+
params: Optional[List[Param]] = Field(
|
|
1558
|
+
None, description="Configure params and pass it to create different job runs"
|
|
1559
|
+
)
|
|
1560
|
+
env: Optional[Dict[str, str]] = Field(
|
|
1561
|
+
None,
|
|
1562
|
+
description="Configure environment variables to be injected in the service either as plain text or secrets. [Docs](https://docs.truefoundry.com/docs/env-variables)",
|
|
1563
|
+
)
|
|
1564
|
+
resources: Optional[Resources] = None
|
|
1565
|
+
alerts: Optional[List[JobAlert]] = Field(
|
|
1566
|
+
None,
|
|
1567
|
+
description="Configure alerts to be sent when the job starts/fails/completes",
|
|
1568
|
+
)
|
|
1569
|
+
retries: conint(ge=0, le=10) = Field(
|
|
1570
|
+
0,
|
|
1571
|
+
description="Specify the maximum number of attempts to retry a job before it is marked as failed.",
|
|
1572
|
+
)
|
|
1573
|
+
timeout: Optional[conint(le=432000, gt=0)] = Field(
|
|
1574
|
+
None, description="Job timeout in seconds."
|
|
1575
|
+
)
|
|
1576
|
+
concurrency_limit: Optional[PositiveInt] = Field(
|
|
1577
|
+
None, description="Number of runs that can run concurrently"
|
|
1578
|
+
)
|
|
1579
|
+
service_account: Optional[str] = Field(None, description="")
|
|
1580
|
+
mounts: Optional[List[Union[SecretMount, StringDataMount, VolumeMount]]] = Field(
|
|
1581
|
+
None,
|
|
1582
|
+
description="Configure data to be mounted to job pod(s) as a string, secret or volume. [Docs](https://docs.truefoundry.com/docs/mounting-volumes-job)",
|
|
1583
|
+
)
|
|
1584
|
+
labels: Optional[Dict[str, str]] = Field(None, description="")
|
|
1585
|
+
kustomize: Optional[Kustomize] = None
|
|
1586
|
+
workspace_fqn: Optional[str] = Field(
|
|
1587
|
+
None, description="Fully qualified name of the workspace"
|
|
1588
|
+
)
|
|
1589
|
+
|
|
1590
|
+
|
|
1531
1591
|
class Workflow(BaseModel):
|
|
1532
1592
|
"""
|
|
1533
1593
|
Describes the configuration for the worflow
|
|
@@ -1546,6 +1606,7 @@ class Workflow(BaseModel):
|
|
|
1546
1606
|
flyte_entities: Optional[List[Union[FlyteTask, FlyteWorkflow, FlyteLaunchPlan]]] = (
|
|
1547
1607
|
Field(None, description="")
|
|
1548
1608
|
)
|
|
1609
|
+
alerts: Optional[List[WorkflowAlert]] = Field(None, description="")
|
|
1549
1610
|
|
|
1550
1611
|
|
|
1551
1612
|
class ApplicationSet(BaseModel):
|
|
@@ -14,7 +14,10 @@ from rich.status import Status
|
|
|
14
14
|
from tqdm import tqdm
|
|
15
15
|
from tqdm.utils import CallbackIOWrapper
|
|
16
16
|
|
|
17
|
-
from truefoundry.common.constants import
|
|
17
|
+
from truefoundry.common.constants import (
|
|
18
|
+
SERVICEFOUNDRY_CLIENT_MAX_RETRIES,
|
|
19
|
+
VERSION_PREFIX,
|
|
20
|
+
)
|
|
18
21
|
from truefoundry.common.request_utils import request_handling
|
|
19
22
|
from truefoundry.common.servicefoundry_client import (
|
|
20
23
|
ServiceFoundryServiceClient as BaseServiceFoundryServiceClient,
|
|
@@ -247,7 +250,7 @@ class ServiceFoundryServiceClient(BaseServiceFoundryServiceClient):
|
|
|
247
250
|
callback: Optional[OutputCallBack] = None,
|
|
248
251
|
) -> socketio.Client:
|
|
249
252
|
callback = callback or OutputCallBack()
|
|
250
|
-
sio = socketio.Client(request_timeout=60)
|
|
253
|
+
sio = socketio.Client(request_timeout=60, reconnection_attempts=10)
|
|
251
254
|
callback.print_line("Waiting for the task to start...")
|
|
252
255
|
next_log_start_timestamp = query_dict.get("startTs")
|
|
253
256
|
|
|
@@ -286,6 +289,7 @@ class ServiceFoundryServiceClient(BaseServiceFoundryServiceClient):
|
|
|
286
289
|
transports="websocket",
|
|
287
290
|
headers=self._get_headers(),
|
|
288
291
|
socketio_path=socketio_path,
|
|
292
|
+
retry=True,
|
|
289
293
|
)
|
|
290
294
|
return sio
|
|
291
295
|
|
|
@@ -298,10 +302,15 @@ class ServiceFoundryServiceClient(BaseServiceFoundryServiceClient):
|
|
|
298
302
|
|
|
299
303
|
@check_min_cli_version
|
|
300
304
|
def get_deployment_statuses(
|
|
301
|
-
self,
|
|
305
|
+
self,
|
|
306
|
+
application_id: str,
|
|
307
|
+
deployment_id: str,
|
|
308
|
+
retry_count: int = SERVICEFOUNDRY_CLIENT_MAX_RETRIES,
|
|
302
309
|
) -> List[AppDeploymentStatusResponse]:
|
|
303
310
|
url = f"{self._api_server_url}/{VERSION_PREFIX}/app/{application_id}/deployments/{deployment_id}/statuses"
|
|
304
|
-
response = session_with_retries().get(
|
|
311
|
+
response = session_with_retries(retries=retry_count).get(
|
|
312
|
+
url, headers=self._get_headers()
|
|
313
|
+
)
|
|
305
314
|
response_data = request_handling(response)
|
|
306
315
|
return parse_obj_as(List[AppDeploymentStatusResponse], response_data)
|
|
307
316
|
|
|
@@ -107,12 +107,20 @@ def _tail_build_logs(build_response: BuildResponse) -> socketio.Client:
|
|
|
107
107
|
|
|
108
108
|
def _deploy_wait_handler( # noqa: C901
|
|
109
109
|
deployment: Deployment,
|
|
110
|
+
tail_logs: bool = True,
|
|
110
111
|
) -> Optional[DeploymentTransitionStatus]:
|
|
112
|
+
tail_logs_or_polling_status_message = (
|
|
113
|
+
"You can press Ctrl + C to exit the tailing of build logs "
|
|
114
|
+
)
|
|
115
|
+
if not tail_logs:
|
|
116
|
+
tail_logs_or_polling_status_message = (
|
|
117
|
+
"you can press Ctrl + C to exit the polling of deployment status "
|
|
118
|
+
)
|
|
111
119
|
_log_application_dashboard_url(
|
|
112
120
|
deployment=deployment,
|
|
113
121
|
log_message=(
|
|
114
122
|
"You can track the progress below or on the dashboard:- '%s'\n"
|
|
115
|
-
"
|
|
123
|
+
f"{tail_logs_or_polling_status_message}"
|
|
116
124
|
"and deployment will continue on the server"
|
|
117
125
|
),
|
|
118
126
|
)
|
|
@@ -130,6 +138,7 @@ def _deploy_wait_handler( # noqa: C901
|
|
|
130
138
|
poll_after_secs=poll_interval_seconds,
|
|
131
139
|
application_id=deployment.applicationId,
|
|
132
140
|
deployment_id=deployment.id,
|
|
141
|
+
retry_count=10,
|
|
133
142
|
):
|
|
134
143
|
if len(deployment_statuses) == 0:
|
|
135
144
|
logger.warning("Did not receive any deployment status")
|
|
@@ -157,12 +166,15 @@ def _deploy_wait_handler( # noqa: C901
|
|
|
157
166
|
latest_deployment_status.transition
|
|
158
167
|
== DeploymentTransitionStatus.BUILDING
|
|
159
168
|
):
|
|
160
|
-
if not socket:
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
169
|
+
if tail_logs and not socket:
|
|
170
|
+
try:
|
|
171
|
+
build_responses = client.get_deployment_build_response(
|
|
172
|
+
application_id=deployment.applicationId,
|
|
173
|
+
deployment_id=deployment.id,
|
|
174
|
+
)
|
|
175
|
+
socket = _tail_build_logs(build_responses[0])
|
|
176
|
+
except Exception as e:
|
|
177
|
+
logger.error("Error tailing build logs: %s", e)
|
|
166
178
|
|
|
167
179
|
time_elapsed = time.monotonic() - start_time
|
|
168
180
|
if time_elapsed > total_timeout_time:
|
|
@@ -25,15 +25,17 @@ from truefoundry.deploy.lib.clients.servicefoundry_client import (
|
|
|
25
25
|
ServiceFoundryServiceClient,
|
|
26
26
|
)
|
|
27
27
|
from truefoundry.deploy.lib.dao.workspace import get_workspace_by_fqn
|
|
28
|
-
from truefoundry.deploy.lib.model.entity import Deployment
|
|
28
|
+
from truefoundry.deploy.lib.model.entity import Deployment, DeploymentTransitionStatus
|
|
29
|
+
from truefoundry.deploy.v2.lib.deploy import _deploy_wait_handler
|
|
29
30
|
from truefoundry.deploy.v2.lib.source import (
|
|
30
31
|
local_source_to_remote_source,
|
|
31
32
|
)
|
|
32
33
|
from truefoundry.logger import logger
|
|
33
34
|
from truefoundry.pydantic_v1 import ValidationError
|
|
34
35
|
from truefoundry.workflow.workflow import (
|
|
36
|
+
TRUEFOUNDRY_ALERTS_CONFIG,
|
|
35
37
|
TRUEFOUNDRY_LAUNCH_PLAN_NAME,
|
|
36
|
-
|
|
38
|
+
truefoundry_config_store,
|
|
37
39
|
)
|
|
38
40
|
|
|
39
41
|
|
|
@@ -132,6 +134,7 @@ def _validate_workflow_entities( # noqa: C901
|
|
|
132
134
|
`from truefoundry.workflow import PythonTaskConfig, ContainerTaskConfig`
|
|
133
135
|
"""
|
|
134
136
|
tasks_without_truefoundry_worflow_package = []
|
|
137
|
+
task_names = set()
|
|
135
138
|
for task in tasks:
|
|
136
139
|
if _is_dynamic_task(task):
|
|
137
140
|
raise ValueError("Dynamic workflows are not supported yet.")
|
|
@@ -141,6 +144,12 @@ def _validate_workflow_entities( # noqa: C901
|
|
|
141
144
|
task.template.id.name
|
|
142
145
|
)
|
|
143
146
|
)
|
|
147
|
+
task_name = task.template.id.name
|
|
148
|
+
if task_name in task_names:
|
|
149
|
+
raise ValueError(
|
|
150
|
+
f"Task name should be unique, task with name {task_name} is repeated in the workflow"
|
|
151
|
+
)
|
|
152
|
+
task_names.add(task_name)
|
|
144
153
|
task_image_spec = task.template.custom["truefoundry"]["image"]
|
|
145
154
|
if task_image_spec["type"] == "task-python-build":
|
|
146
155
|
is_tfy_wf_present_in_task_python_build = (
|
|
@@ -166,7 +175,7 @@ def _validate_workflow_entities( # noqa: C901
|
|
|
166
175
|
# validate that all inputs have default values for cron workflows
|
|
167
176
|
for launch_plan in launch_plans:
|
|
168
177
|
if (
|
|
169
|
-
|
|
178
|
+
truefoundry_config_store.get(launch_plan.spec.workflow_id.name)
|
|
170
179
|
and launch_plan.id.name == TRUEFOUNDRY_LAUNCH_PLAN_NAME
|
|
171
180
|
):
|
|
172
181
|
workflow_inputs = launch_plan.spec.default_inputs.parameters
|
|
@@ -229,7 +238,7 @@ def _generate_manifest_for_workflow(
|
|
|
229
238
|
|
|
230
239
|
# this is the case when someone has a cron schedule. and this line is for handling default launch plan in this case.
|
|
231
240
|
if (
|
|
232
|
-
|
|
241
|
+
truefoundry_config_store.get(workflow_name)
|
|
233
242
|
and workflow_name == entity.id.name
|
|
234
243
|
):
|
|
235
244
|
continue
|
|
@@ -255,6 +264,14 @@ def _generate_manifest_for_workflow(
|
|
|
255
264
|
|
|
256
265
|
workflow.flyte_entities.append(message_dict)
|
|
257
266
|
|
|
267
|
+
alerts_config = truefoundry_config_store.get(TRUEFOUNDRY_ALERTS_CONFIG)
|
|
268
|
+
if alerts_config:
|
|
269
|
+
if not workflow.alerts:
|
|
270
|
+
workflow.alerts = alerts_config
|
|
271
|
+
else:
|
|
272
|
+
logger.warning(
|
|
273
|
+
"Alerts are configured in both workflow decorator as well as in deployment config. Alerts configured in workflow decorator will be ignored."
|
|
274
|
+
)
|
|
258
275
|
# this step is just to verify if pydantic model is still valid after adding flyte_entities
|
|
259
276
|
autogen_models.Workflow.validate({**workflow.dict()})
|
|
260
277
|
|
|
@@ -281,7 +298,7 @@ def deploy_workflow(
|
|
|
281
298
|
|
|
282
299
|
# we need to rest the execution config store as it is a global variable and we don't want to keep the cron execution config for next workflow
|
|
283
300
|
# this is only needed for notebook environment
|
|
284
|
-
|
|
301
|
+
truefoundry_config_store.reset()
|
|
285
302
|
|
|
286
303
|
workspace_id = get_workspace_by_fqn(workspace_fqn).id
|
|
287
304
|
|
|
@@ -307,4 +324,17 @@ def deploy_workflow(
|
|
|
307
324
|
deployment.fqn,
|
|
308
325
|
)
|
|
309
326
|
deployment_url = f"{client.tfy_host.strip('/')}/applications/{deployment.applicationId}?tab=deployments"
|
|
327
|
+
if wait:
|
|
328
|
+
try:
|
|
329
|
+
last_status_printed = _deploy_wait_handler(
|
|
330
|
+
deployment=deployment, tail_logs=False
|
|
331
|
+
)
|
|
332
|
+
if not last_status_printed or DeploymentTransitionStatus.is_failure_state(
|
|
333
|
+
last_status_printed
|
|
334
|
+
):
|
|
335
|
+
deployment_tab_url = f"{client.tfy_host.strip('/')}/applications/{deployment.applicationId}?tab=deployments"
|
|
336
|
+
message = f"Deployment Failed. Please refer to the logs for additional details - {deployment_tab_url}"
|
|
337
|
+
sys.exit(message)
|
|
338
|
+
except KeyboardInterrupt:
|
|
339
|
+
logger.info("Ctrl-c executed. The deployment will still continue.")
|
|
310
340
|
logger.info("You can find the application on the dashboard:- '%s'", deployment_url)
|
|
@@ -563,3 +563,11 @@ class TaskPythonBuild(models.TaskPythonBuild, PatchedModelBase):
|
|
|
563
563
|
|
|
564
564
|
# Deprecated aliases, kept for backward compatibility
|
|
565
565
|
TruefoundryArtifactSource = TrueFoundryArtifactSource
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
class Email(models.Email, PatchedModelBase):
|
|
569
|
+
type: Literal["email"] = "email"
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
class SlackWebhook(models.SlackWebhook, PatchedModelBase):
|
|
573
|
+
type: Literal["slack-webhook"] = "slack-webhook"
|
truefoundry/workflow/workflow.py
CHANGED
|
@@ -10,9 +10,11 @@ from flytekit.core.workflow import (
|
|
|
10
10
|
)
|
|
11
11
|
from flytekit.core.workflow import workflow as flytekit_workflow
|
|
12
12
|
|
|
13
|
+
from truefoundry.deploy._autogen.models import WorkflowAlert
|
|
13
14
|
from truefoundry.pydantic_v1 import BaseModel
|
|
14
15
|
|
|
15
16
|
TRUEFOUNDRY_LAUNCH_PLAN_NAME = "default"
|
|
17
|
+
TRUEFOUNDRY_ALERTS_CONFIG = "tfy_alerts_config"
|
|
16
18
|
|
|
17
19
|
|
|
18
20
|
class ExecutionConfig(BaseModel):
|
|
@@ -22,7 +24,7 @@ class ExecutionConfig(BaseModel):
|
|
|
22
24
|
schedule: str
|
|
23
25
|
|
|
24
26
|
|
|
25
|
-
class
|
|
27
|
+
class TruefoundryConfigStore(BaseModel):
|
|
26
28
|
execution_config_map: Dict[str, ExecutionConfig] = {}
|
|
27
29
|
|
|
28
30
|
def reset(self):
|
|
@@ -35,7 +37,7 @@ class ExecutionConfigStore(BaseModel):
|
|
|
35
37
|
self.execution_config_map[key] = value
|
|
36
38
|
|
|
37
39
|
|
|
38
|
-
|
|
40
|
+
truefoundry_config_store = TruefoundryConfigStore()
|
|
39
41
|
|
|
40
42
|
|
|
41
43
|
def workflow(
|
|
@@ -43,6 +45,7 @@ def workflow(
|
|
|
43
45
|
failure_policy: Optional[WorkflowFailurePolicy] = None,
|
|
44
46
|
on_failure: Optional[Task] = None,
|
|
45
47
|
execution_configs: Optional[List[ExecutionConfig]] = None,
|
|
48
|
+
alerts: Optional[List[WorkflowAlert]] = None,
|
|
46
49
|
) -> Union[
|
|
47
50
|
Callable[[Callable[..., FuncOut]], PythonFunctionWorkflow],
|
|
48
51
|
PythonFunctionWorkflow,
|
|
@@ -85,6 +88,7 @@ def workflow(
|
|
|
85
88
|
failure_policy=failure_policy,
|
|
86
89
|
on_failure=on_failure,
|
|
87
90
|
execution_configs=execution_configs,
|
|
91
|
+
alerts=alerts,
|
|
88
92
|
)
|
|
89
93
|
|
|
90
94
|
return wrapper
|
|
@@ -109,6 +113,9 @@ def workflow(
|
|
|
109
113
|
function_module = _workflow_function.__module__
|
|
110
114
|
function_name = _workflow_function.__name__
|
|
111
115
|
function_path = f"{function_module}.{function_name}"
|
|
112
|
-
|
|
116
|
+
truefoundry_config_store.set(function_path, execution_config)
|
|
117
|
+
|
|
118
|
+
if alerts:
|
|
119
|
+
truefoundry_config_store.set(TRUEFOUNDRY_ALERTS_CONFIG, alerts)
|
|
113
120
|
|
|
114
121
|
return workflow_object
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: truefoundry
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.3
|
|
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.0.
|
|
33
|
+
Requires-Dist: truefoundry-sdk==0.0.8
|
|
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
|
|
@@ -46,11 +46,11 @@ truefoundry/common/servicefoundry_client.py,sha256=2fYhdVPSvLXz5C5tosOq86JD8WM3I
|
|
|
46
46
|
truefoundry/common/session.py,sha256=xeBAPUNEJv2XVFQCRUGeBDTePh5zrKNSok8vmSxBjPw,2813
|
|
47
47
|
truefoundry/common/storage_provider_utils.py,sha256=yURhMw8k0FLFvaviRHDiifhvc6GnuQwGMC9Qd2uM440,10934
|
|
48
48
|
truefoundry/common/types.py,sha256=BMJFCsR1lPJAw66IQBSvLyV4I6o_x5oj78gVsUa9si8,188
|
|
49
|
-
truefoundry/common/utils.py,sha256=
|
|
49
|
+
truefoundry/common/utils.py,sha256=yEQtJW2fT9xbNpRhfRoD9hvhGw-FgGS3agh1oZptmjg,6379
|
|
50
50
|
truefoundry/common/warnings.py,sha256=rs6BHwk7imQYedo07iwh3TWEOywAR3Lqhj0AY4khByg,504
|
|
51
|
-
truefoundry/deploy/__init__.py,sha256=
|
|
51
|
+
truefoundry/deploy/__init__.py,sha256=zNsyzJAOPCPqlhVEEq6sBpfPsa4XkChaXvE-EBCmfzs,2645
|
|
52
52
|
truefoundry/deploy/python_deploy_codegen.py,sha256=AainOFR20XvhNeztJkLPWGZ40lAT_nwc-ZmG77Kum4o,6525
|
|
53
|
-
truefoundry/deploy/_autogen/models.py,sha256=
|
|
53
|
+
truefoundry/deploy/_autogen/models.py,sha256=0m43YJVG0fBhLp0fPkCod_vIcMFob0jTQEGnjk6nOUQ,69804
|
|
54
54
|
truefoundry/deploy/builder/__init__.py,sha256=nGQiR3r16iumRy7xbVQ6q-k0EApmijspsfVpXDE-9po,4953
|
|
55
55
|
truefoundry/deploy/builder/constants.py,sha256=amUkHoHvVKzGv0v_knfiioRuKiJM0V0xW0diERgWiI0,508
|
|
56
56
|
truefoundry/deploy/builder/docker_service.py,sha256=sm7GWeIqyrKaZpxskdLejZlsxcZnM3BTDJr6orvPN4E,3948
|
|
@@ -93,7 +93,7 @@ truefoundry/deploy/lib/session.py,sha256=-FX4gOtiGlc6Jk56JPVZpDqXR9xQza77AIlBvNJ
|
|
|
93
93
|
truefoundry/deploy/lib/util.py,sha256=J7r8San2wKo48A7-BlH2-OKTlBO67zlPjLEhMsL8os0,1059
|
|
94
94
|
truefoundry/deploy/lib/win32.py,sha256=1RcvPTdlOAJ48rt8rCbE2Ufha2ztRqBAE9dueNXArrY,5009
|
|
95
95
|
truefoundry/deploy/lib/clients/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
96
|
-
truefoundry/deploy/lib/clients/servicefoundry_client.py,sha256=
|
|
96
|
+
truefoundry/deploy/lib/clients/servicefoundry_client.py,sha256=NjNGmBsmrIfDZFfqJz12B4TVDHMZeAZDf-5eKSnS9zs,26697
|
|
97
97
|
truefoundry/deploy/lib/dao/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
98
98
|
truefoundry/deploy/lib/dao/application.py,sha256=oMszpueXPUfTUuN_XdKwoRjQyqAgWHhZ-10cbprCVdM,9226
|
|
99
99
|
truefoundry/deploy/lib/dao/apply.py,sha256=5IFERe5sLmZGlavaKTIxL4xPHAme4ZS2Ww0a2rKTyT0,3029
|
|
@@ -104,11 +104,11 @@ truefoundry/deploy/lib/model/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NM
|
|
|
104
104
|
truefoundry/deploy/lib/model/entity.py,sha256=Up-DDOezkwM2tdqibfLdZO6jmT2pVq6SShB5sobBIGI,8531
|
|
105
105
|
truefoundry/deploy/v2/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
106
106
|
truefoundry/deploy/v2/lib/__init__.py,sha256=WEiVMZXOVljzEE3tpGJil14liIn_PCDoACJ6b3tZ6sI,188
|
|
107
|
-
truefoundry/deploy/v2/lib/deploy.py,sha256=
|
|
108
|
-
truefoundry/deploy/v2/lib/deploy_workflow.py,sha256=
|
|
107
|
+
truefoundry/deploy/v2/lib/deploy.py,sha256=FWmqX4N6RUN4-S68i-7xaIifYWnf0wc-zfINdpiFu-4,12901
|
|
108
|
+
truefoundry/deploy/v2/lib/deploy_workflow.py,sha256=G5BzMIbap8pgDX1eY-TITruUxQdkKhYtBmRwLL6lDeY,14342
|
|
109
109
|
truefoundry/deploy/v2/lib/deployable_patched_models.py,sha256=xbHFD3pURflvCm8EODPvjfvRrv67mlSrjPUknY8SMB8,4060
|
|
110
110
|
truefoundry/deploy/v2/lib/models.py,sha256=ogc1UYs1Z2nBdGSKCrde9sk8d0GxFKMkem99uqO5CmM,1148
|
|
111
|
-
truefoundry/deploy/v2/lib/patched_models.py,sha256=
|
|
111
|
+
truefoundry/deploy/v2/lib/patched_models.py,sha256=vVjYs1Gm7mpuTx3C0l40RQ_zvMdQ1S0s6J-f00qO0nA,16557
|
|
112
112
|
truefoundry/deploy/v2/lib/source.py,sha256=d6-8_6Zn5koBglqrBrY6ZLG_7yyPuLdyEmK4iZTw6xY,9405
|
|
113
113
|
truefoundry/ml/__init__.py,sha256=EEEHV7w58Krpo_W9Chd8Y3TdItfFO3LI6j6Izqc4-P8,2219
|
|
114
114
|
truefoundry/ml/constants.py,sha256=vDq72d4C9FSWqr9MMdjgTF4TuyNFApvo_6RVsSeAjB4,2837
|
|
@@ -366,12 +366,12 @@ truefoundry/workflow/container_task.py,sha256=8arieePsX4__OnG337hOtCiNgJwtKJJCsZ
|
|
|
366
366
|
truefoundry/workflow/map_task.py,sha256=f9vcAPRQy0Ttw6bvdZBKUVJMSm4eGQrbE1GHWhepHIU,1864
|
|
367
367
|
truefoundry/workflow/python_task.py,sha256=SRXRLC4vdBqGjhkwuaY39LEWN6iPCpJAuW17URRdWTY,1128
|
|
368
368
|
truefoundry/workflow/task.py,sha256=34m55mALXx6ko9o5HkK6FDtMajdvJzBhOsHwDM2RcBA,1779
|
|
369
|
-
truefoundry/workflow/workflow.py,sha256=
|
|
369
|
+
truefoundry/workflow/workflow.py,sha256=OjKBwEArxTzNDpfJWgnIqkXDQrYQRLXjheRwpOCu3LE,4861
|
|
370
370
|
truefoundry/workflow/remote_filesystem/__init__.py,sha256=LQ95ViEjJ7Ts4JcCGOxMPs7NZmQdZ4bTiq6qXtsjUhE,206
|
|
371
371
|
truefoundry/workflow/remote_filesystem/logger.py,sha256=em2l7D6sw7xTLDP0kQSLpgfRRCLpN14Qw85TN7ujQcE,1022
|
|
372
372
|
truefoundry/workflow/remote_filesystem/tfy_signed_url_client.py,sha256=xcT0wQmQlgzcj0nP3tJopyFSVWT1uv3nhiTIuwfXYeg,12342
|
|
373
373
|
truefoundry/workflow/remote_filesystem/tfy_signed_url_fs.py,sha256=nSGPZu0Gyd_jz0KsEE-7w_BmnTD8CVF1S8cUJoxaCbc,13305
|
|
374
|
-
truefoundry-0.6.
|
|
375
|
-
truefoundry-0.6.
|
|
376
|
-
truefoundry-0.6.
|
|
377
|
-
truefoundry-0.6.
|
|
374
|
+
truefoundry-0.6.3.dist-info/METADATA,sha256=i3AnyI6S08k481_Ht6nvQvl4SXeDkV_9Sq8h_cONET4,2349
|
|
375
|
+
truefoundry-0.6.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
376
|
+
truefoundry-0.6.3.dist-info/entry_points.txt,sha256=xVjn7RMN-MW2-9f7YU-bBdlZSvvrwzhpX1zmmRmsNPU,98
|
|
377
|
+
truefoundry-0.6.3.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|