watchfor 0.1.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.
watchfor-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 WatchFor
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,99 @@
1
+ Metadata-Version: 2.4
2
+ Name: watchfor
3
+ Version: 0.1.0
4
+ Summary: Official Python SDK + CLI for WatchFor — uptime & infrastructure monitoring (monitors, alert rules, incidents, maintenance windows) over the REST API.
5
+ Author-email: WatchFor <hello@watchfor.io>
6
+ License: MIT
7
+ Project-URL: Homepage, https://watchfor.io/docs/api
8
+ Project-URL: Documentation, https://watchfor.io/docs/api
9
+ Project-URL: Source, https://www.npmjs.com/package/watchfor
10
+ Project-URL: Bug Reports, https://watchfor.io/docs/api
11
+ Keywords: watchfor,monitoring,uptime,sdk,cli,incidents,status-page,mcp
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Topic :: System :: Monitoring
17
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
18
+ Requires-Python: >=3.9
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Dynamic: license-file
22
+
23
+ # watchfor (Python)
24
+
25
+ Official Python SDK + CLI for [WatchFor](https://watchfor.io) — uptime &
26
+ infrastructure monitoring (monitors, alert rules, incidents, maintenance
27
+ windows) over the REST API. Zero dependencies (standard library only).
28
+
29
+ There is also a [TypeScript SDK](https://www.npmjs.com/package/watchfor),
30
+ an [MCP server](https://watchfor.io/docs/api/mcp) and an
31
+ [A2A agent](https://watchfor.io/docs/api/a2a) for AI agents.
32
+
33
+ ## Install
34
+
35
+ ```bash
36
+ pip install watchfor
37
+ ```
38
+
39
+ ## Quick start
40
+
41
+ ```python
42
+ from watchfor import WatchFor
43
+
44
+ wf = WatchFor(api_key="wf_live_...") # create keys in Settings → API keys
45
+
46
+ # One-call org snapshot
47
+ print(wf.summary())
48
+
49
+ # List monitors
50
+ for m in wf.monitors.list()["data"]:
51
+ print(m["name"], m["status"])
52
+
53
+ # Create a monitor (idempotency key optional, for safe retries)
54
+ mon = wf.monitors.create(
55
+ {
56
+ "name": "example.com",
57
+ "type": "http",
58
+ "target": "https://example.com",
59
+ "interval": 300,
60
+ "locations": ["<location-id>"], # from wf.locations()
61
+ },
62
+ idempotency_key="create-example-1",
63
+ )
64
+
65
+ # Diagnose: firing incidents right now
66
+ for inc in wf.incidents.list(status="firing")["data"]:
67
+ print(inc["message"], inc["severity"])
68
+ ```
69
+
70
+ Resource namespaces: `wf.monitors`, `wf.alert_rules`, `wf.incidents`,
71
+ `wf.maintenance_windows`, `wf.contacts`, `wf.contact_groups`. Top-level:
72
+ `wf.summary()`, `wf.plan()`, `wf.me()`, `wf.locations()`, `wf.monitor_types()`,
73
+ `wf.incident_stats(period=...)`, `wf.notifications(...)`, `wf.activity(...)`.
74
+
75
+ Errors raise `WatchForError` with `.status`, `.code` and `.message`.
76
+
77
+ ## CLI
78
+
79
+ ```bash
80
+ export WATCHFOR_API_KEY=wf_live_...
81
+ watchfor summary
82
+ watchfor monitors
83
+ watchfor incidents --status firing
84
+ watchfor checks <monitor_id>
85
+ ```
86
+
87
+ ## Auth & scopes
88
+
89
+ Keys carry a scope: `read` (all GET endpoints) or `write` (read plus
90
+ create/update/delete). See
91
+ [authentication](https://watchfor.io/docs/api/authentication). The API is also
92
+ reachable via OAuth 2.1 for MCP clients.
93
+
94
+ ## Reference
95
+
96
+ - OpenAPI spec: <https://watchfor.io/openapi.json>
97
+ - Guides: <https://watchfor.io/docs/api>
98
+
99
+ MIT License.
@@ -0,0 +1,77 @@
1
+ # watchfor (Python)
2
+
3
+ Official Python SDK + CLI for [WatchFor](https://watchfor.io) — uptime &
4
+ infrastructure monitoring (monitors, alert rules, incidents, maintenance
5
+ windows) over the REST API. Zero dependencies (standard library only).
6
+
7
+ There is also a [TypeScript SDK](https://www.npmjs.com/package/watchfor),
8
+ an [MCP server](https://watchfor.io/docs/api/mcp) and an
9
+ [A2A agent](https://watchfor.io/docs/api/a2a) for AI agents.
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ pip install watchfor
15
+ ```
16
+
17
+ ## Quick start
18
+
19
+ ```python
20
+ from watchfor import WatchFor
21
+
22
+ wf = WatchFor(api_key="wf_live_...") # create keys in Settings → API keys
23
+
24
+ # One-call org snapshot
25
+ print(wf.summary())
26
+
27
+ # List monitors
28
+ for m in wf.monitors.list()["data"]:
29
+ print(m["name"], m["status"])
30
+
31
+ # Create a monitor (idempotency key optional, for safe retries)
32
+ mon = wf.monitors.create(
33
+ {
34
+ "name": "example.com",
35
+ "type": "http",
36
+ "target": "https://example.com",
37
+ "interval": 300,
38
+ "locations": ["<location-id>"], # from wf.locations()
39
+ },
40
+ idempotency_key="create-example-1",
41
+ )
42
+
43
+ # Diagnose: firing incidents right now
44
+ for inc in wf.incidents.list(status="firing")["data"]:
45
+ print(inc["message"], inc["severity"])
46
+ ```
47
+
48
+ Resource namespaces: `wf.monitors`, `wf.alert_rules`, `wf.incidents`,
49
+ `wf.maintenance_windows`, `wf.contacts`, `wf.contact_groups`. Top-level:
50
+ `wf.summary()`, `wf.plan()`, `wf.me()`, `wf.locations()`, `wf.monitor_types()`,
51
+ `wf.incident_stats(period=...)`, `wf.notifications(...)`, `wf.activity(...)`.
52
+
53
+ Errors raise `WatchForError` with `.status`, `.code` and `.message`.
54
+
55
+ ## CLI
56
+
57
+ ```bash
58
+ export WATCHFOR_API_KEY=wf_live_...
59
+ watchfor summary
60
+ watchfor monitors
61
+ watchfor incidents --status firing
62
+ watchfor checks <monitor_id>
63
+ ```
64
+
65
+ ## Auth & scopes
66
+
67
+ Keys carry a scope: `read` (all GET endpoints) or `write` (read plus
68
+ create/update/delete). See
69
+ [authentication](https://watchfor.io/docs/api/authentication). The API is also
70
+ reachable via OAuth 2.1 for MCP clients.
71
+
72
+ ## Reference
73
+
74
+ - OpenAPI spec: <https://watchfor.io/openapi.json>
75
+ - Guides: <https://watchfor.io/docs/api>
76
+
77
+ MIT License.
@@ -0,0 +1,43 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "watchfor"
7
+ version = "0.1.0"
8
+ description = "Official Python SDK + CLI for WatchFor — uptime & infrastructure monitoring (monitors, alert rules, incidents, maintenance windows) over the REST API."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "WatchFor", email = "hello@watchfor.io" }]
13
+ keywords = [
14
+ "watchfor",
15
+ "monitoring",
16
+ "uptime",
17
+ "sdk",
18
+ "cli",
19
+ "incidents",
20
+ "status-page",
21
+ "mcp",
22
+ ]
23
+ classifiers = [
24
+ "Development Status :: 4 - Beta",
25
+ "Intended Audience :: Developers",
26
+ "License :: OSI Approved :: MIT License",
27
+ "Programming Language :: Python :: 3",
28
+ "Topic :: System :: Monitoring",
29
+ "Topic :: Software Development :: Libraries :: Python Modules",
30
+ ]
31
+ dependencies = []
32
+
33
+ [project.urls]
34
+ Homepage = "https://watchfor.io/docs/api"
35
+ Documentation = "https://watchfor.io/docs/api"
36
+ "Source" = "https://www.npmjs.com/package/watchfor"
37
+ "Bug Reports" = "https://watchfor.io/docs/api"
38
+
39
+ [project.scripts]
40
+ watchfor = "watchfor.cli:main"
41
+
42
+ [tool.setuptools.packages.find]
43
+ include = ["watchfor*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,9 @@
1
+ """Official Python SDK + CLI for WatchFor — uptime & infrastructure monitoring.
2
+
3
+ See https://watchfor.io/docs/api for guides and the OpenAPI spec.
4
+ """
5
+
6
+ from ._version import __version__
7
+ from .client import DEFAULT_BASE_URL, WatchFor, WatchForError
8
+
9
+ __all__ = ["WatchFor", "WatchForError", "DEFAULT_BASE_URL", "__version__"]
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,90 @@
1
+ """`watchfor` command-line interface — a thin wrapper over the SDK.
2
+
3
+ Reads the API key from ``--api-key`` or the ``WATCHFOR_API_KEY`` env var::
4
+
5
+ export WATCHFOR_API_KEY=wf_live_...
6
+ watchfor summary
7
+ watchfor monitors
8
+ watchfor incidents --status firing
9
+ watchfor checks <monitor_id>
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import argparse
15
+ import json
16
+ import os
17
+ import sys
18
+
19
+ from ._version import __version__
20
+ from .client import WatchFor, WatchForError
21
+
22
+
23
+ def _print(obj: object) -> None:
24
+ print(json.dumps(obj, indent=2, ensure_ascii=False))
25
+
26
+
27
+ def main(argv: list[str] | None = None) -> int:
28
+ parser = argparse.ArgumentParser(
29
+ prog="watchfor",
30
+ description="WatchFor uptime & infrastructure monitoring — CLI.",
31
+ )
32
+ parser.add_argument("--version", action="version", version=f"watchfor {__version__}")
33
+ parser.add_argument(
34
+ "--api-key",
35
+ default=os.environ.get("WATCHFOR_API_KEY"),
36
+ help="API key (or set WATCHFOR_API_KEY).",
37
+ )
38
+ parser.add_argument(
39
+ "--base-url",
40
+ default=os.environ.get("WATCHFOR_BASE_URL", "https://watchfor.io/api/v1"),
41
+ help="API base URL.",
42
+ )
43
+ sub = parser.add_subparsers(dest="command", required=True)
44
+
45
+ sub.add_parser("summary", help="Org snapshot: status counts + active incidents.")
46
+ sub.add_parser("plan", help="Current plan, limits and usage.")
47
+ sub.add_parser("monitors", help="List monitors.")
48
+ sub.add_parser("locations", help="List probe locations.")
49
+ sub.add_parser("monitor-types", help="Catalog of monitor types.")
50
+
51
+ p_inc = sub.add_parser("incidents", help="List incidents.")
52
+ p_inc.add_argument("--status", choices=["firing", "acknowledged", "resolved"])
53
+ p_inc.add_argument("--limit", type=int)
54
+
55
+ p_checks = sub.add_parser("checks", help="Recent checks for a monitor.")
56
+ p_checks.add_argument("monitor_id")
57
+ p_checks.add_argument("--limit", type=int)
58
+
59
+ args = parser.parse_args(argv)
60
+
61
+ if not args.api_key:
62
+ parser.error("No API key. Pass --api-key or set WATCHFOR_API_KEY.")
63
+
64
+ wf = WatchFor(api_key=args.api_key, base_url=args.base_url)
65
+
66
+ try:
67
+ if args.command == "summary":
68
+ _print(wf.summary())
69
+ elif args.command == "plan":
70
+ _print(wf.plan())
71
+ elif args.command == "monitors":
72
+ _print(wf.monitors.list())
73
+ elif args.command == "locations":
74
+ _print(wf.locations())
75
+ elif args.command == "monitor-types":
76
+ _print(wf.monitor_types())
77
+ elif args.command == "incidents":
78
+ _print(wf.incidents.list(status=args.status, limit=args.limit))
79
+ elif args.command == "checks":
80
+ _print(wf.monitors.checks(args.monitor_id, limit=args.limit))
81
+ else: # pragma: no cover - argparse enforces choices
82
+ parser.error(f"Unknown command: {args.command}")
83
+ except WatchForError as exc:
84
+ print(f"error: {exc}", file=sys.stderr)
85
+ return 1
86
+ return 0
87
+
88
+
89
+ if __name__ == "__main__": # pragma: no cover
90
+ raise SystemExit(main())
@@ -0,0 +1,293 @@
1
+ """WatchFor REST API client (zero-dependency, stdlib only).
2
+
3
+ Usage::
4
+
5
+ from watchfor import WatchFor
6
+
7
+ wf = WatchFor(api_key="wf_live_...")
8
+ print(wf.summary())
9
+ for m in wf.monitors.list()["data"]:
10
+ print(m["name"], m["status"])
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import json
16
+ import urllib.error
17
+ import urllib.parse
18
+ import urllib.request
19
+ from typing import Any, Optional
20
+
21
+ from ._version import __version__
22
+
23
+ DEFAULT_BASE_URL = "https://watchfor.io/api/v1"
24
+
25
+ JsonDict = dict[str, Any]
26
+
27
+
28
+ class WatchForError(Exception):
29
+ """Raised when the API returns a non-2xx response.
30
+
31
+ Carries the HTTP ``status`` and the API's structured ``code`` and
32
+ ``message`` (WatchFor errors are always ``{"error":{"code","message"}}``).
33
+ """
34
+
35
+ def __init__(self, status: int, code: str, message: str):
36
+ super().__init__(f"[{status}] {code}: {message}")
37
+ self.status = status
38
+ self.code = code
39
+ self.message = message
40
+
41
+
42
+ def _clean_query(query: Optional[dict[str, Any]]) -> dict[str, str]:
43
+ if not query:
44
+ return {}
45
+ out: dict[str, str] = {}
46
+ for k, v in query.items():
47
+ if v is None or v == "":
48
+ continue
49
+ out[k] = str(v).lower() if isinstance(v, bool) else str(v)
50
+ return out
51
+
52
+
53
+ class WatchFor:
54
+ """Client for the WatchFor REST API (``/api/v1``).
55
+
56
+ :param api_key: A ``wf_live_...`` key created in Settings -> API keys.
57
+ :param base_url: Override the API base (e.g. for a self-hosted instance).
58
+ :param timeout: Per-request timeout in seconds.
59
+ """
60
+
61
+ def __init__(
62
+ self,
63
+ api_key: str,
64
+ base_url: str = DEFAULT_BASE_URL,
65
+ timeout: float = 30.0,
66
+ ):
67
+ if not api_key:
68
+ raise ValueError("api_key is required")
69
+ self.api_key = api_key
70
+ self.base_url = base_url.rstrip("/")
71
+ self.timeout = timeout
72
+ # Resource namespaces (mirror the TypeScript SDK).
73
+ self.monitors = _Monitors(self)
74
+ self.alert_rules = _AlertRules(self)
75
+ self.incidents = _Incidents(self)
76
+ self.maintenance_windows = _MaintenanceWindows(self)
77
+ self.contacts = _Contacts(self)
78
+ self.contact_groups = _ContactGroups(self)
79
+
80
+ # ── core request ────────────────────────────────────────────────
81
+ def request(
82
+ self,
83
+ method: str,
84
+ path: str,
85
+ query: Optional[dict[str, Any]] = None,
86
+ body: Optional[Any] = None,
87
+ idempotency_key: Optional[str] = None,
88
+ ) -> Any:
89
+ url = self.base_url + path
90
+ q = _clean_query(query)
91
+ if q:
92
+ url += "?" + urllib.parse.urlencode(q)
93
+ data = None
94
+ headers = {
95
+ "Authorization": f"Bearer {self.api_key}",
96
+ "Accept": "application/json",
97
+ "User-Agent": f"watchfor-python/{__version__}",
98
+ }
99
+ if body is not None:
100
+ data = json.dumps(body).encode("utf-8")
101
+ headers["Content-Type"] = "application/json"
102
+ if idempotency_key:
103
+ headers["Idempotency-Key"] = idempotency_key
104
+
105
+ req = urllib.request.Request(url, data=data, headers=headers, method=method)
106
+ try:
107
+ with urllib.request.urlopen(req, timeout=self.timeout) as resp:
108
+ raw = resp.read()
109
+ if not raw:
110
+ return {"ok": True, "status": resp.status}
111
+ return json.loads(raw)
112
+ except urllib.error.HTTPError as exc: # noqa: PERF203
113
+ raw = exc.read()
114
+ code, message = "http_error", f"HTTP {exc.code}"
115
+ try:
116
+ parsed = json.loads(raw)
117
+ err = parsed.get("error", {})
118
+ code = err.get("code", code)
119
+ message = err.get("message", message)
120
+ except Exception: # noqa: BLE001
121
+ if raw:
122
+ message = raw.decode("utf-8", "replace")[:200]
123
+ raise WatchForError(exc.code, code, message) from None
124
+
125
+ # ── top-level reads ─────────────────────────────────────────────
126
+ def me(self) -> Any:
127
+ """The authenticated key's identity and organization."""
128
+ return self.request("GET", "/me")
129
+
130
+ def summary(self) -> Any:
131
+ """One-call org snapshot: monitor status counts + active incidents."""
132
+ return self.request("GET", "/summary")
133
+
134
+ def plan(self) -> Any:
135
+ """Current plan, its limits and usage."""
136
+ return self.request("GET", "/plan")
137
+
138
+ def incident_stats(self, period: Optional[str] = None) -> Any:
139
+ """Aggregate incident stats (period: 24h / 7d / 30d / 90d)."""
140
+ return self.request("GET", "/incidents/stats", query={"period": period})
141
+
142
+ def locations(self) -> Any:
143
+ """Probe locations a monitor can run from."""
144
+ return self.request("GET", "/locations")
145
+
146
+ def monitor_types(self) -> Any:
147
+ """Catalog of every monitor type: target format, config, alert metrics."""
148
+ return self.request("GET", "/meta/monitor-types")
149
+
150
+ def notifications(self, **query: Any) -> Any:
151
+ """Alert-notification delivery history."""
152
+ return self.request("GET", "/notifications", query=query)
153
+
154
+ def activity(self, **query: Any) -> Any:
155
+ """Organization audit trail."""
156
+ return self.request("GET", "/activity", query=query)
157
+
158
+
159
+ class _Namespace:
160
+ def __init__(self, client: WatchFor):
161
+ self._c = client
162
+
163
+
164
+ class _Monitors(_Namespace):
165
+ def list(self, **query: Any) -> Any:
166
+ return self._c.request("GET", "/monitors", query=query)
167
+
168
+ def get(self, monitor_id: str) -> Any:
169
+ return self._c.request("GET", f"/monitors/{monitor_id}")
170
+
171
+ def create(self, body: JsonDict, idempotency_key: Optional[str] = None) -> Any:
172
+ return self._c.request(
173
+ "POST", "/monitors", body=body, idempotency_key=idempotency_key
174
+ )
175
+
176
+ def update(self, monitor_id: str, body: JsonDict) -> Any:
177
+ return self._c.request("PATCH", f"/monitors/{monitor_id}", body=body)
178
+
179
+ def delete(self, monitor_id: str) -> Any:
180
+ return self._c.request("DELETE", f"/monitors/{monitor_id}")
181
+
182
+ def pause(self, monitor_id: str) -> Any:
183
+ return self._c.request("POST", f"/monitors/{monitor_id}/pause")
184
+
185
+ def resume(self, monitor_id: str) -> Any:
186
+ return self._c.request("POST", f"/monitors/{monitor_id}/resume")
187
+
188
+ def check_now(self, monitor_id: str) -> Any:
189
+ return self._c.request("POST", f"/monitors/{monitor_id}/check-now")
190
+
191
+ def uptime(self, monitor_id: str, period: Optional[str] = None) -> Any:
192
+ return self._c.request(
193
+ "GET", f"/monitors/{monitor_id}/uptime", query={"period": period}
194
+ )
195
+
196
+ def checks(self, monitor_id: str, **query: Any) -> Any:
197
+ return self._c.request("GET", f"/monitors/{monitor_id}/checks", query=query)
198
+
199
+
200
+ class _AlertRules(_Namespace):
201
+ def list(self, monitor_id: str) -> Any:
202
+ return self._c.request("GET", f"/monitors/{monitor_id}/alert-rules")
203
+
204
+ def get(self, monitor_id: str, rule_id: str) -> Any:
205
+ return self._c.request(
206
+ "GET", f"/monitors/{monitor_id}/alert-rules/{rule_id}"
207
+ )
208
+
209
+ def create(self, monitor_id: str, body: JsonDict) -> Any:
210
+ return self._c.request(
211
+ "POST", f"/monitors/{monitor_id}/alert-rules", body=body
212
+ )
213
+
214
+ def update(self, monitor_id: str, rule_id: str, body: JsonDict) -> Any:
215
+ return self._c.request(
216
+ "PATCH", f"/monitors/{monitor_id}/alert-rules/{rule_id}", body=body
217
+ )
218
+
219
+ def delete(self, monitor_id: str, rule_id: str) -> Any:
220
+ return self._c.request(
221
+ "DELETE", f"/monitors/{monitor_id}/alert-rules/{rule_id}"
222
+ )
223
+
224
+
225
+ class _Incidents(_Namespace):
226
+ def list(self, **query: Any) -> Any:
227
+ return self._c.request("GET", "/incidents", query=query)
228
+
229
+ def get(self, incident_id: int | str) -> Any:
230
+ return self._c.request("GET", f"/incidents/{incident_id}")
231
+
232
+ def acknowledge(self, incident_id: int | str) -> Any:
233
+ return self._c.request("POST", f"/incidents/{incident_id}/acknowledge")
234
+
235
+ def resolve(self, incident_id: int | str) -> Any:
236
+ return self._c.request("POST", f"/incidents/{incident_id}/resolve")
237
+
238
+
239
+ class _MaintenanceWindows(_Namespace):
240
+ def list(self, **query: Any) -> Any:
241
+ return self._c.request("GET", "/maintenance-windows", query=query)
242
+
243
+ def get(self, window_id: str) -> Any:
244
+ return self._c.request("GET", f"/maintenance-windows/{window_id}")
245
+
246
+ def create(self, body: JsonDict, idempotency_key: Optional[str] = None) -> Any:
247
+ return self._c.request(
248
+ "POST", "/maintenance-windows", body=body, idempotency_key=idempotency_key
249
+ )
250
+
251
+ def update(self, window_id: str, body: JsonDict) -> Any:
252
+ return self._c.request("PATCH", f"/maintenance-windows/{window_id}", body=body)
253
+
254
+ def delete(self, window_id: str) -> Any:
255
+ return self._c.request("DELETE", f"/maintenance-windows/{window_id}")
256
+
257
+
258
+ class _Contacts(_Namespace):
259
+ def list(self, **query: Any) -> Any:
260
+ return self._c.request("GET", "/contacts", query=query)
261
+
262
+ def get(self, contact_id: str) -> Any:
263
+ return self._c.request("GET", f"/contacts/{contact_id}")
264
+
265
+ def create(self, body: JsonDict, idempotency_key: Optional[str] = None) -> Any:
266
+ return self._c.request(
267
+ "POST", "/contacts", body=body, idempotency_key=idempotency_key
268
+ )
269
+
270
+ def update(self, contact_id: str, body: JsonDict) -> Any:
271
+ return self._c.request("PATCH", f"/contacts/{contact_id}", body=body)
272
+
273
+ def delete(self, contact_id: str) -> Any:
274
+ return self._c.request("DELETE", f"/contacts/{contact_id}")
275
+
276
+
277
+ class _ContactGroups(_Namespace):
278
+ def list(self, **query: Any) -> Any:
279
+ return self._c.request("GET", "/contact-groups", query=query)
280
+
281
+ def get(self, group_id: str) -> Any:
282
+ return self._c.request("GET", f"/contact-groups/{group_id}")
283
+
284
+ def create(self, body: JsonDict, idempotency_key: Optional[str] = None) -> Any:
285
+ return self._c.request(
286
+ "POST", "/contact-groups", body=body, idempotency_key=idempotency_key
287
+ )
288
+
289
+ def update(self, group_id: str, body: JsonDict) -> Any:
290
+ return self._c.request("PATCH", f"/contact-groups/{group_id}", body=body)
291
+
292
+ def delete(self, group_id: str) -> Any:
293
+ return self._c.request("DELETE", f"/contact-groups/{group_id}")
@@ -0,0 +1,99 @@
1
+ Metadata-Version: 2.4
2
+ Name: watchfor
3
+ Version: 0.1.0
4
+ Summary: Official Python SDK + CLI for WatchFor — uptime & infrastructure monitoring (monitors, alert rules, incidents, maintenance windows) over the REST API.
5
+ Author-email: WatchFor <hello@watchfor.io>
6
+ License: MIT
7
+ Project-URL: Homepage, https://watchfor.io/docs/api
8
+ Project-URL: Documentation, https://watchfor.io/docs/api
9
+ Project-URL: Source, https://www.npmjs.com/package/watchfor
10
+ Project-URL: Bug Reports, https://watchfor.io/docs/api
11
+ Keywords: watchfor,monitoring,uptime,sdk,cli,incidents,status-page,mcp
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Topic :: System :: Monitoring
17
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
18
+ Requires-Python: >=3.9
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Dynamic: license-file
22
+
23
+ # watchfor (Python)
24
+
25
+ Official Python SDK + CLI for [WatchFor](https://watchfor.io) — uptime &
26
+ infrastructure monitoring (monitors, alert rules, incidents, maintenance
27
+ windows) over the REST API. Zero dependencies (standard library only).
28
+
29
+ There is also a [TypeScript SDK](https://www.npmjs.com/package/watchfor),
30
+ an [MCP server](https://watchfor.io/docs/api/mcp) and an
31
+ [A2A agent](https://watchfor.io/docs/api/a2a) for AI agents.
32
+
33
+ ## Install
34
+
35
+ ```bash
36
+ pip install watchfor
37
+ ```
38
+
39
+ ## Quick start
40
+
41
+ ```python
42
+ from watchfor import WatchFor
43
+
44
+ wf = WatchFor(api_key="wf_live_...") # create keys in Settings → API keys
45
+
46
+ # One-call org snapshot
47
+ print(wf.summary())
48
+
49
+ # List monitors
50
+ for m in wf.monitors.list()["data"]:
51
+ print(m["name"], m["status"])
52
+
53
+ # Create a monitor (idempotency key optional, for safe retries)
54
+ mon = wf.monitors.create(
55
+ {
56
+ "name": "example.com",
57
+ "type": "http",
58
+ "target": "https://example.com",
59
+ "interval": 300,
60
+ "locations": ["<location-id>"], # from wf.locations()
61
+ },
62
+ idempotency_key="create-example-1",
63
+ )
64
+
65
+ # Diagnose: firing incidents right now
66
+ for inc in wf.incidents.list(status="firing")["data"]:
67
+ print(inc["message"], inc["severity"])
68
+ ```
69
+
70
+ Resource namespaces: `wf.monitors`, `wf.alert_rules`, `wf.incidents`,
71
+ `wf.maintenance_windows`, `wf.contacts`, `wf.contact_groups`. Top-level:
72
+ `wf.summary()`, `wf.plan()`, `wf.me()`, `wf.locations()`, `wf.monitor_types()`,
73
+ `wf.incident_stats(period=...)`, `wf.notifications(...)`, `wf.activity(...)`.
74
+
75
+ Errors raise `WatchForError` with `.status`, `.code` and `.message`.
76
+
77
+ ## CLI
78
+
79
+ ```bash
80
+ export WATCHFOR_API_KEY=wf_live_...
81
+ watchfor summary
82
+ watchfor monitors
83
+ watchfor incidents --status firing
84
+ watchfor checks <monitor_id>
85
+ ```
86
+
87
+ ## Auth & scopes
88
+
89
+ Keys carry a scope: `read` (all GET endpoints) or `write` (read plus
90
+ create/update/delete). See
91
+ [authentication](https://watchfor.io/docs/api/authentication). The API is also
92
+ reachable via OAuth 2.1 for MCP clients.
93
+
94
+ ## Reference
95
+
96
+ - OpenAPI spec: <https://watchfor.io/openapi.json>
97
+ - Guides: <https://watchfor.io/docs/api>
98
+
99
+ MIT License.
@@ -0,0 +1,12 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ watchfor/__init__.py
5
+ watchfor/_version.py
6
+ watchfor/cli.py
7
+ watchfor/client.py
8
+ watchfor.egg-info/PKG-INFO
9
+ watchfor.egg-info/SOURCES.txt
10
+ watchfor.egg-info/dependency_links.txt
11
+ watchfor.egg-info/entry_points.txt
12
+ watchfor.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ watchfor = watchfor.cli:main
@@ -0,0 +1 @@
1
+ watchfor