watchup 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.
watchup-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,241 @@
1
+ Metadata-Version: 2.4
2
+ Name: watchup
3
+ Version: 0.1.0
4
+ Summary: Official Watchup SDK for Python — error tracking, request tracing, and custom analytics
5
+ Author: Watchup Ltd
6
+ License: MIT
7
+ Project-URL: Homepage, https://watchup.site
8
+ Project-URL: Documentation, https://watchup.site/docs/sdks/python
9
+ Project-URL: Repository, https://github.com/watchupltd/watchup-sdk
10
+ Project-URL: Issues, https://github.com/watchupltd/watchup-sdk/issues
11
+ Keywords: watchup,monitoring,observability,apm,tracing,error-tracking,flask,django,fastapi
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: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Topic :: Software Development :: Libraries
21
+ Classifier: Topic :: System :: Monitoring
22
+ Requires-Python: >=3.9
23
+ Description-Content-Type: text/markdown
24
+ Provides-Extra: flask
25
+ Requires-Dist: flask>=2.0; extra == "flask"
26
+ Provides-Extra: django
27
+ Requires-Dist: django>=3.2; extra == "django"
28
+ Provides-Extra: fastapi
29
+ Requires-Dist: fastapi>=0.95; extra == "fastapi"
30
+ Requires-Dist: starlette>=0.27; extra == "fastapi"
31
+ Provides-Extra: dev
32
+ Requires-Dist: pytest>=8; extra == "dev"
33
+ Requires-Dist: flask>=2.0; extra == "dev"
34
+ Requires-Dist: responses>=0.25; extra == "dev"
35
+
36
+ # watchup
37
+
38
+ Official Python SDK for [Watchup](https://watchup.site) — error tracking, request tracing, and custom analytics for Python web applications.
39
+
40
+ Works with **Flask**, **Django**, **FastAPI**, and any **WSGI** framework. Zero required dependencies — uses the Python standard library only.
41
+
42
+ ---
43
+
44
+ ## Installation
45
+
46
+ ```bash
47
+ pip install watchup
48
+ ```
49
+
50
+ Requires Python 3.9+.
51
+
52
+ ---
53
+
54
+ ## Quick start
55
+
56
+ ```python
57
+ from watchup import Watchup
58
+
59
+ watchup = Watchup(
60
+ api_key="wup_live_xxxxxxxxxxxx", # Dashboard → Project Settings → API Keys
61
+ environment="production",
62
+ release="v1.2.3", # optional: git SHA or version tag
63
+ )
64
+ ```
65
+
66
+ ---
67
+
68
+ ## Flask
69
+
70
+ ```python
71
+ from flask import Flask
72
+ from watchup import Watchup
73
+
74
+ watchup = Watchup(api_key="wup_live_xxxxxxxxxxxx")
75
+ app = Flask(__name__)
76
+
77
+ watchup.init_app(app) # registers before_request, after_request, errorhandler hooks
78
+ ```
79
+
80
+ `init_app` wires up:
81
+
82
+ - **Request tracing** — every request is recorded with method, route, status code, and duration
83
+ - **Error capture** — unhandled exceptions are reported with stack trace and request context
84
+ - **Transparent re-raise** — errors still propagate to your own error handlers
85
+
86
+ ---
87
+
88
+ ## Django
89
+
90
+ Add `WatchupDjangoMiddleware` to your `MIDDLEWARE` list and set `WATCHUP_API_KEY` in `settings.py`:
91
+
92
+ ```python
93
+ # settings.py
94
+ MIDDLEWARE = [
95
+ "watchup.WatchupDjangoMiddleware",
96
+ # ... rest of your middleware
97
+ ]
98
+
99
+ WATCHUP_API_KEY = "wup_live_xxxxxxxxxxxx"
100
+ WATCHUP_ENVIRONMENT = "production" # optional
101
+ WATCHUP_RELEASE = "v1.2.3" # optional
102
+ ```
103
+
104
+ The middleware initialises the client once on first request and reuses it for the lifetime of the process.
105
+
106
+ ---
107
+
108
+ ## FastAPI / Starlette
109
+
110
+ Use the WSGI wrapper or instrument manually with `before` / `after` logic via Starlette middleware:
111
+
112
+ ```python
113
+ from fastapi import FastAPI, Request
114
+ from watchup import Watchup
115
+ import time
116
+
117
+ watchup = Watchup(api_key="wup_live_xxxxxxxxxxxx")
118
+ app = FastAPI()
119
+
120
+ @app.middleware("http")
121
+ async def watchup_middleware(request: Request, call_next):
122
+ start = time.time()
123
+ response = await call_next(request)
124
+ ms = (time.time() - start) * 1000
125
+ # record trace manually
126
+ end = watchup.start_trace(f"{request.method} {request.url.path}")
127
+ end()
128
+ return response
129
+ ```
130
+
131
+ ---
132
+
133
+ ## WSGI middleware (framework-agnostic)
134
+
135
+ ```python
136
+ from watchup import Watchup, WatchupWSGI
137
+
138
+ watchup = Watchup(api_key="wup_live_xxxxxxxxxxxx")
139
+
140
+ # Flask example
141
+ from flask import Flask
142
+ flask_app = Flask(__name__)
143
+ flask_app.wsgi_app = WatchupWSGI(flask_app.wsgi_app, watchup)
144
+
145
+ # Any WSGI app
146
+ application = WatchupWSGI(application, watchup)
147
+ ```
148
+
149
+ ---
150
+
151
+ ## Manual tracking
152
+
153
+ ### Capture an error
154
+
155
+ ```python
156
+ try:
157
+ process_order(order_id)
158
+ except Exception as exc:
159
+ watchup.capture_error(exc, route="job.process_order", order_id=order_id)
160
+ ```
161
+
162
+ ### Track a custom event
163
+
164
+ ```python
165
+ watchup.track("user.signed_up", {"plan": "pro", "source": "invite"})
166
+ watchup.track("order.placed", {"amount": 4999, "currency": "NGN"})
167
+ ```
168
+
169
+ ### Time an operation
170
+
171
+ ```python
172
+ end = watchup.start_trace("db.query_users")
173
+ try:
174
+ rows = db.query("SELECT * FROM users")
175
+ end() # status defaults to "ok"
176
+ except Exception as exc:
177
+ end(status="err", meta={"query": "SELECT * FROM users"})
178
+ raise
179
+ ```
180
+
181
+ ---
182
+
183
+ ## User identification
184
+
185
+ Attach user identity to errors and traces:
186
+
187
+ ```python
188
+ # After authentication — in a middleware or login view
189
+ watchup.set_user("usr_42", email="alice@example.com", name="Alice", plan="pro")
190
+
191
+ # On logout
192
+ watchup.clear_user()
193
+ ```
194
+
195
+ Once set, every `capture_error`, `start_trace`, and request trace will include the user context.
196
+
197
+ ---
198
+
199
+ ## Configuration reference
200
+
201
+ | Parameter | Default | Description |
202
+ |---|---|---|
203
+ | `api_key` | *(required)* | Project API key (`wup_live_…`) |
204
+ | `base_url` | `https://api.watchup.site` | Override for self-hosted deployments |
205
+ | `environment` | `WATCHUP_ENV` env var, or `"production"` | Runtime label on every payload |
206
+ | `release` | `None` | App version / git SHA |
207
+ | `flush_interval` | `5.0` | Seconds between automatic flushes |
208
+ | `max_batch_size` | `100` | Item count that triggers an immediate flush |
209
+ | `sample_rate` | `1.0` | Fraction of requests to trace (0–1) |
210
+ | `debug` | `False` | Log SDK warnings to stderr |
211
+
212
+ ---
213
+
214
+ ## Lifecycle
215
+
216
+ ```python
217
+ # Force an immediate flush
218
+ watchup.flush()
219
+
220
+ # Stop the background timer and flush remaining items (graceful shutdown)
221
+ watchup.shutdown()
222
+ ```
223
+
224
+ The batcher runs on a daemon thread and registers an `atexit` handler, so queued items are flushed automatically when the process exits normally.
225
+
226
+ ---
227
+
228
+ ## Links
229
+
230
+ - **Website:** [watchup.site](https://watchup.site)
231
+ - **Documentation:** [watchup.site/docs](https://watchup.site/docs)
232
+ - **Python SDK docs:** [watchup.site/docs/sdks/python](https://watchup.site/docs/sdks/python)
233
+ - **Getting started:** [watchup.site/docs/getting-started](https://watchup.site/docs/getting-started)
234
+ - **Pricing:** [watchup.site/pricing](https://watchup.site/pricing)
235
+ - **Dashboard:** [app.watchup.site](https://watchup.site/login)
236
+
237
+ ---
238
+
239
+ ## License
240
+
241
+ MIT © Watchup Ltd
@@ -0,0 +1,206 @@
1
+ # watchup
2
+
3
+ Official Python SDK for [Watchup](https://watchup.site) — error tracking, request tracing, and custom analytics for Python web applications.
4
+
5
+ Works with **Flask**, **Django**, **FastAPI**, and any **WSGI** framework. Zero required dependencies — uses the Python standard library only.
6
+
7
+ ---
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ pip install watchup
13
+ ```
14
+
15
+ Requires Python 3.9+.
16
+
17
+ ---
18
+
19
+ ## Quick start
20
+
21
+ ```python
22
+ from watchup import Watchup
23
+
24
+ watchup = Watchup(
25
+ api_key="wup_live_xxxxxxxxxxxx", # Dashboard → Project Settings → API Keys
26
+ environment="production",
27
+ release="v1.2.3", # optional: git SHA or version tag
28
+ )
29
+ ```
30
+
31
+ ---
32
+
33
+ ## Flask
34
+
35
+ ```python
36
+ from flask import Flask
37
+ from watchup import Watchup
38
+
39
+ watchup = Watchup(api_key="wup_live_xxxxxxxxxxxx")
40
+ app = Flask(__name__)
41
+
42
+ watchup.init_app(app) # registers before_request, after_request, errorhandler hooks
43
+ ```
44
+
45
+ `init_app` wires up:
46
+
47
+ - **Request tracing** — every request is recorded with method, route, status code, and duration
48
+ - **Error capture** — unhandled exceptions are reported with stack trace and request context
49
+ - **Transparent re-raise** — errors still propagate to your own error handlers
50
+
51
+ ---
52
+
53
+ ## Django
54
+
55
+ Add `WatchupDjangoMiddleware` to your `MIDDLEWARE` list and set `WATCHUP_API_KEY` in `settings.py`:
56
+
57
+ ```python
58
+ # settings.py
59
+ MIDDLEWARE = [
60
+ "watchup.WatchupDjangoMiddleware",
61
+ # ... rest of your middleware
62
+ ]
63
+
64
+ WATCHUP_API_KEY = "wup_live_xxxxxxxxxxxx"
65
+ WATCHUP_ENVIRONMENT = "production" # optional
66
+ WATCHUP_RELEASE = "v1.2.3" # optional
67
+ ```
68
+
69
+ The middleware initialises the client once on first request and reuses it for the lifetime of the process.
70
+
71
+ ---
72
+
73
+ ## FastAPI / Starlette
74
+
75
+ Use the WSGI wrapper or instrument manually with `before` / `after` logic via Starlette middleware:
76
+
77
+ ```python
78
+ from fastapi import FastAPI, Request
79
+ from watchup import Watchup
80
+ import time
81
+
82
+ watchup = Watchup(api_key="wup_live_xxxxxxxxxxxx")
83
+ app = FastAPI()
84
+
85
+ @app.middleware("http")
86
+ async def watchup_middleware(request: Request, call_next):
87
+ start = time.time()
88
+ response = await call_next(request)
89
+ ms = (time.time() - start) * 1000
90
+ # record trace manually
91
+ end = watchup.start_trace(f"{request.method} {request.url.path}")
92
+ end()
93
+ return response
94
+ ```
95
+
96
+ ---
97
+
98
+ ## WSGI middleware (framework-agnostic)
99
+
100
+ ```python
101
+ from watchup import Watchup, WatchupWSGI
102
+
103
+ watchup = Watchup(api_key="wup_live_xxxxxxxxxxxx")
104
+
105
+ # Flask example
106
+ from flask import Flask
107
+ flask_app = Flask(__name__)
108
+ flask_app.wsgi_app = WatchupWSGI(flask_app.wsgi_app, watchup)
109
+
110
+ # Any WSGI app
111
+ application = WatchupWSGI(application, watchup)
112
+ ```
113
+
114
+ ---
115
+
116
+ ## Manual tracking
117
+
118
+ ### Capture an error
119
+
120
+ ```python
121
+ try:
122
+ process_order(order_id)
123
+ except Exception as exc:
124
+ watchup.capture_error(exc, route="job.process_order", order_id=order_id)
125
+ ```
126
+
127
+ ### Track a custom event
128
+
129
+ ```python
130
+ watchup.track("user.signed_up", {"plan": "pro", "source": "invite"})
131
+ watchup.track("order.placed", {"amount": 4999, "currency": "NGN"})
132
+ ```
133
+
134
+ ### Time an operation
135
+
136
+ ```python
137
+ end = watchup.start_trace("db.query_users")
138
+ try:
139
+ rows = db.query("SELECT * FROM users")
140
+ end() # status defaults to "ok"
141
+ except Exception as exc:
142
+ end(status="err", meta={"query": "SELECT * FROM users"})
143
+ raise
144
+ ```
145
+
146
+ ---
147
+
148
+ ## User identification
149
+
150
+ Attach user identity to errors and traces:
151
+
152
+ ```python
153
+ # After authentication — in a middleware or login view
154
+ watchup.set_user("usr_42", email="alice@example.com", name="Alice", plan="pro")
155
+
156
+ # On logout
157
+ watchup.clear_user()
158
+ ```
159
+
160
+ Once set, every `capture_error`, `start_trace`, and request trace will include the user context.
161
+
162
+ ---
163
+
164
+ ## Configuration reference
165
+
166
+ | Parameter | Default | Description |
167
+ |---|---|---|
168
+ | `api_key` | *(required)* | Project API key (`wup_live_…`) |
169
+ | `base_url` | `https://api.watchup.site` | Override for self-hosted deployments |
170
+ | `environment` | `WATCHUP_ENV` env var, or `"production"` | Runtime label on every payload |
171
+ | `release` | `None` | App version / git SHA |
172
+ | `flush_interval` | `5.0` | Seconds between automatic flushes |
173
+ | `max_batch_size` | `100` | Item count that triggers an immediate flush |
174
+ | `sample_rate` | `1.0` | Fraction of requests to trace (0–1) |
175
+ | `debug` | `False` | Log SDK warnings to stderr |
176
+
177
+ ---
178
+
179
+ ## Lifecycle
180
+
181
+ ```python
182
+ # Force an immediate flush
183
+ watchup.flush()
184
+
185
+ # Stop the background timer and flush remaining items (graceful shutdown)
186
+ watchup.shutdown()
187
+ ```
188
+
189
+ The batcher runs on a daemon thread and registers an `atexit` handler, so queued items are flushed automatically when the process exits normally.
190
+
191
+ ---
192
+
193
+ ## Links
194
+
195
+ - **Website:** [watchup.site](https://watchup.site)
196
+ - **Documentation:** [watchup.site/docs](https://watchup.site/docs)
197
+ - **Python SDK docs:** [watchup.site/docs/sdks/python](https://watchup.site/docs/sdks/python)
198
+ - **Getting started:** [watchup.site/docs/getting-started](https://watchup.site/docs/getting-started)
199
+ - **Pricing:** [watchup.site/pricing](https://watchup.site/pricing)
200
+ - **Dashboard:** [app.watchup.site](https://watchup.site/login)
201
+
202
+ ---
203
+
204
+ ## License
205
+
206
+ MIT © Watchup Ltd
@@ -0,0 +1,56 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "watchup"
7
+ version = "0.1.0"
8
+ description = "Official Watchup SDK for Python — error tracking, request tracing, and custom analytics"
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ authors = [{ name = "Watchup Ltd" }]
12
+ requires-python = ">=3.9"
13
+ keywords = [
14
+ "watchup",
15
+ "monitoring",
16
+ "observability",
17
+ "apm",
18
+ "tracing",
19
+ "error-tracking",
20
+ "flask",
21
+ "django",
22
+ "fastapi",
23
+ ]
24
+ classifiers = [
25
+ "Development Status :: 4 - Beta",
26
+ "Intended Audience :: Developers",
27
+ "License :: OSI Approved :: MIT License",
28
+ "Programming Language :: Python :: 3",
29
+ "Programming Language :: Python :: 3.9",
30
+ "Programming Language :: Python :: 3.10",
31
+ "Programming Language :: Python :: 3.11",
32
+ "Programming Language :: Python :: 3.12",
33
+ "Topic :: Software Development :: Libraries",
34
+ "Topic :: System :: Monitoring",
35
+ ]
36
+
37
+ # No required dependencies — stdlib only.
38
+ # Framework integrations (Flask, Django) import lazily and raise ImportError
39
+ # with a helpful message if the framework is not installed.
40
+ dependencies = []
41
+
42
+ [project.optional-dependencies]
43
+ flask = ["flask>=2.0"]
44
+ django = ["django>=3.2"]
45
+ fastapi = ["fastapi>=0.95", "starlette>=0.27"]
46
+ dev = ["pytest>=8", "flask>=2.0", "responses>=0.25"]
47
+
48
+ [project.urls]
49
+ Homepage = "https://watchup.site"
50
+ Documentation = "https://watchup.site/docs/sdks/python"
51
+ Repository = "https://github.com/watchupltd/watchup-sdk"
52
+ Issues = "https://github.com/watchupltd/watchup-sdk/issues"
53
+
54
+ [tool.setuptools.packages.find]
55
+ where = ["."]
56
+ include = ["watchup*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,28 @@
1
+ """
2
+ watchup — application monitoring SDK for Python
3
+
4
+ Quick start::
5
+
6
+ from watchup import Watchup
7
+
8
+ watchup = Watchup(api_key="wup_live_xxxxxxxxxxxx")
9
+
10
+ # Flask
11
+ watchup.init_app(app)
12
+
13
+ # Manual error capture
14
+ watchup.capture_error(exc, route="job.process_order")
15
+
16
+ # Custom events
17
+ watchup.track("user.signed_up", {"plan": "pro"})
18
+
19
+ # Trace any operation
20
+ end = watchup.start_trace("db.query")
21
+ end()
22
+ """
23
+
24
+ from .client import Watchup
25
+ from .middleware import WatchupDjangoMiddleware, WatchupWSGI
26
+
27
+ __all__ = ["Watchup", "WatchupDjangoMiddleware", "WatchupWSGI"]
28
+ __version__ = "0.1.0"
@@ -0,0 +1,105 @@
1
+ """
2
+ watchup · batcher
3
+
4
+ Accumulates traces/errors/events and flushes them in batches via a
5
+ background daemon thread. The thread is daemonised so it never prevents
6
+ the interpreter from exiting.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import atexit
12
+ import threading
13
+ from typing import List
14
+
15
+ from .transport import Transport
16
+ from .types import ErrorPayload, EventPayload, IngestBatch, TracePayload
17
+
18
+
19
+ class Batcher:
20
+ def __init__(
21
+ self,
22
+ transport: Transport,
23
+ flush_interval: float,
24
+ max_batch_size: int,
25
+ ) -> None:
26
+ self._transport = transport
27
+ self._flush_interval = flush_interval
28
+ self._max_batch_size = max_batch_size
29
+
30
+ self._lock = threading.Lock()
31
+ self._traces: List[TracePayload] = []
32
+ self._errors: List[ErrorPayload] = []
33
+ self._events: List[EventPayload] = []
34
+
35
+ self._timer: threading.Timer | None = None
36
+ self._started = False
37
+
38
+ # ── Lifecycle ─────────────────────────────────────────────────────────────
39
+
40
+ def start(self) -> None:
41
+ if self._started:
42
+ return
43
+ self._started = True
44
+ self._schedule()
45
+ atexit.register(self.flush)
46
+
47
+ def stop(self) -> None:
48
+ self._started = False
49
+ if self._timer is not None:
50
+ self._timer.cancel()
51
+ self._timer = None
52
+
53
+ def _schedule(self) -> None:
54
+ if not self._started:
55
+ return
56
+ self._timer = threading.Timer(self._flush_interval, self._tick)
57
+ self._timer.daemon = True
58
+ self._timer.start()
59
+
60
+ def _tick(self) -> None:
61
+ self.flush()
62
+ self._schedule()
63
+
64
+ # ── Enqueue ───────────────────────────────────────────────────────────────
65
+
66
+ def add_trace(self, trace: TracePayload) -> None:
67
+ with self._lock:
68
+ self._traces.append(trace)
69
+ should_flush = len(self._traces) >= self._max_batch_size
70
+ if should_flush:
71
+ self.flush()
72
+
73
+ def add_error(self, error: ErrorPayload) -> None:
74
+ with self._lock:
75
+ self._errors.append(error)
76
+ # Errors are higher priority — flush at half capacity
77
+ should_flush = len(self._errors) >= max(1, self._max_batch_size // 2)
78
+ if should_flush:
79
+ self.flush()
80
+
81
+ def add_event(self, event: EventPayload) -> None:
82
+ with self._lock:
83
+ self._events.append(event)
84
+ should_flush = len(self._events) >= self._max_batch_size
85
+ if should_flush:
86
+ self.flush()
87
+
88
+ # ── Flush ─────────────────────────────────────────────────────────────────
89
+
90
+ def flush(self) -> None:
91
+ with self._lock:
92
+ traces = self._traces[:]
93
+ errors = self._errors[:]
94
+ events = self._events[:]
95
+ self._traces.clear()
96
+ self._errors.clear()
97
+ self._events.clear()
98
+
99
+ if not traces and not errors and not events:
100
+ return
101
+
102
+ batch = IngestBatch(traces=traces, errors=errors, events=events)
103
+ # Run in a daemon thread so it never blocks the caller
104
+ t = threading.Thread(target=self._transport.send, args=(batch,), daemon=True)
105
+ t.start()