python-sysaid 0.1.0__tar.gz

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.
Files changed (57) hide show
  1. python_sysaid-0.1.0/.gitignore +16 -0
  2. python_sysaid-0.1.0/CHANGELOG.md +13 -0
  3. python_sysaid-0.1.0/LICENSE +21 -0
  4. python_sysaid-0.1.0/PKG-INFO +205 -0
  5. python_sysaid-0.1.0/README.md +174 -0
  6. python_sysaid-0.1.0/pyproject.toml +66 -0
  7. python_sysaid-0.1.0/src/sysaid/__init__.py +37 -0
  8. python_sysaid-0.1.0/src/sysaid/_params.py +80 -0
  9. python_sysaid-0.1.0/src/sysaid/_unverified.py +33 -0
  10. python_sysaid-0.1.0/src/sysaid/auth.py +62 -0
  11. python_sysaid-0.1.0/src/sysaid/client.py +160 -0
  12. python_sysaid-0.1.0/src/sysaid/exceptions.py +95 -0
  13. python_sysaid-0.1.0/src/sysaid/models.py +76 -0
  14. python_sysaid-0.1.0/src/sysaid/oauth.py +85 -0
  15. python_sysaid-0.1.0/src/sysaid/py.typed +0 -0
  16. python_sysaid-0.1.0/src/sysaid/resources/__init__.py +0 -0
  17. python_sysaid-0.1.0/src/sysaid/resources/_base.py +70 -0
  18. python_sysaid-0.1.0/src/sysaid/resources/action_items.py +127 -0
  19. python_sysaid-0.1.0/src/sysaid/resources/addons.py +63 -0
  20. python_sysaid-0.1.0/src/sysaid/resources/assets.py +61 -0
  21. python_sysaid-0.1.0/src/sysaid/resources/cis.py +134 -0
  22. python_sysaid-0.1.0/src/sysaid/resources/filters.py +44 -0
  23. python_sysaid-0.1.0/src/sysaid/resources/lists.py +56 -0
  24. python_sysaid-0.1.0/src/sysaid/resources/password_services.py +74 -0
  25. python_sysaid-0.1.0/src/sysaid/resources/reports.py +30 -0
  26. python_sysaid-0.1.0/src/sysaid/resources/resource_bundle.py +18 -0
  27. python_sysaid-0.1.0/src/sysaid/resources/service_requests.py +260 -0
  28. python_sysaid-0.1.0/src/sysaid/resources/users.py +108 -0
  29. python_sysaid-0.1.0/tests/__init__.py +0 -0
  30. python_sysaid-0.1.0/tests/conftest.py +44 -0
  31. python_sysaid-0.1.0/tests/fixtures/filters.json +5 -0
  32. python_sysaid-0.1.0/tests/fixtures/lists.json +5 -0
  33. python_sysaid-0.1.0/tests/fixtures/sr_list.json +13 -0
  34. python_sysaid-0.1.0/tests/fixtures/users_list.json +12 -0
  35. python_sysaid-0.1.0/tests/integration/__init__.py +0 -0
  36. python_sysaid-0.1.0/tests/integration/conftest.py +36 -0
  37. python_sysaid-0.1.0/tests/integration/test_read.py +166 -0
  38. python_sysaid-0.1.0/tests/integration/test_write.py +126 -0
  39. python_sysaid-0.1.0/tests/unit/__init__.py +0 -0
  40. python_sysaid-0.1.0/tests/unit/test_action_items.py +68 -0
  41. python_sysaid-0.1.0/tests/unit/test_addons.py +37 -0
  42. python_sysaid-0.1.0/tests/unit/test_assets.py +53 -0
  43. python_sysaid-0.1.0/tests/unit/test_cis.py +89 -0
  44. python_sysaid-0.1.0/tests/unit/test_client.py +198 -0
  45. python_sysaid-0.1.0/tests/unit/test_filters.py +29 -0
  46. python_sysaid-0.1.0/tests/unit/test_lists.py +31 -0
  47. python_sysaid-0.1.0/tests/unit/test_models.py +60 -0
  48. python_sysaid-0.1.0/tests/unit/test_oauth.py +66 -0
  49. python_sysaid-0.1.0/tests/unit/test_params.py +57 -0
  50. python_sysaid-0.1.0/tests/unit/test_password_services.py +50 -0
  51. python_sysaid-0.1.0/tests/unit/test_reports.py +26 -0
  52. python_sysaid-0.1.0/tests/unit/test_resource_bundle.py +17 -0
  53. python_sysaid-0.1.0/tests/unit/test_service_requests_read.py +112 -0
  54. python_sysaid-0.1.0/tests/unit/test_service_requests_sub.py +104 -0
  55. python_sysaid-0.1.0/tests/unit/test_service_requests_write.py +102 -0
  56. python_sysaid-0.1.0/tests/unit/test_unverified.py +57 -0
  57. python_sysaid-0.1.0/tests/unit/test_users.py +102 -0
@@ -0,0 +1,16 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ .venv/
5
+ venv/
6
+ build/
7
+ dist/
8
+ .mypy_cache/
9
+ .pytest_cache/
10
+ .ruff_cache/
11
+ .coverage
12
+ .env
13
+
14
+ # Local credentials, never commit
15
+ token
16
+ temp.sh
@@ -0,0 +1,13 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file.
4
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
5
+ and this project adheres to [Semantic Versioning](https://semver.org/).
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [0.1.0] - 2026-10-09
10
+
11
+ ### Added
12
+
13
+ - Initial release.
@@ -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.
@@ -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,174 @@
1
+ # python-sysaid
2
+
3
+ Python wrapper for the SysAid REST API (`/api/v1`, SysAid 15.4+).
4
+
5
+ > Status: alpha, under development. Requests and paths come from the SysAid
6
+ > documentation and have not been validated against a live server yet.
7
+
8
+ ## Installation
9
+
10
+ ```bash
11
+ pip install python-sysaid
12
+ pip install "python-sysaid[oauth]" # optional: OAuth 1.0
13
+ ```
14
+
15
+ Requires Python 3.10+.
16
+
17
+ ## Quick start
18
+
19
+ ```python
20
+ from sysaid import SysAid
21
+
22
+ with SysAid("https://sysaid.example.com", username="sysaid", password="...") as client:
23
+ sr = client.service_requests.get(273, fields=["title", "status"])
24
+ sr["title"] # raw value
25
+ sr.caption("status") # display value
26
+
27
+ for sr in client.service_requests.iter(type="incident", status=[4, 5]):
28
+ print(sr.id, sr["title"])
29
+
30
+ client.service_requests.update(273, status=2, responsibility=66)
31
+ client.service_requests.close(273, solution="restarted")
32
+ ```
33
+
34
+ The client logs in on first use (`POST /login`, the `JSESSIONID` cookie is kept by the
35
+ session). The account must be an administrator with mobile-app permission. Pass
36
+ `account_id=` if your installation needs one, and `verify=`/`timeout=`/`session=` to
37
+ control the underlying `requests` session.
38
+
39
+ ## Records
40
+
41
+ List and get calls return `Record` objects, which behave like a read-only mapping of
42
+ field key to raw value:
43
+
44
+ ```python
45
+ record["status"] # raw value (an id, ms timestamp or text)
46
+ record.caption("status") # display value (valueCaption)
47
+ record.fields["status"] # Field: key_caption, mandatory, editable, type, ...
48
+ record.raw # the original dict (canUpdate, name, group, ...)
49
+ ```
50
+
51
+ ## Pagination
52
+
53
+ `list()` returns one page (`limit`/`offset`); `iter()` yields every record and stops at the
54
+ first page shorter than `page_size` (default 100):
55
+
56
+ ```python
57
+ client.service_requests.list(limit=50, offset=100)
58
+ client.service_requests.iter(page_size=200)
59
+ ```
60
+
61
+ ## Filters and parameters
62
+
63
+ Filter ids come from `client.filters`. Pass them as keyword arguments:
64
+
65
+ ```python
66
+ client.service_requests.list(status=[4, 5], request_user=235) # status=4,5&request_user=235
67
+ client.service_requests.list(archive=True) # archive=1
68
+ ```
69
+
70
+ Use `view=` and `fields=` to choose the returned fields, and `sort=`/`direction=`.
71
+
72
+ ## Dates
73
+
74
+ Datetimes are sent as milliseconds since the epoch in UTC (naive datetimes are taken as
75
+ UTC). A `(from, to)` tuple is a range, with `None` for an open end:
76
+
77
+ ```python
78
+ from datetime import datetime, timezone
79
+
80
+ since = datetime(2024, 1, 1, tzinfo=timezone.utc)
81
+ client.service_requests.list(due_date=(since, None)) # due_date=<ms>,0
82
+ client.service_requests.update(273, due_date=datetime.now(timezone.utc))
83
+ ```
84
+
85
+ ## Writing
86
+
87
+ Write calls take field values as keywords. For field ids that clash with a method's own
88
+ keywords (the SR field `type`), pass a mapping as the first argument:
89
+
90
+ ```python
91
+ from sysaid.resources.service_requests import make_note, problem_type
92
+
93
+ client.service_requests.create(
94
+ {"type": 3},
95
+ type="incident", # the SR type query parameter
96
+ template=39,
97
+ title="Printer down",
98
+ problem_type=problem_type("UserWorkstation", "PC", "Password"),
99
+ notes=[make_note("sysaid", "Created from the API")],
100
+ )
101
+ ```
102
+
103
+ `client.service_requests.template(type="incident")` shows the mandatory fields first.
104
+
105
+ Numbers, booleans and datetimes are sent as strings, which is the only form the server
106
+ accepts for field values. `add_activity` takes the numeric id of the user.
107
+
108
+ ## Resources
109
+
110
+ | Attribute | Covers |
111
+ |---|---|
112
+ | `client.users` | list, iter, get, search, photo get/upload, permissions |
113
+ | `client.filters` / `client.lists` | filter definitions, dropdown id/caption pairs |
114
+ | `client.service_requests` | list, iter, get, search, count, template, create, update, close, links, attachments, activities, `send_message` |
115
+ | `client.resource_bundle` | translate |
116
+
117
+ ### Disabled features
118
+
119
+ These are implemented from the REST guide but have not been verified against a live server
120
+ yet, so calling them raises `UnverifiedFeatureError` before any request is made:
121
+
122
+ | Attribute | Disabled calls |
123
+ |---|---|
124
+ | `client.service_requests` | delete |
125
+ | `client.action_items` | list, iter, count, approve, reject, complete, reopen |
126
+ | `client.assets` | list, iter, get, search |
127
+ | `client.cis` | list, iter, update, types, view_fields, relation types, relations |
128
+ | `client.addons` | list, get, update, test_connection, refresh |
129
+ | `client.password_services` | domains, permissions, questions, unlock, reset, update_password |
130
+ | `client.reports` | operators, run_preview |
131
+ | OAuth 1.0 | `SysAid.from_oauth` and the `sysaid.oauth` helpers |
132
+
133
+ ## Errors
134
+
135
+ Every error derives from `SysAidError`. Non-2xx answers raise a subclass of
136
+ `SysAidHTTPError` (`BadRequestError`, `UnauthorizedError`, `ForbiddenError`,
137
+ `NotFoundError`, `ServerError`) carrying `status_code`, `message` and `response`.
138
+ Failed logins raise `AuthenticationError`. Failed CI relation creation raises
139
+ `RelationError` with the per-item `failures`.
140
+
141
+ ```python
142
+ from sysaid import NotFoundError
143
+
144
+ try:
145
+ client.service_requests.get(999999)
146
+ except NotFoundError as exc:
147
+ print(exc.status_code, exc.message)
148
+ ```
149
+
150
+ ## OAuth 1.0
151
+
152
+ Disabled until verified (see [Disabled features](#disabled-features)). The intended flow
153
+ needs `python-sysaid[oauth]` and a consumer key issued by SysAid:
154
+
155
+ ```python
156
+ from sysaid import SysAid, oauth
157
+
158
+ token = oauth.request_token(url, consumer_key, "https://app/callback")
159
+ print(oauth.authorize_url(url, token["oauth_token"]))
160
+ # ...the user authorizes; SysAid redirects with oauth_verifier...
161
+ access = oauth.access_token(
162
+ url, consumer_key, token["oauth_token"], token["oauth_token_secret"], verifier
163
+ )
164
+ client = SysAid.from_oauth(url, consumer_key, access["oauth_token"], access["oauth_token_secret"])
165
+ ```
166
+
167
+ ## Development
168
+
169
+ See [CONTRIBUTING.md](CONTRIBUTING.md). The endpoint reference is in
170
+ [ANOTATIONS.md](ANOTATIONS.md) and the roadmap in [PLAN.md](PLAN.md).
171
+
172
+ ## License
173
+
174
+ MIT
@@ -0,0 +1,66 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "python-sysaid"
7
+ dynamic = ["version"]
8
+ description = "Python wrapper for the SysAid REST API"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ keywords = ["sysaid", "api", "itsm", "rest"]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Intended Audience :: Developers",
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3.10",
19
+ "Programming Language :: Python :: 3.11",
20
+ "Programming Language :: Python :: 3.12",
21
+ "Programming Language :: Python :: 3.13",
22
+ "Typing :: Typed",
23
+ ]
24
+ dependencies = ["requests>=2.31"]
25
+
26
+ [project.optional-dependencies]
27
+ oauth = ["requests-oauthlib>=1.3"]
28
+ dev = [
29
+ "build",
30
+ "mypy",
31
+ "pytest",
32
+ "requests-oauthlib>=1.3",
33
+ "responses",
34
+ "ruff",
35
+ "twine",
36
+ "types-requests",
37
+ "types-requests-oauthlib",
38
+ ]
39
+
40
+ [tool.hatch.version]
41
+ path = "src/sysaid/__init__.py"
42
+
43
+ [tool.hatch.build.targets.sdist]
44
+ include = ["src", "tests", "CHANGELOG.md"]
45
+
46
+ [tool.hatch.build.targets.wheel]
47
+ packages = ["src/sysaid"]
48
+
49
+ [tool.ruff]
50
+ line-length = 100
51
+ src = ["src", "tests"]
52
+ extend-exclude = ["*.md"]
53
+
54
+ [tool.ruff.lint]
55
+ select = ["E", "F", "I", "UP", "B", "SIM"]
56
+
57
+ [tool.mypy]
58
+ strict = true
59
+ files = ["src", "tests"]
60
+
61
+ [tool.pytest.ini_options]
62
+ testpaths = ["tests"]
63
+ markers = [
64
+ "integration: live tests against a SysAid instance (needs SYSAID_* env vars)",
65
+ "destructive: live tests that write data (also needs SYSAID_ALLOW_WRITES=1)",
66
+ ]
@@ -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
+ ]
@@ -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="")
@@ -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