uptimer-python-sdk 1.6.0rc0__tar.gz → 1.7.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.
- {uptimer_python_sdk-1.6.0rc0 → uptimer_python_sdk-1.7.0}/CHANGELOG.md +14 -0
- {uptimer_python_sdk-1.6.0rc0 → uptimer_python_sdk-1.7.0}/PKG-INFO +195 -2
- {uptimer_python_sdk-1.6.0rc0 → uptimer_python_sdk-1.7.0}/README.md +193 -0
- {uptimer_python_sdk-1.6.0rc0 → uptimer_python_sdk-1.7.0}/Taskfile.yml +13 -2
- {uptimer_python_sdk-1.6.0rc0 → uptimer_python_sdk-1.7.0}/pyproject.toml +10 -2
- {uptimer_python_sdk-1.6.0rc0 → uptimer_python_sdk-1.7.0}/src/uptimer/__init__.py +1 -1
- {uptimer_python_sdk-1.6.0rc0 → uptimer_python_sdk-1.7.0}/src/uptimer/client.py +11 -3
- uptimer_python_sdk-1.7.0/src/uptimer/endpoints/subjects.py +439 -0
- uptimer_python_sdk-1.7.0/src/uptimer/endpoints/v1.py +145 -0
- {uptimer_python_sdk-1.6.0rc0 → uptimer_python_sdk-1.7.0}/src/uptimer/models/v2/__init__.py +26 -0
- uptimer_python_sdk-1.7.0/src/uptimer/models/v2/acknowledgement.py +99 -0
- {uptimer_python_sdk-1.6.0rc0 → uptimer_python_sdk-1.7.0}/src/uptimer/models/v2/deserialize.py +47 -0
- uptimer_python_sdk-1.7.0/src/uptimer/models/v2/maintenance.py +41 -0
- uptimer_python_sdk-1.7.0/src/uptimer/models/v2/subject.py +75 -0
- uptimer_python_sdk-1.7.0/tests/integrations/test_acknowledgement.py +437 -0
- {uptimer_python_sdk-1.6.0rc0 → uptimer_python_sdk-1.7.0}/tests/integrations/test_observations.py +22 -10
- uptimer_python_sdk-1.7.0/tests/integrations/test_subjects.py +126 -0
- uptimer_python_sdk-1.7.0/tests/test_acknowledgement.py +399 -0
- {uptimer_python_sdk-1.6.0rc0 → uptimer_python_sdk-1.7.0}/tests/test_client.py +47 -5
- uptimer_python_sdk-1.7.0/tests/test_maintenance.py +271 -0
- {uptimer_python_sdk-1.6.0rc0 → uptimer_python_sdk-1.7.0}/tests/test_model_namespace.py +17 -0
- uptimer_python_sdk-1.7.0/tests/test_subjects_endpoint.py +223 -0
- {uptimer_python_sdk-1.6.0rc0 → uptimer_python_sdk-1.7.0}/uv.lock +1 -1
- uptimer_python_sdk-1.6.0rc0/src/uptimer/endpoints/subjects.py +0 -154
- {uptimer_python_sdk-1.6.0rc0 → uptimer_python_sdk-1.7.0}/.gitignore +0 -0
- {uptimer_python_sdk-1.6.0rc0 → uptimer_python_sdk-1.7.0}/.pre-commit-config.yaml +0 -0
- {uptimer_python_sdk-1.6.0rc0 → uptimer_python_sdk-1.7.0}/.vscode/extensions.json +0 -0
- {uptimer_python_sdk-1.6.0rc0 → uptimer_python_sdk-1.7.0}/.vscode/launch.json +0 -0
- {uptimer_python_sdk-1.6.0rc0 → uptimer_python_sdk-1.7.0}/.vscode/settings.json +0 -0
- {uptimer_python_sdk-1.6.0rc0 → uptimer_python_sdk-1.7.0}/.vscode/tasks.json +0 -0
- {uptimer_python_sdk-1.6.0rc0 → uptimer_python_sdk-1.7.0}/LICENSE +0 -0
- {uptimer_python_sdk-1.6.0rc0 → uptimer_python_sdk-1.7.0}/NOTICE +0 -0
- {uptimer_python_sdk-1.6.0rc0 → uptimer_python_sdk-1.7.0}/examples/01_client_setup.py +0 -0
- {uptimer_python_sdk-1.6.0rc0 → uptimer_python_sdk-1.7.0}/examples/02_list_workspaces.py +0 -0
- {uptimer_python_sdk-1.6.0rc0 → uptimer_python_sdk-1.7.0}/examples/03_list_locations.py +0 -0
- {uptimer_python_sdk-1.6.0rc0 → uptimer_python_sdk-1.7.0}/examples/04_create_website_monitor.py +0 -0
- {uptimer_python_sdk-1.6.0rc0 → uptimer_python_sdk-1.7.0}/examples/05_open_incidents.py +0 -0
- {uptimer_python_sdk-1.6.0rc0 → uptimer_python_sdk-1.7.0}/examples/README.md +0 -0
- {uptimer_python_sdk-1.6.0rc0 → uptimer_python_sdk-1.7.0}/examples/__init__.py +0 -0
- {uptimer_python_sdk-1.6.0rc0 → uptimer_python_sdk-1.7.0}/src/uptimer/compat.py +0 -0
- {uptimer_python_sdk-1.6.0rc0 → uptimer_python_sdk-1.7.0}/src/uptimer/endpoints/__init__.py +0 -0
- {uptimer_python_sdk-1.6.0rc0 → uptimer_python_sdk-1.7.0}/src/uptimer/endpoints/endpoint.py +0 -0
- {uptimer_python_sdk-1.6.0rc0 → uptimer_python_sdk-1.7.0}/src/uptimer/endpoints/incidents.py +0 -0
- {uptimer_python_sdk-1.6.0rc0 → uptimer_python_sdk-1.7.0}/src/uptimer/endpoints/locations.py +0 -0
- {uptimer_python_sdk-1.6.0rc0 → uptimer_python_sdk-1.7.0}/src/uptimer/endpoints/v2.py +0 -0
- {uptimer_python_sdk-1.6.0rc0 → uptimer_python_sdk-1.7.0}/src/uptimer/endpoints/websites.py +0 -0
- {uptimer_python_sdk-1.6.0rc0 → uptimer_python_sdk-1.7.0}/src/uptimer/endpoints/workspaces.py +0 -0
- {uptimer_python_sdk-1.6.0rc0 → uptimer_python_sdk-1.7.0}/src/uptimer/errors.py +0 -0
- {uptimer_python_sdk-1.6.0rc0 → uptimer_python_sdk-1.7.0}/src/uptimer/http.py +0 -0
- {uptimer_python_sdk-1.6.0rc0 → uptimer_python_sdk-1.7.0}/src/uptimer/models/__init__.py +0 -0
- {uptimer_python_sdk-1.6.0rc0 → uptimer_python_sdk-1.7.0}/src/uptimer/models/errors.py +0 -0
- {uptimer_python_sdk-1.6.0rc0 → uptimer_python_sdk-1.7.0}/src/uptimer/models/v2/incident.py +0 -0
- {uptimer_python_sdk-1.6.0rc0 → uptimer_python_sdk-1.7.0}/src/uptimer/models/v2/location.py +0 -0
- {uptimer_python_sdk-1.6.0rc0 → uptimer_python_sdk-1.7.0}/src/uptimer/models/v2/monitor.py +0 -0
- {uptimer_python_sdk-1.6.0rc0 → uptimer_python_sdk-1.7.0}/src/uptimer/models/v2/observation.py +0 -0
- {uptimer_python_sdk-1.6.0rc0 → uptimer_python_sdk-1.7.0}/src/uptimer/models/v2/workspace.py +0 -0
- {uptimer_python_sdk-1.6.0rc0 → uptimer_python_sdk-1.7.0}/src/uptimer/py.typed +0 -0
- {uptimer_python_sdk-1.6.0rc0 → uptimer_python_sdk-1.7.0}/tests/__init__.py +0 -0
- {uptimer_python_sdk-1.6.0rc0 → uptimer_python_sdk-1.7.0}/tests/conftest.py +0 -0
- {uptimer_python_sdk-1.6.0rc0 → uptimer_python_sdk-1.7.0}/tests/integrations/__init__.py +0 -0
- {uptimer_python_sdk-1.6.0rc0 → uptimer_python_sdk-1.7.0}/tests/integrations/conftest.py +0 -0
- {uptimer_python_sdk-1.6.0rc0 → uptimer_python_sdk-1.7.0}/tests/integrations/test_workspaces.py +0 -0
- {uptimer_python_sdk-1.6.0rc0 → uptimer_python_sdk-1.7.0}/tests/test_endpoint.py +0 -0
- {uptimer_python_sdk-1.6.0rc0 → uptimer_python_sdk-1.7.0}/tests/test_integration_v2.py +0 -0
- {uptimer_python_sdk-1.6.0rc0 → uptimer_python_sdk-1.7.0}/tests/test_observations_endpoint.py +0 -0
- {uptimer_python_sdk-1.6.0rc0 → uptimer_python_sdk-1.7.0}/tests/test_v2_endpoints.py +0 -0
- {uptimer_python_sdk-1.6.0rc0 → uptimer_python_sdk-1.7.0}/tests/test_workspaces_endpoint.py +0 -0
- {uptimer_python_sdk-1.6.0rc0 → uptimer_python_sdk-1.7.0}/tools/mypy-diff.sh +0 -0
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
## 1.7.0 (2026-09-14)
|
|
2
|
+
|
|
3
|
+
### Feat
|
|
4
|
+
|
|
5
|
+
- change maintenance end time on Custom subjects
|
|
6
|
+
- subject maintenance window client
|
|
7
|
+
- acknowledge Website and Custom incidents on separate clients
|
|
8
|
+
|
|
9
|
+
## 1.6.0 (2026-09-08)
|
|
10
|
+
|
|
11
|
+
### Feat
|
|
12
|
+
|
|
13
|
+
- **sdk**: list and create Website/Custom subjects
|
|
14
|
+
|
|
1
15
|
## 1.6.0-rc.0 (2026-08-30)
|
|
2
16
|
|
|
3
17
|
### Feat
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: uptimer-python-sdk
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.7.0
|
|
4
4
|
Summary: A Python SDK for hosted and self-hosted Uptimer
|
|
5
5
|
Project-URL: Homepage, https://myuptime.info
|
|
6
6
|
Project-URL: Documentation, https://uptimer.myuptime.info/latest/reference/python-sdk/
|
|
@@ -153,6 +153,65 @@ except UptimerError: # base error, if you need one
|
|
|
153
153
|
raise
|
|
154
154
|
```
|
|
155
155
|
|
|
156
|
+
### Subjects: your custom monitoring
|
|
157
|
+
|
|
158
|
+
A **subject** is one monitored thing. Every subject is one of two kinds, and the
|
|
159
|
+
kind says how it is configured:
|
|
160
|
+
|
|
161
|
+
- **website** — Uptimer's own probe watches a URL, and the website check form
|
|
162
|
+
owns its signal and its rule;
|
|
163
|
+
- **custom** — yours, reporting through the signals you add to it.
|
|
164
|
+
|
|
165
|
+
Uptimer 1.6.0 splits its API along that line, and so does this SDK: website
|
|
166
|
+
monitoring is `client.v2.monitoring.websites`, and `client.v2.subjects` is the
|
|
167
|
+
**custom** half. Neither serves the other's subjects — passing a website
|
|
168
|
+
subject's slug to a `subjects` call is refused.
|
|
169
|
+
|
|
170
|
+
Requires Uptimer 1.6.0 or later.
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
from uptimer.client import UptimerClient
|
|
174
|
+
from uptimer.models.v2 import SUBJECT_KIND_CUSTOM, CreateSubjectRequest
|
|
175
|
+
|
|
176
|
+
client = UptimerClient(
|
|
177
|
+
api_key="your-api-key-here",
|
|
178
|
+
base_url="http://127.0.0.1:2517/api",
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
# The workspace's custom subjects. Website checks are not here.
|
|
182
|
+
for subject in client.v2.subjects.all("your-workspace-id"):
|
|
183
|
+
print(subject.id, subject.subject_kind, subject.signal_count)
|
|
184
|
+
|
|
185
|
+
# Create an empty custom subject. It arrives with nothing under it: no signal,
|
|
186
|
+
# no rule, no HTTP probe — add a signal to it in the Uptimer UI, then report to
|
|
187
|
+
# that signal with the observations API below.
|
|
188
|
+
created = client.v2.subjects.create(
|
|
189
|
+
CreateSubjectRequest(name="Nightly export", workspace_id="your-workspace-id"),
|
|
190
|
+
)
|
|
191
|
+
assert created.subject_kind == SUBJECT_KIND_CUSTOM
|
|
192
|
+
assert created.signal_count == 0
|
|
193
|
+
|
|
194
|
+
# `id` is the subject's slug — the same name the observation route addresses it
|
|
195
|
+
# by, and it never moves when the subject is renamed.
|
|
196
|
+
fetched = client.v2.subjects.get(created.id, workspace_id="your-workspace-id")
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
`kind` and `subject_kind` are different fields on purpose. `kind` is `"subject"`
|
|
200
|
+
on every one of these objects — it says what you are holding, the way every v2
|
|
201
|
+
object does. `subject_kind` says how the subject is configured, and against a
|
|
202
|
+
1.6.0 server everything these calls return reads `"custom"`; `is_custom` is the
|
|
203
|
+
typed way to read it. `SUBJECT_KIND_WEBSITE` and `is_website` stay in the model
|
|
204
|
+
for a payload from an older server.
|
|
205
|
+
|
|
206
|
+
**Website monitoring is not created here.** It needs a URL, an interval and
|
|
207
|
+
locations, so it has its own call — `client.v2.monitoring.websites.create` —
|
|
208
|
+
and asking for `subject_kind="website"` on this route is refused with a message
|
|
209
|
+
saying so.
|
|
210
|
+
|
|
211
|
+
**Signals and rules are added in the Uptimer UI.** Uptimer 1.6.0 also serves
|
|
212
|
+
them over the API, under `/v2/subjects/{subject}/signals` and
|
|
213
|
+
`/v2/subjects/{subject}/rules`; this SDK does not wrap those routes yet.
|
|
214
|
+
|
|
156
215
|
### Reporting your own observations
|
|
157
216
|
|
|
158
217
|
Uptimer probes websites itself. For anything else — a cron job, a queue worker,
|
|
@@ -210,6 +269,140 @@ Retries are safe. An observation is identified by its signal, its `observed_at`
|
|
|
210
269
|
and its labels, so re-sending the same one replaces it rather than counting
|
|
211
270
|
twice.
|
|
212
271
|
|
|
272
|
+
### Acknowledging an incident
|
|
273
|
+
|
|
274
|
+
**New in 1.7.0.** Acknowledging says a **person has seen** an open incident. It
|
|
275
|
+
changes nothing the engine decided — the verdict, the evidence, the close hold
|
|
276
|
+
and the alerting all carry on — and it is recorded once, with who and when.
|
|
277
|
+
|
|
278
|
+
Each kind of monitoring acknowledges through **its own family**, the same split
|
|
279
|
+
subjects follow: a website incident under its monitor on v1, a custom incident
|
|
280
|
+
under its subject on v2. Neither method falls back to the other, and there is no
|
|
281
|
+
kind-agnostic one: acknowledging is a claim about a specific incident, and an
|
|
282
|
+
SDK that guessed which family it belonged to could claim the wrong one.
|
|
283
|
+
|
|
284
|
+
**Availability.** These methods are part of the SDK's **1.7.0** release: they are
|
|
285
|
+
not in the published 1.6.0 package, so until 1.7.0 is on PyPI use them from a
|
|
286
|
+
checkout of this repository. They need a matching **Uptimer 1.7.0+** server —
|
|
287
|
+
the routes do not exist before that — and on
|
|
288
|
+
[myuptime.info](https://myuptime.info) they arrive when the hosted service picks
|
|
289
|
+
up the 1.7.0 API.
|
|
290
|
+
|
|
291
|
+
**Custom monitoring** — list the subject's open incidents, pick one, acknowledge
|
|
292
|
+
it by id:
|
|
293
|
+
|
|
294
|
+
```python
|
|
295
|
+
from uptimer.client import UptimerClient
|
|
296
|
+
|
|
297
|
+
client = UptimerClient(
|
|
298
|
+
api_key="your-api-key-here",
|
|
299
|
+
base_url="http://127.0.0.1:2517/api",
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
subject = client.v2.subjects("payments-worker", "your-workspace-id")
|
|
303
|
+
|
|
304
|
+
# Open incidents of this subject: all of them, newest trouble first. A subject
|
|
305
|
+
# can have one open per rule, so each names the rule that opened it.
|
|
306
|
+
open_incidents = subject.incidents.all()
|
|
307
|
+
for incident in open_incidents:
|
|
308
|
+
print(incident.id, incident.rule_name, incident.status, incident.acknowledged)
|
|
309
|
+
|
|
310
|
+
# Nothing open is an ordinary answer, not an error.
|
|
311
|
+
if open_incidents:
|
|
312
|
+
# Acknowledge the one you mean, by the id the listing gave you. No body and
|
|
313
|
+
# no actor argument: the person recorded is the owner of the API key, at the
|
|
314
|
+
# time of the call.
|
|
315
|
+
target = open_incidents[0]
|
|
316
|
+
record = subject.incidents(target.id).acknowledge()
|
|
317
|
+
print(record.acknowledged_by, record.acknowledged_at, record.recorded)
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
**Website monitoring** — the ids come from the workspace incident list this SDK
|
|
321
|
+
has had since 1.5.0, which already names each incident's monitor:
|
|
322
|
+
|
|
323
|
+
```python
|
|
324
|
+
# An empty list means nothing is wrong: the loop simply does not run.
|
|
325
|
+
for incident in client.v2.incidents.all("your-workspace-id"):
|
|
326
|
+
record = client.v1.rules(incident.monitor_id).incidents(incident.id).acknowledge()
|
|
327
|
+
print(record.incident_id, record.acknowledged_by, record.recorded)
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
`client.v1` exists for this one route. This is still a v2 client — website
|
|
331
|
+
monitors are read and written through `client.v2.monitoring.websites` — but
|
|
332
|
+
Uptimer serves website acknowledgement under `/v1/rules/...`, because website
|
|
333
|
+
monitoring is v1's resource and custom monitoring is v2's.
|
|
334
|
+
|
|
335
|
+
What the answer says:
|
|
336
|
+
|
|
337
|
+
| field | meaning |
|
|
338
|
+
|---|---|
|
|
339
|
+
| `recorded` | whether **this call** wrote it. `False` means it was already acknowledged and nothing changed |
|
|
340
|
+
| `acknowledged_by` / `acknowledged_at` | the record — on a repeat, the **first** person's name and time, not yours |
|
|
341
|
+
| `status` | the incident's condition, unchanged by acknowledging it |
|
|
342
|
+
| `monitor_id` | set for a website incident; `None` for a custom one |
|
|
343
|
+
| `subject_id` / `rule_id` | set for a custom incident; `None` for a website one |
|
|
344
|
+
| `closed_at` | set if the incident had already closed |
|
|
345
|
+
|
|
346
|
+
**Repeating it is safe.** A second call adds no second history entry and returns
|
|
347
|
+
the original name and time with `recorded=False` — so a retry after a timeout is
|
|
348
|
+
not a second claim.
|
|
349
|
+
|
|
350
|
+
**Refusals are raised, never worked around.** A `DefaultUptimerApiError` means
|
|
351
|
+
the incident is not this parent's — another monitor's, another subject's,
|
|
352
|
+
another workspace's, or the other kind of monitoring. Nothing is retried through
|
|
353
|
+
the other family.
|
|
354
|
+
|
|
355
|
+
**Closing cuts both ways, and the two are different.** A **first**
|
|
356
|
+
acknowledgement of an incident that has already closed is refused
|
|
357
|
+
(`Incident is closed`): there is nothing left to be on, and anything open now is
|
|
358
|
+
a different incident. But an incident acknowledged **while it was open** keeps
|
|
359
|
+
that record after it closes, so asking again is not an error — it answers the
|
|
360
|
+
original name and time with `recorded=False` and `closed_at` set. The look did
|
|
361
|
+
happen.
|
|
362
|
+
|
|
363
|
+
### Maintenance windows
|
|
364
|
+
|
|
365
|
+
**New in 1.7.0.** A maintenance window holds back one subject's **problem**
|
|
366
|
+
notifications until a time you choose — for a deploy, a migration, anything that
|
|
367
|
+
will make it look broken on purpose. Monitoring, incidents and the timeline are
|
|
368
|
+
untouched, and **recoveries are never held back**: "it is back" is the message
|
|
369
|
+
you most want afterwards.
|
|
370
|
+
|
|
371
|
+
Same availability as acknowledgement above: the SDK's 1.7.0 release against an
|
|
372
|
+
uptimer 1.7.0+ server, Custom subjects only (a website check is put into
|
|
373
|
+
maintenance from its page in the dashboard).
|
|
374
|
+
|
|
375
|
+
```python
|
|
376
|
+
maintenance = client.v2.subjects("payments-worker", "your-workspace-id").maintenance
|
|
377
|
+
|
|
378
|
+
# Nothing scheduled is None — an answer, not an error.
|
|
379
|
+
if maintenance.get() is None:
|
|
380
|
+
window = maintenance.start("2026-09-13T18:00:00Z")
|
|
381
|
+
print(window.active, window.ends_at, window.muted)
|
|
382
|
+
|
|
383
|
+
# The work is taking longer: move the end of the SAME window.
|
|
384
|
+
maintenance.update_end("2026-09-13T20:00:00Z")
|
|
385
|
+
|
|
386
|
+
# When it is done. Notifications are normal again immediately.
|
|
387
|
+
maintenance.cancel()
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
`ends_at` is RFC 3339 and carries its own zone, for both `start` and
|
|
391
|
+
`update_end`. The window starts **immediately**.
|
|
392
|
+
|
|
393
|
+
`update_end` is a real update, not a cancel and a new window: it keeps the
|
|
394
|
+
window's identity and its start, so "since when have we been silencing this?"
|
|
395
|
+
keeps one answer and nothing sees the subject briefly leave maintenance. Moving
|
|
396
|
+
the end into the past raises rather than stopping the window — to stop it now,
|
|
397
|
+
`cancel()`.
|
|
398
|
+
|
|
399
|
+
`MaintenanceWindow` tells its three states apart by its fields — `active` true
|
|
400
|
+
is running, `cancelled_at` set is ended early, and neither is a window that ran
|
|
401
|
+
out — and `muted` says what waits, in the server's own words.
|
|
402
|
+
|
|
403
|
+
A past end time, a window already running, a website subject, or a caller who is
|
|
404
|
+
not an editor raise `DefaultUptimerApiError`. Reading takes the viewer role.
|
|
405
|
+
|
|
213
406
|
### Incident status
|
|
214
407
|
|
|
215
408
|
`client.v2.incidents.all()` returns only **open** incidents. `status` carries the
|
|
@@ -137,6 +137,65 @@ except UptimerError: # base error, if you need one
|
|
|
137
137
|
raise
|
|
138
138
|
```
|
|
139
139
|
|
|
140
|
+
### Subjects: your custom monitoring
|
|
141
|
+
|
|
142
|
+
A **subject** is one monitored thing. Every subject is one of two kinds, and the
|
|
143
|
+
kind says how it is configured:
|
|
144
|
+
|
|
145
|
+
- **website** — Uptimer's own probe watches a URL, and the website check form
|
|
146
|
+
owns its signal and its rule;
|
|
147
|
+
- **custom** — yours, reporting through the signals you add to it.
|
|
148
|
+
|
|
149
|
+
Uptimer 1.6.0 splits its API along that line, and so does this SDK: website
|
|
150
|
+
monitoring is `client.v2.monitoring.websites`, and `client.v2.subjects` is the
|
|
151
|
+
**custom** half. Neither serves the other's subjects — passing a website
|
|
152
|
+
subject's slug to a `subjects` call is refused.
|
|
153
|
+
|
|
154
|
+
Requires Uptimer 1.6.0 or later.
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
from uptimer.client import UptimerClient
|
|
158
|
+
from uptimer.models.v2 import SUBJECT_KIND_CUSTOM, CreateSubjectRequest
|
|
159
|
+
|
|
160
|
+
client = UptimerClient(
|
|
161
|
+
api_key="your-api-key-here",
|
|
162
|
+
base_url="http://127.0.0.1:2517/api",
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
# The workspace's custom subjects. Website checks are not here.
|
|
166
|
+
for subject in client.v2.subjects.all("your-workspace-id"):
|
|
167
|
+
print(subject.id, subject.subject_kind, subject.signal_count)
|
|
168
|
+
|
|
169
|
+
# Create an empty custom subject. It arrives with nothing under it: no signal,
|
|
170
|
+
# no rule, no HTTP probe — add a signal to it in the Uptimer UI, then report to
|
|
171
|
+
# that signal with the observations API below.
|
|
172
|
+
created = client.v2.subjects.create(
|
|
173
|
+
CreateSubjectRequest(name="Nightly export", workspace_id="your-workspace-id"),
|
|
174
|
+
)
|
|
175
|
+
assert created.subject_kind == SUBJECT_KIND_CUSTOM
|
|
176
|
+
assert created.signal_count == 0
|
|
177
|
+
|
|
178
|
+
# `id` is the subject's slug — the same name the observation route addresses it
|
|
179
|
+
# by, and it never moves when the subject is renamed.
|
|
180
|
+
fetched = client.v2.subjects.get(created.id, workspace_id="your-workspace-id")
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
`kind` and `subject_kind` are different fields on purpose. `kind` is `"subject"`
|
|
184
|
+
on every one of these objects — it says what you are holding, the way every v2
|
|
185
|
+
object does. `subject_kind` says how the subject is configured, and against a
|
|
186
|
+
1.6.0 server everything these calls return reads `"custom"`; `is_custom` is the
|
|
187
|
+
typed way to read it. `SUBJECT_KIND_WEBSITE` and `is_website` stay in the model
|
|
188
|
+
for a payload from an older server.
|
|
189
|
+
|
|
190
|
+
**Website monitoring is not created here.** It needs a URL, an interval and
|
|
191
|
+
locations, so it has its own call — `client.v2.monitoring.websites.create` —
|
|
192
|
+
and asking for `subject_kind="website"` on this route is refused with a message
|
|
193
|
+
saying so.
|
|
194
|
+
|
|
195
|
+
**Signals and rules are added in the Uptimer UI.** Uptimer 1.6.0 also serves
|
|
196
|
+
them over the API, under `/v2/subjects/{subject}/signals` and
|
|
197
|
+
`/v2/subjects/{subject}/rules`; this SDK does not wrap those routes yet.
|
|
198
|
+
|
|
140
199
|
### Reporting your own observations
|
|
141
200
|
|
|
142
201
|
Uptimer probes websites itself. For anything else — a cron job, a queue worker,
|
|
@@ -194,6 +253,140 @@ Retries are safe. An observation is identified by its signal, its `observed_at`
|
|
|
194
253
|
and its labels, so re-sending the same one replaces it rather than counting
|
|
195
254
|
twice.
|
|
196
255
|
|
|
256
|
+
### Acknowledging an incident
|
|
257
|
+
|
|
258
|
+
**New in 1.7.0.** Acknowledging says a **person has seen** an open incident. It
|
|
259
|
+
changes nothing the engine decided — the verdict, the evidence, the close hold
|
|
260
|
+
and the alerting all carry on — and it is recorded once, with who and when.
|
|
261
|
+
|
|
262
|
+
Each kind of monitoring acknowledges through **its own family**, the same split
|
|
263
|
+
subjects follow: a website incident under its monitor on v1, a custom incident
|
|
264
|
+
under its subject on v2. Neither method falls back to the other, and there is no
|
|
265
|
+
kind-agnostic one: acknowledging is a claim about a specific incident, and an
|
|
266
|
+
SDK that guessed which family it belonged to could claim the wrong one.
|
|
267
|
+
|
|
268
|
+
**Availability.** These methods are part of the SDK's **1.7.0** release: they are
|
|
269
|
+
not in the published 1.6.0 package, so until 1.7.0 is on PyPI use them from a
|
|
270
|
+
checkout of this repository. They need a matching **Uptimer 1.7.0+** server —
|
|
271
|
+
the routes do not exist before that — and on
|
|
272
|
+
[myuptime.info](https://myuptime.info) they arrive when the hosted service picks
|
|
273
|
+
up the 1.7.0 API.
|
|
274
|
+
|
|
275
|
+
**Custom monitoring** — list the subject's open incidents, pick one, acknowledge
|
|
276
|
+
it by id:
|
|
277
|
+
|
|
278
|
+
```python
|
|
279
|
+
from uptimer.client import UptimerClient
|
|
280
|
+
|
|
281
|
+
client = UptimerClient(
|
|
282
|
+
api_key="your-api-key-here",
|
|
283
|
+
base_url="http://127.0.0.1:2517/api",
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
subject = client.v2.subjects("payments-worker", "your-workspace-id")
|
|
287
|
+
|
|
288
|
+
# Open incidents of this subject: all of them, newest trouble first. A subject
|
|
289
|
+
# can have one open per rule, so each names the rule that opened it.
|
|
290
|
+
open_incidents = subject.incidents.all()
|
|
291
|
+
for incident in open_incidents:
|
|
292
|
+
print(incident.id, incident.rule_name, incident.status, incident.acknowledged)
|
|
293
|
+
|
|
294
|
+
# Nothing open is an ordinary answer, not an error.
|
|
295
|
+
if open_incidents:
|
|
296
|
+
# Acknowledge the one you mean, by the id the listing gave you. No body and
|
|
297
|
+
# no actor argument: the person recorded is the owner of the API key, at the
|
|
298
|
+
# time of the call.
|
|
299
|
+
target = open_incidents[0]
|
|
300
|
+
record = subject.incidents(target.id).acknowledge()
|
|
301
|
+
print(record.acknowledged_by, record.acknowledged_at, record.recorded)
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
**Website monitoring** — the ids come from the workspace incident list this SDK
|
|
305
|
+
has had since 1.5.0, which already names each incident's monitor:
|
|
306
|
+
|
|
307
|
+
```python
|
|
308
|
+
# An empty list means nothing is wrong: the loop simply does not run.
|
|
309
|
+
for incident in client.v2.incidents.all("your-workspace-id"):
|
|
310
|
+
record = client.v1.rules(incident.monitor_id).incidents(incident.id).acknowledge()
|
|
311
|
+
print(record.incident_id, record.acknowledged_by, record.recorded)
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
`client.v1` exists for this one route. This is still a v2 client — website
|
|
315
|
+
monitors are read and written through `client.v2.monitoring.websites` — but
|
|
316
|
+
Uptimer serves website acknowledgement under `/v1/rules/...`, because website
|
|
317
|
+
monitoring is v1's resource and custom monitoring is v2's.
|
|
318
|
+
|
|
319
|
+
What the answer says:
|
|
320
|
+
|
|
321
|
+
| field | meaning |
|
|
322
|
+
|---|---|
|
|
323
|
+
| `recorded` | whether **this call** wrote it. `False` means it was already acknowledged and nothing changed |
|
|
324
|
+
| `acknowledged_by` / `acknowledged_at` | the record — on a repeat, the **first** person's name and time, not yours |
|
|
325
|
+
| `status` | the incident's condition, unchanged by acknowledging it |
|
|
326
|
+
| `monitor_id` | set for a website incident; `None` for a custom one |
|
|
327
|
+
| `subject_id` / `rule_id` | set for a custom incident; `None` for a website one |
|
|
328
|
+
| `closed_at` | set if the incident had already closed |
|
|
329
|
+
|
|
330
|
+
**Repeating it is safe.** A second call adds no second history entry and returns
|
|
331
|
+
the original name and time with `recorded=False` — so a retry after a timeout is
|
|
332
|
+
not a second claim.
|
|
333
|
+
|
|
334
|
+
**Refusals are raised, never worked around.** A `DefaultUptimerApiError` means
|
|
335
|
+
the incident is not this parent's — another monitor's, another subject's,
|
|
336
|
+
another workspace's, or the other kind of monitoring. Nothing is retried through
|
|
337
|
+
the other family.
|
|
338
|
+
|
|
339
|
+
**Closing cuts both ways, and the two are different.** A **first**
|
|
340
|
+
acknowledgement of an incident that has already closed is refused
|
|
341
|
+
(`Incident is closed`): there is nothing left to be on, and anything open now is
|
|
342
|
+
a different incident. But an incident acknowledged **while it was open** keeps
|
|
343
|
+
that record after it closes, so asking again is not an error — it answers the
|
|
344
|
+
original name and time with `recorded=False` and `closed_at` set. The look did
|
|
345
|
+
happen.
|
|
346
|
+
|
|
347
|
+
### Maintenance windows
|
|
348
|
+
|
|
349
|
+
**New in 1.7.0.** A maintenance window holds back one subject's **problem**
|
|
350
|
+
notifications until a time you choose — for a deploy, a migration, anything that
|
|
351
|
+
will make it look broken on purpose. Monitoring, incidents and the timeline are
|
|
352
|
+
untouched, and **recoveries are never held back**: "it is back" is the message
|
|
353
|
+
you most want afterwards.
|
|
354
|
+
|
|
355
|
+
Same availability as acknowledgement above: the SDK's 1.7.0 release against an
|
|
356
|
+
uptimer 1.7.0+ server, Custom subjects only (a website check is put into
|
|
357
|
+
maintenance from its page in the dashboard).
|
|
358
|
+
|
|
359
|
+
```python
|
|
360
|
+
maintenance = client.v2.subjects("payments-worker", "your-workspace-id").maintenance
|
|
361
|
+
|
|
362
|
+
# Nothing scheduled is None — an answer, not an error.
|
|
363
|
+
if maintenance.get() is None:
|
|
364
|
+
window = maintenance.start("2026-09-13T18:00:00Z")
|
|
365
|
+
print(window.active, window.ends_at, window.muted)
|
|
366
|
+
|
|
367
|
+
# The work is taking longer: move the end of the SAME window.
|
|
368
|
+
maintenance.update_end("2026-09-13T20:00:00Z")
|
|
369
|
+
|
|
370
|
+
# When it is done. Notifications are normal again immediately.
|
|
371
|
+
maintenance.cancel()
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
`ends_at` is RFC 3339 and carries its own zone, for both `start` and
|
|
375
|
+
`update_end`. The window starts **immediately**.
|
|
376
|
+
|
|
377
|
+
`update_end` is a real update, not a cancel and a new window: it keeps the
|
|
378
|
+
window's identity and its start, so "since when have we been silencing this?"
|
|
379
|
+
keeps one answer and nothing sees the subject briefly leave maintenance. Moving
|
|
380
|
+
the end into the past raises rather than stopping the window — to stop it now,
|
|
381
|
+
`cancel()`.
|
|
382
|
+
|
|
383
|
+
`MaintenanceWindow` tells its three states apart by its fields — `active` true
|
|
384
|
+
is running, `cancelled_at` set is ended early, and neither is a window that ran
|
|
385
|
+
out — and `muted` says what waits, in the server's own words.
|
|
386
|
+
|
|
387
|
+
A past end time, a window already running, a website subject, or a caller who is
|
|
388
|
+
not an editor raise `DefaultUptimerApiError`. Reading takes the viewer role.
|
|
389
|
+
|
|
197
390
|
### Incident status
|
|
198
391
|
|
|
199
392
|
`client.v2.incidents.all()` returns only **open** incidents. `status` carries the
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# Dev + git release tasks for the uptimer Python SDK. All tooling via `uv` (uv.lock).
|
|
2
2
|
# PyPI publish lives in the private product repo: /work → task sdk:publish*
|
|
3
|
+
#
|
|
4
|
+
# HARD RULE: never publish an SDK prerelease (rc/a/b/dev) to TestPyPI or PyPI.
|
|
5
|
+
# OSS/SaaS may cut RCs; this package ships final X.Y.Z only. Verify locally.
|
|
3
6
|
version: '3'
|
|
4
7
|
|
|
5
8
|
tasks:
|
|
@@ -27,9 +30,17 @@ tasks:
|
|
|
27
30
|
- task: test
|
|
28
31
|
|
|
29
32
|
release:
|
|
30
|
-
desc: 'Cut a release: cq, then `cz bump` (
|
|
33
|
+
desc: 'Cut a FINAL release only: cq, then `cz bump` (no --prerelease) + push. Never cut an SDK RC for PyPI. Commit first.'
|
|
31
34
|
cmds:
|
|
32
35
|
- task: cq
|
|
33
36
|
- cz bump --yes
|
|
37
|
+
- |
|
|
38
|
+
ver=$(cz version --project)
|
|
39
|
+
case "$ver" in
|
|
40
|
+
*rc*|*RC*|*alpha*|*beta*|*dev*|*pre*)
|
|
41
|
+
echo "REFUSED: cz produced prerelease $ver — do not push or publish. SDK ships final X.Y.Z only." >&2
|
|
42
|
+
exit 1
|
|
43
|
+
;;
|
|
44
|
+
esac
|
|
34
45
|
- git push --follow-tags
|
|
35
|
-
- 'echo ">> released v$(cz version --project) — publish from /work: task sdk:publish:test, then task sdk:publish"'
|
|
46
|
+
- 'echo ">> released v$(cz version --project) — publish from /work only if FINAL: task sdk:publish:test, then task sdk:publish"'
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
[build-system]
|
|
2
|
-
|
|
2
|
+
# Pinned below 1.28: from that version hatchling writes Metadata-Version 2.5,
|
|
3
|
+
# which twine 6.x (packaging 25) refuses to validate, so `twine check` fails on
|
|
4
|
+
# an artifact that is otherwise fine. 2.4 is what the release path can verify.
|
|
5
|
+
# Lift the pin once twine and packaging accept 2.5.
|
|
6
|
+
requires = ["hatchling<1.28"]
|
|
3
7
|
build-backend = "hatchling.build"
|
|
4
8
|
|
|
5
9
|
[project]
|
|
6
10
|
name = "uptimer-python-sdk"
|
|
7
|
-
version = "1.
|
|
11
|
+
version = "1.7.0"
|
|
8
12
|
description = "A Python SDK for hosted and self-hosted Uptimer"
|
|
9
13
|
readme = "README.md"
|
|
10
14
|
requires-python = ">=3.9"
|
|
@@ -26,6 +30,10 @@ include = ["LICENSE", "NOTICE", "README.md"]
|
|
|
26
30
|
[tool.commitizen]
|
|
27
31
|
name = "cz_conventional_commits"
|
|
28
32
|
tag_format = "$version"
|
|
33
|
+
# Create ANNOTATED tags: `git push --follow-tags` in `task release` SKIPS
|
|
34
|
+
# lightweight ones, so the 1.7.0 bump commit went up without its tag and the
|
|
35
|
+
# tag had to be pushed by hand afterwards.
|
|
36
|
+
annotated_tag = true
|
|
29
37
|
version_scheme = "semver2"
|
|
30
38
|
version_provider = "uv"
|
|
31
39
|
update_changelog_on_bump = true
|
|
@@ -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
|
|
|
@@ -14,14 +15,20 @@ class UptimerClient:
|
|
|
14
15
|
Resources are reached through the API version that serves them:
|
|
15
16
|
`client.v2.workspaces`, `client.v2.locations`, `client.v2.incidents`,
|
|
16
17
|
`client.v2.monitoring.websites` and
|
|
17
|
-
`client.v2.subjects(subject).signals(signal).observations`.
|
|
18
|
-
|
|
19
|
-
|
|
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.
|
|
20
26
|
|
|
21
27
|
`version()` and the compatibility helpers stay here rather than under a
|
|
22
28
|
version namespace, because `/version` is shared and unversioned.
|
|
23
29
|
"""
|
|
24
30
|
|
|
31
|
+
v1: V1Endpoint
|
|
25
32
|
v2: V2Endpoint
|
|
26
33
|
|
|
27
34
|
def __init__(self, api_key: str, base_url: str):
|
|
@@ -30,6 +37,7 @@ class UptimerClient:
|
|
|
30
37
|
self._wire()
|
|
31
38
|
|
|
32
39
|
def _wire(self) -> None:
|
|
40
|
+
self.v1 = V1Endpoint(self._http_lib)
|
|
33
41
|
self.v2 = V2Endpoint(self._http_lib)
|
|
34
42
|
|
|
35
43
|
def version(self) -> str:
|