uptimer-python-sdk 1.7.0__tar.gz → 1.8.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 (80) hide show
  1. uptimer_python_sdk-1.8.0/.cursor/rules/sdk-docs-implemented-only.mdc +47 -0
  2. uptimer_python_sdk-1.8.0/AGENTS.md +32 -0
  3. {uptimer_python_sdk-1.7.0 → uptimer_python_sdk-1.8.0}/CHANGELOG.md +50 -0
  4. {uptimer_python_sdk-1.7.0 → uptimer_python_sdk-1.8.0}/PKG-INFO +300 -10
  5. {uptimer_python_sdk-1.7.0 → uptimer_python_sdk-1.8.0}/README.md +299 -9
  6. {uptimer_python_sdk-1.7.0 → uptimer_python_sdk-1.8.0}/pyproject.toml +5 -1
  7. {uptimer_python_sdk-1.7.0 → uptimer_python_sdk-1.8.0}/src/uptimer/__init__.py +3 -3
  8. uptimer_python_sdk-1.8.0/src/uptimer/endpoints/delivery.py +84 -0
  9. uptimer_python_sdk-1.8.0/src/uptimer/endpoints/notifications.py +414 -0
  10. {uptimer_python_sdk-1.7.0 → uptimer_python_sdk-1.8.0}/src/uptimer/endpoints/subjects.py +227 -21
  11. {uptimer_python_sdk-1.7.0 → uptimer_python_sdk-1.8.0}/src/uptimer/endpoints/v2.py +3 -0
  12. {uptimer_python_sdk-1.7.0 → uptimer_python_sdk-1.8.0}/src/uptimer/endpoints/websites.py +40 -0
  13. {uptimer_python_sdk-1.7.0 → uptimer_python_sdk-1.8.0}/src/uptimer/models/v2/__init__.py +120 -0
  14. {uptimer_python_sdk-1.7.0 → uptimer_python_sdk-1.8.0}/src/uptimer/models/v2/deserialize.py +113 -0
  15. uptimer_python_sdk-1.8.0/src/uptimer/models/v2/notifications.py +332 -0
  16. uptimer_python_sdk-1.8.0/src/uptimer/models/v2/rule.py +222 -0
  17. uptimer_python_sdk-1.8.0/src/uptimer/models/v2/signal.py +90 -0
  18. {uptimer_python_sdk-1.7.0 → uptimer_python_sdk-1.8.0}/src/uptimer/models/v2/subject.py +4 -3
  19. uptimer_python_sdk-1.8.0/tests/integrations/test_notifications.py +201 -0
  20. uptimer_python_sdk-1.8.0/tests/integrations/test_signals_rules.py +135 -0
  21. uptimer_python_sdk-1.8.0/tests/test_alert_delivery_endpoint.py +208 -0
  22. {uptimer_python_sdk-1.7.0 → uptimer_python_sdk-1.8.0}/tests/test_model_namespace.py +65 -0
  23. uptimer_python_sdk-1.8.0/tests/test_notifications_endpoint.py +353 -0
  24. uptimer_python_sdk-1.8.0/tests/test_server_contract_v2_notifications.py +150 -0
  25. uptimer_python_sdk-1.8.0/tests/test_signals_rules_endpoint.py +418 -0
  26. {uptimer_python_sdk-1.7.0 → uptimer_python_sdk-1.8.0}/uv.lock +1 -1
  27. {uptimer_python_sdk-1.7.0 → uptimer_python_sdk-1.8.0}/.gitignore +0 -0
  28. {uptimer_python_sdk-1.7.0 → uptimer_python_sdk-1.8.0}/.pre-commit-config.yaml +0 -0
  29. {uptimer_python_sdk-1.7.0 → uptimer_python_sdk-1.8.0}/.vscode/extensions.json +0 -0
  30. {uptimer_python_sdk-1.7.0 → uptimer_python_sdk-1.8.0}/.vscode/launch.json +0 -0
  31. {uptimer_python_sdk-1.7.0 → uptimer_python_sdk-1.8.0}/.vscode/settings.json +0 -0
  32. {uptimer_python_sdk-1.7.0 → uptimer_python_sdk-1.8.0}/.vscode/tasks.json +0 -0
  33. {uptimer_python_sdk-1.7.0 → uptimer_python_sdk-1.8.0}/LICENSE +0 -0
  34. {uptimer_python_sdk-1.7.0 → uptimer_python_sdk-1.8.0}/NOTICE +0 -0
  35. {uptimer_python_sdk-1.7.0 → uptimer_python_sdk-1.8.0}/Taskfile.yml +0 -0
  36. {uptimer_python_sdk-1.7.0 → uptimer_python_sdk-1.8.0}/examples/01_client_setup.py +0 -0
  37. {uptimer_python_sdk-1.7.0 → uptimer_python_sdk-1.8.0}/examples/02_list_workspaces.py +0 -0
  38. {uptimer_python_sdk-1.7.0 → uptimer_python_sdk-1.8.0}/examples/03_list_locations.py +0 -0
  39. {uptimer_python_sdk-1.7.0 → uptimer_python_sdk-1.8.0}/examples/04_create_website_monitor.py +0 -0
  40. {uptimer_python_sdk-1.7.0 → uptimer_python_sdk-1.8.0}/examples/05_open_incidents.py +0 -0
  41. {uptimer_python_sdk-1.7.0 → uptimer_python_sdk-1.8.0}/examples/README.md +0 -0
  42. {uptimer_python_sdk-1.7.0 → uptimer_python_sdk-1.8.0}/examples/__init__.py +0 -0
  43. {uptimer_python_sdk-1.7.0 → uptimer_python_sdk-1.8.0}/src/uptimer/client.py +0 -0
  44. {uptimer_python_sdk-1.7.0 → uptimer_python_sdk-1.8.0}/src/uptimer/compat.py +0 -0
  45. {uptimer_python_sdk-1.7.0 → uptimer_python_sdk-1.8.0}/src/uptimer/endpoints/__init__.py +0 -0
  46. {uptimer_python_sdk-1.7.0 → uptimer_python_sdk-1.8.0}/src/uptimer/endpoints/endpoint.py +0 -0
  47. {uptimer_python_sdk-1.7.0 → uptimer_python_sdk-1.8.0}/src/uptimer/endpoints/incidents.py +0 -0
  48. {uptimer_python_sdk-1.7.0 → uptimer_python_sdk-1.8.0}/src/uptimer/endpoints/locations.py +0 -0
  49. {uptimer_python_sdk-1.7.0 → uptimer_python_sdk-1.8.0}/src/uptimer/endpoints/v1.py +0 -0
  50. {uptimer_python_sdk-1.7.0 → uptimer_python_sdk-1.8.0}/src/uptimer/endpoints/workspaces.py +0 -0
  51. {uptimer_python_sdk-1.7.0 → uptimer_python_sdk-1.8.0}/src/uptimer/errors.py +0 -0
  52. {uptimer_python_sdk-1.7.0 → uptimer_python_sdk-1.8.0}/src/uptimer/http.py +0 -0
  53. {uptimer_python_sdk-1.7.0 → uptimer_python_sdk-1.8.0}/src/uptimer/models/__init__.py +0 -0
  54. {uptimer_python_sdk-1.7.0 → uptimer_python_sdk-1.8.0}/src/uptimer/models/errors.py +0 -0
  55. {uptimer_python_sdk-1.7.0 → uptimer_python_sdk-1.8.0}/src/uptimer/models/v2/acknowledgement.py +0 -0
  56. {uptimer_python_sdk-1.7.0 → uptimer_python_sdk-1.8.0}/src/uptimer/models/v2/incident.py +0 -0
  57. {uptimer_python_sdk-1.7.0 → uptimer_python_sdk-1.8.0}/src/uptimer/models/v2/location.py +0 -0
  58. {uptimer_python_sdk-1.7.0 → uptimer_python_sdk-1.8.0}/src/uptimer/models/v2/maintenance.py +0 -0
  59. {uptimer_python_sdk-1.7.0 → uptimer_python_sdk-1.8.0}/src/uptimer/models/v2/monitor.py +0 -0
  60. {uptimer_python_sdk-1.7.0 → uptimer_python_sdk-1.8.0}/src/uptimer/models/v2/observation.py +0 -0
  61. {uptimer_python_sdk-1.7.0 → uptimer_python_sdk-1.8.0}/src/uptimer/models/v2/workspace.py +0 -0
  62. {uptimer_python_sdk-1.7.0 → uptimer_python_sdk-1.8.0}/src/uptimer/py.typed +0 -0
  63. {uptimer_python_sdk-1.7.0 → uptimer_python_sdk-1.8.0}/tests/__init__.py +0 -0
  64. {uptimer_python_sdk-1.7.0 → uptimer_python_sdk-1.8.0}/tests/conftest.py +0 -0
  65. {uptimer_python_sdk-1.7.0 → uptimer_python_sdk-1.8.0}/tests/integrations/__init__.py +0 -0
  66. {uptimer_python_sdk-1.7.0 → uptimer_python_sdk-1.8.0}/tests/integrations/conftest.py +0 -0
  67. {uptimer_python_sdk-1.7.0 → uptimer_python_sdk-1.8.0}/tests/integrations/test_acknowledgement.py +0 -0
  68. {uptimer_python_sdk-1.7.0 → uptimer_python_sdk-1.8.0}/tests/integrations/test_observations.py +0 -0
  69. {uptimer_python_sdk-1.7.0 → uptimer_python_sdk-1.8.0}/tests/integrations/test_subjects.py +0 -0
  70. {uptimer_python_sdk-1.7.0 → uptimer_python_sdk-1.8.0}/tests/integrations/test_workspaces.py +0 -0
  71. {uptimer_python_sdk-1.7.0 → uptimer_python_sdk-1.8.0}/tests/test_acknowledgement.py +0 -0
  72. {uptimer_python_sdk-1.7.0 → uptimer_python_sdk-1.8.0}/tests/test_client.py +0 -0
  73. {uptimer_python_sdk-1.7.0 → uptimer_python_sdk-1.8.0}/tests/test_endpoint.py +0 -0
  74. {uptimer_python_sdk-1.7.0 → uptimer_python_sdk-1.8.0}/tests/test_integration_v2.py +0 -0
  75. {uptimer_python_sdk-1.7.0 → uptimer_python_sdk-1.8.0}/tests/test_maintenance.py +0 -0
  76. {uptimer_python_sdk-1.7.0 → uptimer_python_sdk-1.8.0}/tests/test_observations_endpoint.py +0 -0
  77. {uptimer_python_sdk-1.7.0 → uptimer_python_sdk-1.8.0}/tests/test_subjects_endpoint.py +0 -0
  78. {uptimer_python_sdk-1.7.0 → uptimer_python_sdk-1.8.0}/tests/test_v2_endpoints.py +0 -0
  79. {uptimer_python_sdk-1.7.0 → uptimer_python_sdk-1.8.0}/tests/test_workspaces_endpoint.py +0 -0
  80. {uptimer_python_sdk-1.7.0 → uptimer_python_sdk-1.8.0}/tools/mypy-diff.sh +0 -0
@@ -0,0 +1,47 @@
1
+ ---
2
+ description: SDK docs describe only what this package implements — no gaps, no roadmap, no release process
3
+ alwaysApply: true
4
+ ---
5
+
6
+ # SDK docs describe only what this package implements
7
+
8
+ Applies to everything a user of the published package reads: `README.md` (which
9
+ is also the PyPI long description), docstrings under `src/`, and `examples/`.
10
+
11
+ ## The rule
12
+
13
+ **Document only capabilities present in this package version.** That is all.
14
+
15
+ A README is a contract for the package somebody just installed, not a status
16
+ board for the work around it.
17
+
18
+ ## Do not write
19
+
20
+ - "this SDK does not wrap those routes **yet**", "not implemented", "missing",
21
+ "unsupported" — anything that describes an absence;
22
+ - "not in the published package", "until X is on PyPI", "use a checkout of this
23
+ repository" — release sequencing and workarounds;
24
+ - ticket or issue numbers, internal milestones, or which release something is
25
+ planned for;
26
+ - TODOs or roadmap notes in public docstrings.
27
+
28
+ ## Do instead
29
+
30
+ - **Omit it.** A capability this package does not have simply does not appear.
31
+ The reader loses nothing: they cannot call it either way.
32
+ - Where the HTTP API has something the package does not, the
33
+ [REST documentation](https://uptimer.myuptime.info/latest/reference/rest-api/)
34
+ covers it. Link that for the full reference when it helps — without saying
35
+ what is missing here.
36
+ - State the **server** a capability needs when it is not the package minimum
37
+ ("needs an Uptimer 1.7.0+ server"). That is a fact about an implemented
38
+ feature, not a gap.
39
+ - When something ships, add it to the docs in the same change that adds the
40
+ code.
41
+
42
+ ## Why
43
+
44
+ Users read this to decide what to call. "We have not wrapped that yet" answers a
45
+ question nobody asked and dates the moment it is written; a checkout instruction
46
+ sends somebody around a problem that a later release quietly removed. Both have
47
+ happened here.
@@ -0,0 +1,32 @@
1
+ # AGENTS.md — uptimer-python-sdk
2
+
3
+ The public Python client for Uptimer. Fleet-wide rules apply (see the workspace
4
+ `AGENTS.md`): trunk-based on `main`, no agent attribution, Commitizen subjects.
5
+
6
+ ## Documentation policy
7
+
8
+ **SDK docs describe only what this package implements.** That is all.
9
+
10
+ `README.md` (the PyPI long description), the docstrings under `src/`, and
11
+ `examples/` document capabilities present in **this package version**. No "not
12
+ wrapped yet", no checkout workarounds, no ticket numbers, no release
13
+ sequencing. A capability this package does not have is **omitted** — the
14
+ [REST documentation](https://uptimer.myuptime.info/latest/reference/rest-api/)
15
+ covers the HTTP API in full, and may be linked for that.
16
+
17
+ The same rule, in the form Cursor reads:
18
+ [`.cursor/rules/sdk-docs-implemented-only.mdc`](.cursor/rules/sdk-docs-implemented-only.mdc).
19
+
20
+ ## Working here
21
+
22
+ ```sh
23
+ task cq # lint + type-check + tests (ruff, mypy, pytest)
24
+ task test # pytest alone
25
+ task build # sdist + wheel into dist/
26
+ ```
27
+
28
+ - The package version tracks the Uptimer release it targets: 1.8.x speaks to
29
+ Uptimer 1.8.0 and later. `uptimer.__version__` is where the compatibility
30
+ minimum comes from, and `cz` bumps it with `pyproject.toml`.
31
+ - **Never publish a prerelease** (rc/a/b/dev) to TestPyPI or PyPI. This package
32
+ ships final `X.Y.Z` only; publishing is an operator action.
@@ -1,3 +1,53 @@
1
+ ## 1.8.0 (2026-09-19)
2
+
3
+ Targets Uptimer **1.8.0** and later.
4
+
5
+ ### Feat
6
+
7
+ - **notifications**: `client.v2.notifications.destinations` — list, create, get,
8
+ update, delete, plus `set_enabled`, `make_default` and `send_test`. A
9
+ destination is one place a workspace's alerts can go: a Slack incoming
10
+ webhook, or any HTTP endpoint.
11
+ - **notifications**: `client.v2.notifications.transformations` — list, create,
12
+ get, update, delete, plus `samples` (the field vocabulary a template may
13
+ read) and `preview` (the same judgement a save makes, storing nothing). A
14
+ template is stored only once it renders all three messages.
15
+ - **notifications**: `client.v2.notifications.deliveries.all(...)` — the
16
+ delivery log, filtered by destination and by undelivered. Records are kept 30
17
+ days.
18
+ - **delivery**: `client.v2.subjects(slug).delivery` and
19
+ `client.v2.monitoring.websites(id).delivery` — `get`, `replace` and `clear`
20
+ for which destinations one subject tells, and about what. The table is the
21
+ resource: a save replaces it rather than merging.
22
+ - **signals**: `client.v2.subjects(slug).signals` — list, create, get, update
23
+ and delete. Reporting observations to an existing signal is unchanged.
24
+ - **rules**: `client.v2.subjects(slug).rules` — list, create, get, update and
25
+ delete, with typed policy models (`RuleDocument`, `RuleInput`,
26
+ `RuleDecision`, `RuleWait`). A rule input citing another rule is `from_rule`
27
+ in Python, because `from` is a keyword.
28
+
29
+ ### Fix
30
+
31
+ - **subjects**: a `workspace_id` given to `client.v2.subjects(slug, ws)` now
32
+ reaches the signal, rule and observation routes under it. Without it, a
33
+ caller holding the same subject slug in two workspaces was answered
34
+ `Ambiguous subject` by the server — by the one call that could have
35
+ disambiguated.
36
+ - **compat**: `uptimer.__version__` is bumped with the package, so the minimum
37
+ server version it derives is the version this package targets. It had
38
+ drifted: 1.7.0 moved `pyproject.toml` alone, and the published client went on
39
+ accepting a 1.6.0 server.
40
+
41
+ ### Docs
42
+
43
+ - The README documents only what this package implements, with runnable
44
+ examples for destinations, transformations, subject delivery, the delivery
45
+ log, signals and rules. The complete HTTP API — including anything not
46
+ wrapped here — is at
47
+ <https://uptimer.myuptime.info/latest/reference/rest-api/>.
48
+ - That policy is now written down for contributors and agents in `AGENTS.md`
49
+ and `.cursor/rules/sdk-docs-implemented-only.mdc`.
50
+
1
51
  ## 1.7.0 (2026-09-14)
2
52
 
3
53
  ### Feat
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: uptimer-python-sdk
3
- Version: 1.7.0
3
+ Version: 1.8.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/
@@ -41,6 +41,17 @@ or
41
41
  uv add uptimer-python-sdk
42
42
  ```
43
43
 
44
+ **This package targets Uptimer 1.8.0 and later.** The SDK's major.minor tracks
45
+ the server release it speaks to, so install the one that matches yours; patch
46
+ numbers move independently. `client.ensure_compatible()` checks it for you and
47
+ fails with a message that names the fix.
48
+
49
+ Still on API v1? Pin `uptimer-python-sdk<1`. The server's v1 is unchanged and
50
+ supported, so 0.4.x keeps working — it just cannot use anything newer.
51
+
52
+ The complete REST API reference is at
53
+ [uptimer.myuptime.info](https://uptimer.myuptime.info/latest/reference/rest-api/).
54
+
44
55
  ## Usage
45
56
 
46
57
  ### Create client
@@ -208,9 +219,97 @@ locations, so it has its own call — `client.v2.monitoring.websites.create` —
208
219
  and asking for `subject_kind="website"` on this route is refused with a message
209
220
  saying so.
210
221
 
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.
222
+ ### Signals: what reports to a subject
223
+
224
+ A **signal** is one thing that reports. A **heartbeat** is expected to keep
225
+ reporting, so its silence is itself a symptom; an **event** reports only when
226
+ there is something to say, so its silence means nothing. The kind is fixed when
227
+ the signal is created — senders are already posting to it, and changing what
228
+ their silence means underneath them is not a rename.
229
+
230
+ ```python
231
+ from uptimer.models.v2 import (
232
+ SIGNAL_KIND_HEARTBEAT,
233
+ CreateSignalRequest,
234
+ UpdateSignalRequest,
235
+ )
236
+
237
+ signals = client.v2.subjects("nightly-export", "your-workspace-id").signals
238
+
239
+ signal = signals.create(
240
+ CreateSignalRequest(
241
+ name="Worker pulse",
242
+ kind=SIGNAL_KIND_HEARTBEAT,
243
+ meta={"team": "payments"}, # stored and returned untouched
244
+ ),
245
+ )
246
+
247
+ # `id` is the slug a sender posts to. A rename never moves it.
248
+ signals.update(signal.id, UpdateSignalRequest(name="Worker heartbeat", meta={}))
249
+
250
+ for existing in signals.all():
251
+ print(existing.id, existing.signal_kind)
252
+
253
+ # Deleting a signal deletes its observations. One a rule reads is refused:
254
+ # retarget or remove those rules first.
255
+ signals.delete(signal.id)
256
+ ```
257
+
258
+ ### Rules: what counts as a problem
259
+
260
+ A **rule** reads a subject's signals — or another of its rules — and decides
261
+ whether there is a problem. The policy is a document: what it reads, what those
262
+ inputs have to agree on, and how long a state must hold.
263
+
264
+ ```python
265
+ from uptimer.models.v2 import (
266
+ INPUT_MODE_LATEST_VALUE,
267
+ INPUT_MODE_STATUS,
268
+ NEED_ANY,
269
+ CreateRuleRequest,
270
+ RuleDecision,
271
+ RuleDocument,
272
+ RuleInput,
273
+ RuleWait,
274
+ UpdateRuleRequest,
275
+ )
276
+
277
+ rules = client.v2.subjects("nightly-export", "your-workspace-id").rules
278
+
279
+ rule = rules.create(
280
+ CreateRuleRequest(
281
+ name="Export health",
282
+ document=RuleDocument(
283
+ inputs=[
284
+ # The heartbeat stopped, or its last observation said "problem".
285
+ RuleInput(signal="worker-pulse", mode=INPUT_MODE_STATUS,
286
+ no_data_after="5m"),
287
+ # A number out of range.
288
+ RuleInput(signal="queue-depth", mode=INPUT_MODE_LATEST_VALUE,
289
+ compare=">", threshold=1000),
290
+ # Another rule's verdict. `from` is a Python keyword, so it is
291
+ # `from_rule` here and `from` on the wire.
292
+ RuleInput(from_rule="queue-health"),
293
+ ],
294
+ decision=RuleDecision(need=NEED_ANY),
295
+ wait=RuleWait(confirm_after="2m", close_after="2m"),
296
+ ),
297
+ ),
298
+ )
299
+
300
+ # The policy is a REPLACEMENT, not a patch: read it, change it, send the whole
301
+ # thing back. A CHANGED policy increments policy_version; sending the same
302
+ # document back leaves it where it was. Either way the rule keeps its slug, so
303
+ # the incidents already pointing at it stay attached.
304
+ rule.document.wait.confirm_after = "5m"
305
+ rules.update(rule.id, UpdateRuleRequest(name=rule.name, document=rule.document))
306
+
307
+ rules.delete(rule.id)
308
+ ```
309
+
310
+ Durations are strings — `"5m"`, `"2m0s"` — so a stored policy reads the way you
311
+ would write it. Every input must cite a signal or a rule **of this subject**: a
312
+ subject is the boundary, so add the signals first.
214
313
 
215
314
  ### Reporting your own observations
216
315
 
@@ -281,12 +380,8 @@ under its subject on v2. Neither method falls back to the other, and there is no
281
380
  kind-agnostic one: acknowledging is a claim about a specific incident, and an
282
381
  SDK that guessed which family it belonged to could claim the wrong one.
283
382
 
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.
383
+ **Availability.** These methods need an **Uptimer 1.7.0+** server — the routes
384
+ do not exist before that.
290
385
 
291
386
  **Custom monitoring** — list the subject's open incidents, pick one, acknowledge
292
387
  it by id:
@@ -403,6 +498,201 @@ out — and `muted` says what waits, in the server's own words.
403
498
  A past end time, a window already running, a website subject, or a caller who is
404
499
  not an editor raise `DefaultUptimerApiError`. Reading takes the viewer role.
405
500
 
501
+ ### Alert destinations
502
+
503
+ A **destination** is one place a workspace's alerts can go: a Slack incoming
504
+ webhook, or any HTTP endpoint that accepts a POST. A workspace has as many as it
505
+ needs, and each subject chooses which of them it tells.
506
+
507
+ Reading a destination takes the **editor** role, not just membership: a
508
+ destination holds a webhook URL, and a URL is enough for anyone holding it to
509
+ post into your channel.
510
+
511
+ ```python
512
+ from uptimer.models.v2 import (
513
+ DESTINATION_TYPE_SLACK,
514
+ DESTINATION_TYPE_WEBHOOK,
515
+ CreateDestinationRequest,
516
+ UpdateDestinationRequest,
517
+ )
518
+
519
+ destinations = client.v2.notifications.destinations
520
+
521
+ # The FIRST destination in a workspace becomes its default, asked for or not:
522
+ # a workspace whose only destination is not the default notifies nobody.
523
+ slack = destinations.create(
524
+ CreateDestinationRequest(
525
+ name="Acme · #incidents",
526
+ url="https://hooks.slack.com/services/T00/B00/xxxx",
527
+ destination_type=DESTINATION_TYPE_SLACK,
528
+ channel="#incidents", # stored without the '#'
529
+ ),
530
+ workspace_id="your-workspace-id",
531
+ )
532
+
533
+ relay = destinations.create(
534
+ CreateDestinationRequest(
535
+ name="Pager relay",
536
+ url="https://hooks.example.com/uptimer",
537
+ destination_type=DESTINATION_TYPE_WEBHOOK,
538
+ ),
539
+ workspace_id="your-workspace-id",
540
+ )
541
+
542
+ # Move the address, or attach a payload template (see below). The TYPE is fixed
543
+ # at creation: delete and recreate to change it.
544
+ destinations.update(
545
+ relay.id,
546
+ UpdateDestinationRequest(name=relay.name, url="https://hooks.example.com/v2"),
547
+ workspace_id="your-workspace-id",
548
+ )
549
+
550
+ destinations.set_enabled(relay.id, enabled=False, workspace_id="your-workspace-id")
551
+ destinations.make_default(slack.id, workspace_id="your-workspace-id")
552
+
553
+ # A REAL send: same render, same transport, same delivery record as an alert.
554
+ # A destination that refuses it raises, with the far end's own words.
555
+ destinations.send_test(slack.id, workspace_id="your-workspace-id")
556
+
557
+ # Deleting the default promotes nobody — check what you did.
558
+ answer = destinations.delete(relay.id, workspace_id="your-workspace-id")
559
+ assert answer.was_default is False
560
+ ```
561
+
562
+ `workspace_id` is optional everywhere here. It settles an ambiguity rather than
563
+ being required: these resources have no slug of their own, so the server
564
+ searches your memberships when it is absent and says so if the answer is more
565
+ than one.
566
+
567
+ ### Transformations: the shape a destination receives
568
+
569
+ A **transformation** is a named template for the body a destination gets — a
570
+ PagerDuty event, your own JSON, a line of text. A destination with none gets
571
+ Uptimer's built-in Slack-shaped message, which is what `transformation_id=None`
572
+ means.
573
+
574
+ ```python
575
+ from uptimer.models.v2 import (
576
+ CreateTransformationRequest,
577
+ UpdateDestinationRequest,
578
+ )
579
+
580
+ transformations = client.v2.notifications.transformations
581
+
582
+ # The vocabulary: three sample messages, each with every field a template may
583
+ # read. No workspace — these are the product's own fixtures.
584
+ for sample in transformations.samples():
585
+ print(sample.label, sorted(sample.fields))
586
+
587
+ # Ask before you write. `passed` is exactly the condition a save enforces.
588
+ preview = transformations.preview(
589
+ '{"event": "{{ kind }}", "summary": "{{ summary }}"}',
590
+ workspace_id="your-workspace-id",
591
+ )
592
+ assert preview.passed
593
+ for result in preview.results:
594
+ print(result.label, result.ok, result.output or result.error)
595
+
596
+ template = transformations.create(
597
+ CreateTransformationRequest(
598
+ name="PagerDuty compact",
599
+ template='{"event": "{{ kind }}", "summary": "{{ summary }}"}',
600
+ ),
601
+ workspace_id="your-workspace-id",
602
+ )
603
+ assert template.content_type == "application/json"
604
+
605
+ # Attach it. Passing transformation_id=None puts the destination back on
606
+ # Uptimer's own message.
607
+ client.v2.notifications.destinations.update(
608
+ slack.id,
609
+ UpdateDestinationRequest(
610
+ name=slack.name,
611
+ url=slack.url,
612
+ channel=slack.channel,
613
+ transformation_id=template.id,
614
+ ),
615
+ workspace_id="your-workspace-id",
616
+ )
617
+ ```
618
+
619
+ **A template is stored only once it renders all three messages.** There is no
620
+ force flag: a template that breaks on one of them raises, naming the sample that
621
+ broke, and a refused edit leaves the stored template exactly as it was. A
622
+ template starting with `{` or `[` is treated as JSON — values are escaped as
623
+ they are substituted and the result has to parse, so a quoted error cannot break
624
+ the document.
625
+
626
+ ### Alert delivery: which destinations a subject tells
627
+
628
+ The choice lives on the **subject**, not on the workspace: the marketing site
629
+ telling nobody must not stop the payments API paging the on-call.
630
+
631
+ ```python
632
+ from uptimer.models.v2 import (
633
+ ALERT_KIND_NO_DATA,
634
+ ALERT_KIND_PROBLEM,
635
+ ALERT_KIND_RECOVERY,
636
+ DeliverySelection,
637
+ )
638
+
639
+ delivery = client.v2.subjects("nightly-export", "your-workspace-id").delivery
640
+
641
+ table = delivery.get()
642
+ if table.uses_workspace_default:
643
+ print("this subject falls back to", table.default_destination_id)
644
+ elif table.is_silent:
645
+ print("nothing is sent for this subject")
646
+
647
+ # The body is the WHOLE table: what you send is what the subject will have.
648
+ delivery.replace(
649
+ [
650
+ DeliverySelection(
651
+ destination_id=slack.id,
652
+ alert_kinds=[ALERT_KIND_PROBLEM, ALERT_KIND_NO_DATA, ALERT_KIND_RECOVERY],
653
+ ),
654
+ DeliverySelection(destination_id=relay.id, alert_kinds=[ALERT_KIND_PROBLEM]),
655
+ ],
656
+ )
657
+
658
+ # Back to the workspace default — or to silence, if there is no default.
659
+ delivery.clear()
660
+ ```
661
+
662
+ A reminder about an unanswered problem rides with `problem`: a destination that
663
+ hears about problems hears the four-hourly reminders too, which is why there is
664
+ no fourth alert kind.
665
+
666
+ A **website monitor** carries the same table on its own collection, because
667
+ `/v2/subjects` serves Custom subjects only:
668
+
669
+ ```python
670
+ client.v2.monitoring.websites("monitor-id").delivery.get()
671
+ ```
672
+
673
+ Saving a table changes delivery **only**. Signals, rules, incidents,
674
+ acknowledgement and maintenance are untouched, and nothing is sent by saving.
675
+
676
+ ### Delivery log: what was actually sent
677
+
678
+ ```python
679
+ records = client.v2.notifications.deliveries.all(
680
+ workspace_id="your-workspace-id",
681
+ destination_id=slack.id, # optional: one destination
682
+ undelivered=True, # optional: only what did not arrive
683
+ )
684
+
685
+ for record in records:
686
+ print(record.at, record.destination_name, record.status)
687
+ if not record.delivered:
688
+ print(" refused:", record.error)
689
+ ```
690
+
691
+ It is a read — nothing here sends or resends, and there is no retry. Each record
692
+ keeps the destination's **name and type as they were at the attempt**, so a
693
+ rename or a delete later leaves the row still saying where the message went; the
694
+ webhook URL is never recorded. Records are kept **30 days**.
695
+
406
696
  ### Incident status
407
697
 
408
698
  `client.v2.incidents.all()` returns only **open** incidents. `status` carries the