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,205 @@
1
+ Metadata-Version: 2.5
2
+ Name: python-sysaid
3
+ Version: 0.1.0
4
+ Summary: Python wrapper for the SysAid REST API
5
+ License-Expression: MIT
6
+ License-File: LICENSE
7
+ Keywords: api,itsm,rest,sysaid
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Typing :: Typed
16
+ Requires-Python: >=3.10
17
+ Requires-Dist: requests>=2.31
18
+ Provides-Extra: dev
19
+ Requires-Dist: build; extra == 'dev'
20
+ Requires-Dist: mypy; extra == 'dev'
21
+ Requires-Dist: pytest; extra == 'dev'
22
+ Requires-Dist: requests-oauthlib>=1.3; extra == 'dev'
23
+ Requires-Dist: responses; extra == 'dev'
24
+ Requires-Dist: ruff; extra == 'dev'
25
+ Requires-Dist: twine; extra == 'dev'
26
+ Requires-Dist: types-requests; extra == 'dev'
27
+ Requires-Dist: types-requests-oauthlib; extra == 'dev'
28
+ Provides-Extra: oauth
29
+ Requires-Dist: requests-oauthlib>=1.3; extra == 'oauth'
30
+ Description-Content-Type: text/markdown
31
+
32
+ # python-sysaid
33
+
34
+ Python wrapper for the SysAid REST API (`/api/v1`, SysAid 15.4+).
35
+
36
+ > Status: alpha, under development. Requests and paths come from the SysAid
37
+ > documentation and have not been validated against a live server yet.
38
+
39
+ ## Installation
40
+
41
+ ```bash
42
+ pip install python-sysaid
43
+ pip install "python-sysaid[oauth]" # optional: OAuth 1.0
44
+ ```
45
+
46
+ Requires Python 3.10+.
47
+
48
+ ## Quick start
49
+
50
+ ```python
51
+ from sysaid import SysAid
52
+
53
+ with SysAid("https://sysaid.example.com", username="sysaid", password="...") as client:
54
+ sr = client.service_requests.get(273, fields=["title", "status"])
55
+ sr["title"] # raw value
56
+ sr.caption("status") # display value
57
+
58
+ for sr in client.service_requests.iter(type="incident", status=[4, 5]):
59
+ print(sr.id, sr["title"])
60
+
61
+ client.service_requests.update(273, status=2, responsibility=66)
62
+ client.service_requests.close(273, solution="restarted")
63
+ ```
64
+
65
+ The client logs in on first use (`POST /login`, the `JSESSIONID` cookie is kept by the
66
+ session). The account must be an administrator with mobile-app permission. Pass
67
+ `account_id=` if your installation needs one, and `verify=`/`timeout=`/`session=` to
68
+ control the underlying `requests` session.
69
+
70
+ ## Records
71
+
72
+ List and get calls return `Record` objects, which behave like a read-only mapping of
73
+ field key to raw value:
74
+
75
+ ```python
76
+ record["status"] # raw value (an id, ms timestamp or text)
77
+ record.caption("status") # display value (valueCaption)
78
+ record.fields["status"] # Field: key_caption, mandatory, editable, type, ...
79
+ record.raw # the original dict (canUpdate, name, group, ...)
80
+ ```
81
+
82
+ ## Pagination
83
+
84
+ `list()` returns one page (`limit`/`offset`); `iter()` yields every record and stops at the
85
+ first page shorter than `page_size` (default 100):
86
+
87
+ ```python
88
+ client.service_requests.list(limit=50, offset=100)
89
+ client.service_requests.iter(page_size=200)
90
+ ```
91
+
92
+ ## Filters and parameters
93
+
94
+ Filter ids come from `client.filters`. Pass them as keyword arguments:
95
+
96
+ ```python
97
+ client.service_requests.list(status=[4, 5], request_user=235) # status=4,5&request_user=235
98
+ client.service_requests.list(archive=True) # archive=1
99
+ ```
100
+
101
+ Use `view=` and `fields=` to choose the returned fields, and `sort=`/`direction=`.
102
+
103
+ ## Dates
104
+
105
+ Datetimes are sent as milliseconds since the epoch in UTC (naive datetimes are taken as
106
+ UTC). A `(from, to)` tuple is a range, with `None` for an open end:
107
+
108
+ ```python
109
+ from datetime import datetime, timezone
110
+
111
+ since = datetime(2024, 1, 1, tzinfo=timezone.utc)
112
+ client.service_requests.list(due_date=(since, None)) # due_date=<ms>,0
113
+ client.service_requests.update(273, due_date=datetime.now(timezone.utc))
114
+ ```
115
+
116
+ ## Writing
117
+
118
+ Write calls take field values as keywords. For field ids that clash with a method's own
119
+ keywords (the SR field `type`), pass a mapping as the first argument:
120
+
121
+ ```python
122
+ from sysaid.resources.service_requests import make_note, problem_type
123
+
124
+ client.service_requests.create(
125
+ {"type": 3},
126
+ type="incident", # the SR type query parameter
127
+ template=39,
128
+ title="Printer down",
129
+ problem_type=problem_type("UserWorkstation", "PC", "Password"),
130
+ notes=[make_note("sysaid", "Created from the API")],
131
+ )
132
+ ```
133
+
134
+ `client.service_requests.template(type="incident")` shows the mandatory fields first.
135
+
136
+ Numbers, booleans and datetimes are sent as strings, which is the only form the server
137
+ accepts for field values. `add_activity` takes the numeric id of the user.
138
+
139
+ ## Resources
140
+
141
+ | Attribute | Covers |
142
+ |---|---|
143
+ | `client.users` | list, iter, get, search, photo get/upload, permissions |
144
+ | `client.filters` / `client.lists` | filter definitions, dropdown id/caption pairs |
145
+ | `client.service_requests` | list, iter, get, search, count, template, create, update, close, links, attachments, activities, `send_message` |
146
+ | `client.resource_bundle` | translate |
147
+
148
+ ### Disabled features
149
+
150
+ These are implemented from the REST guide but have not been verified against a live server
151
+ yet, so calling them raises `UnverifiedFeatureError` before any request is made:
152
+
153
+ | Attribute | Disabled calls |
154
+ |---|---|
155
+ | `client.service_requests` | delete |
156
+ | `client.action_items` | list, iter, count, approve, reject, complete, reopen |
157
+ | `client.assets` | list, iter, get, search |
158
+ | `client.cis` | list, iter, update, types, view_fields, relation types, relations |
159
+ | `client.addons` | list, get, update, test_connection, refresh |
160
+ | `client.password_services` | domains, permissions, questions, unlock, reset, update_password |
161
+ | `client.reports` | operators, run_preview |
162
+ | OAuth 1.0 | `SysAid.from_oauth` and the `sysaid.oauth` helpers |
163
+
164
+ ## Errors
165
+
166
+ Every error derives from `SysAidError`. Non-2xx answers raise a subclass of
167
+ `SysAidHTTPError` (`BadRequestError`, `UnauthorizedError`, `ForbiddenError`,
168
+ `NotFoundError`, `ServerError`) carrying `status_code`, `message` and `response`.
169
+ Failed logins raise `AuthenticationError`. Failed CI relation creation raises
170
+ `RelationError` with the per-item `failures`.
171
+
172
+ ```python
173
+ from sysaid import NotFoundError
174
+
175
+ try:
176
+ client.service_requests.get(999999)
177
+ except NotFoundError as exc:
178
+ print(exc.status_code, exc.message)
179
+ ```
180
+
181
+ ## OAuth 1.0
182
+
183
+ Disabled until verified (see [Disabled features](#disabled-features)). The intended flow
184
+ needs `python-sysaid[oauth]` and a consumer key issued by SysAid:
185
+
186
+ ```python
187
+ from sysaid import SysAid, oauth
188
+
189
+ token = oauth.request_token(url, consumer_key, "https://app/callback")
190
+ print(oauth.authorize_url(url, token["oauth_token"]))
191
+ # ...the user authorizes; SysAid redirects with oauth_verifier...
192
+ access = oauth.access_token(
193
+ url, consumer_key, token["oauth_token"], token["oauth_token_secret"], verifier
194
+ )
195
+ client = SysAid.from_oauth(url, consumer_key, access["oauth_token"], access["oauth_token_secret"])
196
+ ```
197
+
198
+ ## Development
199
+
200
+ See [CONTRIBUTING.md](CONTRIBUTING.md). The endpoint reference is in
201
+ [ANOTATIONS.md](ANOTATIONS.md) and the roadmap in [PLAN.md](PLAN.md).
202
+
203
+ ## License
204
+
205
+ MIT
@@ -0,0 +1,26 @@
1
+ sysaid/__init__.py,sha256=ErK_rfRygwKRSPXMzuaQ8XGC_MWiiNSVVnOGtSnckUA,723
2
+ sysaid/_params.py,sha256=HyIp23Crj5ECFnprui0F2imV9sABmasFEl5OUd5Sy_U,2797
3
+ sysaid/_unverified.py,sha256=wmxcbCg30ZKG-busTFBMVo4ijV_tSc1n6TXJLm-U1bE,968
4
+ sysaid/auth.py,sha256=dzvMgmDn1WrKkNV18wY7ZqhWenFJMXWcJSsTwPbFtQo,2061
5
+ sysaid/client.py,sha256=UiUMo0DwdB3z0MlleGyt553qbP55A2yOmX5tTkpz-8E,5078
6
+ sysaid/exceptions.py,sha256=nxI8csNKcURxe9ZNRDWfrhu-EHUT_WEQ_5xtWh7mOv8,2668
7
+ sysaid/models.py,sha256=2c5qqE-cQCqr-ArydjKwY8ulVl1ZUV5j_9PHWiGZi4w,2490
8
+ sysaid/oauth.py,sha256=WmbK4vilbt1QMnZgv94f-zsQxjz9BJTd_47WymisS-w,2650
9
+ sysaid/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
+ sysaid/resources/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
+ sysaid/resources/_base.py,sha256=dfEu5iBzeuYj5pvIhC7fCzBmCNJerJVLFN4Fw5AZ5wI,2304
12
+ sysaid/resources/action_items.py,sha256=tkVa3rNRGpZUD9ZJQgcQHNOob6KV-iVoXBUvr4gWec0,4097
13
+ sysaid/resources/addons.py,sha256=L1EWLAeRCtmbvkIhYBif2XpcI12Yx3MKI-ISq1P9cRQ,2083
14
+ sysaid/resources/assets.py,sha256=Yeb5lBo3-ntKD53IPiFMnP394O0X3shjJcG3eAkyfYk,2002
15
+ sysaid/resources/cis.py,sha256=a8Akeha_AEt-BvAReu1wzHROQG65pq0hMvLEDWWs0s4,4626
16
+ sysaid/resources/filters.py,sha256=R1ifN9xHt_KhKSo_d7hRt5P_6xPcdyn5GTe9jEqOjIw,1258
17
+ sysaid/resources/lists.py,sha256=65-c8_0JSxtllMQaf09SfDeH8-I9f_JxiWbO3V1zLlM,1624
18
+ sysaid/resources/password_services.py,sha256=F-Gg_MoemF8DS0Si1GpMCexDZtPlA9YxWzLbBQ3QRbU,2868
19
+ sysaid/resources/reports.py,sha256=wlockOuPc71dDcjoZqolINxwSofftVCjAlNJLdwtYV8,1013
20
+ sysaid/resources/resource_bundle.py,sha256=IjnJJPhxobsZk0Vs-WYkjkG49I1Fv6zNbTwyrnzJ9yE,667
21
+ sysaid/resources/service_requests.py,sha256=uFX9h1WMgX6wWiyQnfuCKjn5EEFfH2T-9yCZdnaC-Ws,9842
22
+ sysaid/resources/users.py,sha256=wq_1CX5LlbNnLio9FCj1H1OxlsDGAoXY8i1k0hNmRtw,3756
23
+ python_sysaid-0.1.0.dist-info/METADATA,sha256=ky-d8skM-f7mlCAo_KLQx-k9d94X-rMPrxYBIA62yRA,7106
24
+ python_sysaid-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
25
+ python_sysaid-0.1.0.dist-info/licenses/LICENSE,sha256=0oGaM8V0v4y5Af4LIP6awcbtI_Ub_t-wsnfYfsFXhXs,1070
26
+ python_sysaid-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Bruno Martins
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
sysaid/__init__.py ADDED
@@ -0,0 +1,37 @@
1
+ """Python wrapper for the SysAid REST API."""
2
+
3
+ from .auth import LoginResult
4
+ from .client import SysAid
5
+ from .exceptions import (
6
+ AuthenticationError,
7
+ BadRequestError,
8
+ ForbiddenError,
9
+ NotFoundError,
10
+ RelationError,
11
+ ServerError,
12
+ SysAidError,
13
+ SysAidHTTPError,
14
+ UnauthorizedError,
15
+ UnverifiedFeatureError,
16
+ )
17
+ from .models import Field, Record
18
+
19
+ __version__ = "0.1.0"
20
+
21
+ __all__ = [
22
+ "AuthenticationError",
23
+ "BadRequestError",
24
+ "Field",
25
+ "ForbiddenError",
26
+ "LoginResult",
27
+ "NotFoundError",
28
+ "Record",
29
+ "RelationError",
30
+ "ServerError",
31
+ "SysAid",
32
+ "SysAidError",
33
+ "SysAidHTTPError",
34
+ "UnauthorizedError",
35
+ "UnverifiedFeatureError",
36
+ "__version__",
37
+ ]
sysaid/_params.py ADDED
@@ -0,0 +1,80 @@
1
+ """Encoding of Python values into SysAid query parameters and JSON bodies."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping
6
+ from datetime import datetime, timezone
7
+ from typing import Any
8
+ from urllib.parse import quote
9
+
10
+
11
+ def to_ms(value: datetime) -> int:
12
+ """Milliseconds since the epoch, UTC. Naive datetimes are taken as UTC."""
13
+ if value.tzinfo is None:
14
+ value = value.replace(tzinfo=timezone.utc)
15
+ return round(value.timestamp() * 1000)
16
+
17
+
18
+ def _is_date_range(value: tuple[Any, ...]) -> bool:
19
+ return (
20
+ len(value) == 2
21
+ and all(item is None or isinstance(item, datetime) for item in value)
22
+ and any(item is not None for item in value)
23
+ )
24
+
25
+
26
+ def encode_value(value: Any) -> str:
27
+ """Encode one query value.
28
+
29
+ ``bool`` -> ``"true"``/``"false"``; ``datetime`` -> ms; a list or tuple -> CSV;
30
+ a ``(from, to)`` tuple of datetimes (``None`` = open end) -> ``"from,to"`` with ``0``
31
+ for the open end.
32
+ """
33
+ if isinstance(value, bool):
34
+ return "true" if value else "false"
35
+ if isinstance(value, datetime):
36
+ return str(to_ms(value))
37
+ if isinstance(value, tuple) and _is_date_range(value):
38
+ return ",".join("0" if item is None else str(to_ms(item)) for item in value)
39
+ if isinstance(value, (list, tuple)):
40
+ return ",".join(encode_value(item) for item in value)
41
+ return str(value)
42
+
43
+
44
+ def build_params(params: Mapping[str, Any]) -> dict[str, str]:
45
+ """Encode query parameters, dropping those whose value is ``None``."""
46
+ return {key: encode_value(value) for key, value in params.items() if value is not None}
47
+
48
+
49
+ def encode_json(value: Any) -> Any:
50
+ """Recursively convert datetimes in a JSON body to ms-epoch integers."""
51
+ if isinstance(value, datetime):
52
+ return to_ms(value)
53
+ if isinstance(value, Mapping):
54
+ return {key: encode_json(item) for key, item in value.items()}
55
+ if isinstance(value, (list, tuple)):
56
+ return [encode_json(item) for item in value]
57
+ return value
58
+
59
+
60
+ def encode_info(fields: Mapping[str, Any]) -> list[dict[str, Any]]:
61
+ """Build the ``info`` array of ``{key, value}`` objects used by write calls.
62
+
63
+ The server only accepts scalar values as strings (a JSON number is answered with
64
+ HTTP 500), so numbers, booleans and datetimes are sent as text. Structured values
65
+ such as ``notes`` keep their JSON shape.
66
+ """
67
+ return [
68
+ {
69
+ "key": key,
70
+ "value": encode_value(value)
71
+ if isinstance(value, (int, float, datetime))
72
+ else encode_json(value),
73
+ }
74
+ for key, value in fields.items()
75
+ ]
76
+
77
+
78
+ def quote_segment(value: object) -> str:
79
+ """Percent-encode one URL path segment (asset ids contain ``:``)."""
80
+ return quote(str(value), safe="")
sysaid/_unverified.py ADDED
@@ -0,0 +1,33 @@
1
+ """Switch-off for features that have not been verified against a live SysAid server."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable
6
+ from functools import wraps
7
+ from typing import ParamSpec, TypeVar
8
+
9
+ from .exceptions import UnverifiedFeatureError
10
+
11
+ P = ParamSpec("P")
12
+ R = TypeVar("R")
13
+
14
+ # The test suite sets this to ``False`` to keep exercising the disabled code.
15
+ DISABLED = True
16
+
17
+
18
+ def unverified(func: Callable[P, R]) -> Callable[P, R]:
19
+ """Make ``func`` raise :class:`UnverifiedFeatureError` instead of running.
20
+
21
+ Remove the decorator once the call has been verified against a live server.
22
+ """
23
+
24
+ @wraps(func)
25
+ def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
26
+ if DISABLED:
27
+ raise UnverifiedFeatureError(
28
+ f"{func.__qualname__} is disabled: "
29
+ "it has not been verified against a live SysAid server"
30
+ )
31
+ return func(*args, **kwargs)
32
+
33
+ return wrapper
sysaid/auth.py ADDED
@@ -0,0 +1,62 @@
1
+ """Session-cookie login (``POST /login``)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import TYPE_CHECKING, Any
7
+
8
+ from .exceptions import AuthenticationError, ForbiddenError, UnauthorizedError
9
+ from .models import Record
10
+
11
+ if TYPE_CHECKING:
12
+ from .client import SysAid
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class LoginResult:
17
+ """Outcome of a successful login."""
18
+
19
+ logged_in: bool
20
+ user_id: str | None
21
+ language: str | None
22
+ sysaid_version: str | None
23
+ date_format: str | None
24
+ error_msg: str | None
25
+ user: Record | None
26
+ raw: dict[str, Any]
27
+
28
+ @classmethod
29
+ def from_dict(cls, data: dict[str, Any]) -> LoginResult:
30
+ """Build from the ``/login`` response body."""
31
+ user = Record(data["user"]) if isinstance(data.get("user"), dict) else None
32
+ user_id = data.get("user_id") or (user.id if user else None)
33
+ return cls(
34
+ logged_in=data.get("logged_in", True) is not False,
35
+ user_id=None if user_id is None else str(user_id),
36
+ language=data.get("language"),
37
+ sysaid_version=data.get("sysaid_version"),
38
+ date_format=data.get("date_format"),
39
+ error_msg=data.get("error_msg"),
40
+ user=user,
41
+ raw=data,
42
+ )
43
+
44
+
45
+ def login(
46
+ client: SysAid, username: str, password: str, account_id: str | None = None
47
+ ) -> LoginResult:
48
+ """Authenticate; the ``JSESSIONID`` cookie is kept by the client's session."""
49
+ body = {"user_name": username, "password": password, "account_id": account_id}
50
+ try:
51
+ data = client.request(
52
+ "POST",
53
+ "/login",
54
+ json={key: value for key, value in body.items() if value is not None},
55
+ auth=False,
56
+ )
57
+ except (UnauthorizedError, ForbiddenError) as exc:
58
+ raise AuthenticationError(exc.message) from exc
59
+ result = LoginResult.from_dict(data if isinstance(data, dict) else {})
60
+ if not result.logged_in:
61
+ raise AuthenticationError(result.error_msg or "login failed")
62
+ return result
sysaid/client.py ADDED
@@ -0,0 +1,160 @@
1
+ """The :class:`SysAid` client."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping
6
+ from types import TracebackType
7
+ from typing import Any
8
+
9
+ import requests
10
+
11
+ from . import auth as _auth
12
+ from ._params import build_params, encode_json
13
+ from ._unverified import unverified
14
+ from .exceptions import AuthenticationError, error_from_response
15
+ from .resources.action_items import ActionItems
16
+ from .resources.addons import Addons
17
+ from .resources.assets import Assets
18
+ from .resources.cis import CIs
19
+ from .resources.filters import Filters
20
+ from .resources.lists import Lists
21
+ from .resources.password_services import PasswordServices
22
+ from .resources.reports import Reports
23
+ from .resources.resource_bundle import ResourceBundle
24
+ from .resources.service_requests import ServiceRequests
25
+ from .resources.users import Users
26
+
27
+ API_PATH = "/api/v1"
28
+
29
+
30
+ def api_url_for(base_url: str) -> str:
31
+ """``https://host`` or ``https://host/api/v1`` -> ``https://host/api/v1``."""
32
+ base = base_url.rstrip("/")
33
+ return base if base.endswith(API_PATH) else base + API_PATH
34
+
35
+
36
+ class SysAid:
37
+ """Client for the SysAid REST API.
38
+
39
+ Credentials are optional: with ``username`` and ``password`` the client logs in
40
+ on first use; without them only unauthenticated calls (password services) work.
41
+ """
42
+
43
+ def __init__(
44
+ self,
45
+ base_url: str,
46
+ username: str | None = None,
47
+ password: str | None = None,
48
+ account_id: str | None = None,
49
+ timeout: float | None = 30,
50
+ verify: bool | str = True,
51
+ session: requests.Session | None = None,
52
+ ) -> None:
53
+ self.api_url = api_url_for(base_url)
54
+ self.account_id = account_id
55
+ self.timeout = timeout
56
+ self.session = session or requests.Session()
57
+ self.session.verify = verify
58
+ self._username = username
59
+ self._password = password
60
+ self._logged_in = False
61
+ self.users = Users(self)
62
+ self.filters = Filters(self)
63
+ self.lists = Lists(self)
64
+ self.service_requests = ServiceRequests(self)
65
+ self.action_items = ActionItems(self)
66
+ self.assets = Assets(self)
67
+ self.cis = CIs(self)
68
+ self.addons = Addons(self)
69
+ self.resource_bundle = ResourceBundle(self)
70
+ self.password_services = PasswordServices(self)
71
+ self.reports = Reports(self)
72
+
73
+ @classmethod
74
+ @unverified
75
+ def from_oauth(
76
+ cls,
77
+ base_url: str,
78
+ consumer_key: str,
79
+ access_token: str,
80
+ access_token_secret: str,
81
+ consumer_secret: str = "",
82
+ **kwargs: Any,
83
+ ) -> SysAid:
84
+ """A client that signs every request with OAuth 1.0 (needs ``python-sysaid[oauth]``)."""
85
+ from .oauth import oauth1
86
+
87
+ client = cls(base_url, **kwargs)
88
+ client.session.auth = oauth1(
89
+ consumer_key,
90
+ consumer_secret,
91
+ resource_owner_key=access_token,
92
+ resource_owner_secret=access_token_secret,
93
+ )
94
+ return client
95
+
96
+ def __repr__(self) -> str:
97
+ return f"SysAid({self.api_url!r}, username={self._username!r})"
98
+
99
+ def __enter__(self) -> SysAid:
100
+ return self
101
+
102
+ def __exit__(
103
+ self,
104
+ exc_type: type[BaseException] | None,
105
+ exc: BaseException | None,
106
+ tb: TracebackType | None,
107
+ ) -> None:
108
+ self.close()
109
+
110
+ def close(self) -> None:
111
+ """Close the underlying HTTP session."""
112
+ self.session.close()
113
+ self._logged_in = False
114
+
115
+ def login(self) -> _auth.LoginResult:
116
+ """Log in with the configured credentials."""
117
+ if self._username is None or self._password is None:
118
+ raise AuthenticationError("username and password are required to log in")
119
+ result = _auth.login(self, self._username, self._password, self.account_id)
120
+ self._logged_in = True
121
+ return result
122
+
123
+ def request(
124
+ self,
125
+ method: str,
126
+ path: str,
127
+ *,
128
+ params: Mapping[str, Any] | None = None,
129
+ json: Any = None,
130
+ data: Mapping[str, Any] | None = None,
131
+ files: Any = None,
132
+ auth: bool = True,
133
+ raw: bool = False,
134
+ ) -> Any:
135
+ """Send a request and return the decoded JSON (text if not JSON, ``None`` if empty).
136
+
137
+ With ``raw=True`` the :class:`requests.Response` is returned instead.
138
+ With ``auth=False`` no login is attempted first.
139
+ """
140
+ if auth and not self._logged_in and self._username is not None:
141
+ self.login()
142
+ response = self.session.request(
143
+ method,
144
+ self.api_url + "/" + path.lstrip("/"),
145
+ params=build_params(params or {}),
146
+ json=encode_json(json),
147
+ data=data,
148
+ files=files,
149
+ timeout=self.timeout,
150
+ )
151
+ if not response.ok:
152
+ raise error_from_response(response)
153
+ if raw:
154
+ return response
155
+ if not response.content:
156
+ return None
157
+ try:
158
+ return response.json()
159
+ except ValueError:
160
+ return response.text