pipekit-sdk 2.1.0__tar.gz → 2.1.2__tar.gz

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.
@@ -1,27 +1,28 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: pipekit-sdk
3
- Version: 2.1.0
3
+ Version: 2.1.2
4
4
  Summary: Pipekit Python SDK
5
- Home-page: https://pipekit.io
6
5
  License: BSD-3-Clause
6
+ License-File: LICENSE
7
7
  Keywords: pipekit,hera,argo,workflows,orchestration
8
8
  Author: Pipekit
9
9
  Author-email: ci@pipekit.io
10
- Requires-Python: >=3.8,<4
10
+ Requires-Python: >=3.9,<4
11
11
  Classifier: License :: OSI Approved :: BSD License
12
12
  Classifier: Operating System :: OS Independent
13
13
  Classifier: Programming Language :: Python :: 3
14
- Classifier: Programming Language :: Python :: 3.8
15
14
  Classifier: Programming Language :: Python :: 3.9
16
15
  Classifier: Programming Language :: Python :: 3.10
17
16
  Classifier: Programming Language :: Python :: 3.11
18
17
  Classifier: Programming Language :: Python :: 3.12
19
18
  Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
20
20
  Requires-Dist: hera (>=5.5.2)
21
21
  Requires-Dist: pydantic (<3)
22
22
  Requires-Dist: pyjwt (>=2.9.0,<3.0.0)
23
23
  Requires-Dist: responses (>=0.25.3,<0.26.0)
24
24
  Project-URL: Documentation, https://docs.pipekit.io/
25
+ Project-URL: Homepage, https://pipekit.io
25
26
  Description-Content-Type: text/markdown
26
27
 
27
28
  [![Pipekit Logo](https://helm.pipekit.io/assets/pipekit-logo.png)](https://pipekit.io)
@@ -96,6 +97,45 @@ pipekit.submit(w, "<cluster-name>")
96
97
  pipekit.print_logs(pipe_run.uuid)
97
98
  ```
98
99
 
100
+ ## Managing CronWorkflows
101
+
102
+ You can create, update, and delete a `CronWorkflow` from Python. The namespace
103
+ must match the one in the manifest on every call (the platform default is
104
+ `argo`). A wrong namespace makes the cron look missing.
105
+
106
+ ```py
107
+ from hera.workflows import Container, CronWorkflow
108
+ from pipekit_sdk.service import PipekitService
109
+
110
+ pipekit = PipekitService(token="<token>")
111
+
112
+ with CronWorkflow(
113
+ name="daily-demand-forecast",
114
+ namespace="argo",
115
+ entrypoint="main",
116
+ # Argo Workflows 3.6 deprecated the singular spec.schedule. Use schedules.
117
+ schedules=["*/5 * * * *"],
118
+ service_account_name="argo",
119
+ ) as cron:
120
+ Container(name="main", image="alpine", command=["sh", "-c", "echo hello"])
121
+
122
+ # Create
123
+ pipekit.create(cron, "<cluster-name>")
124
+
125
+ # Update: the namespace is taken from the manifest when not passed
126
+ updated = pipekit.update_cron(cron, "<cluster-name>")
127
+
128
+ # Suspend / resume scheduling
129
+ pipekit.suspend_cron("<cluster-name>", "argo", "daily-demand-forecast")
130
+ pipekit.resume_cron("<cluster-name>", "argo", "daily-demand-forecast")
131
+
132
+ # Get the current state
133
+ current = pipekit.get_cron("<cluster-name>", "argo", "daily-demand-forecast")
134
+
135
+ # Delete
136
+ pipekit.delete_cron("<cluster-name>", "argo", "daily-demand-forecast")
137
+ ```
138
+
99
139
  ## Further help
100
140
  Please refer to the [Pipekit Documentation](https://docs.pipekit.io) for more information.
101
141
 
@@ -70,5 +70,44 @@ pipekit.submit(w, "<cluster-name>")
70
70
  pipekit.print_logs(pipe_run.uuid)
71
71
  ```
72
72
 
73
+ ## Managing CronWorkflows
74
+
75
+ You can create, update, and delete a `CronWorkflow` from Python. The namespace
76
+ must match the one in the manifest on every call (the platform default is
77
+ `argo`). A wrong namespace makes the cron look missing.
78
+
79
+ ```py
80
+ from hera.workflows import Container, CronWorkflow
81
+ from pipekit_sdk.service import PipekitService
82
+
83
+ pipekit = PipekitService(token="<token>")
84
+
85
+ with CronWorkflow(
86
+ name="daily-demand-forecast",
87
+ namespace="argo",
88
+ entrypoint="main",
89
+ # Argo Workflows 3.6 deprecated the singular spec.schedule. Use schedules.
90
+ schedules=["*/5 * * * *"],
91
+ service_account_name="argo",
92
+ ) as cron:
93
+ Container(name="main", image="alpine", command=["sh", "-c", "echo hello"])
94
+
95
+ # Create
96
+ pipekit.create(cron, "<cluster-name>")
97
+
98
+ # Update: the namespace is taken from the manifest when not passed
99
+ updated = pipekit.update_cron(cron, "<cluster-name>")
100
+
101
+ # Suspend / resume scheduling
102
+ pipekit.suspend_cron("<cluster-name>", "argo", "daily-demand-forecast")
103
+ pipekit.resume_cron("<cluster-name>", "argo", "daily-demand-forecast")
104
+
105
+ # Get the current state
106
+ current = pipekit.get_cron("<cluster-name>", "argo", "daily-demand-forecast")
107
+
108
+ # Delete
109
+ pipekit.delete_cron("<cluster-name>", "argo", "daily-demand-forecast")
110
+ ```
111
+
73
112
  ## Further help
74
113
  Please refer to the [Pipekit Documentation](https://docs.pipekit.io) for more information.
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "pipekit-sdk"
3
- version = "2.1.0"
3
+ version = "2.1.2"
4
4
  description = "Pipekit Python SDK"
5
5
  authors = ["Pipekit <ci@pipekit.io>"]
6
6
  license = "BSD-3-Clause"
@@ -23,7 +23,7 @@ packages = [
23
23
  ]
24
24
 
25
25
  [tool.poetry.dependencies]
26
- python = ">=3.8,<4"
26
+ python = ">=3.9,<4"
27
27
  hera = ">=5.5.2"
28
28
  pydantic = "<3"
29
29
  responses = "^0.25.3"
@@ -35,7 +35,7 @@ mypy = "^1.11.0"
35
35
  pytest = "^8.3.1"
36
36
  hera = {extras = ["cli"], version = "^5.16.1"}
37
37
  types-requests = "^2.32.0.20240712"
38
- datamodel-code-generator = {extras = ["http"], version = "^0.26.0"}
38
+ datamodel-code-generator = {extras = ["http"], version = "^0.35.0"}
39
39
 
40
40
  [build-system]
41
41
  requires = ["poetry-core"]
@@ -132,7 +132,7 @@ class AzureFileVolumeSource(BaseModel):
132
132
  str,
133
133
  Field(
134
134
  alias="secretName",
135
- description=("secretName is the name of secret that contains Azure Storage Account" " Name and Key"),
135
+ description=("secretName is the name of secret that contains Azure Storage Account Name and Key"),
136
136
  ),
137
137
  ]
138
138
  share_name: Annotated[str, Field(alias="shareName", description="shareName is the azure share Name")]
@@ -182,7 +182,7 @@ class ContainerPort(BaseModel):
182
182
  Field(
183
183
  alias="containerPort",
184
184
  description=(
185
- "Number of port to expose on the pod's IP address. This must be a valid" " port number, 0 < x < 65536."
185
+ "Number of port to expose on the pod's IP address. This must be a valid port number, 0 < x < 65536."
186
186
  ),
187
187
  ),
188
188
  ]
@@ -312,7 +312,7 @@ class FlockerVolumeSource(BaseModel):
312
312
  Optional[str],
313
313
  Field(
314
314
  alias="datasetUUID",
315
- description=("datasetUUID is the UUID of the dataset. This is unique identifier of a" " Flocker dataset"),
315
+ description=("datasetUUID is the UUID of the dataset. This is unique identifier of a Flocker dataset"),
316
316
  ),
317
317
  ] = None
318
318
 
@@ -371,7 +371,7 @@ class GCEPersistentDiskVolumeSource(BaseModel):
371
371
  class GRPCAction(BaseModel):
372
372
  port: Annotated[
373
373
  int,
374
- Field(description=("Port number of the gRPC service. Number must be in the range 1 to" " 65535.")),
374
+ Field(description=("Port number of the gRPC service. Number must be in the range 1 to 65535.")),
375
375
  ]
376
376
  service: Annotated[
377
377
  str,
@@ -613,7 +613,7 @@ class ObjectFieldSelector(BaseModel):
613
613
  Optional[str],
614
614
  Field(
615
615
  alias="apiVersion",
616
- description=("Version of the schema the FieldPath is written in terms of, defaults" ' to "v1".'),
616
+ description=('Version of the schema the FieldPath is written in terms of, defaults to "v1".'),
617
617
  ),
618
618
  ] = None
619
619
  field_path: Annotated[
@@ -729,15 +729,11 @@ class OwnerReference(BaseModel):
729
729
  ]
730
730
  name: Annotated[
731
731
  str,
732
- Field(
733
- description=("Name of the referent. More info:" " http://kubernetes.io/docs/user-guide/identifiers#names")
734
- ),
732
+ Field(description=("Name of the referent. More info: http://kubernetes.io/docs/user-guide/identifiers#names")),
735
733
  ]
736
734
  uid: Annotated[
737
735
  str,
738
- Field(
739
- description=("UID of the referent. More info:" " http://kubernetes.io/docs/user-guide/identifiers#uids")
740
- ),
736
+ Field(description=("UID of the referent. More info: http://kubernetes.io/docs/user-guide/identifiers#uids")),
741
737
  ]
742
738
 
743
739
 
@@ -757,7 +753,7 @@ class PersistentVolumeClaimVolumeSource(BaseModel):
757
753
  Optional[bool],
758
754
  Field(
759
755
  alias="readOnly",
760
- description=("readOnly Will force the ReadOnly setting in VolumeMounts. Default" " false."),
756
+ description=("readOnly Will force the ReadOnly setting in VolumeMounts. Default false."),
761
757
  ),
762
758
  ] = None
763
759
 
@@ -826,9 +822,7 @@ class PreferredSchedulingTerm(BaseModel):
826
822
  ]
827
823
  weight: Annotated[
828
824
  int,
829
- Field(
830
- description=("Weight associated with matching the corresponding nodeSelectorTerm, in" " the range 1-100.")
831
- ),
825
+ Field(description=("Weight associated with matching the corresponding nodeSelectorTerm, in the range 1-100.")),
832
826
  ]
833
827
 
834
828
 
@@ -872,7 +866,7 @@ class QuobyteVolumeSource(BaseModel):
872
866
  ] = None
873
867
  volume: Annotated[
874
868
  str,
875
- Field(description=("volume is a string that references an already created Quobyte volume" " by name.")),
869
+ Field(description=("volume is a string that references an already created Quobyte volume by name.")),
876
870
  ]
877
871
 
878
872
 
@@ -894,8 +888,7 @@ class RBDVolumeSource(BaseModel):
894
888
  str,
895
889
  Field(
896
890
  description=(
897
- "image is the rados image name. More info:"
898
- " https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it"
891
+ "image is the rados image name. More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it"
899
892
  )
900
893
  ),
901
894
  ]
@@ -999,9 +992,7 @@ class ScaleIOVolumeSource(BaseModel):
999
992
  Optional[str],
1000
993
  Field(
1001
994
  alias="protectionDomain",
1002
- description=(
1003
- "protectionDomain is the name of the ScaleIO Protection Domain for the" " configured storage."
1004
- ),
995
+ description=("protectionDomain is the name of the ScaleIO Protection Domain for the configured storage."),
1005
996
  ),
1006
997
  ] = None
1007
998
  read_only: Annotated[
@@ -1029,7 +1020,7 @@ class ScaleIOVolumeSource(BaseModel):
1029
1020
  Optional[bool],
1030
1021
  Field(
1031
1022
  alias="sslEnabled",
1032
- description=("sslEnabled Flag enable/disable SSL communication with Gateway, default" " false"),
1023
+ description=("sslEnabled Flag enable/disable SSL communication with Gateway, default false"),
1033
1024
  ),
1034
1025
  ] = None
1035
1026
  storage_mode: Annotated[
@@ -1046,7 +1037,7 @@ class ScaleIOVolumeSource(BaseModel):
1046
1037
  Optional[str],
1047
1038
  Field(
1048
1039
  alias="storagePool",
1049
- description=("storagePool is the ScaleIO Storage Pool associated with the protection" " domain."),
1040
+ description=("storagePool is the ScaleIO Storage Pool associated with the protection domain."),
1050
1041
  ),
1051
1042
  ] = None
1052
1043
  system: Annotated[
@@ -1231,7 +1222,7 @@ class ServiceAccountTokenProjection(BaseModel):
1231
1222
  ] = None
1232
1223
  path: Annotated[
1233
1224
  str,
1234
- Field(description=("path is the path relative to the mount point of the file to project" " the token into.")),
1225
+ Field(description=("path is the path relative to the mount point of the file to project the token into.")),
1235
1226
  ]
1236
1227
 
1237
1228
 
@@ -1396,7 +1387,7 @@ class VolumeDevice(BaseModel):
1396
1387
  str,
1397
1388
  Field(
1398
1389
  alias="devicePath",
1399
- description=("devicePath is the path inside of the container that the device will be" " mapped to."),
1390
+ description=("devicePath is the path inside of the container that the device will be mapped to."),
1400
1391
  ),
1401
1392
  ]
1402
1393
  name: Annotated[
@@ -1410,7 +1401,7 @@ class VolumeMount(BaseModel):
1410
1401
  str,
1411
1402
  Field(
1412
1403
  alias="mountPath",
1413
- description=("Path within the container at which the volume should be mounted. Must" " not contain ':'."),
1404
+ description=("Path within the container at which the volume should be mounted. Must not contain ':'."),
1414
1405
  ),
1415
1406
  ]
1416
1407
  mount_propagation: Annotated[
@@ -1429,9 +1420,7 @@ class VolumeMount(BaseModel):
1429
1420
  Optional[bool],
1430
1421
  Field(
1431
1422
  alias="readOnly",
1432
- description=(
1433
- "Mounted read-only if true, read-write otherwise (false or" " unspecified). Defaults to false."
1434
- ),
1423
+ description=("Mounted read-only if true, read-write otherwise (false or unspecified). Defaults to false."),
1435
1424
  ),
1436
1425
  ] = None
1437
1426
  sub_path: Annotated[
@@ -1485,7 +1474,7 @@ class VsphereVirtualDiskVolumeSource(BaseModel):
1485
1474
  Optional[str],
1486
1475
  Field(
1487
1476
  alias="storagePolicyName",
1488
- description=("storagePolicyName is the storage Policy Based Management (SPBM)" " profile name."),
1477
+ description=("storagePolicyName is the storage Policy Based Management (SPBM) profile name."),
1489
1478
  ),
1490
1479
  ] = None
1491
1480
  volume_path: Annotated[
@@ -1589,7 +1578,7 @@ class CSIVolumeSource(BaseModel):
1589
1578
  Field(
1590
1579
  alias="readOnly",
1591
1580
  description=(
1592
- "readOnly specifies a read-only configuration for the volume. Defaults" " to false (read/write)."
1581
+ "readOnly specifies a read-only configuration for the volume. Defaults to false (read/write)."
1593
1582
  ),
1594
1583
  ),
1595
1584
  ] = None
@@ -1619,7 +1608,7 @@ class CephFSVolumeSource(BaseModel):
1619
1608
  path: Annotated[
1620
1609
  Optional[str],
1621
1610
  Field(
1622
- description=("path is Optional: Used as the mounted root, rather than the full Ceph" " tree, default is /")
1611
+ description=("path is Optional: Used as the mounted root, rather than the full Ceph tree, default is /")
1623
1612
  ),
1624
1613
  ] = None
1625
1614
  read_only: Annotated[
@@ -1695,8 +1684,7 @@ class CinderVolumeSource(BaseModel):
1695
1684
  Field(
1696
1685
  alias="secretRef",
1697
1686
  description=(
1698
- "secretRef is optional: points to a secret object containing parameters"
1699
- " used to connect to OpenStack."
1687
+ "secretRef is optional: points to a secret object containing parameters used to connect to OpenStack."
1700
1688
  ),
1701
1689
  ),
1702
1690
  ] = None
@@ -1825,9 +1813,7 @@ class EnvFromSource(BaseModel):
1825
1813
  ] = None
1826
1814
  prefix: Annotated[
1827
1815
  Optional[str],
1828
- Field(
1829
- description=("An optional identifier to prepend to each key in the ConfigMap. Must" " be a C_IDENTIFIER.")
1830
- ),
1816
+ Field(description=("An optional identifier to prepend to each key in the ConfigMap. Must be a C_IDENTIFIER.")),
1831
1817
  ] = None
1832
1818
  secret_ref: Annotated[
1833
1819
  Optional[SecretEnvSource],
@@ -1918,14 +1904,14 @@ class ISCSIVolumeSource(BaseModel):
1918
1904
  Optional[bool],
1919
1905
  Field(
1920
1906
  alias="chapAuthDiscovery",
1921
- description=("chapAuthDiscovery defines whether support iSCSI Discovery CHAP" " authentication"),
1907
+ description=("chapAuthDiscovery defines whether support iSCSI Discovery CHAP authentication"),
1922
1908
  ),
1923
1909
  ] = None
1924
1910
  chap_auth_session: Annotated[
1925
1911
  Optional[bool],
1926
1912
  Field(
1927
1913
  alias="chapAuthSession",
1928
- description=("chapAuthSession defines whether support iSCSI Session CHAP" " authentication"),
1914
+ description=("chapAuthSession defines whether support iSCSI Session CHAP authentication"),
1929
1915
  ),
1930
1916
  ] = None
1931
1917
  fs_type: Annotated[
@@ -1958,7 +1944,7 @@ class ISCSIVolumeSource(BaseModel):
1958
1944
  Field(
1959
1945
  alias="iscsiInterface",
1960
1946
  description=(
1961
- "iscsiInterface is the interface Name that uses an iSCSI transport." " Defaults to 'default' (tcp)."
1947
+ "iscsiInterface is the interface Name that uses an iSCSI transport. Defaults to 'default' (tcp)."
1962
1948
  ),
1963
1949
  ),
1964
1950
  ] = None
@@ -1977,14 +1963,14 @@ class ISCSIVolumeSource(BaseModel):
1977
1963
  Optional[bool],
1978
1964
  Field(
1979
1965
  alias="readOnly",
1980
- description=("readOnly here will force the ReadOnly setting in VolumeMounts." " Defaults to false."),
1966
+ description=("readOnly here will force the ReadOnly setting in VolumeMounts. Defaults to false."),
1981
1967
  ),
1982
1968
  ] = None
1983
1969
  secret_ref: Annotated[
1984
1970
  Optional[LocalObjectReference],
1985
1971
  Field(
1986
1972
  alias="secretRef",
1987
- description=("secretRef is the CHAP Secret for iSCSI target and initiator" " authentication"),
1973
+ description=("secretRef is the CHAP Secret for iSCSI target and initiator authentication"),
1988
1974
  ),
1989
1975
  ] = None
1990
1976
  target_portal: Annotated[
@@ -2005,7 +1991,7 @@ class LabelSelector(BaseModel):
2005
1991
  Optional[List[LabelSelectorRequirement]],
2006
1992
  Field(
2007
1993
  alias="matchExpressions",
2008
- description=("matchExpressions is a list of label selector requirements. The" " requirements are ANDed."),
1994
+ description=("matchExpressions is a list of label selector requirements. The requirements are ANDed."),
2009
1995
  ),
2010
1996
  ] = None
2011
1997
  match_labels: Annotated[
@@ -2072,7 +2058,7 @@ class ManagedFieldsEntry(BaseModel):
2072
2058
  Optional[FieldsV1],
2073
2059
  Field(
2074
2060
  alias="fieldsV1",
2075
- description=("FieldsV1 holds the first JSON version format as described in the" ' "FieldsV1" type.'),
2061
+ description=('FieldsV1 holds the first JSON version format as described in the "FieldsV1" type.'),
2076
2062
  ),
2077
2063
  ] = None
2078
2064
  manager: Annotated[
@@ -2342,7 +2328,7 @@ class ObjectMeta(BaseModel):
2342
2328
  Field(
2343
2329
  alias="selfLink",
2344
2330
  description=(
2345
- "Deprecated: selfLink is a legacy read-only field that is no longer" " populated by the system."
2331
+ "Deprecated: selfLink is a legacy read-only field that is no longer populated by the system."
2346
2332
  ),
2347
2333
  ),
2348
2334
  ] = None
@@ -2372,12 +2358,12 @@ class PersistentVolumeClaimCondition(BaseModel):
2372
2358
  Optional[Time],
2373
2359
  Field(
2374
2360
  alias="lastTransitionTime",
2375
- description=("lastTransitionTime is the time the condition transitioned from one" " status to another."),
2361
+ description=("lastTransitionTime is the time the condition transitioned from one status to another."),
2376
2362
  ),
2377
2363
  ] = None
2378
2364
  message: Annotated[
2379
2365
  Optional[str],
2380
- Field(description=("message is the human-readable message indicating details about last" " transition.")),
2366
+ Field(description=("message is the human-readable message indicating details about last transition.")),
2381
2367
  ] = None
2382
2368
  reason: Annotated[
2383
2369
  Optional[str],
@@ -2754,7 +2740,7 @@ class Probe(BaseModel):
2754
2740
  Optional[int],
2755
2741
  Field(
2756
2742
  alias="periodSeconds",
2757
- description=("How often (in seconds) to perform the probe. Default to 10 seconds." " Minimum value is 1."),
2743
+ description=("How often (in seconds) to perform the probe. Default to 10 seconds. Minimum value is 1."),
2758
2744
  ),
2759
2745
  ] = None
2760
2746
  success_threshold: Annotated[
@@ -2996,14 +2982,12 @@ class WeightedPodAffinityTerm(BaseModel):
2996
2982
  PodAffinityTerm,
2997
2983
  Field(
2998
2984
  alias="podAffinityTerm",
2999
- description=("Required. A pod affinity term, associated with the corresponding" " weight."),
2985
+ description=("Required. A pod affinity term, associated with the corresponding weight."),
3000
2986
  ),
3001
2987
  ]
3002
2988
  weight: Annotated[
3003
2989
  int,
3004
- Field(
3005
- description=("weight associated with matching the corresponding podAffinityTerm, in" " the range 1-100.")
3006
- ),
2990
+ Field(description=("weight associated with matching the corresponding podAffinityTerm, in the range 1-100.")),
3007
2991
  ]
3008
2992
 
3009
2993
 
@@ -3013,7 +2997,7 @@ class DownwardAPIVolumeFile(BaseModel):
3013
2997
  Field(
3014
2998
  alias="fieldRef",
3015
2999
  description=(
3016
- "Required: Selects a field of the pod: only annotations, labels, name" " and namespace are supported."
3000
+ "Required: Selects a field of the pod: only annotations, labels, name and namespace are supported."
3017
3001
  ),
3018
3002
  ),
3019
3003
  ] = None
@@ -3279,7 +3263,7 @@ class PersistentVolumeClaimSpec(BaseModel):
3279
3263
  Optional[str],
3280
3264
  Field(
3281
3265
  alias="volumeName",
3282
- description=("volumeName is the binding reference to the PersistentVolume backing" " this claim."),
3266
+ description=("volumeName is the binding reference to the PersistentVolume backing this claim."),
3283
3267
  ),
3284
3268
  ] = None
3285
3269
 
@@ -3442,7 +3426,7 @@ class EnvVar(BaseModel):
3442
3426
  Optional[EnvVarSource],
3443
3427
  Field(
3444
3428
  alias="valueFrom",
3445
- description=("Source for the environment variable's value. Cannot be used if value" " is not empty."),
3429
+ description=("Source for the environment variable's value. Cannot be used if value is not empty."),
3446
3430
  ),
3447
3431
  ] = None
3448
3432
 
@@ -3552,7 +3536,7 @@ class VolumeProjection(BaseModel):
3552
3536
  Optional[ServiceAccountTokenProjection],
3553
3537
  Field(
3554
3538
  alias="serviceAccountToken",
3555
- description=("serviceAccountToken is information about the serviceAccountToken data" " to project"),
3539
+ description=("serviceAccountToken is information about the serviceAccountToken data to project"),
3556
3540
  ),
3557
3541
  ] = None
3558
3542
 
@@ -3593,7 +3577,7 @@ class Container(BaseModel):
3593
3577
  ] = None
3594
3578
  env: Annotated[
3595
3579
  Optional[List[EnvVar]],
3596
- Field(description=("List of environment variables to set in the container. Cannot be" " updated.")),
3580
+ Field(description=("List of environment variables to set in the container. Cannot be updated.")),
3597
3581
  ] = None
3598
3582
  env_from: Annotated[
3599
3583
  Optional[List[EnvFromSource]],
@@ -3795,14 +3779,14 @@ class Container(BaseModel):
3795
3779
  Optional[List[VolumeDevice]],
3796
3780
  Field(
3797
3781
  alias="volumeDevices",
3798
- description=("volumeDevices is the list of block devices to be used by the" " container."),
3782
+ description=("volumeDevices is the list of block devices to be used by the container."),
3799
3783
  ),
3800
3784
  ] = None
3801
3785
  volume_mounts: Annotated[
3802
3786
  Optional[List[VolumeMount]],
3803
3787
  Field(
3804
3788
  alias="volumeMounts",
3805
- description=("Pod volumes to mount into the container's filesystem. Cannot be" " updated."),
3789
+ description=("Pod volumes to mount into the container's filesystem. Cannot be updated."),
3806
3790
  ),
3807
3791
  ] = None
3808
3792
  working_dir: Annotated[
@@ -3856,19 +3840,19 @@ class Volume(BaseModel):
3856
3840
  Optional[AzureDiskVolumeSource],
3857
3841
  Field(
3858
3842
  alias="azureDisk",
3859
- description=("azureDisk represents an Azure Data Disk mount on the host and bind" " mount to the pod."),
3843
+ description=("azureDisk represents an Azure Data Disk mount on the host and bind mount to the pod."),
3860
3844
  ),
3861
3845
  ] = None
3862
3846
  azure_file: Annotated[
3863
3847
  Optional[AzureFileVolumeSource],
3864
3848
  Field(
3865
3849
  alias="azureFile",
3866
- description=("azureFile represents an Azure File Service mount on the host and bind" " mount to the pod."),
3850
+ description=("azureFile represents an Azure File Service mount on the host and bind mount to the pod."),
3867
3851
  ),
3868
3852
  ] = None
3869
3853
  cephfs: Annotated[
3870
3854
  Optional[CephFSVolumeSource],
3871
- Field(description=("cephFS represents a Ceph FS mount on the host that shares a pod's" " lifetime")),
3855
+ Field(description=("cephFS represents a Ceph FS mount on the host that shares a pod's lifetime")),
3872
3856
  ] = None
3873
3857
  cinder: Annotated[
3874
3858
  Optional[CinderVolumeSource],
@@ -3900,7 +3884,7 @@ class Volume(BaseModel):
3900
3884
  Optional[DownwardAPIVolumeSource],
3901
3885
  Field(
3902
3886
  alias="downwardAPI",
3903
- description=("downwardAPI represents downward API about the pod that should populate" " this volume"),
3887
+ description=("downwardAPI represents downward API about the pod that should populate this volume"),
3904
3888
  ),
3905
3889
  ] = None
3906
3890
  empty_dir: Annotated[
@@ -4065,18 +4049,16 @@ class Volume(BaseModel):
4065
4049
  Optional[PortworxVolumeSource],
4066
4050
  Field(
4067
4051
  alias="portworxVolume",
4068
- description=(
4069
- "portworxVolume represents a portworx volume attached and mounted on" " kubelets host machine"
4070
- ),
4052
+ description=("portworxVolume represents a portworx volume attached and mounted on kubelets host machine"),
4071
4053
  ),
4072
4054
  ] = None
4073
4055
  projected: Annotated[
4074
4056
  Optional[ProjectedVolumeSource],
4075
- Field(description=("projected items for all in one resources secrets, configmaps, and" " downward API")),
4057
+ Field(description=("projected items for all in one resources secrets, configmaps, and downward API")),
4076
4058
  ] = None
4077
4059
  quobyte: Annotated[
4078
4060
  Optional[QuobyteVolumeSource],
4079
- Field(description=("quobyte represents a Quobyte mount on the host that shares a pod's" " lifetime")),
4061
+ Field(description=("quobyte represents a Quobyte mount on the host that shares a pod's lifetime")),
4080
4062
  ] = None
4081
4063
  rbd: Annotated[
4082
4064
  Optional[RBDVolumeSource],
@@ -4092,9 +4074,7 @@ class Volume(BaseModel):
4092
4074
  Optional[ScaleIOVolumeSource],
4093
4075
  Field(
4094
4076
  alias="scaleIO",
4095
- description=(
4096
- "scaleIO represents a ScaleIO persistent volume attached and mounted on" " Kubernetes nodes."
4097
- ),
4077
+ description=("scaleIO represents a ScaleIO persistent volume attached and mounted on Kubernetes nodes."),
4098
4078
  ),
4099
4079
  ] = None
4100
4080
  secret: Annotated[
@@ -4108,12 +4088,12 @@ class Volume(BaseModel):
4108
4088
  ] = None
4109
4089
  storageos: Annotated[
4110
4090
  Optional[StorageOSVolumeSource],
4111
- Field(description=("storageOS represents a StorageOS volume attached and mounted on" " Kubernetes nodes.")),
4091
+ Field(description=("storageOS represents a StorageOS volume attached and mounted on Kubernetes nodes.")),
4112
4092
  ] = None
4113
4093
  vsphere_volume: Annotated[
4114
4094
  Optional[VsphereVirtualDiskVolumeSource],
4115
4095
  Field(
4116
4096
  alias="vsphereVolume",
4117
- description=("vsphereVolume represents a vSphere volume attached and mounted on" " kubelets host machine"),
4097
+ description=("vsphereVolume represents a vSphere volume attached and mounted on kubelets host machine"),
4118
4098
  ),
4119
4099
  ] = None
@@ -581,7 +581,7 @@ class ContainerNode(BaseModel):
581
581
  dependencies: Optional[List[str]] = None
582
582
  env: Annotated[
583
583
  Optional[List[v1.EnvVar]],
584
- Field(description=("List of environment variables to set in the container. Cannot be" " updated.")),
584
+ Field(description=("List of environment variables to set in the container. Cannot be updated.")),
585
585
  ] = None
586
586
  env_from: Annotated[
587
587
  Optional[List[v1.EnvFromSource]],
@@ -783,14 +783,14 @@ class ContainerNode(BaseModel):
783
783
  Optional[List[v1.VolumeDevice]],
784
784
  Field(
785
785
  alias="volumeDevices",
786
- description=("volumeDevices is the list of block devices to be used by the" " container."),
786
+ description=("volumeDevices is the list of block devices to be used by the container."),
787
787
  ),
788
788
  ] = None
789
789
  volume_mounts: Annotated[
790
790
  Optional[List[v1.VolumeMount]],
791
791
  Field(
792
792
  alias="volumeMounts",
793
- description=("Pod volumes to mount into the container's filesystem. Cannot be" " updated."),
793
+ description=("Pod volumes to mount into the container's filesystem. Cannot be updated."),
794
794
  ),
795
795
  ] = None
796
796
  working_dir: Annotated[
@@ -854,7 +854,7 @@ class ScriptTemplate(BaseModel):
854
854
  ] = None
855
855
  env: Annotated[
856
856
  Optional[List[v1.EnvVar]],
857
- Field(description=("List of environment variables to set in the container. Cannot be" " updated.")),
857
+ Field(description=("List of environment variables to set in the container. Cannot be updated.")),
858
858
  ] = None
859
859
  env_from: Annotated[
860
860
  Optional[List[v1.EnvFromSource]],
@@ -1057,14 +1057,14 @@ class ScriptTemplate(BaseModel):
1057
1057
  Optional[List[v1.VolumeDevice]],
1058
1058
  Field(
1059
1059
  alias="volumeDevices",
1060
- description=("volumeDevices is the list of block devices to be used by the" " container."),
1060
+ description=("volumeDevices is the list of block devices to be used by the container."),
1061
1061
  ),
1062
1062
  ] = None
1063
1063
  volume_mounts: Annotated[
1064
1064
  Optional[List[v1.VolumeMount]],
1065
1065
  Field(
1066
1066
  alias="volumeMounts",
1067
- description=("Pod volumes to mount into the container's filesystem. Cannot be" " updated."),
1067
+ description=("Pod volumes to mount into the container's filesystem. Cannot be updated."),
1068
1068
  ),
1069
1069
  ] = None
1070
1070
  working_dir: Annotated[
@@ -1116,7 +1116,7 @@ class UserContainer(BaseModel):
1116
1116
  ] = None
1117
1117
  env: Annotated[
1118
1118
  Optional[List[v1.EnvVar]],
1119
- Field(description=("List of environment variables to set in the container. Cannot be" " updated.")),
1119
+ Field(description=("List of environment variables to set in the container. Cannot be updated.")),
1120
1120
  ] = None
1121
1121
  env_from: Annotated[
1122
1122
  Optional[List[v1.EnvFromSource]],
@@ -1319,14 +1319,14 @@ class UserContainer(BaseModel):
1319
1319
  Optional[List[v1.VolumeDevice]],
1320
1320
  Field(
1321
1321
  alias="volumeDevices",
1322
- description=("volumeDevices is the list of block devices to be used by the" " container."),
1322
+ description=("volumeDevices is the list of block devices to be used by the container."),
1323
1323
  ),
1324
1324
  ] = None
1325
1325
  volume_mounts: Annotated[
1326
1326
  Optional[List[v1.VolumeMount]],
1327
1327
  Field(
1328
1328
  alias="volumeMounts",
1329
- description=("Pod volumes to mount into the container's filesystem. Cannot be" " updated."),
1329
+ description=("Pod volumes to mount into the container's filesystem. Cannot be updated."),
1330
1330
  ),
1331
1331
  ] = None
1332
1332
  working_dir: Annotated[
@@ -10,12 +10,15 @@ from typing import Any, Generator, List, Optional, cast
10
10
  import jwt
11
11
  import requests
12
12
  from hera.workflows import CronWorkflow, Workflow
13
- from hera.workflows.models import WorkflowCreateRequest
13
+ from hera.workflows.models import CronWorkflow as CronWorkflowModel
14
+ from hera.workflows.models import UpdateCronWorkflowRequest, WorkflowCreateRequest
14
15
  from pydantic import BaseModel, ValidationError
15
16
 
16
17
  from pipekit_sdk._helpers import is_valid_uuid
17
18
  from pipekit_sdk.models.model import Cluster, Logs, Pipe, PipeRun
18
19
 
20
+ _CONTENT_TYPE_JSON = "application/json"
21
+
19
22
 
20
23
  class _LoginResponse(BaseModel):
21
24
  """LoginResponse is a model for Login responses."""
@@ -106,7 +109,7 @@ class PipekitService:
106
109
  login_response = requests.post(
107
110
  login_url,
108
111
  data=json.dumps(login_request),
109
- headers={"Content-Type": "application/json"},
112
+ headers={"Content-Type": _CONTENT_TYPE_JSON},
110
113
  timeout=10,
111
114
  )
112
115
  if login_response.status_code // 100 != 2:
@@ -159,7 +162,7 @@ class PipekitService:
159
162
  login_response = requests.post(
160
163
  login_url,
161
164
  data=json.dumps(login_request),
162
- headers={"Content-Type": "application/json"},
165
+ headers={"Content-Type": _CONTENT_TYPE_JSON},
163
166
  timeout=10,
164
167
  )
165
168
 
@@ -359,6 +362,14 @@ class PipekitService:
359
362
 
360
363
  return None
361
364
 
365
+ def __resolve_cluster_uuid(self, cluster: str) -> str:
366
+ """Return the cluster UUID for a cluster name, or pass through a UUID unchanged."""
367
+ if is_valid_uuid(cluster):
368
+ return cluster
369
+ if cluster_obj := self.get_cluster_by_name(cluster):
370
+ return cluster_obj.uuid
371
+ raise ValueError(f"No cluster found for {cluster}")
372
+
362
373
  def submit(
363
374
  self,
364
375
  workflow: Workflow,
@@ -366,11 +377,7 @@ class PipekitService:
366
377
  pipe_name: str = "",
367
378
  ) -> PipeRun:
368
379
  """Submit a Workflow to a cluster, under an existing or new pipe with the given name."""
369
- if not is_valid_uuid(cluster):
370
- if cluster_obj := self.get_cluster_by_name(cluster):
371
- cluster = cluster_obj.uuid
372
- else:
373
- raise ValueError(f"No cluster found for {cluster}")
380
+ cluster = self.__resolve_cluster_uuid(cluster)
374
381
 
375
382
  submit_url = f"{self.users_url}/api/users/v1/clusters/{cluster}/workflows"
376
383
  if pipe_name != "":
@@ -389,7 +396,7 @@ class PipekitService:
389
396
  ),
390
397
  headers={
391
398
  "Authorization": f"Bearer {self.access_token}",
392
- "Content-Type": "application/json",
399
+ "Content-Type": _CONTENT_TYPE_JSON,
393
400
  },
394
401
  timeout=10,
395
402
  )
@@ -401,11 +408,7 @@ class PipekitService:
401
408
 
402
409
  def create(self, cron_workflow: CronWorkflow, cluster: str, pipe_name: str = "") -> PipeRun:
403
410
  """Create a CronWorkflow on the cluster, under an existing or new pipe with the given name."""
404
- if not is_valid_uuid(cluster):
405
- if cluster_obj := self.get_cluster_by_name(cluster):
406
- cluster = cluster_obj.uuid
407
- else:
408
- raise ValueError(f"No cluster found for {cluster}")
411
+ cluster = self.__resolve_cluster_uuid(cluster)
409
412
 
410
413
  create_url = f"{self.users_url}/api/users/v1/clusters/{cluster}/cron-workflows"
411
414
  if pipe_name != "":
@@ -424,7 +427,7 @@ class PipekitService:
424
427
  ),
425
428
  headers={
426
429
  "Authorization": f"Bearer {self.access_token}",
427
- "Content-Type": "application/json",
430
+ "Content-Type": _CONTENT_TYPE_JSON,
428
431
  },
429
432
  timeout=10,
430
433
  )
@@ -434,6 +437,129 @@ class PipekitService:
434
437
 
435
438
  return PipeRun.model_validate(create_response.json())
436
439
 
440
+ @staticmethod
441
+ def __require_namespace(namespace: str) -> None:
442
+ # A cron lives in the namespace set on its manifest (the platform default
443
+ # is "argo"). An empty or wrong namespace makes the cron look missing, so
444
+ # we reject an empty namespace here instead of sending a request that
445
+ # returns a confusing "no rows" error.
446
+ if namespace == "":
447
+ raise ValueError(
448
+ "namespace is required and must match the CronWorkflow manifest namespace "
449
+ "(the platform default is 'argo')"
450
+ )
451
+
452
+ def __cron_workflow_url(self, cluster_uuid: str, namespace: str, name: str) -> str:
453
+ return f"{self.users_url}/api/users/v1/clusters/{cluster_uuid}/hera/api/v1/cron-workflows/{namespace}/{name}"
454
+
455
+ def __cron_request(self, method: str, url: str, action: str, data: Optional[str] = None) -> requests.Response:
456
+ # Shared HTTP call for the cron methods: attach auth, send, and turn a
457
+ # non-2xx status into an error. `action` names the operation in the
458
+ # error message (for example "Update").
459
+ headers = {"Authorization": f"Bearer {self.access_token}"}
460
+ if data is not None:
461
+ headers["Content-Type"] = _CONTENT_TYPE_JSON
462
+
463
+ response = requests.request(method, url, data=data, headers=headers, timeout=10)
464
+ if response.status_code // 100 != 2:
465
+ raise RuntimeError(f"{action} cron failed: {response.text}")
466
+
467
+ return response
468
+
469
+ def __cron_by_name(self, cluster: str, namespace: str, name: str) -> str:
470
+ # Validate the namespace, resolve the cluster name to a UUID, and return
471
+ # the cron URL. Shared by the methods that act on a cron by name.
472
+ self.__require_namespace(namespace)
473
+ cluster_uuid = self.__resolve_cluster_uuid(cluster)
474
+ return self.__cron_workflow_url(cluster_uuid, namespace, name)
475
+
476
+ def update_cron(
477
+ self,
478
+ cron_workflow: CronWorkflow,
479
+ cluster: str,
480
+ namespace: str = "",
481
+ ) -> CronWorkflowModel:
482
+ """Update an existing CronWorkflow's spec and return the updated object.
483
+
484
+ The namespace must match the one in the CronWorkflow manifest. When
485
+ namespace is empty, it is taken from the CronWorkflow object.
486
+ """
487
+ if namespace == "":
488
+ namespace = cron_workflow.namespace or ""
489
+
490
+ name = cron_workflow.name
491
+ if not name:
492
+ raise ValueError("CronWorkflow metadata.name is required")
493
+
494
+ url = self.__cron_by_name(cluster, namespace, name)
495
+
496
+ built = cron_workflow.build()
497
+ assert built is not None
498
+ request = UpdateCronWorkflowRequest(
499
+ name=name,
500
+ namespace=namespace,
501
+ cron_workflow=built, # type: ignore
502
+ )
503
+ body = request.json(exclude_none=True, by_alias=True, exclude_unset=True, exclude_defaults=True)
504
+
505
+ response = self.__cron_request("PUT", url, "Update", data=body)
506
+ return CronWorkflowModel.parse_raw(response.text)
507
+
508
+ def delete_cron(
509
+ self,
510
+ cluster: str,
511
+ namespace: str,
512
+ name: str,
513
+ ) -> None:
514
+ """Delete a CronWorkflow by cluster, namespace, and name.
515
+
516
+ The namespace must match the CronWorkflow manifest namespace.
517
+ """
518
+ url = self.__cron_by_name(cluster, namespace, name)
519
+ self.__cron_request("DELETE", url, "Delete")
520
+
521
+ def get_cron(
522
+ self,
523
+ cluster: str,
524
+ namespace: str,
525
+ name: str,
526
+ ) -> CronWorkflowModel:
527
+ """Get a CronWorkflow by cluster, namespace, and name.
528
+
529
+ The namespace must match the CronWorkflow manifest namespace.
530
+ """
531
+ url = self.__cron_by_name(cluster, namespace, name)
532
+ response = self.__cron_request("GET", url, "Get")
533
+ return CronWorkflowModel.parse_raw(response.text)
534
+
535
+ def suspend_cron(
536
+ self,
537
+ cluster: str,
538
+ namespace: str,
539
+ name: str,
540
+ ) -> CronWorkflowModel:
541
+ """Suspend a CronWorkflow so it stops scheduling new runs.
542
+
543
+ The namespace must match the CronWorkflow manifest namespace.
544
+ """
545
+ url = self.__cron_by_name(cluster, namespace, name) + "/suspend"
546
+ response = self.__cron_request("PUT", url, "Suspend")
547
+ return CronWorkflowModel.parse_raw(response.text)
548
+
549
+ def resume_cron(
550
+ self,
551
+ cluster: str,
552
+ namespace: str,
553
+ name: str,
554
+ ) -> CronWorkflowModel:
555
+ """Resume a suspended CronWorkflow so it schedules runs again.
556
+
557
+ The namespace must match the CronWorkflow manifest namespace.
558
+ """
559
+ url = self.__cron_by_name(cluster, namespace, name) + "/resume"
560
+ response = self.__cron_request("PUT", url, "Resume")
561
+ return CronWorkflowModel.parse_raw(response.text)
562
+
437
563
  def __apply_run_action(
438
564
  self,
439
565
  run_uuid: str,
@@ -444,7 +570,7 @@ class PipekitService:
444
570
  action_url,
445
571
  headers={
446
572
  "Authorization": f"Bearer {self.access_token}",
447
- "Content-Type": "application/json",
573
+ "Content-Type": _CONTENT_TYPE_JSON,
448
574
  },
449
575
  timeout=10,
450
576
  )
@@ -478,13 +604,16 @@ class PipekitService:
478
604
  def __get_run_container_logs(
479
605
  self,
480
606
  run_uuid: str,
481
- pod_name: str = "",
482
- container_name: str = "",
607
+ pod_name: List[str] | str = [],
608
+ container_name: List[str] | str = [],
483
609
  ) -> list[Logs]:
484
- logs_url = f"{self.users_url}/api/users/v1/runs/{run_uuid}/container_logs?pod-name={pod_name}&container-name={container_name}"
610
+ logs_url = f"{self.users_url}/api/users/v1/runs/{run_uuid}/container_logs"
485
611
 
612
+ # empty "" / [] must be omitted, not sent as ?pod-name= (the API filters to a pod named "")
613
+ params = {k: v for k, v in {"pod-name": pod_name, "container-name": container_name}.items() if v}
486
614
  logs_response = requests.get(
487
615
  logs_url,
616
+ params=params,
488
617
  headers={"Authorization": f"Bearer {self.access_token}"},
489
618
  timeout=10,
490
619
  )
@@ -497,14 +626,17 @@ class PipekitService:
497
626
  def __get_logs_stream(
498
627
  self,
499
628
  run_uuid: str,
500
- pod_name: str = "",
501
- container_name: str = "",
629
+ pod_name: List[str] | str = "",
630
+ container_name: List[str] | str = "",
502
631
  last_event_id: str = "",
503
632
  ) -> requests.Response:
504
- logs_url = f"{self.users_url}/api/users/v1/runs/{run_uuid}/container_logs_stream?pod-name={pod_name}&container-name={container_name}"
633
+ logs_url = f"{self.users_url}/api/users/v1/runs/{run_uuid}/container_logs_stream"
505
634
 
635
+ # empty "" / [] must be omitted, not sent as ?pod-name= (the API filters to a pod named "")
636
+ params = {k: v for k, v in {"pod-name": pod_name, "container-name": container_name}.items() if v}
506
637
  logs_response = requests.get(
507
638
  logs_url,
639
+ params=params,
508
640
  stream=True,
509
641
  timeout=None, # nosec B113
510
642
  headers={
@@ -523,8 +655,8 @@ class PipekitService:
523
655
  def get_logs(
524
656
  self,
525
657
  run_uuid: str,
526
- pod_name: str = "",
527
- container_name: str = "",
658
+ pod_name: List[str] | str = "",
659
+ container_name: List[str] | str = "",
528
660
  ) -> list[Logs]:
529
661
  """Get logs for a given run."""
530
662
  return self.__get_run_container_logs(run_uuid, pod_name, container_name)
@@ -532,8 +664,8 @@ class PipekitService:
532
664
  def follow_logs(
533
665
  self,
534
666
  run_uuid: str,
535
- pod_name: str = "",
536
- container_name: str = "",
667
+ pod_name: List[str] | str = "",
668
+ container_name: List[str] | str = "",
537
669
  ) -> Generator[Logs, Any, None]:
538
670
  """Return a generator that yields pod logs for a given run."""
539
671
  data_msg_prefix = "data: "
@@ -579,14 +711,15 @@ class PipekitService:
579
711
  def print_logs(
580
712
  self,
581
713
  run_uuid: str,
582
- pod_name: str = "",
583
- container_name: str = "",
714
+ pod_name: List[str] | str = "",
715
+ container_name: List[str] | str = "",
584
716
  follow: bool = True,
585
717
  ) -> None:
586
718
  """Print pod logs for a given run."""
587
719
  if follow:
588
720
  for log in self.follow_logs(run_uuid, pod_name=pod_name, container_name=container_name):
589
721
  self.__print_log_message(log)
722
+ return
590
723
 
591
724
  for log in self.get_logs(run_uuid, pod_name=pod_name, container_name=container_name):
592
725
  self.__print_log_message(log)
File without changes