zosmfrestapi 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,49 @@
1
+ import logging
2
+ from . import ZosmfBase
3
+ from dacite import from_dict
4
+ from zosmfrestapi.utils import remap
5
+ from zosmfrestapi.models.topology import Group, System
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+
10
+ class TopologyServices(ZosmfBase):
11
+ """Read groups and systems from the z/OSMF topology REST API."""
12
+
13
+ def get_groups(self) -> dict[str, Group]:
14
+ """Return z/OSMF groups keyed by each group's name.
15
+
16
+ Returns:
17
+ A mapping from group name to its parsed :class:`Group` model.
18
+
19
+ Raises:
20
+ ZosmfAuthError: If z/OSMF returns HTTP 401.
21
+ ZosmfError: If z/OSMF returns another unsuccessful HTTP status.
22
+ httpx2.RequestError: If the request fails before an HTTP response
23
+ is received.
24
+ """
25
+ resp = self.http.get("/zosmf/resttopology/groups")
26
+ self._raise_for_status(resp)
27
+ return {
28
+ item["name"]: from_dict(Group, data=remap(item, Group))
29
+ for item in resp.json()["items"]
30
+ }
31
+
32
+ def get_systems(self) -> dict[str, System]:
33
+ """Return z/OSMF systems keyed by each system's topology name.
34
+
35
+ Returns:
36
+ A mapping from the system name to its parsed :class:`System` model.
37
+
38
+ Raises:
39
+ ZosmfAuthError: If z/OSMF returns HTTP 401.
40
+ ZosmfError: If z/OSMF returns another unsuccessful HTTP status.
41
+ httpx2.RequestError: If the request fails before an HTTP response
42
+ is received.
43
+ """
44
+ resp = self.http.get("/zosmf/resttopology/systems")
45
+ self._raise_for_status(resp)
46
+ return {
47
+ item["systemName"]: from_dict(System, data=remap(item, System))
48
+ for item in resp.json()["items"]
49
+ }
@@ -0,0 +1,350 @@
1
+ import time
2
+ from typing import Any, Iterator, Literal, overload
3
+
4
+ from dacite import from_dict
5
+
6
+ from zosmfrestapi.exceptions.workflow import (
7
+ ZosmfWorkflowBaseError,
8
+ ZosmfWorkflowError,
9
+ )
10
+ from zosmfrestapi.models.workflow import (
11
+ WorkflowArchivedListItem,
12
+ WorkflowInstance,
13
+ WorkflowItem,
14
+ WorkflowListItem,
15
+ )
16
+ from zosmfrestapi.utils import remap, to_dict
17
+
18
+ from . import ZosmfBase
19
+
20
+
21
+ class WorkflowServices(ZosmfBase):
22
+ """List, inspect, create, and control z/OSMF workflow instances.
23
+
24
+ Workflow records are converted to the dataclasses in
25
+ :mod:`zosmfrestapi.models.workflow`. Methods that wait for a workflow return
26
+ an iterator so callers can observe each status update while polling.
27
+ """
28
+
29
+ def _get_workflow(self, workflow_id: str, steps: bool, variables: bool) -> dict[str, Any]:
30
+ """Fetch the raw workflow response, optionally including steps/variables.
31
+
32
+ This internal helper returns the JSON object before model conversion.
33
+ """
34
+ resp = self.http.get(
35
+ f"/zosmf/workflow/rest/1.0/workflows/{workflow_id}",
36
+ params={
37
+ "returnData": (
38
+ "steps,variables"
39
+ if all([steps, variables])
40
+ else "steps" if steps else "variables" if variables else ""
41
+ )
42
+ },
43
+ )
44
+ self._raise_for_status(resp, exc_handler=ZosmfWorkflowError)
45
+ return resp.json()
46
+
47
+ def get_workflow(
48
+ self, workflow_id: str, steps: bool = False, variables: bool = False
49
+ ) -> WorkflowInstance:
50
+ """Retrieve one workflow instance by its workflow key.
51
+
52
+ Args:
53
+ workflow_id: UUID/key identifying the workflow instance.
54
+ steps: Include detailed workflow step data in the response.
55
+ variables: Include workflow variables in the response.
56
+
57
+ Returns:
58
+ The parsed workflow state, including optional step and variable
59
+ data when requested.
60
+
61
+ Raises:
62
+ ZosmfAuthError: If z/OSMF returns HTTP 401.
63
+ ZosmfWorkflowError: If z/OSMF returns another unsuccessful HTTP
64
+ status.
65
+ httpx2.RequestError: If the request fails before an HTTP response
66
+ is received.
67
+ """
68
+ return from_dict(
69
+ WorkflowInstance,
70
+ data=remap(
71
+ self._get_workflow(workflow_id, steps=steps, variables=variables),
72
+ WorkflowInstance,
73
+ ),
74
+ )
75
+
76
+ def get_workflows(
77
+ self,
78
+ name: str | None = None,
79
+ category: str | None = None,
80
+ system: str | None = None,
81
+ status: str | None = None,
82
+ owner: str | None = None,
83
+ vendor: str | None = None,
84
+ ) -> list[WorkflowListItem]:
85
+ """List workflow instances matching the supplied filters.
86
+
87
+ Filters are combined in one z/OSMF request. ``name`` may be a regular
88
+ expression supported by the z/OSMF workflow API.
89
+
90
+ Args:
91
+ name: Optional workflow name or supported regular expression.
92
+ category: Optional workflow category.
93
+ system: Optional system nickname.
94
+ status: Optional status. Supported values are ``"in-progress"``,
95
+ ``"complete"``, ``"automation-in-progress"``, and
96
+ ``"canceled"``.
97
+ owner: Optional workflow owner.
98
+ vendor: Optional workflow vendor.
99
+
100
+ Returns:
101
+ Parsed workflow list items matching all supplied filters.
102
+
103
+ Raises:
104
+ ZosmfWorkflowBaseError: If ``status`` is not one of the supported
105
+ values.
106
+ ZosmfAuthError: If z/OSMF returns HTTP 401.
107
+ ZosmfWorkflowError: If z/OSMF returns another unsuccessful HTTP
108
+ status.
109
+ httpx2.RequestError: If the request fails before an HTTP response
110
+ is received.
111
+ """
112
+ if status and status not in [
113
+ "in-progress",
114
+ "complete",
115
+ "automation-in-progress",
116
+ "canceled",
117
+ ]:
118
+ raise ZosmfWorkflowBaseError(
119
+ f"Invalid status '{status}'. Valid options are: 'in-progress'"
120
+ ", 'complete', 'automation-in-progress', 'canceled'.",
121
+ )
122
+ resp = self.http.get(
123
+ "/zosmf/workflow/rest/1.0/workflows",
124
+ params={
125
+ "workflowName": name,
126
+ "category": category,
127
+ "system": system,
128
+ "statusName": status,
129
+ "owner": owner,
130
+ "vendor": vendor,
131
+ },
132
+ )
133
+ self._raise_for_status(resp, exc_handler=ZosmfWorkflowError)
134
+ return [
135
+ from_dict(
136
+ WorkflowListItem,
137
+ data=remap(workflow, WorkflowListItem),
138
+ )
139
+ for workflow in resp.json()["workflows"]
140
+ ]
141
+
142
+ def get_archived_workflows(
143
+ self,
144
+ ) -> list[WorkflowArchivedListItem]:
145
+ """List archived workflow instances.
146
+
147
+ Returns:
148
+ Parsed archived workflow list items.
149
+
150
+ Raises:
151
+ ZosmfAuthError: If z/OSMF returns HTTP 401.
152
+ ZosmfWorkflowError: If z/OSMF returns another unsuccessful HTTP
153
+ status.
154
+ httpx2.RequestError: If the request fails before an HTTP response
155
+ is received.
156
+ """
157
+ resp = self.http.get("/zosmf/workflow/rest/1.0/archivedworkflows")
158
+ self._raise_for_status(resp, exc_handler=ZosmfWorkflowError)
159
+ return [
160
+ from_dict(
161
+ WorkflowArchivedListItem,
162
+ data=remap(archived_workflow, WorkflowArchivedListItem),
163
+ )
164
+ for archived_workflow in resp.json()["archivedWorkflows"]
165
+ ]
166
+
167
+ def create_workflow(self, workflow: WorkflowItem) -> WorkflowInstance:
168
+ """Create a workflow instance from a workflow definition.
169
+
170
+ The supplied :class:`WorkflowItem` is serialized to the request body.
171
+ After creation, the method retrieves and returns the new instance using
172
+ the workflow key returned by z/OSMF.
173
+
174
+ Args:
175
+ workflow: Definition, system, owner, and optional variables/settings
176
+ used to create the instance.
177
+
178
+ Returns:
179
+ The newly created and parsed workflow instance.
180
+
181
+ Raises:
182
+ ZosmfAuthError: If z/OSMF returns HTTP 401.
183
+ ZosmfWorkflowError: If creation or retrieval returns another
184
+ unsuccessful HTTP status.
185
+ httpx2.RequestError: If either request fails before an HTTP response
186
+ is received.
187
+ """
188
+ resp = self.http.post("/zosmf/workflow/rest/1.0/workflows", json=to_dict(workflow))
189
+ self._raise_for_status(resp, exc_handler=ZosmfWorkflowError)
190
+ return self.get_workflow(resp.json()["workflowKey"])
191
+
192
+ @overload
193
+ def start_workflow(
194
+ self, workflow_id: str, wait: Literal[False] = False
195
+ ) -> Literal[True]: ...
196
+
197
+ @overload
198
+ def start_workflow(
199
+ self, workflow_id: str, wait: Literal[True]
200
+ ) -> Iterator[WorkflowInstance]: ...
201
+
202
+ @overload
203
+ def start_workflow(
204
+ self, workflow_id: str, wait: bool
205
+ ) -> Literal[True] | Iterator[WorkflowInstance]: ...
206
+
207
+ def start_workflow(
208
+ self, workflow_id: str, wait: bool = False
209
+ ) -> Literal[True] | Iterator[WorkflowInstance]:
210
+ """Start a workflow instance.
211
+
212
+ When ``wait`` is false, this returns as soon as z/OSMF accepts the
213
+ request. When true, it returns a lazy iterator that polls the workflow
214
+ as the caller consumes it.
215
+
216
+ Args:
217
+ workflow_id: UUID of the workflow to start.
218
+ wait: Whether to wait for the workflow to complete.
219
+
220
+ Returns:
221
+ ``True`` when ``wait`` is false; otherwise an iterator yielding
222
+ :class:`WorkflowInstance` snapshots until the instance is complete
223
+ or canceled.
224
+
225
+ Raises:
226
+ ZosmfAuthError: If z/OSMF returns HTTP 401.
227
+ ZosmfWorkflowError: If the start request or a subsequent poll
228
+ returns another unsuccessful HTTP status.
229
+ httpx2.RequestError: If a request fails before an HTTP response is
230
+ received.
231
+ """
232
+ resp = self.http.put(
233
+ f"/zosmf/workflow/rest/1.0/workflows/{workflow_id}/operations/start",
234
+ )
235
+ self._raise_for_status(resp, exc_handler=ZosmfWorkflowError)
236
+ if not wait:
237
+ return True
238
+ return self.wait_for_workflow(workflow_id)
239
+
240
+ def wait_for_workflow(self, workflow_id: str) -> Iterator[WorkflowInstance]:
241
+ """Yield workflow snapshots until the instance reaches a terminal state.
242
+
243
+ The workflow is polled every 0.5 seconds. Iteration stops after yielding
244
+ a snapshot whose status is ``"complete"`` or ``"canceled"``.
245
+
246
+ Args:
247
+ workflow_id: UUID of the workflow to wait for.
248
+
249
+ Returns:
250
+ An iterator of parsed workflow snapshots, including the terminal
251
+ snapshot.
252
+
253
+ Raises:
254
+ ZosmfAuthError: If a poll returns HTTP 401.
255
+ ZosmfWorkflowError: If a poll returns another unsuccessful HTTP
256
+ status.
257
+ httpx2.RequestError: If a poll fails before an HTTP response is
258
+ received.
259
+ """
260
+ while True:
261
+ workflow = self.get_workflow(workflow_id)
262
+ yield workflow
263
+ if workflow.status in ("complete", "canceled"):
264
+ break
265
+ time.sleep(0.5)
266
+
267
+ def cancel_workflow(self, workflow_id: str) -> None:
268
+ """Request cancellation of a workflow instance.
269
+
270
+ Args:
271
+ workflow_id: UUID of the workflow to cancel.
272
+
273
+ Returns:
274
+ ``None`` after z/OSMF accepts the cancellation request. This method
275
+ does not wait for cancellation to finish.
276
+
277
+ Raises:
278
+ ZosmfAuthError: If z/OSMF returns HTTP 401.
279
+ ZosmfWorkflowError: If z/OSMF returns another unsuccessful HTTP
280
+ status.
281
+ httpx2.RequestError: If the request fails before an HTTP response
282
+ is received.
283
+ """
284
+ resp = self.http.put(
285
+ f"/zosmf/workflow/rest/1.0/workflows/{workflow_id}/operations/cancel",
286
+ )
287
+ self._raise_for_status(resp, exc_handler=ZosmfWorkflowError)
288
+
289
+ def archive_workflow(self, workflow_id: str) -> None:
290
+ """Archive a workflow instance.
291
+
292
+ Args:
293
+ workflow_id: UUID of the workflow to archive.
294
+
295
+ Returns:
296
+ ``None`` after z/OSMF accepts the archive request.
297
+
298
+ Raises:
299
+ ZosmfAuthError: If z/OSMF returns HTTP 401.
300
+ ZosmfWorkflowError: If z/OSMF returns another unsuccessful HTTP
301
+ status.
302
+ httpx2.RequestError: If the request fails before an HTTP response
303
+ is received.
304
+ """
305
+ resp = self.http.post(
306
+ f"/zosmf/workflow/rest/1.0/workflows/{workflow_id}/operations/archive",
307
+ )
308
+ self._raise_for_status(resp, exc_handler=ZosmfWorkflowError)
309
+
310
+ def remove_workflow(self, workflow_id: str) -> None:
311
+ """Delete an active workflow instance.
312
+
313
+ Args:
314
+ workflow_id: Identifier of the workflow to delete.
315
+
316
+ Returns:
317
+ ``None`` after z/OSMF accepts the delete request.
318
+
319
+ Raises:
320
+ ZosmfAuthError: If z/OSMF returns HTTP 401.
321
+ ZosmfWorkflowError: If z/OSMF returns another unsuccessful HTTP
322
+ status.
323
+ httpx2.RequestError: If the request fails before an HTTP response
324
+ is received.
325
+ """
326
+ resp = self.http.delete(
327
+ f"/zosmf/workflow/rest/1.0/workflows/{workflow_id}",
328
+ )
329
+ self._raise_for_status(resp, exc_handler=ZosmfWorkflowError)
330
+
331
+ def remove_archived_workflow(self, workflow_id: str) -> None:
332
+ """Delete an archived workflow instance.
333
+
334
+ Args:
335
+ workflow_id: Identifier of the archived workflow to delete.
336
+
337
+ Returns:
338
+ ``None`` after z/OSMF accepts the delete request.
339
+
340
+ Raises:
341
+ ZosmfAuthError: If z/OSMF returns HTTP 401.
342
+ ZosmfWorkflowError: If z/OSMF returns another unsuccessful HTTP
343
+ status.
344
+ httpx2.RequestError: If the request fails before an HTTP response
345
+ is received.
346
+ """
347
+ resp = self.http.delete(
348
+ f"/zosmf/workflow/rest/1.0/archivedworkflows/{workflow_id}",
349
+ )
350
+ self._raise_for_status(resp, exc_handler=ZosmfWorkflowError)
@@ -0,0 +1,11 @@
1
+ # flake8: noqa
2
+
3
+ from zosmfrestapi.testing.authentication import *
4
+ from zosmfrestapi.testing.software import *
5
+ from zosmfrestapi.testing.software_updates import *
6
+ from zosmfrestapi.testing.topology import *
7
+ from zosmfrestapi.testing.workflows import *
8
+
9
+ __all__ = [
10
+ name for name in globals() if name.startswith("make_")
11
+ ] # pyright: ignore[reportUnsupportedDunderAll]
@@ -0,0 +1,48 @@
1
+ from unittest.mock import ANY
2
+
3
+ from zosmfrestapi.models.authentication import LoginOk
4
+
5
+ ltpa_token = "x2q29998U26AwfYy4vc0GlK5zItaZkkXEbGQ/...<REDACTED>.../NiM1t3FQpvWRbzTsmyr2"
6
+
7
+ jwt_token = (
8
+ "eyJraWQiOiJBTk9OWU1JWkVEX0tJRCIsInR5cCI6IkpXVCIsImFsZyI6IlJTMjU2In0.eyJ0b2tlbl90"
9
+ "eXBlIjoiQmVhcmVyIiwic3ViIjoiPFVTRVI+IiwidXBuIjoiPFVTRVI+IiwiZ3JvdXBzIjpbIkdST1VQX"
10
+ "0EiLCJHUk9VUF9CIiwiR1JPVVBfQyJdLCJyZWFsbSI6IjxSRUFMTT4iLCJpc3MiOiI8SVNTVUVSPiIsImV"
11
+ "4cCI6MTIzNDU2Nzg5MCwiaWF0IjoxMjM0NTYwMDAwfQ.<SIGNATURE_REDACTED>"
12
+ )
13
+
14
+
15
+ def make_loginok():
16
+ return LoginOk(
17
+ returnCode=0,
18
+ reasonCode=0,
19
+ message="Success.",
20
+ ltpa_token=ANY,
21
+ jwt_token=ANY,
22
+ )
23
+
24
+
25
+ def make_loginok_ltpa():
26
+ return LoginOk(
27
+ returnCode=0,
28
+ reasonCode=0,
29
+ message="Success.",
30
+ ltpa_token=ltpa_token,
31
+ jwt_token=ANY,
32
+ )
33
+
34
+
35
+ def make_loginok_jwt():
36
+ return LoginOk(
37
+ returnCode=0, reasonCode=0, message="Success.", ltpa_token=None, jwt_token=jwt_token
38
+ )
39
+
40
+
41
+ def make_loginok_ltpa_jwt():
42
+ return LoginOk(
43
+ returnCode=0,
44
+ reasonCode=0,
45
+ message="Success.",
46
+ ltpa_token=ltpa_token,
47
+ jwt_token=jwt_token,
48
+ )
@@ -0,0 +1,9 @@
1
+ # tests/helpers.py
2
+
3
+ import pendulum
4
+
5
+
6
+ def pdt(value: str) -> pendulum.DateTime:
7
+ result = pendulum.parse(value)
8
+ assert isinstance(result, pendulum.DateTime)
9
+ return result