fastapi-radar 0.1.9__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.9/fastapi_radar.egg-info → fastapi_radar-0.2.0}/PKG-INFO +2 -1
  2. {fastapi_radar-0.1.9 → fastapi_radar-0.2.0}/fastapi_radar/__init__.py +1 -1
  3. {fastapi_radar-0.1.9 → fastapi_radar-0.2.0}/fastapi_radar/capture.py +38 -10
  4. fastapi_radar-0.1.9/fastapi_radar/dashboard/dist/assets/index-Dj9HCQum.js → fastapi_radar-0.2.0/fastapi_radar/dashboard/dist/assets/index-31zorKsE.js +62 -67
  5. {fastapi_radar-0.1.9 → fastapi_radar-0.2.0}/fastapi_radar/dashboard/dist/index.html +1 -1
  6. {fastapi_radar-0.1.9 → fastapi_radar-0.2.0}/fastapi_radar/middleware.py +15 -7
  7. {fastapi_radar-0.1.9 → fastapi_radar-0.2.0/fastapi_radar.egg-info}/PKG-INFO +2 -1
  8. {fastapi_radar-0.1.9 → fastapi_radar-0.2.0}/fastapi_radar.egg-info/SOURCES.txt +2 -1
  9. {fastapi_radar-0.1.9 → fastapi_radar-0.2.0}/fastapi_radar.egg-info/requires.txt +1 -0
  10. {fastapi_radar-0.1.9 → fastapi_radar-0.2.0}/pyproject.toml +2 -1
  11. {fastapi_radar-0.1.9 → fastapi_radar-0.2.0}/setup.py +1 -1
  12. fastapi_radar-0.2.0/tests/test_async_radar.py +56 -0
  13. {fastapi_radar-0.1.9 → fastapi_radar-0.2.0}/CONTRIBUTING.md +0 -0
  14. {fastapi_radar-0.1.9 → fastapi_radar-0.2.0}/LICENSE +0 -0
  15. {fastapi_radar-0.1.9 → fastapi_radar-0.2.0}/MANIFEST.in +0 -0
  16. {fastapi_radar-0.1.9 → fastapi_radar-0.2.0}/README.md +0 -0
  17. {fastapi_radar-0.1.9 → fastapi_radar-0.2.0}/fastapi_radar/api.py +0 -0
  18. {fastapi_radar-0.1.9 → fastapi_radar-0.2.0}/fastapi_radar/dashboard/dist/assets/index-XlGcZj49.css +0 -0
  19. {fastapi_radar-0.1.9 → fastapi_radar-0.2.0}/fastapi_radar/models.py +0 -0
  20. {fastapi_radar-0.1.9 → fastapi_radar-0.2.0}/fastapi_radar/radar.py +0 -0
  21. {fastapi_radar-0.1.9 → fastapi_radar-0.2.0}/fastapi_radar/tracing.py +0 -0
  22. {fastapi_radar-0.1.9 → fastapi_radar-0.2.0}/fastapi_radar/utils.py +0 -0
  23. {fastapi_radar-0.1.9 → fastapi_radar-0.2.0}/fastapi_radar.egg-info/dependency_links.txt +0 -0
  24. {fastapi_radar-0.1.9 → fastapi_radar-0.2.0}/fastapi_radar.egg-info/not-zip-safe +0 -0
  25. {fastapi_radar-0.1.9 → fastapi_radar-0.2.0}/fastapi_radar.egg-info/top_level.txt +0 -0
  26. {fastapi_radar-0.1.9 → fastapi_radar-0.2.0}/setup.cfg +0 -0
  27. {fastapi_radar-0.1.9 → fastapi_radar-0.2.0}/tests/__init__.py +0 -0
  28. {fastapi_radar-0.1.9 → 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.9
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"
@@ -2,5 +2,5 @@
2
2
 
3
3
  from .radar import Radar
4
4
 
5
- __version__ = "0.1.9"
5
+ __version__ = "0.2.0"
6
6
  __all__ = ["Radar"]
@@ -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