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.
- {uptimer_python_sdk-1.5.0 → uptimer_python_sdk-1.7.0}/CHANGELOG.md +20 -0
- uptimer_python_sdk-1.7.0/PKG-INFO +537 -0
- uptimer_python_sdk-1.7.0/README.md +521 -0
- {uptimer_python_sdk-1.5.0 → uptimer_python_sdk-1.7.0}/Taskfile.yml +13 -2
- {uptimer_python_sdk-1.5.0 → uptimer_python_sdk-1.7.0}/pyproject.toml +10 -2
- {uptimer_python_sdk-1.5.0 → uptimer_python_sdk-1.7.0}/src/uptimer/__init__.py +3 -3
- {uptimer_python_sdk-1.5.0 → uptimer_python_sdk-1.7.0}/src/uptimer/client.py +13 -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.5.0 → uptimer_python_sdk-1.7.0}/src/uptimer/endpoints/v2.py +3 -0
- {uptimer_python_sdk-1.5.0 → uptimer_python_sdk-1.7.0}/src/uptimer/models/v2/__init__.py +52 -0
- uptimer_python_sdk-1.7.0/src/uptimer/models/v2/acknowledgement.py +99 -0
- {uptimer_python_sdk-1.5.0 → uptimer_python_sdk-1.7.0}/src/uptimer/models/v2/deserialize.py +59 -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/observation.py +71 -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.7.0/tests/integrations/test_observations.py +155 -0
- 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.5.0 → 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.5.0 → uptimer_python_sdk-1.7.0}/tests/test_model_namespace.py +30 -0
- uptimer_python_sdk-1.7.0/tests/test_observations_endpoint.py +217 -0
- uptimer_python_sdk-1.7.0/tests/test_subjects_endpoint.py +223 -0
- {uptimer_python_sdk-1.5.0 → uptimer_python_sdk-1.7.0}/uv.lock +1 -1
- uptimer_python_sdk-1.5.0/PKG-INFO +0 -287
- uptimer_python_sdk-1.5.0/README.md +0 -271
- {uptimer_python_sdk-1.5.0 → uptimer_python_sdk-1.7.0}/.gitignore +0 -0
- {uptimer_python_sdk-1.5.0 → uptimer_python_sdk-1.7.0}/.pre-commit-config.yaml +0 -0
- {uptimer_python_sdk-1.5.0 → uptimer_python_sdk-1.7.0}/.vscode/extensions.json +0 -0
- {uptimer_python_sdk-1.5.0 → uptimer_python_sdk-1.7.0}/.vscode/launch.json +0 -0
- {uptimer_python_sdk-1.5.0 → uptimer_python_sdk-1.7.0}/.vscode/settings.json +0 -0
- {uptimer_python_sdk-1.5.0 → uptimer_python_sdk-1.7.0}/.vscode/tasks.json +0 -0
- {uptimer_python_sdk-1.5.0 → uptimer_python_sdk-1.7.0}/LICENSE +0 -0
- {uptimer_python_sdk-1.5.0 → uptimer_python_sdk-1.7.0}/NOTICE +0 -0
- {uptimer_python_sdk-1.5.0 → uptimer_python_sdk-1.7.0}/examples/01_client_setup.py +0 -0
- {uptimer_python_sdk-1.5.0 → uptimer_python_sdk-1.7.0}/examples/02_list_workspaces.py +0 -0
- {uptimer_python_sdk-1.5.0 → uptimer_python_sdk-1.7.0}/examples/03_list_locations.py +0 -0
- {uptimer_python_sdk-1.5.0 → uptimer_python_sdk-1.7.0}/examples/04_create_website_monitor.py +0 -0
- {uptimer_python_sdk-1.5.0 → uptimer_python_sdk-1.7.0}/examples/05_open_incidents.py +0 -0
- {uptimer_python_sdk-1.5.0 → uptimer_python_sdk-1.7.0}/examples/README.md +0 -0
- {uptimer_python_sdk-1.5.0 → uptimer_python_sdk-1.7.0}/examples/__init__.py +0 -0
- {uptimer_python_sdk-1.5.0 → uptimer_python_sdk-1.7.0}/src/uptimer/compat.py +0 -0
- {uptimer_python_sdk-1.5.0 → uptimer_python_sdk-1.7.0}/src/uptimer/endpoints/__init__.py +0 -0
- {uptimer_python_sdk-1.5.0 → uptimer_python_sdk-1.7.0}/src/uptimer/endpoints/endpoint.py +0 -0
- {uptimer_python_sdk-1.5.0 → uptimer_python_sdk-1.7.0}/src/uptimer/endpoints/incidents.py +0 -0
- {uptimer_python_sdk-1.5.0 → uptimer_python_sdk-1.7.0}/src/uptimer/endpoints/locations.py +0 -0
- {uptimer_python_sdk-1.5.0 → uptimer_python_sdk-1.7.0}/src/uptimer/endpoints/websites.py +0 -0
- {uptimer_python_sdk-1.5.0 → uptimer_python_sdk-1.7.0}/src/uptimer/endpoints/workspaces.py +0 -0
- {uptimer_python_sdk-1.5.0 → uptimer_python_sdk-1.7.0}/src/uptimer/errors.py +0 -0
- {uptimer_python_sdk-1.5.0 → uptimer_python_sdk-1.7.0}/src/uptimer/http.py +0 -0
- {uptimer_python_sdk-1.5.0 → uptimer_python_sdk-1.7.0}/src/uptimer/models/__init__.py +0 -0
- {uptimer_python_sdk-1.5.0 → uptimer_python_sdk-1.7.0}/src/uptimer/models/errors.py +0 -0
- {uptimer_python_sdk-1.5.0 → uptimer_python_sdk-1.7.0}/src/uptimer/models/v2/incident.py +0 -0
- {uptimer_python_sdk-1.5.0 → uptimer_python_sdk-1.7.0}/src/uptimer/models/v2/location.py +0 -0
- {uptimer_python_sdk-1.5.0 → uptimer_python_sdk-1.7.0}/src/uptimer/models/v2/monitor.py +0 -0
- {uptimer_python_sdk-1.5.0 → uptimer_python_sdk-1.7.0}/src/uptimer/models/v2/workspace.py +0 -0
- {uptimer_python_sdk-1.5.0 → uptimer_python_sdk-1.7.0}/src/uptimer/py.typed +0 -0
- {uptimer_python_sdk-1.5.0 → uptimer_python_sdk-1.7.0}/tests/__init__.py +0 -0
- {uptimer_python_sdk-1.5.0 → uptimer_python_sdk-1.7.0}/tests/conftest.py +0 -0
- {uptimer_python_sdk-1.5.0 → uptimer_python_sdk-1.7.0}/tests/integrations/__init__.py +0 -0
- {uptimer_python_sdk-1.5.0 → uptimer_python_sdk-1.7.0}/tests/integrations/conftest.py +0 -0
- {uptimer_python_sdk-1.5.0 → uptimer_python_sdk-1.7.0}/tests/integrations/test_workspaces.py +0 -0
- {uptimer_python_sdk-1.5.0 → uptimer_python_sdk-1.7.0}/tests/test_endpoint.py +0 -0
- {uptimer_python_sdk-1.5.0 → uptimer_python_sdk-1.7.0}/tests/test_integration_v2.py +0 -0
- {uptimer_python_sdk-1.5.0 → uptimer_python_sdk-1.7.0}/tests/test_v2_endpoints.py +0 -0
- {uptimer_python_sdk-1.5.0 → uptimer_python_sdk-1.7.0}/tests/test_workspaces_endpoint.py +0 -0
- {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.
|