fastapi-calf 0.1.1__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.
@@ -0,0 +1,106 @@
1
+ Metadata-Version: 2.4
2
+ Name: fastapi-calf
3
+ Version: 0.1.1
4
+ Summary: A lightweight real-time terminal monitor for FastAPI servers
5
+ Requires-Python: >=3.10
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: rich
8
+ Requires-Dist: psutil
9
+
10
+ # 🐮 fastapi-calf
11
+
12
+ **A lightweight, real-time observability for FastAPI.**
13
+
14
+ `fastapi-calf` watches decorated API routes and presents request activity,
15
+ latency, throughput, failures, CPU, memory, and worker information in a compact
16
+ terminal UI. Monitoring is intentionally best-effort: if the dashboard is not
17
+ available, your API continues serving requests normally.
18
+
19
+ ## Highlights
20
+
21
+ - ⚡ Live requests-per-second and latency sparklines
22
+ - 🧭 Per-route request, failure, status, and response-time statistics
23
+ - 🖥️ CPU and memory usage across multiple application workers
24
+ - 🎮 `CUDA_VISIBLE_DEVICES` reporting for GPU-aware deployments
25
+ - 🛡️ Short-timeout event delivery that never takes down the application
26
+
27
+ ## Quick start
28
+
29
+ Install the runtime dependencies from the repository root:
30
+
31
+ ```powershell
32
+ python -m pip install fastapi uvicorn rich psutil
33
+ ```
34
+
35
+ Start the monitoring dashboard on port `8005`:
36
+
37
+ ```powershell
38
+ python -m fastapi_calf.daemon --port 8005
39
+ ```
40
+
41
+ In another terminal, start the included example API with four workers:
42
+
43
+ ```powershell
44
+ python -m uvicorn server.app:app --port 8000 --workers 4
45
+ ```
46
+
47
+ Visit `http://127.0.0.1:8000/docs` to explore the API while the terminal
48
+ dashboard updates in real time.
49
+
50
+ ## Instrumenting an API
51
+
52
+ Configure the monitor once, then decorate the routes you want to observe:
53
+
54
+ ```python
55
+ from fastapi import FastAPI, Request
56
+ from fastapi_calf import Calf, lookout
57
+
58
+ app = FastAPI()
59
+ Calf.listen(port=8005)
60
+
61
+
62
+ @app.get("/health")
63
+ @lookout
64
+ def health(request: Request):
65
+ return {"status": "healthy"}
66
+
67
+
68
+ @app.post("/jobs")
69
+ @lookout
70
+ async def create_job(request: Request):
71
+ payload = await request.json()
72
+ return {"accepted": True, "job": payload}
73
+ ```
74
+
75
+ The `Request` parameter allows `@lookout` to capture the method, path, query
76
+ parameters, request size, status, and timing without changing the response.
77
+
78
+ ## Tests and traffic simulation
79
+
80
+ Install the testing tools and run the correctness suite:
81
+
82
+ ```powershell
83
+ python -m pip install -r requirements-test.txt
84
+ python -m pytest tests/test_api.py -q
85
+ ```
86
+
87
+ The load runner starts four Uvicorn workers, verifies every sample endpoint,
88
+ and pushes **100 concurrent API calls** in a short burst:
89
+
90
+ ```powershell
91
+ python -m tests.load_test
92
+ ```
93
+
94
+ Customize a run with `--requests`, `--concurrency`, and `--workers`. See
95
+ [TESTING.md](TESTING.md) for additional examples.
96
+
97
+ ## Project layout
98
+
99
+ ```text
100
+ fastapi_calf/ monitoring library and terminal dashboard
101
+ server/ instrumented example FastAPI application
102
+ tests/ endpoint and concurrent load tests
103
+ ```
104
+
105
+ > **Status:** This is an early-stage project intended for experimentations with ML pipelines and POCs.
106
+
@@ -0,0 +1,97 @@
1
+ # 🐮 fastapi-calf
2
+
3
+ **A lightweight, real-time observability for FastAPI.**
4
+
5
+ `fastapi-calf` watches decorated API routes and presents request activity,
6
+ latency, throughput, failures, CPU, memory, and worker information in a compact
7
+ terminal UI. Monitoring is intentionally best-effort: if the dashboard is not
8
+ available, your API continues serving requests normally.
9
+
10
+ ## Highlights
11
+
12
+ - ⚡ Live requests-per-second and latency sparklines
13
+ - 🧭 Per-route request, failure, status, and response-time statistics
14
+ - 🖥️ CPU and memory usage across multiple application workers
15
+ - 🎮 `CUDA_VISIBLE_DEVICES` reporting for GPU-aware deployments
16
+ - 🛡️ Short-timeout event delivery that never takes down the application
17
+
18
+ ## Quick start
19
+
20
+ Install the runtime dependencies from the repository root:
21
+
22
+ ```powershell
23
+ python -m pip install fastapi uvicorn rich psutil
24
+ ```
25
+
26
+ Start the monitoring dashboard on port `8005`:
27
+
28
+ ```powershell
29
+ python -m fastapi_calf.daemon --port 8005
30
+ ```
31
+
32
+ In another terminal, start the included example API with four workers:
33
+
34
+ ```powershell
35
+ python -m uvicorn server.app:app --port 8000 --workers 4
36
+ ```
37
+
38
+ Visit `http://127.0.0.1:8000/docs` to explore the API while the terminal
39
+ dashboard updates in real time.
40
+
41
+ ## Instrumenting an API
42
+
43
+ Configure the monitor once, then decorate the routes you want to observe:
44
+
45
+ ```python
46
+ from fastapi import FastAPI, Request
47
+ from fastapi_calf import Calf, lookout
48
+
49
+ app = FastAPI()
50
+ Calf.listen(port=8005)
51
+
52
+
53
+ @app.get("/health")
54
+ @lookout
55
+ def health(request: Request):
56
+ return {"status": "healthy"}
57
+
58
+
59
+ @app.post("/jobs")
60
+ @lookout
61
+ async def create_job(request: Request):
62
+ payload = await request.json()
63
+ return {"accepted": True, "job": payload}
64
+ ```
65
+
66
+ The `Request` parameter allows `@lookout` to capture the method, path, query
67
+ parameters, request size, status, and timing without changing the response.
68
+
69
+ ## Tests and traffic simulation
70
+
71
+ Install the testing tools and run the correctness suite:
72
+
73
+ ```powershell
74
+ python -m pip install -r requirements-test.txt
75
+ python -m pytest tests/test_api.py -q
76
+ ```
77
+
78
+ The load runner starts four Uvicorn workers, verifies every sample endpoint,
79
+ and pushes **100 concurrent API calls** in a short burst:
80
+
81
+ ```powershell
82
+ python -m tests.load_test
83
+ ```
84
+
85
+ Customize a run with `--requests`, `--concurrency`, and `--workers`. See
86
+ [TESTING.md](TESTING.md) for additional examples.
87
+
88
+ ## Project layout
89
+
90
+ ```text
91
+ fastapi_calf/ monitoring library and terminal dashboard
92
+ server/ instrumented example FastAPI application
93
+ tests/ endpoint and concurrent load tests
94
+ ```
95
+
96
+ > **Status:** This is an early-stage project intended for experimentations with ML pipelines and POCs.
97
+
@@ -0,0 +1,4 @@
1
+ from .calf import Calf
2
+ from .decorator import lookout
3
+
4
+ __all__ = ["Calf", "lookout"]
@@ -0,0 +1,45 @@
1
+ import os
2
+ import time
3
+
4
+ from .config import config
5
+ from .emitter import emit_event
6
+
7
+
8
+ class Calf:
9
+ @staticmethod
10
+ def listen(app, port: int, host: str = "127.0.0.1"):
11
+ config.host = host
12
+ config.port = port
13
+
14
+ cuda_devices = os.getenv("CUDA_VISIBLE_DEVICES")
15
+
16
+ emit_event({
17
+ "pid": os.getpid(),
18
+ "cuda_visible_devices": cuda_devices,
19
+ })
20
+
21
+ @app.middleware("http")
22
+ async def calf_middleware(request, call_next):
23
+ start = time.perf_counter()
24
+
25
+ status = 500 # defaults to failure
26
+
27
+ try:
28
+ response = await call_next(request)
29
+
30
+ status = response.status_code
31
+
32
+ return response
33
+
34
+ finally:
35
+ latency_ms = (time.perf_counter() - start) * 1000
36
+
37
+ emit_event({
38
+ "pid": os.getpid(),
39
+ "cuda_visible_devices": cuda_devices,
40
+ "method": request.method,
41
+ "path": request.url.path,
42
+ "status": status,
43
+ "latency_ms": latency_ms,
44
+ "last_called": time.time(),
45
+ })
@@ -0,0 +1,11 @@
1
+ import psutil
2
+
3
+ def cleanup_workers(workers):
4
+ dead_pids = []
5
+
6
+ for pid in workers:
7
+ if not psutil.pid_exists(pid):
8
+ dead_pids.append(pid)
9
+
10
+ for pid in dead_pids:
11
+ workers.pop(pid, None)
@@ -0,0 +1,6 @@
1
+ class CalfConfig:
2
+ host = "127.0.0.1"
3
+ port = 8765
4
+
5
+
6
+ config = CalfConfig()
@@ -0,0 +1,327 @@
1
+ import json
2
+ import time
3
+ import asyncio
4
+ import argparse
5
+
6
+ from rich.live import Live
7
+ from rich.table import Table
8
+ from rich.text import Text
9
+ from rich.align import Align
10
+ from rich.console import Group
11
+
12
+ from .system import register_process, get_process_stats
13
+ from .history import cpu_history, latency_history, rps_history
14
+ from .metrics import record_request, get_rps, get_window_latency
15
+ from .sparkline import sparkline
16
+ from .cleanup import cleanup_workers
17
+
18
+ workers = {
19
+ # pid: {
20
+ # "requests": 0,
21
+ # "cuda_visible_devices": None,
22
+ # }
23
+ }
24
+
25
+ stats = {}
26
+
27
+ start_time = time.time()
28
+
29
+ last_history_sample = 0
30
+
31
+
32
+ def process_event(event):
33
+ pid = event.get("pid")
34
+
35
+ if pid is not None:
36
+ if pid not in workers:
37
+ register_process(pid)
38
+
39
+ workers[pid] = {
40
+ "requests": 0,
41
+ "cuda_visible_devices": event.get("cuda_visible_devices"),
42
+ }
43
+
44
+ # Update metadata if supplied again
45
+ if "cuda_visible_devices" in event:
46
+ workers[pid]["cuda_visible_devices"] = event["cuda_visible_devices"]
47
+
48
+ # This was only a process/system event
49
+ if "method" not in event or "path" not in event:
50
+ return
51
+
52
+ if pid is not None:
53
+ workers[pid]["requests"] += 1
54
+
55
+ key = (event["method"], event["path"])
56
+
57
+ record_request(event["latency_ms"])
58
+
59
+ if key not in stats:
60
+ stats[key] = {
61
+ "method": event["method"],
62
+ "path": event["path"],
63
+ "requests": 0,
64
+ "failed": 0,
65
+ "total_latency": 0,
66
+ "last_latency": 0,
67
+ "total_bytes": 0,
68
+ "last_called": 0,
69
+ "last_status": 200,
70
+ }
71
+
72
+ item = stats[key]
73
+
74
+ item["requests"] += 1
75
+ item["total_latency"] += event["latency_ms"]
76
+ item["last_latency"] = event["latency_ms"]
77
+ item["last_called"] = time.time()
78
+ item["last_status"] = event["status"]
79
+ #
80
+ # NOTE: Disabled due to future changes &
81
+ # due to removal of `request: Request`
82
+ # item["total_bytes"] += event["request_size"]
83
+ #
84
+ item["total_bytes"] += event.get("request_size", 0)
85
+
86
+ # Count failed requests
87
+ if event["status"] >= 400:
88
+ item["failed"] += 1
89
+
90
+
91
+ def sample_history(process_stats):
92
+ global last_history_sample
93
+
94
+ now = time.time()
95
+
96
+ if now - last_history_sample < 1:
97
+ return
98
+
99
+ if process_stats:
100
+ cpu_history.append(process_stats["cpu"])
101
+
102
+ latency_history.append(get_window_latency())
103
+
104
+ rps_history.append(get_rps())
105
+
106
+ last_history_sample = now
107
+
108
+
109
+ def latest(values, default=0):
110
+ return values[-1] if values else default
111
+
112
+
113
+ def build_history_panel():
114
+ return Group(
115
+ Text(
116
+ f"CPU "
117
+ f"{latest(cpu_history):5.1f}% "
118
+ f"{sparkline(cpu_history, 0, 100)}"
119
+ ),
120
+ Text(
121
+ f"Latency "
122
+ f"{latest(latency_history):5.1f}ms "
123
+ f"{sparkline(latency_history)}"
124
+ ),
125
+ Text(
126
+ f"RPS "
127
+ f"{latest(rps_history):5.1f} "
128
+ f"{sparkline(rps_history)}"
129
+ ),
130
+ )
131
+
132
+
133
+ def get_total_process_stats():
134
+ total_cpu = 0
135
+ total_ram = 0
136
+ count = 0
137
+
138
+ for pid in workers:
139
+ process_stats = get_process_stats(pid)
140
+
141
+ if process_stats is None:
142
+ continue
143
+
144
+ total_cpu += process_stats["cpu"]
145
+ total_ram += process_stats["ram_mb"]
146
+ count += 1
147
+
148
+ if count == 0:
149
+ return None
150
+
151
+ return {
152
+ "cpu": total_cpu,
153
+ "ram_mb": total_ram,
154
+ }
155
+
156
+
157
+ def create_table():
158
+ table = Table(expand=True)
159
+
160
+ table.add_column("Endpoint")
161
+ table.add_column("Method")
162
+ table.add_column("Activity")
163
+ table.add_column("Requests", justify="right")
164
+ table.add_column("Failed", justify="right")
165
+ table.add_column("Avg latency", justify="right")
166
+ table.add_column("Last", justify="right")
167
+ table.add_column("Status", justify="right")
168
+
169
+ now = time.time()
170
+
171
+ for item in stats.values():
172
+ avg_latency = (item["total_latency"] / item["requests"] if item["requests"] > 0 else 0)
173
+
174
+ active = ("● ● ●" if now - item["last_called"] < 0.5 else "")
175
+
176
+ failed_display = (f"[red]{item['failed']}[/red]" if item["failed"] > 0 else "0")
177
+
178
+ style = ("bold red" if item["last_status"] >= 400 else None)
179
+
180
+ table.add_row(
181
+ item["path"],
182
+ item["method"],
183
+ active,
184
+ str(item["requests"]),
185
+ failed_display,
186
+ f"{avg_latency:.2f} ms",
187
+ f"{item['last_latency']:.2f} ms",
188
+ str(item["last_status"]),
189
+ style=style,
190
+ )
191
+
192
+ process_stats = get_total_process_stats()
193
+
194
+ if process_stats:
195
+ system_text = (
196
+ f"CPU {process_stats['cpu']:.0f}%"
197
+ f" Memory {process_stats['ram_mb']:.0f} MB"
198
+ f" Workers {len(workers)}"
199
+ )
200
+ else:
201
+ system_text = "CPU -- Memory -- Workers --"
202
+
203
+ sample_history(process_stats)
204
+
205
+ workers_table = create_workers_table()
206
+
207
+ return Group(
208
+ Align.center(Text("🐮 fastapi-calf 🐮", style="bold")),
209
+ Align.center(Text(system_text)),
210
+ Text(""),
211
+ build_history_panel(),
212
+ Text(""),
213
+ workers_table,
214
+ Text(""),
215
+ table,
216
+ )
217
+
218
+
219
+ def create_workers_table():
220
+ table = Table(title="Workers", expand=True)
221
+
222
+ table.add_column("PID")
223
+ table.add_column("CPU", justify="right")
224
+ table.add_column("Memory", justify="right")
225
+ table.add_column("Requests", justify="right")
226
+ table.add_column("GPU", justify="right")
227
+
228
+ dead_workers = []
229
+
230
+ now = time.time()
231
+ workers_to_remove = []
232
+
233
+
234
+ for pid, worker in workers.items():
235
+ process_stats = get_process_stats(pid)
236
+
237
+ if process_stats is None:
238
+ if worker["dead_since"] is None:
239
+ worker["dead_since"] = now
240
+
241
+ # Remove only after 5 seconds
242
+ if now - worker["dead_since"] >= 5:
243
+ workers_to_remove.append(pid)
244
+ continue
245
+
246
+ table.add_row(
247
+ str(pid),
248
+ "--",
249
+ "--",
250
+ str(worker["requests"]),
251
+ "none"
252
+ if worker["cuda_visible_devices"] is None
253
+ else str(worker["cuda_visible_devices"]),
254
+ style="dim",
255
+ )
256
+
257
+ continue
258
+
259
+ worker["dead_since"] = None
260
+
261
+ table.add_row(
262
+ str(pid),
263
+ f"{process_stats['cpu']:.1f}%",
264
+ f"{process_stats['ram_mb']:.1f} MB",
265
+ str(worker["requests"]),
266
+ "none"
267
+ if worker["cuda_visible_devices"] is None
268
+ else str(worker["cuda_visible_devices"]),
269
+ )
270
+
271
+ for pid in dead_workers:
272
+ workers.pop(pid, None)
273
+
274
+ return table
275
+
276
+
277
+ async def handle_client(reader, writer):
278
+ data = await reader.readline()
279
+
280
+ if data:
281
+ try:
282
+ event = json.loads(data)
283
+ process_event(event)
284
+ except json.JSONDecodeError:
285
+ pass
286
+
287
+ writer.close()
288
+ await writer.wait_closed()
289
+
290
+
291
+ async def run_server(port: int):
292
+ server = await asyncio.start_server(handle_client, "127.0.0.1", port)
293
+ async with server:
294
+ await server.serve_forever()
295
+
296
+
297
+ async def run_ui():
298
+ with Live(create_table(), refresh_per_second=10, screen=True) as live:
299
+ while True:
300
+ # NOTE: ---------------------------------------------------------------------------------------
301
+ # When uncommented, cleanup_workers will instantly remove the dead workers off the table
302
+ # During testing if u wanna observe dead workers, comment below line `cleanup_workers(workers)`
303
+ cleanup_workers(workers)
304
+ # ---------------------------------------------------------------------------------------------
305
+
306
+ live.update(create_table())
307
+ await asyncio.sleep(0.1)
308
+
309
+
310
+ async def run(port: int):
311
+ await asyncio.gather(run_server(port), run_ui())
312
+
313
+ def main():
314
+ try:
315
+ parser = argparse.ArgumentParser()
316
+ parser.add_argument("-p", "--port", type=int, default=8765, help="Port for fastapi-calf")
317
+
318
+ args = parser.parse_args()
319
+
320
+ asyncio.run(run(args.port))
321
+
322
+ except KeyboardInterrupt:
323
+ print("fastapi-calf stopped.")
324
+
325
+
326
+ if __name__ == "__main__":
327
+ main()
@@ -0,0 +1,56 @@
1
+ import inspect
2
+ import time
3
+ import os
4
+
5
+ from datetime import datetime, timezone
6
+ from functools import wraps
7
+
8
+ from .emitter import emit_event
9
+
10
+
11
+ def lookout(func):
12
+ @wraps(func)
13
+ async def wrapper(*args, **kwargs):
14
+ request = kwargs.get("request")
15
+
16
+ start_timestamp = datetime.now(timezone.utc)
17
+ start_timer = time.perf_counter()
18
+
19
+ status_code = 200
20
+ error = None
21
+
22
+ try:
23
+ if inspect.iscoroutinefunction(func):
24
+ response = await func(*args, **kwargs)
25
+ else:
26
+ response = func(*args, **kwargs)
27
+ return response
28
+
29
+ except Exception as exc:
30
+ status_code = 500
31
+ error = str(exc)
32
+ raise
33
+
34
+ finally:
35
+ end_timer = time.perf_counter()
36
+ latency_ms = (end_timer - start_timer) * 1000
37
+
38
+ if request:
39
+ body = await request.body()
40
+
41
+ event = {
42
+ "timestamp": start_timestamp.isoformat(),
43
+ "function": func.__name__,
44
+ "method": request.method,
45
+ "path": request.url.path,
46
+ "query": dict(request.query_params),
47
+ "request_size": len(body),
48
+ "latency_ms": latency_ms,
49
+ "status": status_code,
50
+ "error": error,
51
+ "pid": os.getpid(),
52
+ }
53
+
54
+ emit_event(event)
55
+
56
+ return wrapper
@@ -0,0 +1,21 @@
1
+ import json
2
+ import socket
3
+
4
+ from .config import config
5
+
6
+
7
+ def emit_event(event: dict):
8
+ try:
9
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
10
+ sock.settimeout(0.05)
11
+
12
+ sock.connect((config.host, config.port))
13
+
14
+ payload = json.dumps(event).encode("utf-8") + b"\n"
15
+
16
+ sock.sendall(payload)
17
+
18
+ except (ConnectionRefusedError, TimeoutError, OSError) as error:
19
+ # Monitoring should NEVER crash the API
20
+ print("Calf emitter error:", error)
21
+ pass
@@ -0,0 +1 @@
1
+ from .history import cpu_history, latency_history, rps_history
@@ -0,0 +1,10 @@
1
+ from collections import deque
2
+
3
+
4
+ HISTORY_SIZE = 60
5
+
6
+ cpu_history = deque(maxlen=HISTORY_SIZE)
7
+ latency_history = deque(maxlen=HISTORY_SIZE)
8
+ rps_history = deque(maxlen=HISTORY_SIZE)
9
+
10
+ request_times = deque()
@@ -0,0 +1 @@
1
+ from .metrics import record_request, get_rps, get_window_latency
@@ -0,0 +1,32 @@
1
+ import time
2
+ from collections import deque
3
+
4
+ from ..history.history import request_times
5
+
6
+
7
+ recent_latencies = []
8
+
9
+
10
+ def record_request(latency_ms):
11
+ request_times.append(time.time())
12
+ recent_latencies.append(latency_ms)
13
+
14
+
15
+ def get_rps():
16
+ now = time.time()
17
+
18
+ while request_times and request_times[0] < now - 1:
19
+ request_times.popleft()
20
+
21
+ return len(request_times)
22
+
23
+
24
+ def get_window_latency():
25
+ if not recent_latencies:
26
+ return 0.0
27
+
28
+ avg = sum(recent_latencies) / len(recent_latencies)
29
+
30
+ recent_latencies.clear()
31
+
32
+ return avg
@@ -0,0 +1 @@
1
+ from .sparkline import sparkline
@@ -0,0 +1,26 @@
1
+ SPARK_CHARS = "▁▂▃▄▅▆▇█"
2
+
3
+
4
+ def sparkline(values, minimum=None, maximum=None):
5
+ if not values:
6
+ return ""
7
+
8
+ min_value = minimum if minimum is not None else min(values)
9
+ max_value = maximum if maximum is not None else max(values)
10
+
11
+ if max_value == min_value:
12
+ return SPARK_CHARS[0] * len(values)
13
+
14
+ result = []
15
+
16
+ for value in values:
17
+ value = max(min_value, min(value, max_value))
18
+
19
+ normalized = (value - min_value) / (max_value - min_value)
20
+
21
+ index = int(normalized * (len(SPARK_CHARS) - 1))
22
+
23
+ result.append(SPARK_CHARS[index])
24
+
25
+ return "".join(result)
26
+
@@ -0,0 +1 @@
1
+ from .cpu import register_process, get_process_stats
@@ -0,0 +1,37 @@
1
+ import psutil
2
+
3
+
4
+ _processes = {}
5
+
6
+
7
+ def register_process(pid: int):
8
+ if pid in _processes:
9
+ return
10
+
11
+ try:
12
+ process = psutil.Process(pid)
13
+
14
+ # Prime cpu_percent
15
+ process.cpu_percent(interval=None)
16
+
17
+ _processes[pid] = process
18
+
19
+ except psutil.NoSuchProcess:
20
+ pass
21
+
22
+
23
+ def get_process_stats(pid: int):
24
+ process = _processes.get(pid)
25
+
26
+ if process is None:
27
+ return None
28
+
29
+ try:
30
+ return {
31
+ "cpu": process.cpu_percent(interval=None),
32
+ "ram_mb": process.memory_info().rss / 1024 / 1024,
33
+ }
34
+
35
+ except psutil.NoSuchProcess:
36
+ _processes.pop(pid, None)
37
+ return None
File without changes
@@ -0,0 +1,106 @@
1
+ Metadata-Version: 2.4
2
+ Name: fastapi-calf
3
+ Version: 0.1.1
4
+ Summary: A lightweight real-time terminal monitor for FastAPI servers
5
+ Requires-Python: >=3.10
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: rich
8
+ Requires-Dist: psutil
9
+
10
+ # 🐮 fastapi-calf
11
+
12
+ **A lightweight, real-time observability for FastAPI.**
13
+
14
+ `fastapi-calf` watches decorated API routes and presents request activity,
15
+ latency, throughput, failures, CPU, memory, and worker information in a compact
16
+ terminal UI. Monitoring is intentionally best-effort: if the dashboard is not
17
+ available, your API continues serving requests normally.
18
+
19
+ ## Highlights
20
+
21
+ - ⚡ Live requests-per-second and latency sparklines
22
+ - 🧭 Per-route request, failure, status, and response-time statistics
23
+ - 🖥️ CPU and memory usage across multiple application workers
24
+ - 🎮 `CUDA_VISIBLE_DEVICES` reporting for GPU-aware deployments
25
+ - 🛡️ Short-timeout event delivery that never takes down the application
26
+
27
+ ## Quick start
28
+
29
+ Install the runtime dependencies from the repository root:
30
+
31
+ ```powershell
32
+ python -m pip install fastapi uvicorn rich psutil
33
+ ```
34
+
35
+ Start the monitoring dashboard on port `8005`:
36
+
37
+ ```powershell
38
+ python -m fastapi_calf.daemon --port 8005
39
+ ```
40
+
41
+ In another terminal, start the included example API with four workers:
42
+
43
+ ```powershell
44
+ python -m uvicorn server.app:app --port 8000 --workers 4
45
+ ```
46
+
47
+ Visit `http://127.0.0.1:8000/docs` to explore the API while the terminal
48
+ dashboard updates in real time.
49
+
50
+ ## Instrumenting an API
51
+
52
+ Configure the monitor once, then decorate the routes you want to observe:
53
+
54
+ ```python
55
+ from fastapi import FastAPI, Request
56
+ from fastapi_calf import Calf, lookout
57
+
58
+ app = FastAPI()
59
+ Calf.listen(port=8005)
60
+
61
+
62
+ @app.get("/health")
63
+ @lookout
64
+ def health(request: Request):
65
+ return {"status": "healthy"}
66
+
67
+
68
+ @app.post("/jobs")
69
+ @lookout
70
+ async def create_job(request: Request):
71
+ payload = await request.json()
72
+ return {"accepted": True, "job": payload}
73
+ ```
74
+
75
+ The `Request` parameter allows `@lookout` to capture the method, path, query
76
+ parameters, request size, status, and timing without changing the response.
77
+
78
+ ## Tests and traffic simulation
79
+
80
+ Install the testing tools and run the correctness suite:
81
+
82
+ ```powershell
83
+ python -m pip install -r requirements-test.txt
84
+ python -m pytest tests/test_api.py -q
85
+ ```
86
+
87
+ The load runner starts four Uvicorn workers, verifies every sample endpoint,
88
+ and pushes **100 concurrent API calls** in a short burst:
89
+
90
+ ```powershell
91
+ python -m tests.load_test
92
+ ```
93
+
94
+ Customize a run with `--requests`, `--concurrency`, and `--workers`. See
95
+ [TESTING.md](TESTING.md) for additional examples.
96
+
97
+ ## Project layout
98
+
99
+ ```text
100
+ fastapi_calf/ monitoring library and terminal dashboard
101
+ server/ instrumented example FastAPI application
102
+ tests/ endpoint and concurrent load tests
103
+ ```
104
+
105
+ > **Status:** This is an early-stage project intended for experimentations with ML pipelines and POCs.
106
+
@@ -0,0 +1,25 @@
1
+ README.md
2
+ pyproject.toml
3
+ fastapi_calf/__init__.py
4
+ fastapi_calf/calf.py
5
+ fastapi_calf/cleanup.py
6
+ fastapi_calf/config.py
7
+ fastapi_calf/daemon.py
8
+ fastapi_calf/decorator.py
9
+ fastapi_calf/emitter.py
10
+ fastapi_calf.egg-info/PKG-INFO
11
+ fastapi_calf.egg-info/SOURCES.txt
12
+ fastapi_calf.egg-info/dependency_links.txt
13
+ fastapi_calf.egg-info/entry_points.txt
14
+ fastapi_calf.egg-info/requires.txt
15
+ fastapi_calf.egg-info/top_level.txt
16
+ fastapi_calf/history/__init__.py
17
+ fastapi_calf/history/history.py
18
+ fastapi_calf/metrics/__init__.py
19
+ fastapi_calf/metrics/metrics.py
20
+ fastapi_calf/sparkline/__init__.py
21
+ fastapi_calf/sparkline/sparkline.py
22
+ fastapi_calf/system/__init__.py
23
+ fastapi_calf/system/cpu.py
24
+ fastapi_calf/system/gpu.py
25
+ tests/test.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ calf = fastapi_calf.daemon:main
@@ -0,0 +1,2 @@
1
+ rich
2
+ psutil
@@ -0,0 +1 @@
1
+ fastapi_calf
@@ -0,0 +1,21 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "fastapi-calf"
7
+ version = "0.1.1"
8
+ description = "A lightweight real-time terminal monitor for FastAPI servers"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+
12
+ dependencies = [
13
+ "rich",
14
+ "psutil",
15
+ ]
16
+
17
+ [project.scripts]
18
+ calf = "fastapi_calf.daemon:main"
19
+
20
+ [tool.setuptools.packages.find]
21
+ include = ["fastapi_calf*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
File without changes