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,114 @@
1
+ from dataclasses import dataclass, field
2
+ from typing import Any
3
+
4
+
5
+ @dataclass
6
+ class WorkflowAutomationStatus:
7
+ """Automation progress and current-step details for a workflow."""
8
+
9
+ startUser: str | None = None
10
+ startedTime: int | str | None = None
11
+ stoppedTime: int | str | None = None
12
+ currentStepName: str | None = None
13
+ currentStepNumber: str | None = None
14
+ currentStepTitle: str | None = None
15
+ messageID: str | None = None
16
+ messageText: str | None = None
17
+
18
+
19
+ @dataclass
20
+ class WorkflowInstance:
21
+ """Current state and optional detailed data for a workflow instance."""
22
+
23
+ name: str = field(metadata={"json_name": "workflowName"})
24
+ owner: str
25
+ system: str
26
+ scope: str = ""
27
+ percentComplete: int | None = None
28
+ workflowDefinitionFileMD5Value: str | None = None
29
+ workflowArchiveSAFID: str | None = None
30
+ jobStatement: str | list[str] | None = None
31
+ globalVariableGroup: str | None = None
32
+ jobsOutputDirectory: str | None = None
33
+ status: str | None = field(default="", metadata={"json_name": "statusName"})
34
+ vendor: str | None = None
35
+ deleteCompletedJobs: bool | None = None
36
+ autoDeleteOnCompletion: bool | None = None
37
+ isInstanceVariableWithoutPrefix: bool | None = None
38
+ workflowVersion: str | None = None
39
+ steps: list[dict[str, Any]] = field(default_factory=list)
40
+ access: str | None = None
41
+ productVersion: str | None = None
42
+ productID: str | None = None
43
+ variables: list[dict[str, Any]] = field(default_factory=list)
44
+ category: str | None = None
45
+ workflowDescription: str | None = None
46
+ workflowID: str | None = None
47
+ accountInfo: str | None = None
48
+ automationStatus: WorkflowAutomationStatus | None = None
49
+ containsParallelSteps: bool | None = None
50
+ isCallable: bool | None = None
51
+ workflowKey: str | None = None
52
+ productName: str | None = None
53
+
54
+
55
+ @dataclass
56
+ class WorkflowListItem:
57
+ """Summary record returned when listing active workflow instances."""
58
+
59
+ instanceURI: str | None = None
60
+ owner: str | None = None
61
+ vendor: str | None = None
62
+ workflowDefinitionFileMD5Value: str | None = None
63
+ workflowDescription: str | None = None
64
+ workflowID: str | None = None
65
+ workflowKey: str | None = None
66
+ workflowName: str | None = None
67
+ workflowVersion: str | None = None
68
+
69
+
70
+ @dataclass
71
+ class WorkflowArchivedListItem:
72
+ """Summary record returned when listing archived workflow instances."""
73
+
74
+ archivedInstanceURI: str | None = None
75
+ workflowKey: str | None = None
76
+ workflowName: str | None = None
77
+
78
+
79
+ @dataclass
80
+ class WorkflowVariableItem:
81
+ """Name and value of a workflow input variable."""
82
+
83
+ name: str | None = None
84
+ value: str | None = None
85
+
86
+
87
+ @dataclass
88
+ class WorkflowItem:
89
+ """Request data used to create a z/OSMF workflow instance."""
90
+
91
+ name: str = field(metadata={"json_name": "workflowName"})
92
+ definition: str = field(metadata={"json_name": "workflowDefinitionFile"})
93
+ system: str
94
+ owner: str
95
+ system_definition: str | None = field(
96
+ default=None,
97
+ metadata={
98
+ "json_name": "workflowDefinitionFileSystem",
99
+ },
100
+ )
101
+ variableInputFile: str | None = None
102
+ variables: list[WorkflowVariableItem] = field(default_factory=list)
103
+ resolveGlobalConflictByUsing: str | None = None
104
+ workflowArchiveSAFID: str | None = None
105
+ comments: str | None = None
106
+ assignToOwner: bool | None = None
107
+ accessType: str | None = None
108
+ accountInfo: str | None = None
109
+ jobStatement: str | list[str] | None = None
110
+ deleteCompletedJobs: bool | None = None
111
+ autoDeleteOnCompletion: bool | None = None
112
+ jobsOutputDirectory: str | None = None
113
+ targetSystemuid: str | None = None
114
+ targetSystempwd: str | None = field(default=None, repr=False)
zosmfrestapi/py.typed ADDED
File without changes
@@ -0,0 +1,176 @@
1
+ import logging
2
+ import ssl
3
+ from types import TracebackType
4
+ from typing import Literal, Self
5
+
6
+ import httpx2
7
+ from zosmfrestapi.exceptions import ZosmfAuthError, ZosmfError
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ class ZosmfBase:
13
+ """Shared HTTP client and request handling for the service mixins.
14
+
15
+ :class:`ZosmfClient` inherits the operations provided by the service mixins;
16
+ this base class configures their shared ``httpx2.Client`` and manages its
17
+ lifetime. Use a client as a context manager when possible so the connection
18
+ pool is closed deterministically.
19
+ """
20
+
21
+ def __init__(
22
+ self,
23
+ server: str,
24
+ username: str | None = None,
25
+ password: str | None = None,
26
+ cert: str | tuple[str, str] | None = None,
27
+ key_password: str | None = None,
28
+ ltpa_token: str | None = None,
29
+ jwt_token: str | None = None,
30
+ jwt_cookie_name: str = "jwtToken",
31
+ proxy: str | None = None,
32
+ proxyusername: str = "",
33
+ proxypassword: str = "",
34
+ insecure: bool = False,
35
+ cafile: None | str = None,
36
+ encoding: str | None = None,
37
+ ) -> None:
38
+ """Create an HTTP client for communicating with z/OSMF.
39
+
40
+ Args:
41
+ server: Base URL of the z/OSMF server, including its scheme and
42
+ optional port or base path.
43
+ username: Optional user ID for HTTP basic authentication. Basic
44
+ authentication is configured only when both ``username`` and
45
+ ``password`` are non-empty.
46
+ password: Password paired with ``username`` for basic
47
+ authentication.
48
+ cert: Optional client certificate configuration. Pass a path to a
49
+ PEM file containing both the certificate and private key, or
50
+ a ``(certificate_path, private_key_path)`` tuple for separate
51
+ files.
52
+ key_password: Optional private key password. Pass the private key
53
+ password if the key is password protected.
54
+ ltpa_token: Optional LTPA token sent as the ``LtpaToken2`` cookie.
55
+ jwt_token: Optional JWT sent as the cookie named by
56
+ ``jwt_cookie_name``. If both token types are provided, both
57
+ cookies are sent.
58
+ jwt_cookie_name: Cookie name used for ``jwt_token``. Defaults to
59
+ ``"jwtToken"``.
60
+ proxy: Optional proxy URL used for HTTP requests.
61
+ proxyusername: Optional proxy user ID. Used with
62
+ ``proxypassword`` when proxy authentication is configured.
63
+ proxypassword: Optional proxy password.
64
+ insecure: Disable TLS certificate verification when true. This
65
+ cannot be combined with ``cert`` or ``cafile``.
66
+ cafile: Optional CA certificate file added to the default trust
67
+ store. It can be combined with ``cert``.
68
+ encoding: Reserved compatibility parameter; currently unused.
69
+
70
+ Raises:
71
+ ZosmfError: If ``insecure`` is combined with ``cert`` or ``cafile``.
72
+ """
73
+ if insecure and (cafile or cert):
74
+ raise ZosmfError("Cannot set insecure mode when cafile or cert is provided.")
75
+
76
+ verify: bool | ssl.SSLContext
77
+ if insecure:
78
+ logger.debug("Insecure mode enabled, TLS certificate verification is disabled.")
79
+ verify = False
80
+ else:
81
+ ctx = ssl.create_default_context()
82
+ if isinstance(cert, tuple):
83
+ certfile, keyfile = cert
84
+ ctx.load_cert_chain(certfile=certfile, keyfile=keyfile, password=key_password)
85
+ elif cert:
86
+ ctx.load_cert_chain(certfile=cert)
87
+ if cafile:
88
+ ctx.load_verify_locations(cafile=cafile)
89
+ verify = ctx # <------ linea 88
90
+
91
+ proxy_config: str | httpx2.Proxy | None = proxy
92
+ if proxy:
93
+ proxy_config = httpx2.Proxy(proxy)
94
+ proxy_config.auth = (
95
+ (proxyusername, proxypassword) if proxyusername or proxypassword else None
96
+ )
97
+ auth = (username, password) if username and password else None
98
+ headers = {}
99
+ if ltpa_token:
100
+ headers = {"Cookie": f"LtpaToken2={ltpa_token}"}
101
+ if jwt_token:
102
+ headers = {"Cookie": f"{jwt_cookie_name}={jwt_token}"}
103
+ if ltpa_token and jwt_token:
104
+ headers = {"Cookie": f"LtpaToken2={ltpa_token}; {jwt_cookie_name}={jwt_token}"}
105
+ self.http = httpx2.Client(
106
+ base_url=server,
107
+ proxy=proxy_config,
108
+ verify=verify,
109
+ auth=auth,
110
+ headers=headers,
111
+ )
112
+
113
+ self.base_url = server
114
+ self.username = username
115
+ self.password = password
116
+ self.insecure = insecure
117
+ self.cert = cert
118
+ self.cafile = cafile
119
+ self.ltpa_token = ltpa_token
120
+ self.jwt_token = jwt_token
121
+ self.jwt_cookie_name = jwt_cookie_name
122
+ self.proxy = proxy_config
123
+ self.proxyusername = proxyusername
124
+ self.proxypassword = proxypassword
125
+
126
+ def _raise_for_status(
127
+ self,
128
+ response: httpx2.Response,
129
+ exc_handler: type[ZosmfError] = ZosmfError,
130
+ ) -> None:
131
+ """Raise a package exception for an unsuccessful HTTP response.
132
+
133
+ HTTP 401 responses are translated to :class:`ZosmfAuthError`; other
134
+ HTTP status errors are wrapped using ``exc_handler``.
135
+
136
+ Args:
137
+ response: Response returned by the shared HTTP client.
138
+ exc_handler: Exception class used for non-authentication status
139
+ errors. Defaults to :class:`ZosmfError`.
140
+
141
+ Raises:
142
+ ZosmfAuthError: If the response status is 401.
143
+ ZosmfError: Or ``exc_handler`` for another unsuccessful status.
144
+ """
145
+ try:
146
+ response.raise_for_status()
147
+ except httpx2.HTTPStatusError as exc:
148
+ if exc.response.status_code == 401:
149
+ raise ZosmfAuthError(exc) from exc
150
+ raise exc_handler(exc) from exc
151
+
152
+ def close(self) -> None:
153
+ """Close the shared HTTP client and release its connection resources.
154
+
155
+ Call this when the client is no longer needed. Exiting a ``with`` block
156
+ also calls this method.
157
+ """
158
+ self.http.close()
159
+
160
+ def __enter__(self) -> Self:
161
+ """Return this client for use in a ``with`` statement."""
162
+ return self
163
+
164
+ def __exit__(
165
+ self,
166
+ exception_type: type[BaseException] | None,
167
+ exception: BaseException | None,
168
+ traceback: TracebackType | None,
169
+ ) -> Literal[False]:
170
+ """Close the HTTP client without suppressing an exception from the block.
171
+
172
+ The exception arguments follow the context-manager protocol; they are
173
+ not inspected by this implementation.
174
+ """
175
+ self.close()
176
+ return False
@@ -0,0 +1,52 @@
1
+ import logging
2
+
3
+ from zosmfrestapi.exceptions import ZosmfAuthError
4
+ from zosmfrestapi.exceptions.authentication import ZosmfLoginError
5
+ from zosmfrestapi.models.authentication import LoginOk
6
+
7
+ from . import ZosmfBase
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ class AuthenticationServices(ZosmfBase):
13
+ """Authentication operations for the shared z/OSMF client.
14
+
15
+ The configured credentials and tokens are sent by the shared HTTP client.
16
+ The client certificate, basic authentication, and token cookies are all
17
+ configured during :class:`ZosmfBase` initialization.
18
+ """
19
+
20
+ def authenticate(self) -> LoginOk | None:
21
+ """Authenticate against the z/OSMF authentication endpoint.
22
+
23
+ This method posts to ``/zosmf/services/authenticate`` using the
24
+ authentication configured when the client was created. It does not
25
+ perform a separate credential negotiation or clear the configured
26
+ token cookies.
27
+
28
+ Returns:
29
+ A ``LoginOk`` containing the response return/reason codes and
30
+ message, plus any LTPA or JWT tokens returned as response cookies.
31
+
32
+ Raises:
33
+ ZosmfLoginError: If z/OSMF responds with HTTP 401.
34
+ ZosmfAuthError: If another unsuccessful HTTP status is returned.
35
+ httpx2.RequestError: If the request fails before an HTTP response
36
+ is received.
37
+ """
38
+ logger.debug("Authenticating with z/OSMF using all available methods")
39
+ resp = self.http.post("/zosmf/services/authenticate")
40
+ if resp.status_code == 200:
41
+ logger.debug("Authentication succeeded with z/OSMF")
42
+ return LoginOk(
43
+ **resp.json(),
44
+ ltpa_token=resp.cookies.get("LtpaToken2", self.ltpa_token),
45
+ jwt_token=resp.cookies.get(self.jwt_cookie_name, self.jwt_token),
46
+ )
47
+ elif resp.status_code == 401:
48
+ logger.debug("Authentication failed with z/OSMF")
49
+ raise ZosmfLoginError(resp)
50
+ logger.debug("Unexpected authentication failure with z/OSMF")
51
+ self._raise_for_status(resp, exc_handler=ZosmfAuthError)
52
+ raise AssertionError("Unreachable") # pragma: no cover
@@ -0,0 +1,42 @@
1
+ import logging
2
+
3
+ from zosmfrestapi.exceptions.dataset import ZosmfDatasetError, ZosmfDatasetNotFoundError
4
+
5
+ from . import ZosmfBase
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+
10
+ class DatasetServices(ZosmfBase):
11
+ """Read sequential or partitioned data sets through z/OSMF Files REST APIs."""
12
+
13
+ def _read_dataset(self, path: str) -> str:
14
+ resp = self.http.get(path)
15
+ if resp.status_code == 404:
16
+ raise ZosmfDatasetNotFoundError(resp)
17
+ self._raise_for_status(resp, exc_handler=ZosmfDatasetError)
18
+ return resp.text
19
+
20
+ def read_dataset(self, dataset: str, volser: str | None = None) -> str:
21
+ """Read a dataset or dataset member through the z/OSMF Files REST API.
22
+
23
+ Args:
24
+ dataset: Fully qualified data set name, optionally including a
25
+ member in parentheses, for example ``USER.PDS(MEMBER)``.
26
+ volser: Optional volume serial for a data set that is not cataloged.
27
+ When supplied, it is included in the z/OSMF request path.
28
+
29
+ Returns:
30
+ The contents returned by z/OSMF as text.
31
+
32
+ Raises:
33
+ ZosmfDatasetNotFoundError: If z/OSMF returns HTTP 404 for the data
34
+ set request.
35
+ ZosmfAuthError: If z/OSMF rejects the request with HTTP 401.
36
+ ZosmfDatasetError: If z/OSMF returns another unsuccessful HTTP
37
+ status.
38
+ httpx2.HTTPError: If a transport-level HTTP error occurs.
39
+ """
40
+ if volser:
41
+ return self._read_dataset(f"/zosmf/restfiles/ds/-({volser})/{dataset}")
42
+ return self._read_dataset(f"/zosmf/restfiles/ds/{dataset}")