fastapi-radar 0.1.8__tar.gz → 0.2.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.

Potentially problematic release.


This version of fastapi-radar might be problematic. Click here for more details.

Files changed (28) hide show
  1. {fastapi_radar-0.1.8/fastapi_radar.egg-info → fastapi_radar-0.2.0}/PKG-INFO +19 -1
  2. {fastapi_radar-0.1.8 → fastapi_radar-0.2.0}/README.md +17 -0
  3. {fastapi_radar-0.1.8 → fastapi_radar-0.2.0}/fastapi_radar/__init__.py +1 -1
  4. {fastapi_radar-0.1.8 → fastapi_radar-0.2.0}/fastapi_radar/api.py +4 -4
  5. {fastapi_radar-0.1.8 → fastapi_radar-0.2.0}/fastapi_radar/capture.py +38 -10
  6. fastapi_radar-0.1.8/fastapi_radar/dashboard/dist/assets/index-By5DXl8Z.js → fastapi_radar-0.2.0/fastapi_radar/dashboard/dist/assets/index-31zorKsE.js +62 -67
  7. {fastapi_radar-0.1.8 → fastapi_radar-0.2.0}/fastapi_radar/dashboard/dist/index.html +1 -1
  8. {fastapi_radar-0.1.8 → fastapi_radar-0.2.0}/fastapi_radar/middleware.py +15 -7
  9. {fastapi_radar-0.1.8 → fastapi_radar-0.2.0}/fastapi_radar/models.py +20 -8
  10. {fastapi_radar-0.1.8 → fastapi_radar-0.2.0}/fastapi_radar/radar.py +73 -10
  11. {fastapi_radar-0.1.8 → fastapi_radar-0.2.0}/fastapi_radar/tracing.py +6 -6
  12. {fastapi_radar-0.1.8 → fastapi_radar-0.2.0/fastapi_radar.egg-info}/PKG-INFO +19 -1
  13. {fastapi_radar-0.1.8 → fastapi_radar-0.2.0}/fastapi_radar.egg-info/SOURCES.txt +2 -1
  14. {fastapi_radar-0.1.8 → fastapi_radar-0.2.0}/fastapi_radar.egg-info/requires.txt +1 -0
  15. {fastapi_radar-0.1.8 → fastapi_radar-0.2.0}/pyproject.toml +2 -1
  16. {fastapi_radar-0.1.8 → fastapi_radar-0.2.0}/setup.py +1 -1
  17. fastapi_radar-0.2.0/tests/test_async_radar.py +56 -0
  18. {fastapi_radar-0.1.8 → fastapi_radar-0.2.0}/CONTRIBUTING.md +0 -0
  19. {fastapi_radar-0.1.8 → fastapi_radar-0.2.0}/LICENSE +0 -0
  20. {fastapi_radar-0.1.8 → fastapi_radar-0.2.0}/MANIFEST.in +0 -0
  21. {fastapi_radar-0.1.8 → fastapi_radar-0.2.0}/fastapi_radar/dashboard/dist/assets/index-XlGcZj49.css +0 -0
  22. {fastapi_radar-0.1.8 → fastapi_radar-0.2.0}/fastapi_radar/utils.py +0 -0
  23. {fastapi_radar-0.1.8 → fastapi_radar-0.2.0}/fastapi_radar.egg-info/dependency_links.txt +0 -0
  24. {fastapi_radar-0.1.8 → fastapi_radar-0.2.0}/fastapi_radar.egg-info/not-zip-safe +0 -0
  25. {fastapi_radar-0.1.8 → fastapi_radar-0.2.0}/fastapi_radar.egg-info/top_level.txt +0 -0
  26. {fastapi_radar-0.1.8 → fastapi_radar-0.2.0}/setup.cfg +0 -0
  27. {fastapi_radar-0.1.8 → fastapi_radar-0.2.0}/tests/__init__.py +0 -0
  28. {fastapi_radar-0.1.8 → fastapi_radar-0.2.0}/tests/test_radar.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastapi-radar
3
- Version: 0.1.8
3
+ Version: 0.2.0
4
4
  Summary: A debugging dashboard for FastAPI applications with real-time monitoring
5
5
  Home-page: https://github.com/doganarif/fastapi-radar
6
6
  Author: Arif Dogan
@@ -29,6 +29,7 @@ Requires-Dist: pydantic
29
29
  Requires-Dist: starlette
30
30
  Requires-Dist: duckdb==1.1.3
31
31
  Requires-Dist: duckdb-engine==0.17.0
32
+ Requires-Dist: aiosqlite>=0.21.0
32
33
  Provides-Extra: dev
33
34
  Requires-Dist: pytest; extra == "dev"
34
35
  Requires-Dist: pytest-asyncio; extra == "dev"
@@ -163,6 +164,23 @@ radar = Radar(app, db_path="./data")
163
164
 
164
165
  If the specified path cannot be created, FastAPI Radar will fallback to using the current directory with a warning.
165
166
 
167
+ ### Development Mode with Auto-Reload
168
+
169
+ When running your FastAPI application with `fastapi dev` (which uses auto-reload), FastAPI Radar automatically switches to an in-memory database to avoid file locking issues. This means:
170
+
171
+ - **No file locking errors** - The dashboard will work seamlessly in development
172
+ - **Data doesn't persist between reloads** - Each reload starts with a fresh database
173
+ - **Production behavior unchanged** - When using `fastapi run` or deploying, the normal file-based database is used
174
+
175
+ ```python
176
+ # With fastapi dev (auto-reload enabled):
177
+ # Automatically uses in-memory database - no configuration needed!
178
+ radar = Radar(app)
179
+ radar.create_tables() # Safe to call - handles multiple processes gracefully
180
+ ```
181
+
182
+ This behavior only applies when using the development server with auto-reload (`fastapi dev`). In production or when using `fastapi run`, the standard file-based DuckDB storage is used.
183
+
166
184
  ## What Gets Captured?
167
185
 
168
186
  - ✅ HTTP requests and responses
@@ -118,6 +118,23 @@ radar = Radar(app, db_path="./data")
118
118
 
119
119
  If the specified path cannot be created, FastAPI Radar will fallback to using the current directory with a warning.
120
120
 
121
+ ### Development Mode with Auto-Reload
122
+
123
+ When running your FastAPI application with `fastapi dev` (which uses auto-reload), FastAPI Radar automatically switches to an in-memory database to avoid file locking issues. This means:
124
+
125
+ - **No file locking errors** - The dashboard will work seamlessly in development
126
+ - **Data doesn't persist between reloads** - Each reload starts with a fresh database
127
+ - **Production behavior unchanged** - When using `fastapi run` or deploying, the normal file-based database is used
128
+
129
+ ```python
130
+ # With fastapi dev (auto-reload enabled):
131
+ # Automatically uses in-memory database - no configuration needed!
132
+ radar = Radar(app)
133
+ radar.create_tables() # Safe to call - handles multiple processes gracefully
134
+ ```
135
+
136
+ This behavior only applies when using the development server with auto-reload (`fastapi dev`). In production or when using `fastapi run`, the standard file-based DuckDB storage is used.
137
+
121
138
  ## What Gets Captured?
122
139
 
123
140
  - ✅ HTTP requests and responses
@@ -2,5 +2,5 @@
2
2
 
3
3
  from .radar import Radar
4
4
 
5
- __version__ = "0.1.8"
5
+ __version__ = "0.2.0"
6
6
  __all__ = ["Radar"]
@@ -1,6 +1,6 @@
1
1
  """API endpoints for FastAPI Radar dashboard."""
2
2
 
3
- from datetime import datetime, timedelta
3
+ from datetime import datetime, timedelta, timezone
4
4
  from typing import Any, Dict, List, Optional, Union
5
5
 
6
6
  from fastapi import APIRouter, Depends, HTTPException, Query
@@ -302,7 +302,7 @@ def create_api_router(get_session_context) -> APIRouter:
302
302
  slow_threshold: int = Query(100),
303
303
  session: Session = Depends(get_db),
304
304
  ):
305
- since = datetime.utcnow() - timedelta(hours=hours)
305
+ since = datetime.now(timezone.utc) - timedelta(hours=hours)
306
306
 
307
307
  requests = (
308
308
  session.query(CapturedRequest)
@@ -359,7 +359,7 @@ def create_api_router(get_session_context) -> APIRouter:
359
359
  older_than_hours: Optional[int] = None, session: Session = Depends(get_db)
360
360
  ):
361
361
  if older_than_hours:
362
- cutoff = datetime.utcnow() - timedelta(hours=older_than_hours)
362
+ cutoff = datetime.now(timezone.utc) - timedelta(hours=older_than_hours)
363
363
  session.query(CapturedRequest).filter(
364
364
  CapturedRequest.created_at < cutoff
365
365
  ).delete()
@@ -382,7 +382,7 @@ def create_api_router(get_session_context) -> APIRouter:
382
382
  session: Session = Depends(get_db),
383
383
  ):
384
384
  """List traces."""
385
- since = datetime.utcnow() - timedelta(hours=hours)
385
+ since = datetime.now(timezone.utc) - timedelta(hours=hours)
386
386
  query = session.query(Trace).filter(Trace.created_at >= since)
387
387
 
388
388
  if status:
@@ -1,13 +1,18 @@
1
1
  """SQLAlchemy query capture for FastAPI Radar."""
2
2
 
3
3
  import time
4
- from typing import Any, Callable, Dict, List, Union
5
-
4
+ from typing import Any, Callable, Dict, List, Optional, Union
6
5
  from sqlalchemy import event
7
6
  from sqlalchemy.engine import Engine
8
7
 
8
+ try: # SQLAlchemy async support is optional
9
+ from sqlalchemy.ext.asyncio import AsyncEngine
10
+ except Exception: # pragma: no cover - module might not exist in older SQLAlchemy
11
+ AsyncEngine = None # type: ignore[assignment]
9
12
  from .middleware import request_context
10
13
  from .models import CapturedQuery
14
+
15
+
11
16
  from .utils import format_sql
12
17
  from .tracing import get_current_trace_context
13
18
 
@@ -23,14 +28,20 @@ class QueryCapture:
23
28
  self.capture_bindings = capture_bindings
24
29
  self.slow_query_threshold = slow_query_threshold
25
30
  self._query_start_times = {}
31
+ self._registered_engines: Dict[int, Engine] = {}
26
32
 
27
33
  def register(self, engine: Engine) -> None:
28
- event.listen(engine, "before_cursor_execute", self._before_cursor_execute)
29
- event.listen(engine, "after_cursor_execute", self._after_cursor_execute)
34
+ sync_engine = self._resolve_engine(engine)
35
+ event.listen(sync_engine, "before_cursor_execute", self._before_cursor_execute)
36
+ event.listen(sync_engine, "after_cursor_execute", self._after_cursor_execute)
37
+ self._registered_engines[id(engine)] = sync_engine
30
38
 
31
39
  def unregister(self, engine: Engine) -> None:
32
- event.remove(engine, "before_cursor_execute", self._before_cursor_execute)
33
- event.remove(engine, "after_cursor_execute", self._after_cursor_execute)
40
+ sync_engine = self._registered_engines.pop(id(engine), None)
41
+ if not sync_engine:
42
+ sync_engine = self._resolve_engine(engine)
43
+ event.remove(sync_engine, "before_cursor_execute", self._before_cursor_execute)
44
+ event.remove(sync_engine, "after_cursor_execute", self._after_cursor_execute)
34
45
 
35
46
  def _before_cursor_execute(
36
47
  self,
@@ -44,14 +55,14 @@ class QueryCapture:
44
55
  request_id = request_context.get()
45
56
  if not request_id:
46
57
  return
47
-
48
58
  context_id = id(context)
49
59
  self._query_start_times[context_id] = time.time()
50
-
60
+ setattr(context, "_radar_request_id", request_id)
51
61
  trace_ctx = get_current_trace_context()
52
62
  if trace_ctx:
53
63
  formatted_sql = format_sql(statement)
54
64
  operation_type = self._get_operation_type(statement)
65
+ db_tags = self._get_db_tags(conn)
55
66
  span_id = trace_ctx.create_span(
56
67
  operation_name=f"DB {operation_type}",
57
68
  span_kind="client",
@@ -59,6 +70,7 @@ class QueryCapture:
59
70
  "db.statement": formatted_sql[:500], # limit SQL length
60
71
  "db.operation_type": operation_type,
61
72
  "component": "database",
73
+ **db_tags,
62
74
  },
63
75
  )
64
76
  setattr(context, "_radar_span_id", span_id)
@@ -74,8 +86,9 @@ class QueryCapture:
74
86
  ) -> None:
75
87
  request_id = request_context.get()
76
88
  if not request_id:
77
- return
78
-
89
+ request_id = getattr(context, "_radar_request_id", None)
90
+ if not request_id:
91
+ return
79
92
  start_time = self._query_start_times.pop(id(context), None)
80
93
  if start_time is None:
81
94
  return
@@ -160,3 +173,18 @@ class QueryCapture:
160
173
  return {k: str(v) for k, v in list(parameters.items())[:100]}
161
174
 
162
175
  return [str(parameters)]
176
+
177
+ def _resolve_engine(self, engine: Engine) -> Engine:
178
+ if AsyncEngine is not None and isinstance(engine, AsyncEngine):
179
+ return engine.sync_engine
180
+ return engine
181
+
182
+ def _get_db_tags(self, conn: Any) -> Dict[str, Optional[str]]:
183
+ tags: Dict[str, Optional[str]] = {}
184
+ engine = getattr(conn, "engine", None)
185
+ if engine and getattr(engine, "dialect", None):
186
+ tags["db.system"] = engine.dialect.name
187
+ url = getattr(engine, "url", None)
188
+ if url is not None:
189
+ tags["db.name"] = getattr(url, "database", None)
190
+ return tags