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.
- python_sysaid-0.1.0.dist-info/METADATA +205 -0
- python_sysaid-0.1.0.dist-info/RECORD +26 -0
- python_sysaid-0.1.0.dist-info/WHEEL +4 -0
- python_sysaid-0.1.0.dist-info/licenses/LICENSE +21 -0
- sysaid/__init__.py +37 -0
- sysaid/_params.py +80 -0
- sysaid/_unverified.py +33 -0
- sysaid/auth.py +62 -0
- sysaid/client.py +160 -0
- sysaid/exceptions.py +95 -0
- sysaid/models.py +76 -0
- sysaid/oauth.py +85 -0
- sysaid/py.typed +0 -0
- sysaid/resources/__init__.py +0 -0
- sysaid/resources/_base.py +70 -0
- sysaid/resources/action_items.py +127 -0
- sysaid/resources/addons.py +63 -0
- sysaid/resources/assets.py +61 -0
- sysaid/resources/cis.py +134 -0
- sysaid/resources/filters.py +44 -0
- sysaid/resources/lists.py +56 -0
- sysaid/resources/password_services.py +74 -0
- sysaid/resources/reports.py +30 -0
- sysaid/resources/resource_bundle.py +18 -0
- sysaid/resources/service_requests.py +260 -0
- sysaid/resources/users.py +108 -0
sysaid/resources/cis.py
ADDED
|
@@ -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
|
+
)
|