truefoundry 0.5.5rc1__py3-none-any.whl → 0.5.6rc1__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.

Files changed (34) hide show
  1. truefoundry/deploy/__init__.py +9 -2
  2. truefoundry/deploy/auto_gen/models.py +177 -57
  3. truefoundry/deploy/cli/commands/__init__.py +0 -1
  4. truefoundry/deploy/lib/clients/servicefoundry_client.py +20 -16
  5. truefoundry/deploy/v2/lib/deploy.py +23 -15
  6. truefoundry/deploy/v2/lib/deployable_patched_models.py +10 -0
  7. truefoundry/deploy/v2/lib/patched_models.py +9 -1
  8. truefoundry/ml/autogen/client/__init__.py +4 -4
  9. truefoundry/ml/autogen/client/api/experiments_api.py +0 -156
  10. truefoundry/ml/autogen/client/api/mlfoundry_artifacts_api.py +49 -305
  11. truefoundry/ml/autogen/client/models/__init__.py +4 -4
  12. truefoundry/ml/autogen/client/models/agent.py +64 -13
  13. truefoundry/ml/autogen/client/models/agent_open_api_tool.py +63 -17
  14. truefoundry/ml/autogen/client/models/agent_open_api_tool_with_fqn.py +63 -17
  15. truefoundry/ml/autogen/client/models/agent_with_fqn.py +64 -12
  16. truefoundry/ml/autogen/client/models/artifact_version_manifest.py +39 -4
  17. truefoundry/ml/autogen/client/models/chat_prompt.py +60 -22
  18. truefoundry/ml/autogen/client/models/{external_artifact_source.py → external_blob_storage_source.py} +10 -11
  19. truefoundry/ml/autogen/client/models/model_version_manifest.py +39 -4
  20. truefoundry/ml/autogen/client/models/source.py +22 -22
  21. truefoundry/ml/autogen/client/models/source1.py +22 -22
  22. truefoundry/ml/autogen/client/models/{true_foundry_artifact_source.py → true_foundry_managed_source.py} +12 -11
  23. truefoundry/ml/autogen/client_README.md +2 -3
  24. truefoundry/ml/autogen/entities/artifacts.py +87 -69
  25. truefoundry/ml/log_types/artifacts/artifact.py +26 -18
  26. truefoundry/ml/log_types/artifacts/general_artifact.py +4 -13
  27. truefoundry/ml/log_types/artifacts/model.py +28 -21
  28. truefoundry/ml/mlfoundry_api.py +2 -6
  29. truefoundry/workflow/task.py +1 -1
  30. {truefoundry-0.5.5rc1.dist-info → truefoundry-0.5.6rc1.dist-info}/METADATA +2 -2
  31. {truefoundry-0.5.5rc1.dist-info → truefoundry-0.5.6rc1.dist-info}/RECORD +33 -34
  32. truefoundry/deploy/cli/commands/build_logs_command.py +0 -89
  33. {truefoundry-0.5.5rc1.dist-info → truefoundry-0.5.6rc1.dist-info}/WHEEL +0 -0
  34. {truefoundry-0.5.5rc1.dist-info → truefoundry-0.5.6rc1.dist-info}/entry_points.txt +0 -0
@@ -9,6 +9,10 @@ from truefoundry.deploy.auto_gen.models import (
9
9
  Kustomize,
10
10
  ParamType,
11
11
  Protocol,
12
+ SparkDriverConfig,
13
+ SparkExecutorConfig,
14
+ SparkExecutorDynamicScaling,
15
+ SparkExecutorFixedInstances,
12
16
  WorkbenchImage,
13
17
  )
14
18
  from truefoundry.deploy.lib.dao.application import (
@@ -36,11 +40,12 @@ from truefoundry.deploy.v2.lib.deployable_patched_models import (
36
40
  Application,
37
41
  ApplicationSet,
38
42
  AsyncService,
39
- Codeserver,
40
43
  Helm,
41
44
  Job,
42
45
  Notebook,
46
+ RStudio,
43
47
  Service,
48
+ SparkJob,
44
49
  SSHServer,
45
50
  Volume,
46
51
  Workflow,
@@ -110,7 +115,9 @@ from truefoundry.deploy.v2.lib.patched_models import (
110
115
  StaticVolumeConfig,
111
116
  StringDataMount,
112
117
  TPUType,
113
- TruefoundryArtifactSource,
118
+ TrueFoundryArtifactSource,
119
+ TruefoundryArtifactSource, # deprecated, kept for backwards compatibility
120
+ TrueFoundryInteractiveLogin,
114
121
  VolumeBrowser,
115
122
  VolumeMount,
116
123
  WorkerConfig,
@@ -1,6 +1,6 @@
1
1
  # generated by datamodel-codegen:
2
2
  # filename: application.json
3
- # timestamp: 2024-12-16T11:59:07+00:00
3
+ # timestamp: 2025-01-29T10:40:43+00:00
4
4
 
5
5
  from __future__ import annotations
6
6
 
@@ -149,7 +149,7 @@ class BaseAutoscaling(BaseModel):
149
149
 
150
150
  class BasicAuthCreds(BaseModel):
151
151
  """
152
- +label=Username and password for service auth
152
+ +label=Username and password
153
153
  """
154
154
 
155
155
  type: Literal["basic_auth"] = Field(..., description="+value=basic_auth")
@@ -659,6 +659,10 @@ class Profile(str, Enum):
659
659
 
660
660
  class NvidiaMIGGPU(BaseModel):
661
661
  type: Literal["nvidia_mig_gpu"] = Field(..., description="+value=nvidia_mig_gpu")
662
+ name: Optional[str] = Field(
663
+ None,
664
+ description="+label=GPU Name\n+usage=Name of the Nvidia GPU. One of [P4, P100, V100, T4, A10G, A100_40GB, A100_80GB]\nThis field is required for Node Selector and can be ignored in Nodepool Selector.\nOne instance of the card contains the following amount of memory -\nP4: 8 GB, P100: 16 GB, V100: 16 GB, T4: 16 GB, A10G: 24 GB, A100_40GB: 40GB, A100_80GB: 80 GB",
665
+ )
662
666
  profile: Profile = Field(
663
667
  ...,
664
668
  description="+label=MIG Profile\n+usage=Name of the MIG profile to use. One of [1g.5gb, 2g.10gb, 3g.20gb, 1g.10gb, 2g.20gb, 3g.40gb]",
@@ -669,6 +673,10 @@ class NvidiaTimeslicingGPU(BaseModel):
669
673
  type: Literal["nvidia_timeslicing_gpu"] = Field(
670
674
  ..., description="+value=nvidia_timeslicing_gpu"
671
675
  )
676
+ name: Optional[str] = Field(
677
+ None,
678
+ description="+label=GPU Name\n+usage=Name of the Nvidia GPU. One of [P4, P100, V100, T4, A10G, A100_40GB, A100_80GB]\nThis field is required for Node Selector and can be ignored in Nodepool Selector.\nOne instance of the card contains the following amount of memory -\nP4: 8 GB, P100: 16 GB, V100: 16 GB, T4: 16 GB, A10G: 24 GB, A100_40GB: 40GB, A100_80GB: 80 GB",
679
+ )
672
680
  gpu_memory: conint(ge=1, le=200000) = Field(
673
681
  ...,
674
682
  description="+label=GPU Memory (MB)\n+usage=Amount of GPU memory (in MB) to allocate. Please note, this limit is not being enforced today but will be in future. Applications are expected to operate in co-opertative mode",
@@ -735,45 +743,6 @@ class AppProtocol(str, Enum):
735
743
  tcp = "tcp"
736
744
 
737
745
 
738
- class Port(BaseModel):
739
- """
740
- +docs=Describes the ports the service should be exposed to.
741
- """
742
-
743
- port: conint(ge=1, le=65535) = Field(
744
- 80, description="+usage=Port number to expose."
745
- )
746
- protocol: Protocol = Field("TCP", description="+usage=Protocol for the port.")
747
- expose: bool = Field(True, description="+usage=Expose the port")
748
- app_protocol: AppProtocol = Field(
749
- "http",
750
- description="+label=Application Protocol\n+usage=Application Protocol for the port.\nSelect the application protocol used by your service. For most use cases, this should be `http`(HTTP/1.1).\nIf you are running a gRPC server, select the `grpc` option.\nThis is only applicable if `expose=true`.",
751
- )
752
- host: Optional[
753
- constr(
754
- 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]?)))$"
755
- )
756
- ] = Field(
757
- None,
758
- description="+usage=Host e.g. ai.example.com, app.truefoundry.com\n+message=Upto 253 characters, each part of host should be at most 63 characters long, can contain alphabets, digits and hypen, must begin and end with an alphanumeric characters. Parts must be separated by periods (.)",
759
- )
760
- path: Optional[
761
- constr(regex=r"^(/([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-_\.]*[a-zA-Z0-9]))*/$")
762
- ] = Field(
763
- None,
764
- description="+usage=Path e.g. /v1/api/ml/, /v2/docs/\n+message=Should begin and end with a forward slash (/). Each part can can contain alphabets, digits and hypen, must begin and end with an alphanumeric characters. Parts should be separated by forward slashes (/)",
765
- )
766
- rewrite_path_to: Optional[
767
- constr(regex=r"^(/([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-_\.]*[a-zA-Z0-9]))*/$")
768
- ] = Field(
769
- None,
770
- description="+label=Rewrite Path to\n+usage=Rewrite the path prefix to a different path.\nIf `path` is `/v1/api` and `rewrite_path_to` is `/api`. The URI in the HTTP request `http://0.0.0.0:8080/v1/api/houses` will be rewritten to `http://0.0.0.0:8080/api/houses` before the request is forwarded your service.\nDefaults to `/`.\nThis is only applicable if `path` is given.\n+message=Should begin and end with a forward slash (/). Each part can can contain alphabets, digits and hypen, must begin and end with an alphanumeric characters. Parts should be separated by forward slashes (/)",
771
- )
772
- auth: Optional[Union[BasicAuthCreds, JwtAuthCreds]] = Field(
773
- None, description="+usage=Username and Password for service auth"
774
- )
775
-
776
-
777
746
  class PythonBuild(BaseModel):
778
747
  """
779
748
  +docs=Describes that we are using python to build a container image with a specific python version and pip packages installed.
@@ -1010,7 +979,7 @@ class SecretMount(BaseModel):
1010
979
  )
1011
980
  secret_fqn: constr(regex=r"^tfy-secret:\/\/.+:.+:.+$") = Field(
1012
981
  ...,
1013
- description="+label=Secret\n+usage=The Truefoundry secret whose value will be the file content.",
982
+ description="+label=Secret\n+usage=The TrueFoundry secret whose value will be the file content.",
1014
983
  )
1015
984
 
1016
985
 
@@ -1021,6 +990,42 @@ class ServiceAutoscaling(BaseAutoscaling):
1021
990
  )
1022
991
 
1023
992
 
993
+ class SparkDriverConfig(BaseModel):
994
+ """
995
+ +label=Driver Config
996
+ """
997
+
998
+ ui_endpoint: Endpoint
999
+ resources: Optional[Resources] = None
1000
+
1001
+
1002
+ class SparkExecutorDynamicScaling(BaseModel):
1003
+ """
1004
+ +label=Dynamic Scaling
1005
+ """
1006
+
1007
+ type: Literal["dynamic"] = Field(..., description="+value=dynamic")
1008
+ min: conint(ge=0, le=500) = Field(
1009
+ 1,
1010
+ description="+label=Min Instances\n+usage=Minimum number of instances to start / scale down to\n+sort=100",
1011
+ )
1012
+ max: conint(ge=0, le=500) = Field(
1013
+ 1,
1014
+ description="+label=Max Instances\n+usage=Maximum number of instances to scale up to\n+sort=200",
1015
+ )
1016
+
1017
+
1018
+ class SparkExecutorFixedInstances(BaseModel):
1019
+ """
1020
+ +label=Fixed Instances
1021
+ """
1022
+
1023
+ type: Literal["fixed"] = Field(..., description="+value=fixed")
1024
+ count: conint(ge=0, le=500) = Field(
1025
+ 1, description="+label=Instances Count\n+usage=Number of instances to start"
1026
+ )
1027
+
1028
+
1024
1029
  class StaticVolumeConfig(BaseModel):
1025
1030
  """
1026
1031
  +label=Static Volume Config
@@ -1106,10 +1111,10 @@ class TaskPythonBuild(BaseModel):
1106
1111
  )
1107
1112
 
1108
1113
 
1109
- class TruefoundryArtifactSource(BaseModel):
1114
+ class TrueFoundryArtifactSource(BaseModel):
1110
1115
  """
1111
- +docs=Input for Artifact from Truefoundry Artifact Registry
1112
- +label=Truefoundry Artifact Source
1116
+ +docs=Input for Artifact from TrueFoundry Artifact Registry
1117
+ +label=TrueFoundry Artifact Source
1113
1118
  """
1114
1119
 
1115
1120
  type: Literal["truefoundry-artifact"] = Field(
@@ -1125,6 +1130,16 @@ class TruefoundryArtifactSource(BaseModel):
1125
1130
  )
1126
1131
 
1127
1132
 
1133
+ class TrueFoundryInteractiveLogin(BaseModel):
1134
+ """
1135
+ +label=Login with truefoundry
1136
+ """
1137
+
1138
+ type: Literal["truefoundry_oauth"] = Field(
1139
+ ..., description="+value=truefoundry_oauth"
1140
+ )
1141
+
1142
+
1128
1143
  class VolumeBrowser(BaseModel):
1129
1144
  """
1130
1145
  +label=Volume Browser
@@ -1157,7 +1172,7 @@ class VolumeMount(BaseModel):
1157
1172
  )
1158
1173
  volume_fqn: constr(regex=r"^tfy-volume:\/\/.+:.+:.+$") = Field(
1159
1174
  ...,
1160
- description="+label=Volume\n+usage=The Truefoundry volume that needs to be mounted.",
1175
+ description="+label=Volume\n+usage=The TrueFoundry volume that needs to be mounted.",
1161
1176
  )
1162
1177
 
1163
1178
 
@@ -1188,7 +1203,7 @@ class ArtifactsDownload(BaseModel):
1188
1203
  """
1189
1204
 
1190
1205
  cache_volume: Optional[ArtifactsCacheVolume] = None
1191
- artifacts: List[Union[TruefoundryArtifactSource, HuggingfaceArtifactSource]] = (
1206
+ artifacts: List[Union[TrueFoundryArtifactSource, HuggingfaceArtifactSource]] = (
1192
1207
  Field(
1193
1208
  ..., description="+label=Artifacts\n+usage=List of artifacts to be cached"
1194
1209
  )
@@ -1213,7 +1228,7 @@ class BaseWorkbenchInput(BaseModel):
1213
1228
  +docs=Describes the configuration for the service
1214
1229
  """
1215
1230
 
1216
- name: constr(regex=r"^[a-z][a-z0-9\-]{1,30}[a-z0-9]$") = Field(
1231
+ name: constr(regex=r"^[a-z](?:[a-z0-9]|-(?!-)){1,30}[a-z0-9]$") = Field(
1217
1232
  ...,
1218
1233
  description="+usage=Name of the workbench. This uniquely identifies this workbench in the workspace.\n> Name can only contain alphanumeric characters and '-' and can be atmost 25 characters long\n+sort=1\n+message=3 to 32 lower case characters long alphanumeric word, may contain - in between, cannot start with a number",
1219
1234
  )
@@ -1279,9 +1294,8 @@ class Codeserver(BaseWorkbenchInput):
1279
1294
  +docs=Describes the configuration for the code server
1280
1295
  """
1281
1296
 
1282
- type: Literal["codeserver"] = Field(..., description="+value=Code Server")
1297
+ type: Literal["codeserver"] = Field(..., description="+value=codeserver")
1283
1298
  image: WorkbenchImage
1284
- auth: Optional[BasicAuthCreds] = None
1285
1299
 
1286
1300
 
1287
1301
  class ContainerTaskConfig(BaseModel):
@@ -1364,7 +1378,7 @@ class HealthProbe(BaseModel):
1364
1378
 
1365
1379
  class Helm(BaseModel):
1366
1380
  type: Literal["helm"] = Field(..., description="+value=helm")
1367
- name: constr(regex=r"^[a-z][a-z0-9\-]{1,30}[a-z0-9]$") = Field(
1381
+ name: constr(regex=r"^[a-z](?:[a-z0-9]|-(?!-)){1,30}[a-z0-9]$") = Field(
1368
1382
  ...,
1369
1383
  description="+sort=1\n+message=3 to 32 lower case characters long alphanumeric word, may contain - in between, cannot start with a number\n+usage=Name of the Helm deployment. This will be set as the release name of the chart you are deploying.",
1370
1384
  )
@@ -1391,7 +1405,7 @@ class Job(BaseModel):
1391
1405
  """
1392
1406
 
1393
1407
  type: Literal["job"] = Field(..., description="+value=job")
1394
- name: constr(regex=r"^[a-z][a-z0-9\-]{1,30}[a-z0-9]$") = Field(
1408
+ name: constr(regex=r"^[a-z](?:[a-z0-9]|-(?!-)){1,30}[a-z0-9]$") = Field(
1395
1409
  ...,
1396
1410
  description="+usage=Name of the job\n+sort=1\n+message=3 to 32 lower case characters long alphanumeric word, may contain - in between, cannot start with a number",
1397
1411
  )
@@ -1549,13 +1563,51 @@ class Notebook(BaseWorkbenchInput):
1549
1563
 
1550
1564
  type: Literal["notebook"] = Field(..., description="+value=notebook")
1551
1565
  image: WorkbenchImage
1552
- auth: Optional[BasicAuthCreds] = None
1553
1566
  cull_timeout: conint(ge=5) = Field(
1554
1567
  30,
1555
1568
  description="+label=Stop after (minutes of inactivity)\n+usage=Stop the notebook instance after this much time in minutes of inactivity.\nThe notebook instance will be stopped even if the notebook is open in your browser, but nothing is running on the notebook.\n+sort=5",
1556
1569
  )
1557
1570
 
1558
1571
 
1572
+ class Port(BaseModel):
1573
+ """
1574
+ +docs=Describes the ports the service should be exposed to.
1575
+ """
1576
+
1577
+ port: conint(ge=1, le=65535) = Field(
1578
+ 80, description="+usage=Port number to expose."
1579
+ )
1580
+ protocol: Protocol = Field("TCP", description="+usage=Protocol for the port.")
1581
+ expose: bool = Field(True, description="+usage=Expose the port")
1582
+ app_protocol: AppProtocol = Field(
1583
+ "http",
1584
+ description="+label=Application Protocol\n+usage=Application Protocol for the port.\nSelect the application protocol used by your service. For most use cases, this should be `http`(HTTP/1.1).\nIf you are running a gRPC server, select the `grpc` option.\nThis is only applicable if `expose=true`.",
1585
+ )
1586
+ host: Optional[
1587
+ constr(
1588
+ 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]?)))$"
1589
+ )
1590
+ ] = Field(
1591
+ None,
1592
+ description="+usage=Host e.g. ai.example.com, app.truefoundry.com\n+message=Upto 253 characters, each part of host should be at most 63 characters long, can contain alphabets, digits and hypen, must begin and end with an alphanumeric characters. Parts must be separated by periods (.)",
1593
+ )
1594
+ path: Optional[
1595
+ constr(regex=r"^(/([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-_\.]*[a-zA-Z0-9]))*/$")
1596
+ ] = Field(
1597
+ None,
1598
+ description="+usage=Path e.g. /v1/api/ml/, /v2/docs/\n+message=Should begin and end with a forward slash (/). Each part can can contain alphabets, digits and hypen, must begin and end with an alphanumeric characters. Parts should be separated by forward slashes (/)",
1599
+ )
1600
+ rewrite_path_to: Optional[
1601
+ constr(regex=r"^(/([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-_\.]*[a-zA-Z0-9]))*/$")
1602
+ ] = Field(
1603
+ None,
1604
+ description="+label=Rewrite Path to\n+usage=Rewrite the path prefix to a different path.\nIf `path` is `/v1/api` and `rewrite_path_to` is `/api`. The URI in the HTTP request `http://0.0.0.0:8080/v1/api/houses` will be rewritten to `http://0.0.0.0:8080/api/houses` before the request is forwarded your service.\nDefaults to `/`.\nThis is only applicable if `path` is given.\n+message=Should begin and end with a forward slash (/). Each part can can contain alphabets, digits and hypen, must begin and end with an alphanumeric characters. Parts should be separated by forward slashes (/)",
1605
+ )
1606
+ auth: Optional[Union[BasicAuthCreds, JwtAuthCreds, TrueFoundryInteractiveLogin]] = (
1607
+ Field(None, description="+usage=Authentication method for inbound traffic")
1608
+ )
1609
+
1610
+
1559
1611
  class PythonTaskConfig(BaseModel):
1560
1612
  """
1561
1613
  +docs=Describes the configuration for the python function task
@@ -1582,22 +1634,88 @@ class PythonTaskConfig(BaseModel):
1582
1634
  )
1583
1635
 
1584
1636
 
1637
+ class RStudio(BaseWorkbenchInput):
1638
+ """
1639
+ +docs=Describes the configuration for the Rstudio server
1640
+ """
1641
+
1642
+ type: Literal["rstudio"] = Field(..., description="+value=rstudio")
1643
+ image: WorkbenchImage
1644
+
1645
+
1585
1646
  class SSHServer(BaseWorkbenchInput):
1586
1647
  """
1587
1648
  +docs=Describes the configuration for the ssh server
1588
1649
  """
1589
1650
 
1590
- type: Literal["ssh-server"] = Field(..., description="+value=SSH Server")
1651
+ type: Literal["ssh-server"] = Field(..., description="+value=ssh-server")
1591
1652
  image: WorkbenchImage
1592
1653
  ssh_public_key: str = Field(
1593
1654
  ...,
1594
1655
  description="+label: SSH Public Key\n+usage=Add Your SSH Public Key, this will be used to authenticate you to the SSH Server. \\\nYou can find it using `cat ~/.ssh/id_rsa.pub` in Mac/Linux or `type $home\\.ssh\\id_rsa.pub` in Windows Powershell. \\\nYou can also generate a new SSH key pair using `ssh-keygen -t rsa` in your local terminal. (same for both Mac/Linux and Windows Powershell)\n+uiType=TextArea\n+sort=4",
1595
1656
  )
1657
+ cull_timeout: Optional[conint(ge=5)] = Field(
1658
+ None,
1659
+ description="+label=Stop after (minutes of inactivity)\n+usage=Stop the SSH Server instance after this much time in minutes of inactivity. \\\nThe instance is considered active if there is at least one active SSH connection (a client connected to the SSH server), \\\nor if a background job is running using tmux or screen, or if the pod has restarted.\n+sort=5",
1660
+ )
1661
+
1662
+
1663
+ class SparkExecutorConfig(BaseModel):
1664
+ """
1665
+ +label=Executor Config
1666
+ """
1667
+
1668
+ instances: Union[SparkExecutorFixedInstances, SparkExecutorDynamicScaling] = Field(
1669
+ {"type": "fixed", "count": 1}, description="+label=Executor Instances"
1670
+ )
1671
+ resources: Optional[Resources] = None
1672
+
1673
+
1674
+ class SparkJob(BaseModel):
1675
+ type: Literal["spark-job"] = Field(..., description="+value=spark-job\n+sort=1")
1676
+ name: constr(regex=r"^[a-z](?:[a-z0-9]|-(?!-)){1,30}[a-z0-9]$") = Field(
1677
+ ...,
1678
+ description="+label=Name\n+usage=Name of the job\n+message=3 to 32 lower case characters long alphanumeric word, may contain - in between, cannot start with a number\n+sort=2",
1679
+ )
1680
+ image: Image
1681
+ spark_version: str = Field(
1682
+ "3.5.2",
1683
+ description="+label=Spark Version\n+usage=Spark version should match the spark version installed in the image.\n+sort=2000",
1684
+ )
1685
+ main_application_file: str = Field(
1686
+ ...,
1687
+ description="+label=Main Application File\n+usage=The main application file to be executed by the spark job.\n+sort=3000",
1688
+ )
1689
+ arguments: Optional[str] = Field(
1690
+ None,
1691
+ description="+label=Arguments\n+usage=Arguments to be passed to the main application file.\n+sort=4000",
1692
+ )
1693
+ driver_config: SparkDriverConfig
1694
+ executor_config: SparkExecutorConfig
1695
+ env: Optional[Dict[str, Any]] = Field(
1696
+ None,
1697
+ description="+label=Environment Variables\n+usage=Configure environment variables to be injected in the service either as plain text. [Docs](https://docs.truefoundry.com/docs/env-variables)\n+icon=fa-globe\n+sort=21000",
1698
+ )
1699
+ mounts: Optional[List[VolumeMount]] = Field(
1700
+ None,
1701
+ description="+label=Mounts\n+usage=Configure volumes to be mounted to driver and executors. [Docs](https://docs.truefoundry.com/docs/mounting-volumes-job)\n+sort=22000\n+uiType=Mounts",
1702
+ )
1703
+ retries: conint(ge=0, le=10) = Field(
1704
+ 0,
1705
+ description="+label=Retries\n+usage=Specify the maximum number of attempts to retry a job before it is marked as failed.\n+icon=fa-repeat\n+sort=23000",
1706
+ )
1707
+ service_account: Optional[str] = Field(
1708
+ None, description="+label=Service Account\n+sort=24000"
1709
+ )
1710
+ workspace_fqn: Optional[str] = Field(
1711
+ None,
1712
+ description="+label=Workspace FQN\n+docs=Fully qualified name of the workspace\n+uiType=Hidden",
1713
+ )
1596
1714
 
1597
1715
 
1598
1716
  class Volume(BaseModel):
1599
1717
  type: Literal["volume"] = Field(..., description="+value=volume")
1600
- name: constr(regex=r"^[a-z][a-z0-9\-]{1,30}[a-z0-9]$") = Field(
1718
+ name: constr(regex=r"^[a-z](?:[a-z0-9]|-(?!-)){1,30}[a-z0-9]$") = Field(
1601
1719
  ...,
1602
1720
  description="+sort=1\n+message=3 to 32 lower case characters long alphanumeric word, may contain - in between, cannot start with a number\n+usage=Name of the Volume. This will be set as the volume name.",
1603
1721
  )
@@ -1632,7 +1750,7 @@ class WorkerConfig(BaseModel):
1632
1750
 
1633
1751
 
1634
1752
  class BaseService(BaseModel):
1635
- name: constr(regex=r"^[a-z][a-z0-9\-]{1,30}[a-z0-9]$") = Field(
1753
+ name: constr(regex=r"^[a-z](?:[a-z0-9]|-(?!-)){1,30}[a-z0-9]$") = Field(
1636
1754
  ...,
1637
1755
  description="+usage=Name of the service. This uniquely identifies this service in the workspace.\n> Name can only contain alphanumeric characters and '-' and can be atmost 25 characters long\n+sort=1\n+message=3 to 32 lower case characters long alphanumeric word, may contain - in between, cannot start with a number",
1638
1756
  )
@@ -1727,7 +1845,7 @@ class Workflow(BaseModel):
1727
1845
  """
1728
1846
 
1729
1847
  type: Literal["workflow"] = Field(..., description="+value=workflow")
1730
- name: constr(regex=r"^[a-z][a-z0-9\-]{1,30}[a-z0-9]$") = Field(
1848
+ name: constr(regex=r"^[a-z](?:[a-z0-9]|-(?!-)){1,30}[a-z0-9]$") = Field(
1731
1849
  ...,
1732
1850
  description="+usage=Name of the workflow\n+sort=1\n+message=3 to 32 lower case characters long alphanumeric word, may contain - in between, cannot start with a number",
1733
1851
  )
@@ -1780,8 +1898,10 @@ class Application(BaseModel):
1780
1898
  Notebook,
1781
1899
  Codeserver,
1782
1900
  SSHServer,
1901
+ RStudio,
1783
1902
  Helm,
1784
1903
  Volume,
1785
1904
  ApplicationSet,
1786
1905
  Workflow,
1906
+ SparkJob,
1787
1907
  ]
@@ -1,6 +1,5 @@
1
1
  from truefoundry.deploy.cli.commands.apply_command import get_apply_command
2
2
  from truefoundry.deploy.cli.commands.build_command import get_build_command
3
- from truefoundry.deploy.cli.commands.build_logs_command import get_build_logs_command
4
3
  from truefoundry.deploy.cli.commands.create_command import get_create_command
5
4
  from truefoundry.deploy.cli.commands.delete_command import get_delete_command
6
5
  from truefoundry.deploy.cli.commands.deploy_command import get_deploy_command
@@ -273,20 +273,33 @@ class ServiceFoundryServiceClient(BaseServiceFoundryServiceClient):
273
273
  subscribe_message: str,
274
274
  socketio_path: str = "socket.io",
275
275
  callback=None,
276
- wait=True,
277
- ):
276
+ ) -> socketio.Client:
278
277
  callback = callback or OutputCallBack()
279
278
  sio = socketio.Client(request_timeout=60)
280
279
  callback.print_line("Waiting for the task to start...")
280
+ next_log_start_timestamp = query_dict.get("startTs")
281
281
 
282
282
  @sio.on(subscribe_message)
283
283
  def logs(data):
284
284
  try:
285
285
  _log = json.loads(data)
286
- callback.print_line(self._get_log_print_line(_log["body"]))
286
+ body = _log["body"]
287
+ callback.print_line(self._get_log_print_line(body))
288
+ nonlocal next_log_start_timestamp
289
+ next_log_start_timestamp = body["time"]
287
290
  except Exception:
288
291
  logger.exception(f"Error while parsing log line, {data!r}")
289
292
 
293
+ @sio.on("connect")
294
+ def on_connect():
295
+ # TODO: We should have have a timeout here. `emit` does
296
+ # not support timeout. Explore `sio.call`.
297
+ query_dict["startTs"] = next_log_start_timestamp
298
+ sio.emit(
299
+ subscribe_message,
300
+ json.dumps(query_dict),
301
+ )
302
+
290
303
  def sio_disconnect_no_exception():
291
304
  try:
292
305
  sio.disconnect()
@@ -300,14 +313,7 @@ class ServiceFoundryServiceClient(BaseServiceFoundryServiceClient):
300
313
  headers=self._get_header(),
301
314
  socketio_path=socketio_path,
302
315
  )
303
- # TODO: We should have have a timeout here. `emit` does
304
- # not support timeout. Explore `sio.call`.
305
- sio.emit(
306
- subscribe_message,
307
- json.dumps(query_dict),
308
- )
309
- if wait:
310
- sio.wait()
316
+ return sio
311
317
 
312
318
  @check_min_cli_version
313
319
  def get_deployment(self, application_id: str, deployment_id: str) -> Deployment:
@@ -372,11 +378,10 @@ class ServiceFoundryServiceClient(BaseServiceFoundryServiceClient):
372
378
  self,
373
379
  build_response: BuildResponse,
374
380
  callback=None,
375
- wait: bool = True,
376
- ):
381
+ ) -> socketio.Client:
377
382
  callback = callback or OutputCallBack()
378
383
  tail_logs_obj = json.loads(build_response.tailLogsUrl)
379
- self._tail_logs(
384
+ socket = self._tail_logs(
380
385
  tail_logs_url=urljoin(
381
386
  tail_logs_obj["uri"], f"/?type={BUILD_LOGS_SUBSCRIBE_MESSAGE}"
382
387
  ),
@@ -386,9 +391,9 @@ class ServiceFoundryServiceClient(BaseServiceFoundryServiceClient):
386
391
  "startTs": build_response.logsStartTs,
387
392
  },
388
393
  callback=callback,
389
- wait=wait,
390
394
  subscribe_message=BUILD_LOGS_SUBSCRIBE_MESSAGE,
391
395
  )
396
+ return socket
392
397
 
393
398
  @check_min_cli_version
394
399
  def tail_logs_for_deployment(
@@ -416,7 +421,6 @@ class ServiceFoundryServiceClient(BaseServiceFoundryServiceClient):
416
421
  },
417
422
  },
418
423
  callback=callback,
419
- wait=wait,
420
424
  subscribe_message=DEPLOYMENT_LOGS_SUBSCRIBE_MESSAGE,
421
425
  )
422
426
 
@@ -1,7 +1,8 @@
1
1
  import sys
2
2
  import time
3
- from typing import List, Optional, TypeVar
3
+ from typing import Optional, TypeVar
4
4
 
5
+ import socketio
5
6
  from rich.status import Status
6
7
 
7
8
  from truefoundry.common.utils import poll_for_function
@@ -96,18 +97,15 @@ def _log_application_dashboard_url(deployment: Deployment, log_message: str):
96
97
  logger.info(log_message, url)
97
98
 
98
99
 
99
- def _tail_build_logs(build_responses: List[BuildResponse]) -> None:
100
+ def _tail_build_logs(build_response: BuildResponse) -> socketio.Client:
100
101
  client = ServiceFoundryServiceClient()
101
102
 
102
- # TODO: Explore other options like,
103
- # https://rich.readthedocs.io/en/stable/live.html#live-display
104
- # How does docker/compose does multiple build logs?
105
- for build_response in build_responses:
106
- logger.info("Tailing build logs for '%s'", build_response.componentName)
107
- client.tail_build_logs(build_response=build_response, wait=True)
103
+ logger.info("Tailing build logs for '%s'", build_response.componentName)
104
+ socket = client.tail_build_logs(build_response=build_response)
105
+ return socket
108
106
 
109
107
 
110
- def _deploy_wait_handler(
108
+ def _deploy_wait_handler( # noqa: C901
111
109
  deployment: Deployment,
112
110
  ) -> Optional[DeploymentTransitionStatus]:
113
111
  _log_application_dashboard_url(
@@ -122,9 +120,10 @@ def _deploy_wait_handler(
122
120
  last_status_printed = None
123
121
  client = ServiceFoundryServiceClient()
124
122
  start_time = time.monotonic()
125
- total_timeout_time: int = 300
123
+ total_timeout_time: int = 600
126
124
  poll_interval_seconds = 5
127
125
  time_elapsed = 0
126
+ socket: socketio.Client = None
128
127
 
129
128
  for deployment_statuses in poll_for_function(
130
129
  client.get_deployment_statuses,
@@ -150,20 +149,29 @@ def _deploy_wait_handler(
150
149
  last_status_printed = status_to_print
151
150
 
152
151
  if latest_deployment_status.state.isTerminalState:
152
+ if socket and socket.connected:
153
+ socket.disconnect()
153
154
  break
154
155
 
155
156
  if (
156
157
  latest_deployment_status.transition
157
158
  == DeploymentTransitionStatus.BUILDING
158
159
  ):
159
- build_responses = client.get_deployment_build_response(
160
- application_id=deployment.applicationId, deployment_id=deployment.id
161
- )
162
- _tail_build_logs(build_responses)
160
+ if not socket:
161
+ build_responses = client.get_deployment_build_response(
162
+ application_id=deployment.applicationId,
163
+ deployment_id=deployment.id,
164
+ )
165
+ socket = _tail_build_logs(build_responses[0])
163
166
 
164
167
  time_elapsed = time.monotonic() - start_time
165
168
  if time_elapsed > total_timeout_time:
166
- logger.warning("Polled server for %s secs.", int(time_elapsed))
169
+ logger.warning(
170
+ "Polled server for %s secs. Disconnecting from server, the deployment will still continue.",
171
+ int(time_elapsed),
172
+ )
173
+ if socket and socket.connected:
174
+ socket.disconnect()
167
175
  break
168
176
 
169
177
  return last_status_printed
@@ -30,6 +30,10 @@ class Job(models.Job, DeployablePatchedModelBase):
30
30
  resources: models.Resources = Field(default_factory=models.Resources)
31
31
 
32
32
 
33
+ class SparkJob(models.SparkJob, DeployablePatchedModelBase):
34
+ type: Literal["spark-job"] = "spark-job"
35
+
36
+
33
37
  class Notebook(models.Notebook, DeployablePatchedModelBase):
34
38
  type: Literal["notebook"] = "notebook"
35
39
  resources: models.Resources = Field(default_factory=models.Resources)
@@ -37,6 +41,12 @@ class Notebook(models.Notebook, DeployablePatchedModelBase):
37
41
 
38
42
  class Codeserver(models.Codeserver, DeployablePatchedModelBase):
39
43
  type: Literal["codeserver"] = "codeserver"
44
+ resources: models.Resources = Field(default_factory=models.Resources)
45
+
46
+
47
+ class RStudio(models.RStudio, DeployablePatchedModelBase):
48
+ type: Literal["rstudio"] = "rstudio"
49
+ resources: models.Resources = Field(default_factory=models.Resources)
40
50
 
41
51
 
42
52
  class Helm(models.Helm, DeployablePatchedModelBase):
@@ -256,6 +256,10 @@ class JwtAuthCreds(models.JwtAuthCreds, PatchedModelBase):
256
256
  type: Literal["jwt_auth"] = "jwt_auth"
257
257
 
258
258
 
259
+ class TrueFoundryInteractiveLogin(models.TrueFoundryInteractiveLogin, PatchedModelBase):
260
+ type: Literal["truefoundry_oauth"] = "truefoundry_oauth"
261
+
262
+
259
263
  class HealthProbe(models.HealthProbe, PatchedModelBase):
260
264
  pass
261
265
 
@@ -500,10 +504,14 @@ class HuggingfaceArtifactSource(models.HuggingfaceArtifactSource, PatchedModelBa
500
504
  type: Literal["huggingface-hub"] = "huggingface-hub"
501
505
 
502
506
 
503
- class TruefoundryArtifactSource(models.TruefoundryArtifactSource, PatchedModelBase):
507
+ class TrueFoundryArtifactSource(models.TrueFoundryArtifactSource, PatchedModelBase):
504
508
  type: Literal["truefoundry-artifact"] = "truefoundry-artifact"
505
509
 
506
510
 
511
+ # Deprecated alias, kept for backward compatibility
512
+ TruefoundryArtifactSource = TrueFoundryArtifactSource
513
+
514
+
507
515
  class ArtifactsDownload(models.ArtifactsDownload, PatchedModelBase):
508
516
  pass
509
517
 
@@ -175,8 +175,8 @@ from truefoundry.ml.autogen.client.models.experiment_tag_dto import ExperimentTa
175
175
  from truefoundry.ml.autogen.client.models.export_deployment_files_request_dto import (
176
176
  ExportDeploymentFilesRequestDto,
177
177
  )
178
- from truefoundry.ml.autogen.client.models.external_artifact_source import (
179
- ExternalArtifactSource,
178
+ from truefoundry.ml.autogen.client.models.external_blob_storage_source import (
179
+ ExternalBlobStorageSource,
180
180
  )
181
181
  from truefoundry.ml.autogen.client.models.fast_ai_framework import FastAIFramework
182
182
  from truefoundry.ml.autogen.client.models.file_info_dto import FileInfoDto
@@ -403,8 +403,8 @@ from truefoundry.ml.autogen.client.models.trigger_job_run_config_request_dto imp
403
403
  from truefoundry.ml.autogen.client.models.trigger_job_run_config_response_dto import (
404
404
  TriggerJobRunConfigResponseDto,
405
405
  )
406
- from truefoundry.ml.autogen.client.models.true_foundry_artifact_source import (
407
- TrueFoundryArtifactSource,
406
+ from truefoundry.ml.autogen.client.models.true_foundry_managed_source import (
407
+ TrueFoundryManagedSource,
408
408
  )
409
409
  from truefoundry.ml.autogen.client.models.update_artifact_version_request_dto import (
410
410
  UpdateArtifactVersionRequestDto,