python-sysaid 0.1.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,134 @@
1
+ """Configuration items: ``/ci``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Iterable, Iterator, Mapping, Sequence
6
+ from typing import Any
7
+
8
+ from .._params import encode_info, quote_segment
9
+ from .._unverified import unverified
10
+ from ..exceptions import BadRequestError, RelationError
11
+ from ..models import Record
12
+ from ._base import DEFAULT_PAGE_SIZE, JSONList, RecordList, Resource, query
13
+
14
+ Relation = tuple[int, int] | Mapping[str, Any]
15
+
16
+
17
+ def _relations_body(relations: Iterable[Relation]) -> list[dict[str, Any]]:
18
+ """Accept ``(dest, relation_type_id)`` tuples or ready ``{dest, ciRelationType}`` dicts."""
19
+ return [
20
+ {"dest": item[0], "ciRelationType": item[1]} if isinstance(item, tuple) else dict(item)
21
+ for item in relations
22
+ ]
23
+
24
+
25
+ class CIs(Resource):
26
+ """Configuration items and their relations."""
27
+
28
+ @unverified
29
+ def list(
30
+ self,
31
+ *,
32
+ ids: Sequence[int | str] | None = None,
33
+ view: str | None = None,
34
+ fields: Sequence[str] | None = None,
35
+ offset: int | None = None,
36
+ limit: int | None = None,
37
+ sort: str | None = None,
38
+ direction: str | None = None,
39
+ support_barcode: bool | None = None,
40
+ **filters: Any,
41
+ ) -> RecordList:
42
+ """One page of CIs. Extra keyword arguments are filters."""
43
+ params = query(
44
+ view=view,
45
+ fields=fields,
46
+ sort=sort,
47
+ direction=direction,
48
+ ids=ids,
49
+ offset=offset,
50
+ limit=limit,
51
+ supportBarcode=support_barcode,
52
+ **filters,
53
+ )
54
+ return self._list("/ci", params)
55
+
56
+ @unverified
57
+ def iter(
58
+ self,
59
+ *,
60
+ ids: Sequence[int | str] | None = None,
61
+ view: str | None = None,
62
+ fields: Sequence[str] | None = None,
63
+ sort: str | None = None,
64
+ direction: str | None = None,
65
+ support_barcode: bool | None = None,
66
+ page_size: int = DEFAULT_PAGE_SIZE,
67
+ **filters: Any,
68
+ ) -> Iterator[Record]:
69
+ """Every matching CI, fetching pages transparently."""
70
+ params = query(
71
+ view=view,
72
+ fields=fields,
73
+ sort=sort,
74
+ direction=direction,
75
+ ids=ids,
76
+ supportBarcode=support_barcode,
77
+ **filters,
78
+ )
79
+ return self._iter("/ci", params, page_size=page_size)
80
+
81
+ @unverified
82
+ def update(
83
+ self, ci_id: int | str, values: Mapping[str, Any] | None = None, /, **fields: Any
84
+ ) -> None:
85
+ """Update the given fields only."""
86
+ body = {"id": str(ci_id), "info": encode_info({**(values or {}), **fields})}
87
+ self._client.request("PUT", f"/ci/{quote_segment(ci_id)}", json=body)
88
+
89
+ @unverified
90
+ def types(self, *, support_barcode: bool | None = None) -> JSONList:
91
+ """CI types."""
92
+ result: JSONList = self._client.request(
93
+ "GET", "/ci/type", params={"supportBarcode": support_barcode}
94
+ )
95
+ return result
96
+
97
+ @unverified
98
+ def view_fields(self, ci_type_id: int | str, *, view: str | None = None) -> Any:
99
+ """Fields of a view for a CI type, as returned by the server."""
100
+ return self._client.request(
101
+ "GET", f"/ci/view/{quote_segment(ci_type_id)}", params={"view": view}
102
+ )
103
+
104
+ @unverified
105
+ def relation_types(self) -> JSONList:
106
+ """Available relation types."""
107
+ result: JSONList = self._client.request("GET", "/ci/relationtypes")
108
+ return result
109
+
110
+ @unverified
111
+ def relations(self, ci_id: int | str) -> JSONList:
112
+ """Relations whose source is the given CI."""
113
+ result: JSONList = self._client.request("GET", f"/ci/{quote_segment(ci_id)}/relation")
114
+ return result
115
+
116
+ @unverified
117
+ def create_relations(self, ci_id: int | str, relations: Iterable[Relation]) -> None:
118
+ """Create relations; existing ones are not duplicated.
119
+
120
+ Raises :class:`RelationError` (HTTP 400) listing each failing item.
121
+ """
122
+ try:
123
+ self._client.request(
124
+ "POST", f"/ci/{quote_segment(ci_id)}/relation", json=_relations_body(relations)
125
+ )
126
+ except BadRequestError as exc:
127
+ raise RelationError(exc.status_code, exc.message, exc.response) from exc
128
+
129
+ @unverified
130
+ def delete_relations(self, ci_id: int | str, relations: Iterable[Relation]) -> None:
131
+ """Delete relations; the server answers OK even for relations that do not exist."""
132
+ self._client.request(
133
+ "DELETE", f"/ci/{quote_segment(ci_id)}/relation", json=_relations_body(relations)
134
+ )
@@ -0,0 +1,44 @@
1
+ """Filters: ``/filters``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Sequence
6
+
7
+ from .._params import quote_segment
8
+ from ._base import JSONDict, JSONList, Resource, query
9
+
10
+
11
+ class Filters(Resource):
12
+ """Valid query parameters (and their values) for list calls such as ``/sr``."""
13
+
14
+ def list(
15
+ self,
16
+ *,
17
+ view: str | None = None,
18
+ fields: Sequence[str] | None = None,
19
+ offset: int | None = None,
20
+ limit: int | None = None,
21
+ ) -> JSONList:
22
+ """``offset``/``limit`` page the filter *values*."""
23
+ result: JSONList = self._client.request(
24
+ "GET",
25
+ "/filters",
26
+ params=query(view=view, fields=fields, offset=offset, limit=limit),
27
+ )
28
+ return result
29
+
30
+ def get(
31
+ self,
32
+ filter_id: str,
33
+ *,
34
+ view: str | None = None,
35
+ offset: int | None = None,
36
+ limit: int | None = None,
37
+ ) -> JSONDict:
38
+ """One filter with its values; ``offset``/``limit`` page the values."""
39
+ result: JSONDict = self._client.request(
40
+ "GET",
41
+ f"/filters/{quote_segment(filter_id)}",
42
+ params=query(view=view, offset=offset, limit=limit),
43
+ )
44
+ return result
@@ -0,0 +1,56 @@
1
+ """Lists (dropdown id/caption pairs): ``/list``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Sequence
6
+
7
+ from .._params import quote_segment
8
+ from ._base import JSONDict, JSONList, Resource, query
9
+
10
+
11
+ class Lists(Resource):
12
+ """Id/caption pairs behind dropdown fields."""
13
+
14
+ def list(
15
+ self,
16
+ *,
17
+ entity: str | None = None,
18
+ fields: Sequence[str] | None = None,
19
+ offset: int | None = None,
20
+ limit: int | None = None,
21
+ ) -> JSONList:
22
+ """All lists of an entity (server default ``sr``)."""
23
+ result: JSONList = self._client.request(
24
+ "GET",
25
+ "/list",
26
+ params=query(entity=entity, fields=fields, offset=offset, limit=limit),
27
+ )
28
+ return result
29
+
30
+ def get(
31
+ self,
32
+ list_id: str,
33
+ *,
34
+ entity: str | None = None,
35
+ entity_id: int | str | None = None,
36
+ entity_type: int | None = None,
37
+ fields: Sequence[str] | None = None,
38
+ offset: int | None = None,
39
+ limit: int | None = None,
40
+ key: str | None = None,
41
+ ) -> JSONDict:
42
+ """One list. ``entity_id`` applies per-record filtering; ``key`` is ``id`` or ``name``."""
43
+ result: JSONDict = self._client.request(
44
+ "GET",
45
+ f"/list/{quote_segment(list_id)}",
46
+ params=query(
47
+ entity=entity,
48
+ entityId=entity_id,
49
+ entityType=entity_type,
50
+ fields=fields,
51
+ offset=offset,
52
+ limit=limit,
53
+ key=key,
54
+ ),
55
+ )
56
+ return result
@@ -0,0 +1,74 @@
1
+ """Password self-service: ``/ps``. None of these calls needs a login."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping, Sequence
6
+ from typing import Any
7
+
8
+ from .._unverified import unverified
9
+ from ._base import JSONDict, Resource
10
+
11
+
12
+ class PasswordServices(Resource):
13
+ """Flow: :meth:`permissions` -> :meth:`questions` -> :meth:`unlock` or :meth:`reset`
14
+ (-> :meth:`update_password` when the reset method is ``user``).
15
+ """
16
+
17
+ @unverified
18
+ def domains(self) -> list[str]:
19
+ """LDAP domain names."""
20
+ result: list[str] = self._client.request("GET", "/ps/domain", auth=False)
21
+ return result
22
+
23
+ @unverified
24
+ def permissions(self) -> JSONDict:
25
+ """Which self-service features are enabled."""
26
+ result: JSONDict = self._client.request("GET", "/ps/permission", auth=False)
27
+ return result
28
+
29
+ @unverified
30
+ def questions(self, method: str, user_name: str, domain_name: str | None = None) -> JSONDict:
31
+ """Security questions for ``method`` (``reset`` or ``unlock``); keep ``userRefId``."""
32
+ if method not in ("reset", "unlock"):
33
+ raise ValueError("method must be 'reset' or 'unlock'")
34
+ body = {"userName": user_name, "domainName": domain_name}
35
+ result: JSONDict = self._client.request(
36
+ "POST",
37
+ f"/ps/{method}/question",
38
+ json={key: value for key, value in body.items() if value is not None},
39
+ auth=False,
40
+ )
41
+ return result
42
+
43
+ @unverified
44
+ def unlock(self, user_ref_id: int, questions: Sequence[Mapping[str, Any]]) -> JSONDict:
45
+ """Unlock an account. ``questions`` are the items returned by :meth:`questions`,
46
+ each with an added ``answer``."""
47
+ return self._answered("/ps/unlock", user_ref_id, questions)
48
+
49
+ @unverified
50
+ def reset(self, user_ref_id: int, questions: Sequence[Mapping[str, Any]]) -> JSONDict:
51
+ """Start a password reset; the response depends on the configured reset method."""
52
+ return self._answered("/ps/reset", user_ref_id, questions)
53
+
54
+ @unverified
55
+ def update_password(self, user_ref_id: int, new_password: str, token: str) -> JSONDict:
56
+ """Set the new password using the one-time ``token`` from :meth:`reset`."""
57
+ result: JSONDict = self._client.request(
58
+ "POST",
59
+ "/ps/reset/update",
60
+ json={"userRefId": user_ref_id, "newPassword": new_password, "token": token},
61
+ auth=False,
62
+ )
63
+ return result
64
+
65
+ def _answered(
66
+ self, path: str, user_ref_id: int, questions: Sequence[Mapping[str, Any]]
67
+ ) -> JSONDict:
68
+ result: JSONDict = self._client.request(
69
+ "POST",
70
+ path,
71
+ json={"userRefId": user_ref_id, "userSecurityQuestionsList": list(questions)},
72
+ auth=False,
73
+ )
74
+ return result
@@ -0,0 +1,30 @@
1
+ """Reports: ``/reports``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping
6
+ from typing import Any
7
+
8
+ from .._params import quote_segment
9
+ from .._unverified import unverified
10
+ from ._base import JSONList, Resource
11
+
12
+
13
+ class Reports(Resource):
14
+ """Report metadata and preview runs."""
15
+
16
+ @unverified
17
+ def operators(self, type: str | None = None) -> JSONList:
18
+ """Field operators, optionally for one data type (``string``, ``date``, ``int``...)."""
19
+ result: JSONList = self._client.request("GET", "/reports/operators", params={"type": type})
20
+ return result
21
+
22
+ @unverified
23
+ def run_preview(self, report_id: int | str, definition: Mapping[str, Any]) -> Any:
24
+ """Run a report in preview mode and return the raw JSON.
25
+
26
+ ``definition`` is the report body (``entity``, ``select``, ``filter``, ``layout``).
27
+ """
28
+ return self._client.request(
29
+ "POST", f"/reports/{quote_segment(report_id)}/runPreview", json=definition
30
+ )
@@ -0,0 +1,18 @@
1
+ """Resource bundle translations: ``/rb``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Sequence
6
+
7
+ from .._params import quote_segment
8
+ from ._base import Resource
9
+
10
+
11
+ class ResourceBundle(Resource):
12
+ """Resource-bundle key translation."""
13
+
14
+ def translate(self, keys: Sequence[str], locale: str | None = None) -> dict[str, str]:
15
+ """Translate resource-bundle keys, for the account locale or the given one."""
16
+ path = "/rb" if locale is None else f"/rb/{quote_segment(locale)}"
17
+ result = self._client.request("POST", path, json=[{"key": key} for key in keys])
18
+ return {item["key"]: item["value"] for item in result}
@@ -0,0 +1,260 @@
1
+ """Service requests (incident, request, problem, change): ``/sr``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from collections.abc import Iterator, Mapping, Sequence
7
+ from datetime import datetime, timezone
8
+ from os import PathLike
9
+ from typing import Any
10
+
11
+ from .._params import encode_info, encode_value, quote_segment
12
+ from .._unverified import unverified
13
+ from ..models import Record
14
+ from ._base import DEFAULT_PAGE_SIZE, RecordList, Resource, query, read_upload
15
+
16
+ SrType = str | Sequence[str]
17
+
18
+
19
+ def make_note(user_name: str, text: str, created: datetime | None = None) -> dict[str, Any]:
20
+ """An entry for the ``notes`` field."""
21
+ return {
22
+ "userName": user_name,
23
+ "createDate": created or datetime.now(timezone.utc),
24
+ "text": text,
25
+ }
26
+
27
+
28
+ def problem_type(*levels: str) -> str:
29
+ """Join up to three category levels into the ``problem_type`` value."""
30
+ if not 1 <= len(levels) <= 3:
31
+ raise ValueError("problem_type takes one to three category levels")
32
+ return "_".join(levels)
33
+
34
+
35
+ class ServiceRequests(Resource):
36
+ """Service requests: incidents, requests, problems and changes."""
37
+
38
+ def list(
39
+ self,
40
+ *,
41
+ type: SrType | None = None,
42
+ ids: Sequence[int | str] | None = None,
43
+ archive: bool | None = None,
44
+ view: str | None = None,
45
+ fields: Sequence[str] | None = None,
46
+ offset: int | None = None,
47
+ limit: int | None = None,
48
+ sort: str | None = None,
49
+ direction: str | None = None,
50
+ **filters: Any,
51
+ ) -> RecordList:
52
+ """One page of SRs. Extra keyword arguments are filters from ``client.filters``."""
53
+ params = self._list_params(type, ids, archive, view, fields, sort, direction, filters)
54
+ return self._list("/sr", {**params, "offset": offset, "limit": limit})
55
+
56
+ def iter(
57
+ self,
58
+ *,
59
+ type: SrType | None = None,
60
+ ids: Sequence[int | str] | None = None,
61
+ archive: bool | None = None,
62
+ view: str | None = None,
63
+ fields: Sequence[str] | None = None,
64
+ sort: str | None = None,
65
+ direction: str | None = None,
66
+ page_size: int = DEFAULT_PAGE_SIZE,
67
+ **filters: Any,
68
+ ) -> Iterator[Record]:
69
+ """Every matching SR, fetching pages transparently."""
70
+ params = self._list_params(type, ids, archive, view, fields, sort, direction, filters)
71
+ return self._iter("/sr", params, page_size=page_size)
72
+
73
+ def get(
74
+ self, sr_id: int | str, *, view: str | None = None, fields: Sequence[str] | None = None
75
+ ) -> Record:
76
+ """The SR as a form: fields carry ``mandatory``/``editable``/``type`` metadata."""
77
+ return self._get(f"/sr/{quote_segment(sr_id)}", query(view=view, fields=fields))
78
+
79
+ def search(
80
+ self,
81
+ text: str,
82
+ *,
83
+ type: SrType | None = None,
84
+ archive: bool | None = None,
85
+ view: str | None = None,
86
+ fields: Sequence[str] | None = None,
87
+ offset: int | None = None,
88
+ limit: int | None = None,
89
+ sort: str | None = None,
90
+ direction: str | None = None,
91
+ **filters: Any,
92
+ ) -> RecordList:
93
+ """Search SRs; the server defaults ``type`` to ``incident`` here."""
94
+ params = self._list_params(type, None, archive, view, fields, sort, direction, filters)
95
+ return self._list("/sr/search", {**params, "query": text, "offset": offset, "limit": limit})
96
+
97
+ def count(self, **filters: Any) -> int:
98
+ """Number of SRs matching the filters."""
99
+ result = self._client.request("GET", "/sr/count", params=filters)
100
+ return int(result["count"])
101
+
102
+ def template(
103
+ self,
104
+ *,
105
+ type: str | None = None,
106
+ template: int | str | None = None,
107
+ view: str | None = None,
108
+ fields: Sequence[str] | None = None,
109
+ ) -> Record:
110
+ """A blank SR (``id`` ``"0"``) showing mandatory fields and defaults."""
111
+ return self._get(
112
+ "/sr/template", query(view=view, fields=fields, type=type, template=template)
113
+ )
114
+
115
+ @staticmethod
116
+ def _list_params(
117
+ type: SrType | None,
118
+ ids: Sequence[int | str] | None,
119
+ archive: bool | None,
120
+ view: str | None,
121
+ fields: Sequence[str] | None,
122
+ sort: str | None,
123
+ direction: str | None,
124
+ filters: Mapping[str, Any],
125
+ ) -> dict[str, Any]:
126
+ params = query(view=view, fields=fields, sort=sort, direction=direction, type=type, ids=ids)
127
+ if archive is not None:
128
+ params["archive"] = 1 if archive else 0
129
+ return {**params, **filters}
130
+
131
+ def create(
132
+ self,
133
+ values: Mapping[str, Any] | None = None,
134
+ /,
135
+ *,
136
+ type: str | None = None,
137
+ template: int | str | None = None,
138
+ view: str | None = None,
139
+ return_fields: Sequence[str] | None = None,
140
+ **fields: Any,
141
+ ) -> Record:
142
+ """Create an SR from field values, given as keywords and/or a mapping.
143
+
144
+ Use the mapping for field ids that clash with this method's own keywords
145
+ (e.g. the SR field ``type``). ``type`` and ``template`` select the SR type and
146
+ template; ``view`` and ``return_fields`` shape the returned record.
147
+ """
148
+ body = {"info": encode_info({**(values or {}), **fields})}
149
+ params = query(view=view, fields=return_fields, type=type, template=template)
150
+ return Record(self._client.request("POST", "/sr", params=params, json=body))
151
+
152
+ def update(
153
+ self, sr_id: int | str, values: Mapping[str, Any] | None = None, /, **fields: Any
154
+ ) -> None:
155
+ """Update the given fields only. Datetimes become ms-epoch UTC."""
156
+ body = {"id": str(sr_id), "info": encode_info({**(values or {}), **fields})}
157
+ self._client.request("PUT", f"/sr/{quote_segment(sr_id)}", json=body)
158
+
159
+ def close(self, sr_id: int | str, solution: str | None = None) -> None:
160
+ """Set the SR to the default *Close* status."""
161
+ body = None if solution is None else {"solution": solution}
162
+ self._client.request("PUT", f"/sr/{quote_segment(sr_id)}/close", json=body)
163
+
164
+ @unverified
165
+ def delete(self, ids: int | str | Sequence[int | str]) -> None:
166
+ """Delete one or more SRs."""
167
+ id_list = [ids] if isinstance(ids, (int, str)) else ids
168
+ self._client.request("DELETE", "/sr", params={"ids": id_list})
169
+
170
+ def add_link(self, sr_id: int | str, name: str, link: str) -> None:
171
+ """Add a named link to the SR."""
172
+ self._client.request(
173
+ "POST", f"/sr/{quote_segment(sr_id)}/link", json={"name": name, "link": link}
174
+ )
175
+
176
+ def delete_link(self, sr_id: int | str, name: str) -> None:
177
+ """Delete a link from the SR by name."""
178
+ self._client.request("DELETE", f"/sr/{quote_segment(sr_id)}/link", json={"name": name})
179
+
180
+ def add_attachment(
181
+ self, sr_id: int | str, file: bytes | str | PathLike[str], filename: str | None = None
182
+ ) -> None:
183
+ """Attach a file, given as bytes or a path (multipart part ``file``)."""
184
+ name, content = read_upload(file, filename)
185
+ self._client.request(
186
+ "POST", f"/sr/{quote_segment(sr_id)}/attachment", files={"file": (name, content)}
187
+ )
188
+
189
+ def delete_attachment(self, sr_id: int | str, file_id: str) -> None:
190
+ """Delete an attachment by its file id."""
191
+ self._client.request(
192
+ "DELETE", f"/sr/{quote_segment(sr_id)}/attachment", json={"fileId": file_id}
193
+ )
194
+
195
+ def add_activity(
196
+ self,
197
+ sr_id: int | str,
198
+ user_id: int | str,
199
+ from_time: datetime | int,
200
+ to_time: datetime | int,
201
+ description: str,
202
+ ) -> None:
203
+ """Log an activity for the user with the given numeric id.
204
+
205
+ Times are datetimes or ms-epoch integers.
206
+ """
207
+ body = {
208
+ "userId": user_id,
209
+ "fromTime": from_time,
210
+ "toTime": to_time,
211
+ "description": description,
212
+ }
213
+ self._client.request("POST", f"/sr/{quote_segment(sr_id)}/activity", json=body)
214
+
215
+ def delete_activity(self, sr_id: int | str, activity_id: int) -> None:
216
+ """Delete an activity by id."""
217
+ self._client.request(
218
+ "DELETE", f"/sr/{quote_segment(sr_id)}/activity", json={"id": activity_id}
219
+ )
220
+
221
+ def send_message(
222
+ self,
223
+ sr_id: int | str,
224
+ to_users: str | Sequence[int | str],
225
+ *,
226
+ from_user_id: int | str,
227
+ subject: str | None = None,
228
+ body: str | None = None,
229
+ cc_users: str | Sequence[int | str] | None = None,
230
+ method: str | None = None,
231
+ add_attachment_to_sr: bool | None = None,
232
+ add_sr_details: bool | None = None,
233
+ attachments: Sequence[bytes | str | PathLike[str]] = (),
234
+ ) -> None:
235
+ """Send a message from the SR.
236
+
237
+ Recipients are user ids; a group id goes in brackets, e.g. ``"[3]"``. ``method`` is
238
+ ``email`` (default), ``sms``, ``broadcast`` or ``im``.
239
+ """
240
+ message = {
241
+ "fromUserId": str(from_user_id),
242
+ "toUsers": encode_value(to_users),
243
+ "ccUsers": None if cc_users is None else encode_value(cc_users),
244
+ "msgSubject": subject,
245
+ "msgBody": body,
246
+ }
247
+ parts: list[tuple[str, tuple[str | None, Any]]] = [
248
+ ("message", (None, json.dumps({k: v for k, v in message.items() if v is not None})))
249
+ ]
250
+ parts += [("file", read_upload(attachment, None)) for attachment in attachments]
251
+ self._client.request(
252
+ "POST",
253
+ f"/sr/{quote_segment(sr_id)}/message",
254
+ params={
255
+ "method": method,
256
+ "addAttachmentToSr": add_attachment_to_sr,
257
+ "addSrDetails": add_sr_details,
258
+ },
259
+ files=parts,
260
+ )