uptimer-python-sdk 1.5.0__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.
Files changed (69) hide show
  1. {uptimer_python_sdk-1.5.0 → uptimer_python_sdk-1.7.0}/CHANGELOG.md +20 -0
  2. uptimer_python_sdk-1.7.0/PKG-INFO +537 -0
  3. uptimer_python_sdk-1.7.0/README.md +521 -0
  4. {uptimer_python_sdk-1.5.0 → uptimer_python_sdk-1.7.0}/Taskfile.yml +13 -2
  5. {uptimer_python_sdk-1.5.0 → uptimer_python_sdk-1.7.0}/pyproject.toml +10 -2
  6. {uptimer_python_sdk-1.5.0 → uptimer_python_sdk-1.7.0}/src/uptimer/__init__.py +3 -3
  7. {uptimer_python_sdk-1.5.0 → uptimer_python_sdk-1.7.0}/src/uptimer/client.py +13 -3
  8. uptimer_python_sdk-1.7.0/src/uptimer/endpoints/subjects.py +439 -0
  9. uptimer_python_sdk-1.7.0/src/uptimer/endpoints/v1.py +145 -0
  10. {uptimer_python_sdk-1.5.0 → uptimer_python_sdk-1.7.0}/src/uptimer/endpoints/v2.py +3 -0
  11. {uptimer_python_sdk-1.5.0 → uptimer_python_sdk-1.7.0}/src/uptimer/models/v2/__init__.py +52 -0
  12. uptimer_python_sdk-1.7.0/src/uptimer/models/v2/acknowledgement.py +99 -0
  13. {uptimer_python_sdk-1.5.0 → uptimer_python_sdk-1.7.0}/src/uptimer/models/v2/deserialize.py +59 -0
  14. uptimer_python_sdk-1.7.0/src/uptimer/models/v2/maintenance.py +41 -0
  15. uptimer_python_sdk-1.7.0/src/uptimer/models/v2/observation.py +71 -0
  16. uptimer_python_sdk-1.7.0/src/uptimer/models/v2/subject.py +75 -0
  17. uptimer_python_sdk-1.7.0/tests/integrations/test_acknowledgement.py +437 -0
  18. uptimer_python_sdk-1.7.0/tests/integrations/test_observations.py +155 -0
  19. uptimer_python_sdk-1.7.0/tests/integrations/test_subjects.py +126 -0
  20. uptimer_python_sdk-1.7.0/tests/test_acknowledgement.py +399 -0
  21. {uptimer_python_sdk-1.5.0 → uptimer_python_sdk-1.7.0}/tests/test_client.py +47 -5
  22. uptimer_python_sdk-1.7.0/tests/test_maintenance.py +271 -0
  23. {uptimer_python_sdk-1.5.0 → uptimer_python_sdk-1.7.0}/tests/test_model_namespace.py +30 -0
  24. uptimer_python_sdk-1.7.0/tests/test_observations_endpoint.py +217 -0
  25. uptimer_python_sdk-1.7.0/tests/test_subjects_endpoint.py +223 -0
  26. {uptimer_python_sdk-1.5.0 → uptimer_python_sdk-1.7.0}/uv.lock +1 -1
  27. uptimer_python_sdk-1.5.0/PKG-INFO +0 -287
  28. uptimer_python_sdk-1.5.0/README.md +0 -271
  29. {uptimer_python_sdk-1.5.0 → uptimer_python_sdk-1.7.0}/.gitignore +0 -0
  30. {uptimer_python_sdk-1.5.0 → uptimer_python_sdk-1.7.0}/.pre-commit-config.yaml +0 -0
  31. {uptimer_python_sdk-1.5.0 → uptimer_python_sdk-1.7.0}/.vscode/extensions.json +0 -0
  32. {uptimer_python_sdk-1.5.0 → uptimer_python_sdk-1.7.0}/.vscode/launch.json +0 -0
  33. {uptimer_python_sdk-1.5.0 → uptimer_python_sdk-1.7.0}/.vscode/settings.json +0 -0
  34. {uptimer_python_sdk-1.5.0 → uptimer_python_sdk-1.7.0}/.vscode/tasks.json +0 -0
  35. {uptimer_python_sdk-1.5.0 → uptimer_python_sdk-1.7.0}/LICENSE +0 -0
  36. {uptimer_python_sdk-1.5.0 → uptimer_python_sdk-1.7.0}/NOTICE +0 -0
  37. {uptimer_python_sdk-1.5.0 → uptimer_python_sdk-1.7.0}/examples/01_client_setup.py +0 -0
  38. {uptimer_python_sdk-1.5.0 → uptimer_python_sdk-1.7.0}/examples/02_list_workspaces.py +0 -0
  39. {uptimer_python_sdk-1.5.0 → uptimer_python_sdk-1.7.0}/examples/03_list_locations.py +0 -0
  40. {uptimer_python_sdk-1.5.0 → uptimer_python_sdk-1.7.0}/examples/04_create_website_monitor.py +0 -0
  41. {uptimer_python_sdk-1.5.0 → uptimer_python_sdk-1.7.0}/examples/05_open_incidents.py +0 -0
  42. {uptimer_python_sdk-1.5.0 → uptimer_python_sdk-1.7.0}/examples/README.md +0 -0
  43. {uptimer_python_sdk-1.5.0 → uptimer_python_sdk-1.7.0}/examples/__init__.py +0 -0
  44. {uptimer_python_sdk-1.5.0 → uptimer_python_sdk-1.7.0}/src/uptimer/compat.py +0 -0
  45. {uptimer_python_sdk-1.5.0 → uptimer_python_sdk-1.7.0}/src/uptimer/endpoints/__init__.py +0 -0
  46. {uptimer_python_sdk-1.5.0 → uptimer_python_sdk-1.7.0}/src/uptimer/endpoints/endpoint.py +0 -0
  47. {uptimer_python_sdk-1.5.0 → uptimer_python_sdk-1.7.0}/src/uptimer/endpoints/incidents.py +0 -0
  48. {uptimer_python_sdk-1.5.0 → uptimer_python_sdk-1.7.0}/src/uptimer/endpoints/locations.py +0 -0
  49. {uptimer_python_sdk-1.5.0 → uptimer_python_sdk-1.7.0}/src/uptimer/endpoints/websites.py +0 -0
  50. {uptimer_python_sdk-1.5.0 → uptimer_python_sdk-1.7.0}/src/uptimer/endpoints/workspaces.py +0 -0
  51. {uptimer_python_sdk-1.5.0 → uptimer_python_sdk-1.7.0}/src/uptimer/errors.py +0 -0
  52. {uptimer_python_sdk-1.5.0 → uptimer_python_sdk-1.7.0}/src/uptimer/http.py +0 -0
  53. {uptimer_python_sdk-1.5.0 → uptimer_python_sdk-1.7.0}/src/uptimer/models/__init__.py +0 -0
  54. {uptimer_python_sdk-1.5.0 → uptimer_python_sdk-1.7.0}/src/uptimer/models/errors.py +0 -0
  55. {uptimer_python_sdk-1.5.0 → uptimer_python_sdk-1.7.0}/src/uptimer/models/v2/incident.py +0 -0
  56. {uptimer_python_sdk-1.5.0 → uptimer_python_sdk-1.7.0}/src/uptimer/models/v2/location.py +0 -0
  57. {uptimer_python_sdk-1.5.0 → uptimer_python_sdk-1.7.0}/src/uptimer/models/v2/monitor.py +0 -0
  58. {uptimer_python_sdk-1.5.0 → uptimer_python_sdk-1.7.0}/src/uptimer/models/v2/workspace.py +0 -0
  59. {uptimer_python_sdk-1.5.0 → uptimer_python_sdk-1.7.0}/src/uptimer/py.typed +0 -0
  60. {uptimer_python_sdk-1.5.0 → uptimer_python_sdk-1.7.0}/tests/__init__.py +0 -0
  61. {uptimer_python_sdk-1.5.0 → uptimer_python_sdk-1.7.0}/tests/conftest.py +0 -0
  62. {uptimer_python_sdk-1.5.0 → uptimer_python_sdk-1.7.0}/tests/integrations/__init__.py +0 -0
  63. {uptimer_python_sdk-1.5.0 → uptimer_python_sdk-1.7.0}/tests/integrations/conftest.py +0 -0
  64. {uptimer_python_sdk-1.5.0 → uptimer_python_sdk-1.7.0}/tests/integrations/test_workspaces.py +0 -0
  65. {uptimer_python_sdk-1.5.0 → uptimer_python_sdk-1.7.0}/tests/test_endpoint.py +0 -0
  66. {uptimer_python_sdk-1.5.0 → uptimer_python_sdk-1.7.0}/tests/test_integration_v2.py +0 -0
  67. {uptimer_python_sdk-1.5.0 → uptimer_python_sdk-1.7.0}/tests/test_v2_endpoints.py +0 -0
  68. {uptimer_python_sdk-1.5.0 → uptimer_python_sdk-1.7.0}/tests/test_workspaces_endpoint.py +0 -0
  69. {uptimer_python_sdk-1.5.0 → uptimer_python_sdk-1.7.0}/tools/mypy-diff.sh +0 -0
@@ -1,3 +1,23 @@
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
+
15
+ ## 1.6.0-rc.0 (2026-08-30)
16
+
17
+ ### Feat
18
+
19
+ - **api**: post custom observations via client.v2 subjects/signals
20
+
1
21
  ## 1.5.0 (2026-08-20)
2
22
 
3
23
  ### BREAKING CHANGE
@@ -0,0 +1,537 @@
1
+ Metadata-Version: 2.4
2
+ Name: uptimer-python-sdk
3
+ Version: 1.7.0
4
+ Summary: A Python SDK for hosted and self-hosted Uptimer
5
+ Project-URL: Homepage, https://myuptime.info
6
+ Project-URL: Documentation, https://uptimer.myuptime.info/latest/reference/python-sdk/
7
+ Project-URL: Repository, https://github.com/myuptime-info/uptimer-python-sdk
8
+ Project-URL: Changelog, https://myuptime.info/product-updates
9
+ Author-email: Roman Zadoev <zadoev@gmail.com>
10
+ License: MIT
11
+ License-File: LICENSE
12
+ License-File: NOTICE
13
+ Requires-Python: >=3.9
14
+ Requires-Dist: httpx>=0.28.1
15
+ Description-Content-Type: text/markdown
16
+
17
+ # Uptimer Python SDK
18
+
19
+ A Python SDK for hosted and self-hosted Uptimer.
20
+
21
+ * [Hosted Uptimer](https://myuptime.info)
22
+ * [Self-hosted documentation](https://uptimer.myuptime.info)
23
+ * [PyPI package](https://pypi.org/project/uptimer-python-sdk/)
24
+ * [Uptimer resources](https://myuptime.info/resources)
25
+ * [Product updates](https://myuptime.info/product-updates)
26
+
27
+ ## License
28
+
29
+ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
30
+
31
+ For third-party license information, see the [NOTICE](NOTICE) file.
32
+
33
+ ## Installation
34
+
35
+ ```shell
36
+ pip install uptimer-python-sdk
37
+ ```
38
+
39
+ or
40
+ ```shell
41
+ uv add uptimer-python-sdk
42
+ ```
43
+
44
+ ## Usage
45
+
46
+ ### Create client
47
+
48
+ #### self-hosted
49
+
50
+ ```python
51
+ from uptimer.client import UptimerClient
52
+ client = UptimerClient(
53
+ api_key="your-api-key-here",
54
+ base_url="http://127.0.0.1:2517/api", # or your custom base URL
55
+ )
56
+ ```
57
+
58
+ #### cloud
59
+ ```python
60
+ from uptimer.client import UptimerCloudClient
61
+ client = UptimerCloudClient(
62
+ api_key="your-api-key-here",
63
+ )
64
+ ```
65
+
66
+ ### Basic example
67
+
68
+ ```python
69
+ from uptimer.client import UptimerClient
70
+ from uptimer.errors import (
71
+ DefaultUptimerApiError,
72
+ IncompatibleServerError,
73
+ UptimerError,
74
+ UptimerInvalidHttpCodeError,
75
+ )
76
+ from uptimer.models.v2 import (
77
+ AGREEMENT_MAJORITY,
78
+ CreateWebsiteMonitorRequest,
79
+ UpdateWebsiteMonitorRequest,
80
+ WebsiteMonitorRequest,
81
+ WebsiteMonitorResponse,
82
+ WebsiteMonitorResponseBody,
83
+ )
84
+
85
+ client = UptimerClient(
86
+ api_key="your-api-key-here",
87
+ base_url="http://127.0.0.1:2517/api", # or your custom base URL
88
+ )
89
+
90
+ # Optional: fail fast with a message that names the fix, rather than a 404 on
91
+ # the first real call.
92
+ print("server:", client.check_compatibility())
93
+
94
+ workspace = client.v2.workspaces.all()[0]
95
+ locations = [location.name for location in client.v2.locations.all()]
96
+
97
+ monitor = client.v2.monitoring.websites.create(
98
+ CreateWebsiteMonitorRequest(
99
+ name="Checkout API",
100
+ interval=60, # seconds between probes
101
+ workspace_id=workspace.id,
102
+ request=WebsiteMonitorRequest(
103
+ url="https://checkout.example/health",
104
+ method="GET", # one of GET, POST, PATCH, OPTIONS
105
+ content_type="application/json",
106
+ data="",
107
+ ),
108
+ response=WebsiteMonitorResponse(
109
+ statuses=[200, 201], # any of these means the site is up
110
+ body=WebsiteMonitorResponseBody(content="ok"), # expected substring
111
+ ),
112
+ locations=locations,
113
+ # How many locations must report a problem before this monitor does:
114
+ # "any", "majority" or "all". Omit to keep the server default.
115
+ agreement=AGREEMENT_MAJORITY,
116
+ ),
117
+ )
118
+
119
+ monitor = client.v2.monitoring.websites.update(
120
+ monitor.id,
121
+ UpdateWebsiteMonitorRequest(
122
+ name="Checkout API",
123
+ interval=120,
124
+ request=WebsiteMonitorRequest(url="https://checkout.example/health", method="GET"),
125
+ response=WebsiteMonitorResponse(statuses=[200]),
126
+ locations=locations,
127
+ # Omitting agreement here keeps the stored one.
128
+ ),
129
+ )
130
+
131
+ # What is wrong right now. Only open incidents come back.
132
+ for incident in client.v2.incidents.all(workspace.id):
133
+ print(incident.monitor_name, incident.status, incident.locations.failing)
134
+
135
+ try:
136
+ client.v2.monitoring.websites.delete(monitor.id)
137
+ except DefaultUptimerApiError as e:
138
+ # error responses from the uptimer server
139
+ print(
140
+ e.message, # user message
141
+ e.code, # error id
142
+ e.error_type, # class of error
143
+ e.details, # detailed message for a developer
144
+ )
145
+ except IncompatibleServerError as e:
146
+ # the server does not provide API v2 — see Migrating from 0.4.x below
147
+ print(e)
148
+ except UptimerInvalidHttpCodeError as e:
149
+ # the uptimer api always returns 200; anything else is a transport error.
150
+ # a 404 really is "no such URL", not "no object with that id".
151
+ print(e.url, e.status_code)
152
+ except UptimerError: # base error, if you need one
153
+ raise
154
+ ```
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
+
215
+ ### Reporting your own observations
216
+
217
+ Uptimer probes websites itself. For anything else — a cron job, a queue worker,
218
+ a nightly export — you add a **custom signal** to a subject in the Uptimer UI
219
+ and report to it yourself.
220
+
221
+ Requires Uptimer 1.6.0 or later, and a **custom heartbeat or event** signal. The
222
+ platform HTTP signal of a website monitor is written by Uptimer's own probe and
223
+ refuses posted observations.
224
+
225
+ ```python
226
+ from uptimer.client import UptimerClient
227
+ from uptimer.models.v2 import (
228
+ OBSERVATION_STATUS_OK,
229
+ OBSERVATION_STATUS_PROBLEM,
230
+ CreateObservationRequest,
231
+ )
232
+
233
+ client = UptimerClient(
234
+ api_key="your-api-key-here",
235
+ base_url="http://127.0.0.1:2517/api",
236
+ )
237
+
238
+ # The two slugs are the address: the subject, and the signal within it. Both
239
+ # are shown on the signal's page in the Uptimer UI.
240
+ observations = client.v2.subjects("checkout-api").signals("worker-pulse").observations
241
+
242
+ # A heartbeat: "I ran, and I am fine."
243
+ stored = observations.create(CreateObservationRequest(status=OBSERVATION_STATUS_OK))
244
+
245
+ # Everything except status is optional.
246
+ stored = observations.create(
247
+ CreateObservationRequest(
248
+ status=OBSERVATION_STATUS_PROBLEM,
249
+ observed_at="2026-08-30T12:00:00Z", # RFC 3339; omit to mean "now"
250
+ value=0.0, # optional numeric reading
251
+ error="queue backlog over threshold",
252
+ labels={"instance": "worker-3", "env": "prod"},
253
+ ),
254
+ )
255
+
256
+ print(stored.accepted, stored.reject_reason)
257
+ ```
258
+
259
+ `accepted` reports **acceptance, not health**: it says Uptimer stored the
260
+ observation and may evaluate it, not that anything is wrong or fine. Whether an
261
+ observation raises an incident is decided by a *rule* that selects the signal.
262
+
263
+ An observation Uptimer keeps but will not evaluate — one stamped too far in the
264
+ future, say — comes back with `accepted=False` and a `reject_reason` such as
265
+ `clock_skew`. It is **returned, not raised**: it was received. An exception
266
+ means nothing was stored.
267
+
268
+ Retries are safe. An observation is identified by its signal, its `observed_at`
269
+ and its labels, so re-sending the same one replaces it rather than counting
270
+ twice.
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
+
406
+ ### Incident status
407
+
408
+ `client.v2.incidents.all()` returns only **open** incidents. `status` carries the
409
+ same words the Uptimer screens show, so a client and the UI cannot disagree:
410
+
411
+ | status | meaning |
412
+ |---|---|
413
+ | `problem` | confirmed, and notifications have gone out |
414
+ | `pending` | failing, but inside the confirm hold — **nobody has been notified yet** |
415
+ | `recovering` | reporting ok again while the incident is still open |
416
+ | `no_data` | nothing usable arrived; a silent location counts toward the agreement |
417
+ | `ok` | healthy |
418
+
419
+ `locations.failing` / `.unknown` / `.ok` is the evidence the verdict was taken
420
+ from. A location that has never reported stays in `unknown` — that is a real
421
+ state, not a missing one.
422
+
423
+ ### Migrating from 0.4.x
424
+
425
+ **1.5.0 targets API v2 only.** Your existing 0.4.x code keeps working against
426
+ the server — API v1 is unchanged and supported — but it must stay on the 0.4.x
427
+ SDK. Pin `uptimer-python-sdk<1` if you are not ready to move.
428
+
429
+ What changed:
430
+
431
+ | 0.4.x (API v1) | 1.5.0 (API v2) |
432
+ |---|---|
433
+ | `client.v1.workspaces` | `client.v2.workspaces` |
434
+ | `client.v1.regions` | `client.v2.locations` |
435
+ | `client.v1.rules` | `client.v2.monitoring.websites` |
436
+ | `Region` | `Location` |
437
+ | `Rule`, `CreateRuleRequest` | `WebsiteMonitor`, `CreateWebsiteMonitorRequest` |
438
+ | `regions=[...]` | `locations=[...]` |
439
+ | — | `agreement="any"|"majority"|"all"` |
440
+ | — | `client.v2.incidents` |
441
+ | `from uptimer.models import …` | `from uptimer.models.v2 import …` |
442
+
443
+ **The version namespace stays, and now covers the types too.** As in 0.4.x,
444
+ resources sit under the API version that serves them — `client.v1.*` becomes
445
+ `client.v2.*`, not a bare `client.*` — and the models follow: import them from
446
+ `uptimer.models.v2`, not from `uptimer.models`. The HTTP API is versioned by
447
+ path, so the SDK shows the same thing rather than hiding it. There are no
448
+ root-level aliases for either surface, so a stale flat import fails loudly
449
+ instead of silently binding to the wrong thing.
450
+
451
+ The deserialization exceptions (`ModelError`, `TypeMismatchError`, …) stay on
452
+ `uptimer.models`: the same error is raised whichever API version produced the
453
+ payload, so versioning them would say something untrue.
454
+
455
+ Why `monitoring.websites` rather than `monitors`: website monitoring is a
456
+ built-in template, not the general model. Keeping the bare name free lets other
457
+ monitor types arrive later without renaming this one.
458
+
459
+ `client.version()`, `client.check_compatibility()` and
460
+ `client.ensure_compatible()` are unchanged and stay on the client itself —
461
+ `/version` is a shared global endpoint, not a versioned one, so it works against
462
+ any server, including one too old for the rest of this SDK.
463
+
464
+ **Why 1.5.0 and not 1.0.0:** the SDK's major.minor tracks the uptimer release it
465
+ targets, so the version is the compatibility statement — 1.5.x speaks to uptimer
466
+ 1.5.0 and later. Patch numbers are independent, so an SDK fix can ship without a
467
+ server release.
468
+
469
+ Also, check out the [examples directory](https://github.com/myuptime-info/uptimer-python-sdk/tree/main/examples).
470
+
471
+ ### Development Setup
472
+
473
+ 1. Clone the repository:
474
+
475
+ ```bash
476
+ git clone <repository-url>
477
+ cd uptimer-python-sdk
478
+ ```
479
+
480
+ 2. Install dependencies:
481
+
482
+ ```bash
483
+ uv sync --dev
484
+ # for integration tests
485
+ uv run playwright install chromium
486
+ ```
487
+
488
+ 3. Run tests:
489
+
490
+ ```bash
491
+ uv run pytest
492
+ # integration
493
+ docker pull ghcr.io/myuptime-info/uptimer:1.3.0
494
+ docker run -p 2517:2517 ghcr.io/myuptime-info/uptimer:1.3.0
495
+ UPTIMER_URL=http://localhost:2517 uv run --integration
496
+ ```
497
+
498
+ 4. Run linting:
499
+
500
+ ```bash
501
+ uv run ruff check .
502
+ uv run mypy src
503
+ ```
504
+
505
+ 5. Format code:
506
+
507
+ ```bash
508
+ uv run ruff format .
509
+ ```
510
+
511
+ 6. Run pre-commit hooks:
512
+
513
+ ```bash
514
+ uv run pre-commit run --all-files
515
+ ```
516
+
517
+ ## Third-Party Licenses
518
+
519
+ This project uses the following third-party libraries:
520
+
521
+ ### Production Dependencies
522
+
523
+ - **httpx** (BSD 3-Clause License) - HTTP client for Python
524
+
525
+ ### Development Dependencies
526
+
527
+ - **mypy** (Apache 2.0 License) - Static type checker
528
+ - **playwright** (Apache 2.0 License) - Browser automation
529
+ - **pre-commit** (MIT License) - Git hooks framework
530
+ - **pytest** (MIT License) - Testing framework
531
+ - **pytest-cov** (MIT License) - Coverage plugin for pytest
532
+ - **pytest-httpx** (MIT License) - HTTPX plugin for pytest
533
+ - **pytest-playwright** (MIT License) - Playwright plugin for pytest
534
+ - **responses** (Apache 2.0 License) - Mock library for requests
535
+ - **ruff** (MIT License) - Fast Python linter and formatter
536
+
537
+ All third-party licenses are compatible with the MIT License used by this project. Note that the BSD 3-Clause License (used by httpx) includes an additional restriction prohibiting the use of the copyright holder's name for endorsement without permission.