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/exceptions.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Exceptions raised by the SysAid client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from http.client import responses
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import requests
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SysAidError(Exception):
|
|
12
|
+
"""Base class for every error raised by this package."""
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class AuthenticationError(SysAidError):
|
|
16
|
+
"""Login failed, or credentials are missing."""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class UnverifiedFeatureError(SysAidError):
|
|
20
|
+
"""The feature is disabled until it has been verified against a live server."""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class SysAidHTTPError(SysAidError):
|
|
24
|
+
"""The server answered with a non-2xx status."""
|
|
25
|
+
|
|
26
|
+
def __init__(
|
|
27
|
+
self, status_code: int, message: str, response: requests.Response | None = None
|
|
28
|
+
) -> None:
|
|
29
|
+
super().__init__(f"HTTP {status_code}: {message}")
|
|
30
|
+
self.status_code = status_code
|
|
31
|
+
self.message = message
|
|
32
|
+
self.response = response
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class BadRequestError(SysAidHTTPError):
|
|
36
|
+
"""HTTP 400."""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class RelationError(BadRequestError):
|
|
40
|
+
"""CI relations could not be created; ``failures`` lists each failing item."""
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def failures(self) -> list[str]:
|
|
44
|
+
"""The per-item messages from the server's CSV error text."""
|
|
45
|
+
return [part.strip() for part in self.message.split(",") if part.strip()]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class UnauthorizedError(SysAidHTTPError):
|
|
49
|
+
"""HTTP 401."""
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class ForbiddenError(SysAidHTTPError):
|
|
53
|
+
"""HTTP 403."""
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class NotFoundError(SysAidHTTPError):
|
|
57
|
+
"""HTTP 404."""
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class ServerError(SysAidHTTPError):
|
|
61
|
+
"""HTTP 5xx."""
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
_BY_STATUS: dict[int, type[SysAidHTTPError]] = {
|
|
65
|
+
400: BadRequestError,
|
|
66
|
+
401: UnauthorizedError,
|
|
67
|
+
403: ForbiddenError,
|
|
68
|
+
404: NotFoundError,
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _message_from(response: requests.Response) -> str:
|
|
73
|
+
"""Return ``message`` from a ``{"status", "message"}`` body, else the raw text.
|
|
74
|
+
|
|
75
|
+
Unrouted or filtered requests get the servlet container's HTML error page; for
|
|
76
|
+
those the reason phrase is used instead of the markup.
|
|
77
|
+
"""
|
|
78
|
+
try:
|
|
79
|
+
body: Any = response.json()
|
|
80
|
+
except ValueError:
|
|
81
|
+
if "html" in response.headers.get("Content-Type", ""):
|
|
82
|
+
return response.reason or responses.get(response.status_code, "")
|
|
83
|
+
return response.text or response.reason or ""
|
|
84
|
+
if isinstance(body, dict) and body.get("message") is not None:
|
|
85
|
+
return str(body["message"])
|
|
86
|
+
return response.text
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def error_from_response(response: requests.Response) -> SysAidHTTPError:
|
|
90
|
+
"""Build the typed exception matching a non-2xx response."""
|
|
91
|
+
status = response.status_code
|
|
92
|
+
cls = _BY_STATUS.get(status)
|
|
93
|
+
if cls is None:
|
|
94
|
+
cls = ServerError if status >= 500 else SysAidHTTPError
|
|
95
|
+
return cls(status, _message_from(response), response)
|
sysaid/models.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Light wrappers around the ``{id, info[]}`` objects returned by SysAid."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterator, Mapping
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _as_bool(value: Any) -> bool | None:
|
|
11
|
+
if isinstance(value, bool):
|
|
12
|
+
return value
|
|
13
|
+
if isinstance(value, str) and value.lower() in ("true", "false"):
|
|
14
|
+
return value.lower() == "true"
|
|
15
|
+
return None
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True)
|
|
19
|
+
class Field:
|
|
20
|
+
"""One ``info`` entry. Metadata is only present on form and template calls."""
|
|
21
|
+
|
|
22
|
+
key: str
|
|
23
|
+
value: Any = None
|
|
24
|
+
key_caption: str | None = None
|
|
25
|
+
value_caption: Any = None
|
|
26
|
+
mandatory: bool | None = None
|
|
27
|
+
editable: bool | None = None
|
|
28
|
+
type: str | None = None
|
|
29
|
+
default_value: Any = None
|
|
30
|
+
|
|
31
|
+
@classmethod
|
|
32
|
+
def from_dict(cls, data: Mapping[str, Any]) -> Field:
|
|
33
|
+
"""Build from one ``info`` entry (accepts both caption spellings)."""
|
|
34
|
+
# Assets use snake_case captions, everything else camelCase.
|
|
35
|
+
return cls(
|
|
36
|
+
key=str(data["key"]),
|
|
37
|
+
value=data.get("value"),
|
|
38
|
+
key_caption=data.get("keyCaption", data.get("key_caption")),
|
|
39
|
+
value_caption=data.get("valueCaption", data.get("value_caption")),
|
|
40
|
+
mandatory=_as_bool(data.get("mandatory")),
|
|
41
|
+
editable=_as_bool(data.get("editable")),
|
|
42
|
+
type=data.get("type"),
|
|
43
|
+
default_value=data.get("defaultValue"),
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class Record(Mapping[str, Any]):
|
|
48
|
+
"""An entity with an ``id`` and an ``info`` array, read like a mapping of key -> value."""
|
|
49
|
+
|
|
50
|
+
def __init__(self, raw: Mapping[str, Any]) -> None:
|
|
51
|
+
self.raw: dict[str, Any] = dict(raw)
|
|
52
|
+
self.fields: dict[str, Field] = {
|
|
53
|
+
field.key: field for field in map(Field.from_dict, self.raw.get("info") or [])
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def id(self) -> str | None:
|
|
58
|
+
"""The record id as a string, if present."""
|
|
59
|
+
value = self.raw.get("id")
|
|
60
|
+
return None if value is None else str(value)
|
|
61
|
+
|
|
62
|
+
def __getitem__(self, key: str) -> Any:
|
|
63
|
+
return self.fields[key].value
|
|
64
|
+
|
|
65
|
+
def __iter__(self) -> Iterator[str]:
|
|
66
|
+
return iter(self.fields)
|
|
67
|
+
|
|
68
|
+
def __len__(self) -> int:
|
|
69
|
+
return len(self.fields)
|
|
70
|
+
|
|
71
|
+
def caption(self, key: str) -> Any:
|
|
72
|
+
"""Display value (``valueCaption``) of a field."""
|
|
73
|
+
return self.fields[key].value_caption
|
|
74
|
+
|
|
75
|
+
def __repr__(self) -> str:
|
|
76
|
+
return f"Record(id={self.id!r}, fields={list(self.fields)!r})"
|
sysaid/oauth.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""OAuth 1.0 helpers (optional extra: ``pip install python-sysaid[oauth]``).
|
|
2
|
+
|
|
3
|
+
Three-legged flow: :func:`request_token` -> send the user to :func:`authorize_url` ->
|
|
4
|
+
:func:`access_token` with the ``oauth_verifier`` from the callback. Then build a client
|
|
5
|
+
with :meth:`sysaid.SysAid.from_oauth`.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Any
|
|
11
|
+
from urllib.parse import urlencode
|
|
12
|
+
|
|
13
|
+
import requests
|
|
14
|
+
|
|
15
|
+
from ._unverified import unverified
|
|
16
|
+
from .client import api_url_for
|
|
17
|
+
from .exceptions import error_from_response
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def oauth1(client_key: str, client_secret: str, **kwargs: Any) -> Any:
|
|
21
|
+
"""Build a ``requests_oauthlib.OAuth1`` auth object (HMAC-SHA1)."""
|
|
22
|
+
try:
|
|
23
|
+
from requests_oauthlib import OAuth1
|
|
24
|
+
except ImportError as exc:
|
|
25
|
+
raise ImportError(
|
|
26
|
+
"OAuth support needs requests-oauthlib: pip install 'python-sysaid[oauth]'"
|
|
27
|
+
) from exc
|
|
28
|
+
return OAuth1(client_key, client_secret=client_secret, **kwargs)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _post(
|
|
32
|
+
base_url: str, path: str, auth: Any, verify: bool | str, timeout: float | None
|
|
33
|
+
) -> dict[str, Any]:
|
|
34
|
+
response = requests.post(
|
|
35
|
+
api_url_for(base_url) + path, auth=auth, verify=verify, timeout=timeout
|
|
36
|
+
)
|
|
37
|
+
if not response.ok:
|
|
38
|
+
raise error_from_response(response)
|
|
39
|
+
token: dict[str, Any] = response.json()
|
|
40
|
+
return token
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@unverified
|
|
44
|
+
def request_token(
|
|
45
|
+
base_url: str,
|
|
46
|
+
consumer_key: str,
|
|
47
|
+
callback_url: str,
|
|
48
|
+
consumer_secret: str = "",
|
|
49
|
+
*,
|
|
50
|
+
verify: bool | str = True,
|
|
51
|
+
timeout: float | None = 30,
|
|
52
|
+
) -> dict[str, Any]:
|
|
53
|
+
"""Step 1. Returns ``oauth_token``, ``oauth_token_secret`` and ``oauth_callback_confirmed``."""
|
|
54
|
+
auth = oauth1(consumer_key, consumer_secret, callback_uri=callback_url)
|
|
55
|
+
return _post(base_url, "/oauth/request_token", auth, verify, timeout)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@unverified
|
|
59
|
+
def authorize_url(base_url: str, request_token: str) -> str:
|
|
60
|
+
"""Step 2. The URL to send the user's browser to."""
|
|
61
|
+
query = urlencode({"oauth_token": request_token})
|
|
62
|
+
return f"{api_url_for(base_url)}/oauth/authorize?{query}"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@unverified
|
|
66
|
+
def access_token(
|
|
67
|
+
base_url: str,
|
|
68
|
+
consumer_key: str,
|
|
69
|
+
request_token: str,
|
|
70
|
+
request_token_secret: str,
|
|
71
|
+
verifier: str,
|
|
72
|
+
consumer_secret: str = "",
|
|
73
|
+
*,
|
|
74
|
+
verify: bool | str = True,
|
|
75
|
+
timeout: float | None = 30,
|
|
76
|
+
) -> dict[str, Any]:
|
|
77
|
+
"""Step 3. Returns the access ``oauth_token`` and ``oauth_token_secret``."""
|
|
78
|
+
auth = oauth1(
|
|
79
|
+
consumer_key,
|
|
80
|
+
consumer_secret,
|
|
81
|
+
resource_owner_key=request_token,
|
|
82
|
+
resource_owner_secret=request_token_secret,
|
|
83
|
+
verifier=verifier,
|
|
84
|
+
)
|
|
85
|
+
return _post(base_url, "/oauth/access_token", auth, verify, timeout)
|
sysaid/py.typed
ADDED
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Shared plumbing for the resource classes."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterator, Mapping, Sequence
|
|
6
|
+
from os import PathLike
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import TYPE_CHECKING, Any
|
|
9
|
+
|
|
10
|
+
from ..models import Record
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from ..client import SysAid
|
|
14
|
+
|
|
15
|
+
DEFAULT_PAGE_SIZE = 100
|
|
16
|
+
|
|
17
|
+
# Resource classes define methods named ``list``/``iter``, which would shadow the
|
|
18
|
+
# builtins in annotations inside the class body; these aliases avoid that.
|
|
19
|
+
RecordList = list[Record]
|
|
20
|
+
JSONDict = dict[str, Any]
|
|
21
|
+
JSONList = list[dict[str, Any]]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def query(
|
|
25
|
+
*,
|
|
26
|
+
view: str | None = None,
|
|
27
|
+
fields: Sequence[str] | None = None,
|
|
28
|
+
sort: str | None = None,
|
|
29
|
+
direction: str | None = None,
|
|
30
|
+
**extra: Any,
|
|
31
|
+
) -> dict[str, Any]:
|
|
32
|
+
"""Common list parameters (``dir`` is exposed as ``direction``) plus extras."""
|
|
33
|
+
return {"view": view, "fields": fields, "sort": sort, "dir": direction, **extra}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def read_upload(file: bytes | str | PathLike[str], filename: str | None) -> tuple[str, bytes]:
|
|
37
|
+
"""Resolve an upload given as bytes or a path into ``(filename, content)``."""
|
|
38
|
+
if isinstance(file, bytes):
|
|
39
|
+
return filename or "file", file
|
|
40
|
+
path = Path(file)
|
|
41
|
+
return filename or path.name, path.read_bytes()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class Resource:
|
|
45
|
+
"""Base class: holds the client and the shared GET/list/paginate helpers."""
|
|
46
|
+
|
|
47
|
+
def __init__(self, client: SysAid) -> None:
|
|
48
|
+
self._client = client
|
|
49
|
+
|
|
50
|
+
def _get(self, path: str, params: Mapping[str, Any] | None = None) -> Record:
|
|
51
|
+
return Record(self._client.request("GET", path, params=params))
|
|
52
|
+
|
|
53
|
+
def _list(self, path: str, params: Mapping[str, Any] | None = None) -> RecordList:
|
|
54
|
+
return [Record(item) for item in self._client.request("GET", path, params=params)]
|
|
55
|
+
|
|
56
|
+
def _iter(
|
|
57
|
+
self,
|
|
58
|
+
path: str,
|
|
59
|
+
params: Mapping[str, Any] | None = None,
|
|
60
|
+
*,
|
|
61
|
+
page_size: int = DEFAULT_PAGE_SIZE,
|
|
62
|
+
offset: int = 0,
|
|
63
|
+
) -> Iterator[Record]:
|
|
64
|
+
"""Yield every record, fetching pages until one is shorter than ``page_size``."""
|
|
65
|
+
while True:
|
|
66
|
+
page = self._list(path, {**(params or {}), "offset": offset, "limit": page_size})
|
|
67
|
+
yield from page
|
|
68
|
+
if len(page) < page_size:
|
|
69
|
+
return
|
|
70
|
+
offset += page_size
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""Action items: ``/action_item``."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterator, Sequence
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from .._params import quote_segment
|
|
9
|
+
from .._unverified import unverified
|
|
10
|
+
from ..models import Record
|
|
11
|
+
from ._base import DEFAULT_PAGE_SIZE, RecordList, Resource, query
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _filter_params(
|
|
15
|
+
type: str | Sequence[str] | None,
|
|
16
|
+
ids: Sequence[int | str] | None,
|
|
17
|
+
archive: bool | None,
|
|
18
|
+
static_filter_id: str | None,
|
|
19
|
+
text: str | None,
|
|
20
|
+
filters: dict[str, Any],
|
|
21
|
+
) -> dict[str, Any]:
|
|
22
|
+
params: dict[str, Any] = {
|
|
23
|
+
"type": type,
|
|
24
|
+
"ids": ids,
|
|
25
|
+
"staticFilterId": static_filter_id,
|
|
26
|
+
"query": text,
|
|
27
|
+
}
|
|
28
|
+
if archive is not None:
|
|
29
|
+
params["archive"] = 1 if archive else 0
|
|
30
|
+
return {**params, **filters}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ActionItems(Resource):
|
|
34
|
+
"""Action items attached to service requests."""
|
|
35
|
+
|
|
36
|
+
@unverified
|
|
37
|
+
def list(
|
|
38
|
+
self,
|
|
39
|
+
*,
|
|
40
|
+
type: str | Sequence[str] | None = None,
|
|
41
|
+
ids: Sequence[int | str] | None = None,
|
|
42
|
+
archive: bool | None = None,
|
|
43
|
+
static_filter_id: str | None = None,
|
|
44
|
+
text: str | None = None,
|
|
45
|
+
view: str | None = None,
|
|
46
|
+
fields: Sequence[str] | None = None,
|
|
47
|
+
offset: int | None = None,
|
|
48
|
+
limit: int | None = None,
|
|
49
|
+
sort: str | None = None,
|
|
50
|
+
direction: str | None = None,
|
|
51
|
+
**filters: Any,
|
|
52
|
+
) -> RecordList:
|
|
53
|
+
"""One page of action items. ``ids`` are the *SR* ids whose items to return."""
|
|
54
|
+
params = query(
|
|
55
|
+
view=view,
|
|
56
|
+
fields=fields,
|
|
57
|
+
sort=sort,
|
|
58
|
+
direction=direction,
|
|
59
|
+
offset=offset,
|
|
60
|
+
limit=limit,
|
|
61
|
+
**_filter_params(type, ids, archive, static_filter_id, text, filters),
|
|
62
|
+
)
|
|
63
|
+
return self._list("/action_item", params)
|
|
64
|
+
|
|
65
|
+
@unverified
|
|
66
|
+
def iter(
|
|
67
|
+
self,
|
|
68
|
+
*,
|
|
69
|
+
type: str | Sequence[str] | None = None,
|
|
70
|
+
ids: Sequence[int | str] | None = None,
|
|
71
|
+
archive: bool | None = None,
|
|
72
|
+
static_filter_id: str | None = None,
|
|
73
|
+
text: str | None = None,
|
|
74
|
+
view: str | None = None,
|
|
75
|
+
fields: Sequence[str] | None = None,
|
|
76
|
+
sort: str | None = None,
|
|
77
|
+
direction: str | None = None,
|
|
78
|
+
page_size: int = DEFAULT_PAGE_SIZE,
|
|
79
|
+
**filters: Any,
|
|
80
|
+
) -> Iterator[Record]:
|
|
81
|
+
"""Every matching action item, fetching pages transparently."""
|
|
82
|
+
params = query(
|
|
83
|
+
view=view,
|
|
84
|
+
fields=fields,
|
|
85
|
+
sort=sort,
|
|
86
|
+
direction=direction,
|
|
87
|
+
**_filter_params(type, ids, archive, static_filter_id, text, filters),
|
|
88
|
+
)
|
|
89
|
+
return self._iter("/action_item", params, page_size=page_size)
|
|
90
|
+
|
|
91
|
+
@unverified
|
|
92
|
+
def count(
|
|
93
|
+
self,
|
|
94
|
+
*,
|
|
95
|
+
type: str | Sequence[str] | None = None,
|
|
96
|
+
ids: Sequence[int | str] | None = None,
|
|
97
|
+
archive: bool | None = None,
|
|
98
|
+
static_filter_id: str | None = None,
|
|
99
|
+
text: str | None = None,
|
|
100
|
+
**filters: Any,
|
|
101
|
+
) -> int:
|
|
102
|
+
"""Number of action items matching the filters."""
|
|
103
|
+
params = _filter_params(type, ids, archive, static_filter_id, text, filters)
|
|
104
|
+
return int(self._client.request("GET", "/action_item/count", params=params)["count"])
|
|
105
|
+
|
|
106
|
+
@unverified
|
|
107
|
+
def approve(self, action_item_id: int | str) -> None:
|
|
108
|
+
"""Approve the action item."""
|
|
109
|
+
self._change_state(action_item_id, "approve")
|
|
110
|
+
|
|
111
|
+
@unverified
|
|
112
|
+
def reject(self, action_item_id: int | str) -> None:
|
|
113
|
+
"""Reject the action item."""
|
|
114
|
+
self._change_state(action_item_id, "reject")
|
|
115
|
+
|
|
116
|
+
@unverified
|
|
117
|
+
def complete(self, action_item_id: int | str) -> None:
|
|
118
|
+
"""Mark the action item as complete."""
|
|
119
|
+
self._change_state(action_item_id, "complete")
|
|
120
|
+
|
|
121
|
+
@unverified
|
|
122
|
+
def reopen(self, action_item_id: int | str) -> None:
|
|
123
|
+
"""Reopen the action item."""
|
|
124
|
+
self._change_state(action_item_id, "reopen")
|
|
125
|
+
|
|
126
|
+
def _change_state(self, action_item_id: int | str, action: str) -> None:
|
|
127
|
+
self._client.request("PUT", f"/action_item/{quote_segment(action_item_id)}/{action}")
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Add-ons: ``/addons``."""
|
|
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 JSONDict, JSONList, Resource
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _payload(name: str, active: bool | None, params: Mapping[str, Any] | None) -> JSONDict:
|
|
14
|
+
body: JSONDict = {"name": name}
|
|
15
|
+
if active is not None:
|
|
16
|
+
body["active"] = active
|
|
17
|
+
if params is not None:
|
|
18
|
+
body["params"] = [{"name": key, "value": value} for key, value in params.items()]
|
|
19
|
+
return body
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Addons(Resource):
|
|
23
|
+
"""SysAid add-ons and their parameters."""
|
|
24
|
+
|
|
25
|
+
@unverified
|
|
26
|
+
def list(self) -> JSONList:
|
|
27
|
+
"""All add-ons (``params`` is always ``None`` in this list)."""
|
|
28
|
+
result: JSONList = self._client.request("GET", "/addons")
|
|
29
|
+
return result
|
|
30
|
+
|
|
31
|
+
@unverified
|
|
32
|
+
def get(self, name: str) -> JSONDict:
|
|
33
|
+
"""The add-on with its ``params``."""
|
|
34
|
+
result: JSONDict = self._client.request("GET", f"/addons/{quote_segment(name)}")
|
|
35
|
+
return result
|
|
36
|
+
|
|
37
|
+
@unverified
|
|
38
|
+
def update(
|
|
39
|
+
self, name: str, *, active: bool | None = None, params: Mapping[str, Any] | None = None
|
|
40
|
+
) -> Any:
|
|
41
|
+
"""Set ``active`` and/or parameter values (``{param name: value}``).
|
|
42
|
+
|
|
43
|
+
Only these are updated server-side. The server answers with a message.
|
|
44
|
+
"""
|
|
45
|
+
return self._client.request(
|
|
46
|
+
"PUT", f"/addons/{quote_segment(name)}", json=_payload(name, active, params)
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
@unverified
|
|
50
|
+
def test_connection(
|
|
51
|
+
self, name: str, *, active: bool | None = None, params: Mapping[str, Any] | None = None
|
|
52
|
+
) -> Any:
|
|
53
|
+
"""Same payload as :meth:`update`, but only tests; nothing is saved."""
|
|
54
|
+
return self._client.request(
|
|
55
|
+
"PUT",
|
|
56
|
+
f"/addons/{quote_segment(name)}/testConnection",
|
|
57
|
+
json=_payload(name, active, params),
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
@unverified
|
|
61
|
+
def refresh(self) -> Any:
|
|
62
|
+
"""Refresh the add-ons list immediately."""
|
|
63
|
+
return self._client.request("GET", "/addons/refresh")
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Assets: ``/asset``. Ids look like ``497db453:147bee7ec09:-7ff2`` and are URL-encoded."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterator, Sequence
|
|
6
|
+
|
|
7
|
+
from .._params import quote_segment
|
|
8
|
+
from .._unverified import unverified
|
|
9
|
+
from ..models import Record
|
|
10
|
+
from ._base import DEFAULT_PAGE_SIZE, RecordList, Resource, query
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Assets(Resource):
|
|
14
|
+
"""Read-only access to assets."""
|
|
15
|
+
|
|
16
|
+
@unverified
|
|
17
|
+
def list(
|
|
18
|
+
self,
|
|
19
|
+
*,
|
|
20
|
+
type: str | None = None,
|
|
21
|
+
view: str | None = None,
|
|
22
|
+
fields: Sequence[str] | None = None,
|
|
23
|
+
offset: int | None = None,
|
|
24
|
+
limit: int | None = None,
|
|
25
|
+
) -> RecordList:
|
|
26
|
+
"""``type`` is listed in the Help index only; it is passed through when given."""
|
|
27
|
+
params = query(view=view, fields=fields, type=type, offset=offset, limit=limit)
|
|
28
|
+
return self._list("/asset", params)
|
|
29
|
+
|
|
30
|
+
@unverified
|
|
31
|
+
def iter(
|
|
32
|
+
self,
|
|
33
|
+
*,
|
|
34
|
+
type: str | None = None,
|
|
35
|
+
view: str | None = None,
|
|
36
|
+
fields: Sequence[str] | None = None,
|
|
37
|
+
page_size: int = DEFAULT_PAGE_SIZE,
|
|
38
|
+
) -> Iterator[Record]:
|
|
39
|
+
"""Every asset, fetching pages transparently."""
|
|
40
|
+
return self._iter("/asset", query(view=view, fields=fields, type=type), page_size=page_size)
|
|
41
|
+
|
|
42
|
+
@unverified
|
|
43
|
+
def get(
|
|
44
|
+
self, asset_id: str, *, view: str | None = None, fields: Sequence[str] | None = None
|
|
45
|
+
) -> Record:
|
|
46
|
+
"""One asset; ``view`` is an Asset Form view."""
|
|
47
|
+
return self._get(f"/asset/{quote_segment(asset_id)}", query(view=view, fields=fields))
|
|
48
|
+
|
|
49
|
+
@unverified
|
|
50
|
+
def search(
|
|
51
|
+
self,
|
|
52
|
+
text: str,
|
|
53
|
+
*,
|
|
54
|
+
view: str | None = None,
|
|
55
|
+
fields: Sequence[str] | None = None,
|
|
56
|
+
offset: int | None = None,
|
|
57
|
+
limit: int | None = None,
|
|
58
|
+
) -> RecordList:
|
|
59
|
+
"""Search assets by free text."""
|
|
60
|
+
params = query(view=view, fields=fields, query=text, offset=offset, limit=limit)
|
|
61
|
+
return self._list("/asset/search", params)
|