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 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.5.x speaks to
8
- uptimer 1.5.0 and later. Patch numbers are independent, so an SDK fix can ship
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.5.0"
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` and
16
- `client.v2.monitoring.websites`. This SDK does not speak API v1 — see the
17
- migration note in the README if you are coming from 0.4.x.
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)
@@ -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)