uptimer-python-sdk 1.5.0__py3-none-any.whl → 1.7.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.
- uptimer/__init__.py +3 -3
- uptimer/client.py +13 -3
- uptimer/endpoints/subjects.py +439 -0
- uptimer/endpoints/v1.py +145 -0
- uptimer/endpoints/v2.py +3 -0
- uptimer/models/v2/__init__.py +52 -0
- uptimer/models/v2/acknowledgement.py +99 -0
- uptimer/models/v2/deserialize.py +59 -0
- uptimer/models/v2/maintenance.py +41 -0
- uptimer/models/v2/observation.py +71 -0
- uptimer/models/v2/subject.py +75 -0
- uptimer_python_sdk-1.7.0.dist-info/METADATA +537 -0
- {uptimer_python_sdk-1.5.0.dist-info → uptimer_python_sdk-1.7.0.dist-info}/RECORD +16 -10
- {uptimer_python_sdk-1.5.0.dist-info → uptimer_python_sdk-1.7.0.dist-info}/WHEEL +1 -1
- uptimer_python_sdk-1.5.0.dist-info/METADATA +0 -287
- {uptimer_python_sdk-1.5.0.dist-info → uptimer_python_sdk-1.7.0.dist-info}/licenses/LICENSE +0 -0
- {uptimer_python_sdk-1.5.0.dist-info → uptimer_python_sdk-1.7.0.dist-info}/licenses/NOTICE +0 -0
uptimer/__init__.py
CHANGED
|
@@ -4,9 +4,9 @@ Uptimer Python SDK.
|
|
|
4
4
|
Targets Uptimer API v2 only. Code written against 0.4.x keeps working against
|
|
5
5
|
the server — API v1 is unchanged and supported — but must stay on the 0.4.x SDK.
|
|
6
6
|
|
|
7
|
-
The version tracks the uptimer release this SDK targets: 1.
|
|
8
|
-
uptimer 1.
|
|
7
|
+
The version tracks the uptimer release this SDK targets: 1.6.x speaks to
|
|
8
|
+
uptimer 1.6.0 and later. Patch numbers are independent, so an SDK fix can ship
|
|
9
9
|
without a server release. See product Decision 0013.
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
|
-
__version__ = "1.
|
|
12
|
+
__version__ = "1.6.0"
|
uptimer/client.py
CHANGED
|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
from typing import cast
|
|
4
4
|
|
|
5
5
|
from uptimer.compat import ensure_v2_supported
|
|
6
|
+
from uptimer.endpoints.v1 import V1Endpoint
|
|
6
7
|
from uptimer.endpoints.v2 import V2Endpoint
|
|
7
8
|
from uptimer.http import UptimerHttpLib
|
|
8
9
|
|
|
@@ -12,14 +13,22 @@ class UptimerClient:
|
|
|
12
13
|
The Uptimer API client.
|
|
13
14
|
|
|
14
15
|
Resources are reached through the API version that serves them:
|
|
15
|
-
`client.v2.workspaces`, `client.v2.locations`, `client.v2.incidents
|
|
16
|
-
`client.v2.monitoring.websites
|
|
17
|
-
|
|
16
|
+
`client.v2.workspaces`, `client.v2.locations`, `client.v2.incidents`,
|
|
17
|
+
`client.v2.monitoring.websites` and
|
|
18
|
+
`client.v2.subjects(subject).signals(signal).observations`.
|
|
19
|
+
|
|
20
|
+
This is a v2 client. `client.v1` exists for one thing only: uptimer 1.7.0
|
|
21
|
+
serves WEBSITE incident acknowledgement under `/v1/rules/...`, because
|
|
22
|
+
website monitoring is v1's resource and custom monitoring is v2's. Reading
|
|
23
|
+
and writing website monitors themselves stays on
|
|
24
|
+
`client.v2.monitoring.websites` — see the migration note in the README if
|
|
25
|
+
you are coming from 0.4.x.
|
|
18
26
|
|
|
19
27
|
`version()` and the compatibility helpers stay here rather than under a
|
|
20
28
|
version namespace, because `/version` is shared and unversioned.
|
|
21
29
|
"""
|
|
22
30
|
|
|
31
|
+
v1: V1Endpoint
|
|
23
32
|
v2: V2Endpoint
|
|
24
33
|
|
|
25
34
|
def __init__(self, api_key: str, base_url: str):
|
|
@@ -28,6 +37,7 @@ class UptimerClient:
|
|
|
28
37
|
self._wire()
|
|
29
38
|
|
|
30
39
|
def _wire(self) -> None:
|
|
40
|
+
self.v1 = V1Endpoint(self._http_lib)
|
|
31
41
|
self.v2 = V2Endpoint(self._http_lib)
|
|
32
42
|
|
|
33
43
|
def version(self) -> str:
|
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import asdict
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
from urllib.parse import quote
|
|
6
|
+
|
|
7
|
+
from uptimer.endpoints.endpoint import BaseEndpoint
|
|
8
|
+
from uptimer.models.v2 import (
|
|
9
|
+
from_api_acknowledgement,
|
|
10
|
+
from_api_maintenance,
|
|
11
|
+
from_api_observation,
|
|
12
|
+
from_api_subject,
|
|
13
|
+
from_api_subject_incident,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from uptimer.http import UptimerHttpLib
|
|
18
|
+
from uptimer.models.v2 import (
|
|
19
|
+
CreateObservationRequest,
|
|
20
|
+
CreateSubjectRequest,
|
|
21
|
+
IncidentAcknowledgement,
|
|
22
|
+
MaintenanceWindow,
|
|
23
|
+
Observation,
|
|
24
|
+
Subject,
|
|
25
|
+
SubjectIncident,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _slug(value: str, what: str) -> str:
|
|
30
|
+
"""
|
|
31
|
+
Escape one slug for a URL path segment.
|
|
32
|
+
|
|
33
|
+
A slug is the API name of a subject or signal, and it lands in the path. It
|
|
34
|
+
is quoted rather than trusted so a value containing a slash addresses a
|
|
35
|
+
signal named that, instead of silently reaching a different resource.
|
|
36
|
+
"""
|
|
37
|
+
if not value or not value.strip():
|
|
38
|
+
message = f"{what} slug is required"
|
|
39
|
+
raise ValueError(message)
|
|
40
|
+
return quote(value, safe="")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _payload(observation: CreateObservationRequest) -> dict:
|
|
44
|
+
"""
|
|
45
|
+
Serialize an observation, omitting what was not set.
|
|
46
|
+
|
|
47
|
+
The server rejects unknown fields and distinguishes an absent optional from
|
|
48
|
+
a null one — an absent `observed_at` means "stamp it now", where a null
|
|
49
|
+
would be a malformed timestamp. So unset fields are left out rather than
|
|
50
|
+
sent as None.
|
|
51
|
+
"""
|
|
52
|
+
body = asdict(observation)
|
|
53
|
+
return {key: value for key, value in body.items() if value is not None}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class ObservationsEndpoint(BaseEndpoint):
|
|
57
|
+
"""
|
|
58
|
+
The observations of one signal.
|
|
59
|
+
|
|
60
|
+
Reached through the signal that owns them —
|
|
61
|
+
`client.v2.subjects(subject).signals(signal).observations` — because a
|
|
62
|
+
signal slug is unique within its subject, not across the workspace. The pair
|
|
63
|
+
is the address.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
def __init__(
|
|
67
|
+
self,
|
|
68
|
+
http: UptimerHttpLib,
|
|
69
|
+
parent_segments: str | list[str] | None = None,
|
|
70
|
+
):
|
|
71
|
+
super().__init__(http, "observations", parent_segments)
|
|
72
|
+
|
|
73
|
+
def create(self, observation: CreateObservationRequest) -> Observation:
|
|
74
|
+
"""
|
|
75
|
+
Report one observation, and return it as the server stored it.
|
|
76
|
+
|
|
77
|
+
Only custom heartbeat and event signals accept this. A platform HTTP
|
|
78
|
+
signal is written by Uptimer's own probe and refuses posted
|
|
79
|
+
observations, which arrives as a DefaultUptimerApiError.
|
|
80
|
+
|
|
81
|
+
A stored observation the engine will not evaluate is RETURNED, not
|
|
82
|
+
raised: check `accepted` and `reject_reason` on the result. An error is
|
|
83
|
+
raised only when nothing was stored — a bad status, an unparsable
|
|
84
|
+
timestamp, a signal that does not exist, or no permission.
|
|
85
|
+
"""
|
|
86
|
+
response = self.http.client.post(self.url, json=_payload(observation))
|
|
87
|
+
result = self.http.parse_response(response=response)
|
|
88
|
+
return from_api_observation(result)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class SignalEndpoint(BaseEndpoint):
|
|
92
|
+
"""One signal of a subject, addressed by its slug."""
|
|
93
|
+
|
|
94
|
+
observations: ObservationsEndpoint
|
|
95
|
+
|
|
96
|
+
def __init__(
|
|
97
|
+
self,
|
|
98
|
+
http: UptimerHttpLib,
|
|
99
|
+
signal_slug: str,
|
|
100
|
+
parent_segments: str | list[str] | None = None,
|
|
101
|
+
):
|
|
102
|
+
super().__init__(http, _slug(signal_slug, "signal"), parent_segments)
|
|
103
|
+
self.observations = ObservationsEndpoint(
|
|
104
|
+
http,
|
|
105
|
+
[*self._parent_segments, self.segment],
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class SignalsEndpoint(BaseEndpoint):
|
|
110
|
+
"""
|
|
111
|
+
The signals of one custom subject.
|
|
112
|
+
|
|
113
|
+
Call it with a slug to reach one:
|
|
114
|
+
`client.v2.subjects("nightly-export").signals("worker-pulse")`.
|
|
115
|
+
|
|
116
|
+
There is no listing or authoring here. Uptimer 1.6.0 serves those routes —
|
|
117
|
+
the Signals screen has an API half — but this SDK does not wrap them yet:
|
|
118
|
+
add a signal in the Uptimer UI, and use this to report data to one that
|
|
119
|
+
already exists.
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
def __init__(
|
|
123
|
+
self,
|
|
124
|
+
http: UptimerHttpLib,
|
|
125
|
+
parent_segments: str | list[str] | None = None,
|
|
126
|
+
):
|
|
127
|
+
super().__init__(http, "signals", parent_segments)
|
|
128
|
+
|
|
129
|
+
def __call__(self, signal_slug: str) -> SignalEndpoint:
|
|
130
|
+
return SignalEndpoint(self.http, signal_slug, self._parent_segments_with_self())
|
|
131
|
+
|
|
132
|
+
def _parent_segments_with_self(self) -> list[str]:
|
|
133
|
+
return [*self._parent_segments, self.segment]
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class SubjectIncidentEndpoint(BaseEndpoint):
|
|
137
|
+
"""One incident of a custom subject, addressed by its opaque id."""
|
|
138
|
+
|
|
139
|
+
def __init__(
|
|
140
|
+
self,
|
|
141
|
+
http: UptimerHttpLib,
|
|
142
|
+
incident_id: str,
|
|
143
|
+
parent_segments: str | list[str] | None = None,
|
|
144
|
+
workspace_id: str | None = None,
|
|
145
|
+
):
|
|
146
|
+
super().__init__(http, _slug(incident_id, "incident"), parent_segments)
|
|
147
|
+
self._workspace_id = workspace_id
|
|
148
|
+
|
|
149
|
+
def acknowledge(self) -> IncidentAcknowledgement:
|
|
150
|
+
"""
|
|
151
|
+
Record that you have seen THIS incident. Requires uptimer 1.7.0+.
|
|
152
|
+
|
|
153
|
+
It says a person looked; it changes nothing the engine decided. The
|
|
154
|
+
verdict, the evidence and the close hold carry on. Its one effect on
|
|
155
|
+
alerting is that the four-hour reminders for this incident stop
|
|
156
|
+
(uptimer 1.7.0); nothing else is silenced, and the recovery still
|
|
157
|
+
arrives.
|
|
158
|
+
|
|
159
|
+
There is no body and no actor argument: the person recorded is the owner
|
|
160
|
+
of the API key, and the time is the time of the call. The server refuses
|
|
161
|
+
a body rather than letting one client file an acknowledgement under
|
|
162
|
+
another person's name.
|
|
163
|
+
|
|
164
|
+
Repeating it is safe. A second call records nothing, adds no second
|
|
165
|
+
history entry, and returns the FIRST person's name and time with
|
|
166
|
+
`recorded=False`.
|
|
167
|
+
|
|
168
|
+
Raises DefaultUptimerApiError when the incident is not this subject's —
|
|
169
|
+
another subject's, another workspace's, or a website monitor's — and
|
|
170
|
+
when it has closed. Nothing is retried through the other family: a
|
|
171
|
+
website incident is acknowledged through `client.v1.rules(...)`, and
|
|
172
|
+
this method will not do it for you.
|
|
173
|
+
"""
|
|
174
|
+
params = {"workspace_id": self._workspace_id} if self._workspace_id else None
|
|
175
|
+
response = self.http.client.post(f"{self.url}/acknowledge", params=params)
|
|
176
|
+
result = self.http.parse_response(response=response)
|
|
177
|
+
return from_api_acknowledgement(result)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class SubjectIncidentsEndpoint(BaseEndpoint):
|
|
181
|
+
"""
|
|
182
|
+
The OPEN incidents of one custom subject. Requires uptimer 1.7.0+.
|
|
183
|
+
|
|
184
|
+
This is where an acknowledgement target comes from. The acknowledge call
|
|
185
|
+
takes one exact incident id and refuses to guess — a subject can have
|
|
186
|
+
several incidents open at once — so the flow is: list these, choose the one
|
|
187
|
+
you mean, acknowledge it by id.
|
|
188
|
+
|
|
189
|
+
`client.v2.incidents` is the WEBSITE list and does not serve custom
|
|
190
|
+
subjects; this is its custom counterpart, scoped to one subject.
|
|
191
|
+
"""
|
|
192
|
+
|
|
193
|
+
def __init__(
|
|
194
|
+
self,
|
|
195
|
+
http: UptimerHttpLib,
|
|
196
|
+
parent_segments: str | list[str] | None = None,
|
|
197
|
+
workspace_id: str | None = None,
|
|
198
|
+
):
|
|
199
|
+
super().__init__(http, "incidents", parent_segments)
|
|
200
|
+
self._workspace_id = workspace_id
|
|
201
|
+
|
|
202
|
+
def __call__(self, incident_id: str) -> SubjectIncidentEndpoint:
|
|
203
|
+
return SubjectIncidentEndpoint(
|
|
204
|
+
self.http,
|
|
205
|
+
incident_id,
|
|
206
|
+
[*self._parent_segments, self.segment],
|
|
207
|
+
self._workspace_id,
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
def all(self) -> list[SubjectIncident]:
|
|
211
|
+
"""
|
|
212
|
+
Every open incident of this subject, newest trouble first.
|
|
213
|
+
|
|
214
|
+
Open ones only, and all of them: pending, recovering and no-data
|
|
215
|
+
included, and already-acknowledged ones too — that somebody is on one is
|
|
216
|
+
half of what this answers. A subject with nothing wrong returns [].
|
|
217
|
+
|
|
218
|
+
Closed history is not here; it lives on the subject timeline in the
|
|
219
|
+
dashboard.
|
|
220
|
+
"""
|
|
221
|
+
params = {"workspace_id": self._workspace_id} if self._workspace_id else None
|
|
222
|
+
response = self.http.client.get(self.url, params=params)
|
|
223
|
+
result = self.http.parse_response(response=response)
|
|
224
|
+
return [from_api_subject_incident(item) for item in result]
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
class MaintenanceEndpoint(BaseEndpoint):
|
|
228
|
+
"""
|
|
229
|
+
The maintenance window of one custom subject. Requires uptimer 1.7.0+.
|
|
230
|
+
|
|
231
|
+
A window holds back this subject's PROBLEM notifications until the time you
|
|
232
|
+
choose. Nothing else changes: monitoring runs, incidents open and close, the
|
|
233
|
+
timeline records all of it — so afterwards the outage reads exactly as it
|
|
234
|
+
happened. Recoveries are never held back.
|
|
235
|
+
|
|
236
|
+
Four operations: read it, start one, move its end, end it early. Moving the
|
|
237
|
+
end is a real update — the window keeps its identity and its start, and
|
|
238
|
+
nothing that reads it sees the subject briefly leave maintenance.
|
|
239
|
+
"""
|
|
240
|
+
|
|
241
|
+
def __init__(
|
|
242
|
+
self,
|
|
243
|
+
http: UptimerHttpLib,
|
|
244
|
+
parent_segments: str | list[str] | None = None,
|
|
245
|
+
workspace_id: str | None = None,
|
|
246
|
+
):
|
|
247
|
+
super().__init__(http, "maintenance", parent_segments)
|
|
248
|
+
self._workspace_id = workspace_id
|
|
249
|
+
|
|
250
|
+
def _params(self) -> dict | None:
|
|
251
|
+
return {"workspace_id": self._workspace_id} if self._workspace_id else None
|
|
252
|
+
|
|
253
|
+
def get(self) -> MaintenanceWindow | None:
|
|
254
|
+
"""
|
|
255
|
+
Return the window running on this subject, or None.
|
|
256
|
+
|
|
257
|
+
None is an ANSWER, not an error: a script checking whether it is safe to
|
|
258
|
+
deploy should not have to catch an exception for the ordinary case.
|
|
259
|
+
"""
|
|
260
|
+
response = self.http.client.get(self.url, params=self._params())
|
|
261
|
+
result = self.http.parse_response(response=response)
|
|
262
|
+
if result is None:
|
|
263
|
+
return None
|
|
264
|
+
return from_api_maintenance(result)
|
|
265
|
+
|
|
266
|
+
def start(self, ends_at: str) -> MaintenanceWindow:
|
|
267
|
+
"""
|
|
268
|
+
Start a window now, ending at `ends_at`.
|
|
269
|
+
|
|
270
|
+
`ends_at` is an RFC 3339 timestamp and carries its own zone, so there is
|
|
271
|
+
nothing to guess — pass "2026-09-13T18:00:00Z" or your own offset.
|
|
272
|
+
|
|
273
|
+
Raises DefaultUptimerApiError when the time has already passed, when a
|
|
274
|
+
window is already running on this subject (cancel it first), when the
|
|
275
|
+
subject is a website check — those are managed from the dashboard — or
|
|
276
|
+
when the caller is not an editor of that workspace.
|
|
277
|
+
"""
|
|
278
|
+
response = self.http.client.post(
|
|
279
|
+
self.url,
|
|
280
|
+
params=self._params(),
|
|
281
|
+
json={"ends_at": ends_at},
|
|
282
|
+
)
|
|
283
|
+
result = self.http.parse_response(response=response)
|
|
284
|
+
return from_api_maintenance(result)
|
|
285
|
+
|
|
286
|
+
def update_end(self, ends_at: str) -> MaintenanceWindow:
|
|
287
|
+
"""
|
|
288
|
+
Move the end of the window that is already running.
|
|
289
|
+
|
|
290
|
+
It is an UPDATE, not a cancel and a new window: the window keeps its
|
|
291
|
+
identity and its start, so "since when have we been silencing this?"
|
|
292
|
+
keeps one answer, and nothing that reads it sees the subject briefly
|
|
293
|
+
leave maintenance. Nothing is notified — moving an end time is a
|
|
294
|
+
correction to a plan, not an event.
|
|
295
|
+
|
|
296
|
+
`ends_at` is RFC 3339, as it is for `start`. A time that has already
|
|
297
|
+
passed raises rather than ending the window: to stop it now, call
|
|
298
|
+
`cancel()`. So does a subject with nothing running — there is no end to
|
|
299
|
+
move — and a caller who is not an editor.
|
|
300
|
+
"""
|
|
301
|
+
response = self.http.client.post(
|
|
302
|
+
f"{self.url}/ends_at",
|
|
303
|
+
params=self._params(),
|
|
304
|
+
json={"ends_at": ends_at},
|
|
305
|
+
)
|
|
306
|
+
result = self.http.parse_response(response=response)
|
|
307
|
+
return from_api_maintenance(result)
|
|
308
|
+
|
|
309
|
+
def cancel(self) -> MaintenanceWindow:
|
|
310
|
+
"""
|
|
311
|
+
End the running window now, and return it as it was recorded.
|
|
312
|
+
|
|
313
|
+
Notifications are back to normal immediately. Raises
|
|
314
|
+
DefaultUptimerApiError when there is nothing to cancel: "it was already
|
|
315
|
+
over" is worth knowing rather than reporting as success.
|
|
316
|
+
"""
|
|
317
|
+
response = self.http.client.delete(self.url, params=self._params())
|
|
318
|
+
result = self.http.parse_response(response=response)
|
|
319
|
+
return from_api_maintenance(result)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
class SubjectEndpoint(BaseEndpoint):
|
|
323
|
+
"""One monitored subject, addressed by its slug."""
|
|
324
|
+
|
|
325
|
+
signals: SignalsEndpoint
|
|
326
|
+
incidents: SubjectIncidentsEndpoint
|
|
327
|
+
maintenance: MaintenanceEndpoint
|
|
328
|
+
|
|
329
|
+
def __init__(
|
|
330
|
+
self,
|
|
331
|
+
http: UptimerHttpLib,
|
|
332
|
+
subject_slug: str,
|
|
333
|
+
parent_segments: str | list[str] | None = None,
|
|
334
|
+
workspace_id: str | None = None,
|
|
335
|
+
):
|
|
336
|
+
super().__init__(http, _slug(subject_slug, "subject"), parent_segments)
|
|
337
|
+
self.signals = SignalsEndpoint(http, [*self._parent_segments, self.segment])
|
|
338
|
+
self.incidents = SubjectIncidentsEndpoint(
|
|
339
|
+
http,
|
|
340
|
+
[*self._parent_segments, self.segment],
|
|
341
|
+
workspace_id,
|
|
342
|
+
)
|
|
343
|
+
self.maintenance = MaintenanceEndpoint(
|
|
344
|
+
http,
|
|
345
|
+
[*self._parent_segments, self.segment],
|
|
346
|
+
workspace_id,
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
class SubjectsEndpoint(BaseEndpoint):
|
|
351
|
+
"""
|
|
352
|
+
The workspace's CUSTOM monitored subjects.
|
|
353
|
+
|
|
354
|
+
Uptimer splits its API by subject kind: website monitoring is
|
|
355
|
+
`client.v2.monitoring.websites`, and this is the custom half. Neither one
|
|
356
|
+
serves the other's subjects — a website subject's slug is refused here.
|
|
357
|
+
|
|
358
|
+
Two ways in, because there are two things to do with a subject:
|
|
359
|
+
|
|
360
|
+
- call it with a slug to reach what is under one —
|
|
361
|
+
`client.v2.subjects("nightly-export").signals("worker-pulse").observations`;
|
|
362
|
+
- call the methods here to list, fetch, or create one.
|
|
363
|
+
|
|
364
|
+
There is no update or delete. Deleting a subject takes its whole history
|
|
365
|
+
with it, which is not something to do by accident from a script.
|
|
366
|
+
"""
|
|
367
|
+
|
|
368
|
+
def __init__(
|
|
369
|
+
self,
|
|
370
|
+
http: UptimerHttpLib,
|
|
371
|
+
parent_segments: str | list[str] | None = None,
|
|
372
|
+
):
|
|
373
|
+
super().__init__(http, "subjects", parent_segments)
|
|
374
|
+
|
|
375
|
+
def __call__(self, subject_slug: str, workspace_id: str | None = None) -> SubjectEndpoint:
|
|
376
|
+
"""
|
|
377
|
+
Reach one subject by slug.
|
|
378
|
+
|
|
379
|
+
`workspace_id` settles an ambiguity rather than being required: a slug
|
|
380
|
+
is unique within a workspace, not across them. Pass it when the same
|
|
381
|
+
slug exists in two workspaces you belong to, and the incident routes
|
|
382
|
+
under this subject will carry it.
|
|
383
|
+
"""
|
|
384
|
+
return SubjectEndpoint(
|
|
385
|
+
self.http,
|
|
386
|
+
subject_slug,
|
|
387
|
+
[*self._parent_segments, self.segment],
|
|
388
|
+
workspace_id,
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
def all(self, workspace_id: str) -> list[Subject]:
|
|
392
|
+
"""
|
|
393
|
+
Every CUSTOM subject in a workspace.
|
|
394
|
+
|
|
395
|
+
Website monitoring is not here — it is `client.v2.monitoring.websites`.
|
|
396
|
+
Against a 1.6.0 server every item reads `subject_kind == "custom"`;
|
|
397
|
+
`subject_kind` is still on the model, because an older server answered
|
|
398
|
+
this route with both kinds.
|
|
399
|
+
"""
|
|
400
|
+
response = self.http.client.get(self.url, params={"workspace_id": workspace_id})
|
|
401
|
+
result = self.http.parse_response(response=response)
|
|
402
|
+
return [from_api_subject(item) for item in result]
|
|
403
|
+
|
|
404
|
+
def get(self, subject_slug: str, workspace_id: str | None = None) -> Subject:
|
|
405
|
+
"""
|
|
406
|
+
One custom subject by its slug.
|
|
407
|
+
|
|
408
|
+
`workspace_id` is optional and settles an ambiguity rather than being
|
|
409
|
+
required: a slug is unique within a workspace, not across them, so pass
|
|
410
|
+
it when the same slug exists in two workspaces you belong to. Without
|
|
411
|
+
it the server searches your memberships and says so if the answer is
|
|
412
|
+
more than one.
|
|
413
|
+
|
|
414
|
+
A website subject's slug raises a DefaultUptimerApiError saying it is
|
|
415
|
+
managed elsewhere.
|
|
416
|
+
"""
|
|
417
|
+
params = {"workspace_id": workspace_id} if workspace_id else None
|
|
418
|
+
response = self.http.client.get(
|
|
419
|
+
f"{self.url}/{_slug(subject_slug, 'subject')}",
|
|
420
|
+
params=params,
|
|
421
|
+
)
|
|
422
|
+
result = self.http.parse_response(response=response)
|
|
423
|
+
return from_api_subject(result)
|
|
424
|
+
|
|
425
|
+
def create(self, subject: CreateSubjectRequest) -> Subject:
|
|
426
|
+
"""
|
|
427
|
+
Create one empty Custom subject.
|
|
428
|
+
|
|
429
|
+
It arrives with nothing under it: no signal, no rule, no HTTP probe.
|
|
430
|
+
Add a signal to it in the Uptimer UI, then report to that signal through
|
|
431
|
+
`client.v2.subjects(...).signals(...).observations`.
|
|
432
|
+
|
|
433
|
+
Website monitoring is created by `client.v2.monitoring.websites.create`
|
|
434
|
+
instead — it needs a URL, an interval and locations, and asking for one
|
|
435
|
+
here is refused with a DefaultUptimerApiError saying so.
|
|
436
|
+
"""
|
|
437
|
+
response = self.http.client.post(self.url, json=asdict(subject))
|
|
438
|
+
result = self.http.parse_response(response=response)
|
|
439
|
+
return from_api_subject(result)
|
uptimer/endpoints/v1.py
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
from urllib.parse import quote
|
|
5
|
+
|
|
6
|
+
from uptimer.endpoints.endpoint import BaseEndpoint
|
|
7
|
+
from uptimer.models.v2 import from_api_acknowledgement
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from uptimer.http import UptimerHttpLib
|
|
11
|
+
from uptimer.models.v2 import IncidentAcknowledgement
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _segment(value: str, what: str) -> str:
|
|
15
|
+
"""Escape one identifier for a URL path segment."""
|
|
16
|
+
if not value or not value.strip():
|
|
17
|
+
message = f"{what} is required"
|
|
18
|
+
raise ValueError(message)
|
|
19
|
+
return quote(value, safe="")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class WebsiteIncidentEndpoint(BaseEndpoint):
|
|
23
|
+
"""One incident of a website monitor, addressed by its opaque id."""
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
http: UptimerHttpLib,
|
|
28
|
+
incident_id: str,
|
|
29
|
+
parent_segments: str | list[str] | None = None,
|
|
30
|
+
):
|
|
31
|
+
super().__init__(http, _segment(incident_id, "incident id"), parent_segments)
|
|
32
|
+
|
|
33
|
+
def acknowledge(self) -> IncidentAcknowledgement:
|
|
34
|
+
"""
|
|
35
|
+
Record that you have seen THIS website incident. Requires uptimer 1.7.0+.
|
|
36
|
+
|
|
37
|
+
Find the id with `client.v2.incidents.all(workspace_id)`, which lists
|
|
38
|
+
open website incidents with the monitor each belongs to.
|
|
39
|
+
|
|
40
|
+
It says a person looked; it changes nothing the engine decided — the
|
|
41
|
+
verdict, the evidence and the close hold all carry on. Its one effect on
|
|
42
|
+
alerting is that the four-hour reminders for this incident stop (uptimer
|
|
43
|
+
1.7.0); nothing else is silenced, and the recovery still arrives. There
|
|
44
|
+
is no body and no actor argument: the person recorded is the owner of
|
|
45
|
+
the API key, at the time of the call.
|
|
46
|
+
|
|
47
|
+
Repeating it is safe: a second call records nothing and returns the
|
|
48
|
+
FIRST person's name and time with `recorded=False`.
|
|
49
|
+
|
|
50
|
+
Raises DefaultUptimerApiError when the incident is not this monitor's —
|
|
51
|
+
another monitor's, another workspace's, or a custom subject's — and when
|
|
52
|
+
it has closed. A custom incident is acknowledged through
|
|
53
|
+
`client.v2.subjects(...).incidents(...)`, and this will not do it for
|
|
54
|
+
you: the families do not fall back to one another.
|
|
55
|
+
"""
|
|
56
|
+
response = self.http.client.post(f"{self.url}/acknowledge")
|
|
57
|
+
result = self.http.parse_response(response=response)
|
|
58
|
+
return from_api_acknowledgement(result)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class WebsiteIncidentsEndpoint(BaseEndpoint):
|
|
62
|
+
"""
|
|
63
|
+
The incidents of one website monitor, addressed by id.
|
|
64
|
+
|
|
65
|
+
There is no listing here: open website incidents are listed for a whole
|
|
66
|
+
workspace by `client.v2.incidents.all(workspace_id)`, which is the discovery
|
|
67
|
+
path this SDK has had since 1.5.0 and which already names each incident's
|
|
68
|
+
monitor. This exists to act on one of them.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
def __init__(
|
|
72
|
+
self,
|
|
73
|
+
http: UptimerHttpLib,
|
|
74
|
+
parent_segments: str | list[str] | None = None,
|
|
75
|
+
):
|
|
76
|
+
super().__init__(http, "incidents", parent_segments)
|
|
77
|
+
|
|
78
|
+
def __call__(self, incident_id: str) -> WebsiteIncidentEndpoint:
|
|
79
|
+
return WebsiteIncidentEndpoint(
|
|
80
|
+
self.http,
|
|
81
|
+
incident_id,
|
|
82
|
+
[*self._parent_segments, self.segment],
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class RuleEndpoint(BaseEndpoint):
|
|
87
|
+
"""One website monitor, addressed by its uid."""
|
|
88
|
+
|
|
89
|
+
incidents: WebsiteIncidentsEndpoint
|
|
90
|
+
|
|
91
|
+
def __init__(
|
|
92
|
+
self,
|
|
93
|
+
http: UptimerHttpLib,
|
|
94
|
+
monitor_uid: str,
|
|
95
|
+
parent_segments: str | list[str] | None = None,
|
|
96
|
+
):
|
|
97
|
+
super().__init__(http, _segment(monitor_uid, "monitor uid"), parent_segments)
|
|
98
|
+
self.incidents = WebsiteIncidentsEndpoint(http, [*self._parent_segments, self.segment])
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class RulesEndpoint(BaseEndpoint):
|
|
102
|
+
"""
|
|
103
|
+
Website monitors, as API v1 calls them.
|
|
104
|
+
|
|
105
|
+
Call it with a monitor's uid to reach what is under one:
|
|
106
|
+
`client.v1.rules(monitor_uid).incidents(incident_id).acknowledge()`.
|
|
107
|
+
|
|
108
|
+
Reading and writing monitors themselves stays on
|
|
109
|
+
`client.v2.monitoring.websites` — the same resource in v2's words, and the
|
|
110
|
+
one this SDK is built around.
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
def __init__(
|
|
114
|
+
self,
|
|
115
|
+
http: UptimerHttpLib,
|
|
116
|
+
parent_segments: str | list[str] | None = None,
|
|
117
|
+
):
|
|
118
|
+
super().__init__(http, "rules", parent_segments)
|
|
119
|
+
|
|
120
|
+
def __call__(self, monitor_uid: str) -> RuleEndpoint:
|
|
121
|
+
return RuleEndpoint(self.http, monitor_uid, [*self._parent_segments, self.segment])
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class V1Endpoint(BaseEndpoint):
|
|
125
|
+
"""
|
|
126
|
+
API v1, narrowly: `client.v1`.
|
|
127
|
+
|
|
128
|
+
This SDK is a v2 client, and that has not changed. v1 appears here for one
|
|
129
|
+
reason: uptimer 1.7.0 serves WEBSITE acknowledgement under
|
|
130
|
+
`/v1/rules/{uid}/incidents/{id}/acknowledge`, because website monitoring is
|
|
131
|
+
v1's resource (Decision 0015) and custom monitoring is v2's. Each kind
|
|
132
|
+
acknowledges through its own family, and neither route accepts the other's
|
|
133
|
+
incidents.
|
|
134
|
+
|
|
135
|
+
So this namespace is the acknowledgement door for website monitors, and
|
|
136
|
+
nothing else: no rule listing, no create, no update, no delete. Those live
|
|
137
|
+
on `client.v2.monitoring.websites`, which speaks v2's vocabulary and is
|
|
138
|
+
where a v2 client belongs.
|
|
139
|
+
"""
|
|
140
|
+
|
|
141
|
+
rules: RulesEndpoint
|
|
142
|
+
|
|
143
|
+
def __init__(self, http: UptimerHttpLib):
|
|
144
|
+
super().__init__(http, "v1")
|
|
145
|
+
self.rules = RulesEndpoint(http, [self.segment])
|
uptimer/endpoints/v2.py
CHANGED
|
@@ -5,6 +5,7 @@ from typing import TYPE_CHECKING
|
|
|
5
5
|
from uptimer.endpoints.endpoint import BaseEndpoint
|
|
6
6
|
from uptimer.endpoints.incidents import IncidentsEndpoint
|
|
7
7
|
from uptimer.endpoints.locations import LocationsEndpoint
|
|
8
|
+
from uptimer.endpoints.subjects import SubjectsEndpoint
|
|
8
9
|
from uptimer.endpoints.websites import MonitoringEndpoint
|
|
9
10
|
from uptimer.endpoints.workspaces import WorkspacesEndpoint
|
|
10
11
|
|
|
@@ -28,6 +29,7 @@ class V2Endpoint(BaseEndpoint):
|
|
|
28
29
|
locations: LocationsEndpoint
|
|
29
30
|
incidents: IncidentsEndpoint
|
|
30
31
|
monitoring: MonitoringEndpoint
|
|
32
|
+
subjects: SubjectsEndpoint
|
|
31
33
|
|
|
32
34
|
def __init__(self, http: UptimerHttpLib):
|
|
33
35
|
super().__init__(http, "v2")
|
|
@@ -36,3 +38,4 @@ class V2Endpoint(BaseEndpoint):
|
|
|
36
38
|
self.locations = LocationsEndpoint(http, parent)
|
|
37
39
|
self.incidents = IncidentsEndpoint(http, parent)
|
|
38
40
|
self.monitoring = MonitoringEndpoint(http, parent)
|
|
41
|
+
self.subjects = SubjectsEndpoint(http, parent)
|