fastapi-radar 0.1.8__py3-none-any.whl → 0.2.0__py3-none-any.whl

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.

@@ -4,7 +4,7 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>FastAPI Radar - Debugging Dashboard</title>
7
- <script type="module" crossorigin src="/__radar/assets/index-By5DXl8Z.js"></script>
7
+ <script type="module" crossorigin src="/__radar/assets/index-31zorKsE.js"></script>
8
8
  <link rel="stylesheet" crossorigin href="/__radar/assets/index-XlGcZj49.css">
9
9
  </head>
10
10
  <body>
@@ -7,6 +7,7 @@ import uuid
7
7
  from contextvars import ContextVar
8
8
  from typing import Callable, Optional
9
9
 
10
+ from sqlalchemy.exc import SQLAlchemyError
10
11
  from starlette.middleware.base import BaseHTTPMiddleware
11
12
  from starlette.requests import Request
12
13
  from starlette.responses import Response, StreamingResponse
@@ -118,16 +119,23 @@ class RadarMiddleware(BaseHTTPMiddleware):
118
119
 
119
120
  async def capture_response():
120
121
  response_body = ""
122
+ capturing = True
121
123
  async for chunk in original_response.body_iterator:
122
124
  yield chunk
123
- if len(response_body) < self.max_body_size:
125
+ if capturing:
124
126
  response_body += chunk.decode("utf-8", errors="ignore")
125
- with self.get_session() as session:
126
- captured_request.response_body = truncate_body(
127
- response_body, self.max_body_size
128
- )
129
- session.add(captured_request)
130
- session.commit()
127
+ try:
128
+ with self.get_session() as session:
129
+ captured_request.response_body = truncate_body(
130
+ response_body, self.max_body_size
131
+ )
132
+ session.add(captured_request)
133
+ session.commit()
134
+ except SQLAlchemyError:
135
+ # CapturedRequest record has been deleted.
136
+ capturing = False
137
+ else:
138
+ capturing = len(response_body) < self.max_body_size
131
139
 
132
140
  response = StreamingResponse(
133
141
  content=capture_response(),
fastapi_radar/models.py CHANGED
@@ -1,6 +1,6 @@
1
1
  """Storage models for FastAPI Radar."""
2
2
 
3
- from datetime import datetime
3
+ from datetime import datetime, timezone
4
4
 
5
5
  from sqlalchemy import (
6
6
  Column,
@@ -40,7 +40,9 @@ class CapturedRequest(Base):
40
40
  response_headers = Column(JSON)
41
41
  duration_ms = Column(Float)
42
42
  client_ip = Column(String(50))
43
- created_at = Column(DateTime, default=datetime.utcnow, index=True)
43
+ created_at = Column(
44
+ DateTime, default=lambda: datetime.now(timezone.utc), index=True
45
+ )
44
46
 
45
47
  queries = relationship(
46
48
  "CapturedQuery",
@@ -68,7 +70,9 @@ class CapturedQuery(Base):
68
70
  duration_ms = Column(Float)
69
71
  rows_affected = Column(Integer)
70
72
  connection_name = Column(String(100))
71
- created_at = Column(DateTime, default=datetime.utcnow, index=True)
73
+ created_at = Column(
74
+ DateTime, default=lambda: datetime.now(timezone.utc), index=True
75
+ )
72
76
 
73
77
  request = relationship(
74
78
  "CapturedRequest",
@@ -87,7 +91,9 @@ class CapturedException(Base):
87
91
  exception_type = Column(String(100), nullable=False)
88
92
  exception_value = Column(Text)
89
93
  traceback = Column(Text, nullable=False)
90
- created_at = Column(DateTime, default=datetime.utcnow, index=True)
94
+ created_at = Column(
95
+ DateTime, default=lambda: datetime.now(timezone.utc), index=True
96
+ )
91
97
 
92
98
  request = relationship(
93
99
  "CapturedRequest",
@@ -104,13 +110,17 @@ class Trace(Base):
104
110
  trace_id = Column(String(32), primary_key=True, index=True)
105
111
  service_name = Column(String(100), index=True)
106
112
  operation_name = Column(String(200))
107
- start_time = Column(DateTime, default=datetime.utcnow, index=True)
113
+ start_time = Column(
114
+ DateTime, default=lambda: datetime.now(timezone.utc), index=True
115
+ )
108
116
  end_time = Column(DateTime)
109
117
  duration_ms = Column(Float)
110
118
  span_count = Column(Integer, default=0)
111
119
  status = Column(String(20), default="ok")
112
120
  tags = Column(JSON)
113
- created_at = Column(DateTime, default=datetime.utcnow, index=True)
121
+ created_at = Column(
122
+ DateTime, default=lambda: datetime.now(timezone.utc), index=True
123
+ )
114
124
 
115
125
  spans = relationship(
116
126
  "Span",
@@ -135,7 +145,9 @@ class Span(Base):
135
145
  status = Column(String(20), default="ok")
136
146
  tags = Column(JSON)
137
147
  logs = Column(JSON)
138
- created_at = Column(DateTime, default=datetime.utcnow, index=True)
148
+ created_at = Column(
149
+ DateTime, default=lambda: datetime.now(timezone.utc), index=True
150
+ )
139
151
 
140
152
  trace = relationship(
141
153
  "Trace",
@@ -154,4 +166,4 @@ class SpanRelation(Base):
154
166
  parent_span_id = Column(String(16), index=True)
155
167
  child_span_id = Column(String(16), index=True)
156
168
  depth = Column(Integer, default=0)
157
- created_at = Column(DateTime, default=datetime.utcnow)
169
+ created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))
fastapi_radar/radar.py CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  from contextlib import contextmanager
4
4
  import os
5
+ import sys
6
+ import multiprocessing
5
7
  from pathlib import Path
6
8
  from typing import List, Optional
7
9
 
@@ -16,6 +18,31 @@ from .middleware import RadarMiddleware
16
18
  from .models import Base
17
19
 
18
20
 
21
+ def is_reload_worker() -> bool:
22
+ """Check if we're running in a reload worker process (used by fastapi dev)."""
23
+ # Check for uvicorn reload worker
24
+ if os.environ.get("UVICORN_RELOAD"):
25
+ return True
26
+
27
+ # Check for Werkzeug reloader (used by some dev servers)
28
+ if os.environ.get("WERKZEUG_RUN_MAIN"):
29
+ return True
30
+
31
+ # Check if we're not the main process (common in reload scenarios)
32
+ # On Windows, the main process name is often "MainProcess"
33
+ if hasattr(multiprocessing.current_process(), "name"):
34
+ process_name = multiprocessing.current_process().name
35
+ if process_name != "MainProcess" and "SpawnProcess" in process_name:
36
+ return True
37
+
38
+ return False
39
+
40
+
41
+ def is_windows() -> bool:
42
+ """Check if we're running on Windows."""
43
+ return sys.platform.startswith("win")
44
+
45
+
19
46
  class Radar:
20
47
  query_capture: Optional[QueryCapture]
21
48
 
@@ -95,13 +122,34 @@ class Radar:
95
122
  else:
96
123
  radar_db_path = Path.cwd() / "radar.duckdb"
97
124
  radar_db_path.parent.mkdir(parents=True, exist_ok=True)
98
- self.storage_engine = create_engine(
99
- f"duckdb:///{radar_db_path}",
100
- connect_args={
101
- "read_only": False,
102
- "config": {"memory_limit": "500mb"},
103
- },
104
- )
125
+
126
+ # Handle reload worker scenario (fastapi dev) on all platforms
127
+ if is_reload_worker():
128
+ import warnings
129
+
130
+ warnings.warn(
131
+ "FastAPI Radar: Detected development mode with auto-reload. "
132
+ "Using in-memory database to avoid file locking issues. "
133
+ "Data will not persist between reloads.",
134
+ UserWarning,
135
+ )
136
+ # Use in-memory database for dev mode with reload
137
+ self.storage_engine = create_engine(
138
+ "duckdb:///:memory:",
139
+ connect_args={
140
+ "read_only": False,
141
+ "config": {"memory_limit": "500mb"},
142
+ },
143
+ )
144
+ else:
145
+ # Normal file-based database for production
146
+ self.storage_engine = create_engine(
147
+ f"duckdb:///{radar_db_path}",
148
+ connect_args={
149
+ "read_only": False,
150
+ "config": {"memory_limit": "500mb"},
151
+ },
152
+ )
105
153
 
106
154
  self.SessionLocal = sessionmaker(
107
155
  autocommit=False, autoflush=False, bind=self.storage_engine
@@ -333,19 +381,34 @@ class Radar:
333
381
  )
334
382
 
335
383
  def create_tables(self) -> None:
336
- Base.metadata.create_all(bind=self.storage_engine)
384
+ """Create database tables.
385
+
386
+ With dev mode (fastapi dev), this will safely handle
387
+ multiple process attempts to create tables.
388
+ """
389
+ try:
390
+ Base.metadata.create_all(bind=self.storage_engine)
391
+ except Exception as e:
392
+ # With reload workers, table creation might fail
393
+ # if another process already created them or has a lock
394
+ error_msg = str(e).lower()
395
+ if "already exists" in error_msg or "lock" in error_msg:
396
+ pass # Tables already exist or locked, that's fine in dev mode
397
+ else:
398
+ # Re-raise other exceptions
399
+ raise
337
400
 
338
401
  def drop_tables(self) -> None:
339
402
  Base.metadata.drop_all(bind=self.storage_engine)
340
403
 
341
404
  def cleanup(self, older_than_hours: Optional[int] = None) -> None:
342
- from datetime import datetime, timedelta
405
+ from datetime import datetime, timedelta, timezone
343
406
 
344
407
  from .models import CapturedRequest
345
408
 
346
409
  with self.get_session() as session:
347
410
  hours = older_than_hours or self.retention_hours
348
- cutoff = datetime.utcnow() - timedelta(hours=hours)
411
+ cutoff = datetime.now(timezone.utc) - timedelta(hours=hours)
349
412
 
350
413
  deleted = (
351
414
  session.query(CapturedRequest)
fastapi_radar/tracing.py CHANGED
@@ -1,7 +1,7 @@
1
1
  """Tracing core functionality module."""
2
2
 
3
3
  import uuid
4
- from datetime import datetime
4
+ from datetime import datetime, timezone
5
5
  from typing import Optional, Dict, Any, List
6
6
  from contextvars import ContextVar
7
7
  from sqlalchemy.orm import Session
@@ -23,7 +23,7 @@ class TraceContext:
23
23
  self.root_span_id: Optional[str] = None
24
24
  self.current_span_id: Optional[str] = None
25
25
  self.spans: Dict[str, Dict[str, Any]] = {}
26
- self.start_time = datetime.utcnow()
26
+ self.start_time = datetime.now(timezone.utc)
27
27
 
28
28
  def create_span(
29
29
  self,
@@ -42,7 +42,7 @@ class TraceContext:
42
42
  "operation_name": operation_name,
43
43
  "service_name": self.service_name,
44
44
  "span_kind": span_kind,
45
- "start_time": datetime.utcnow(),
45
+ "start_time": datetime.now(timezone.utc),
46
46
  "tags": tags or {},
47
47
  "logs": [],
48
48
  "status": "ok",
@@ -64,7 +64,7 @@ class TraceContext:
64
64
  return
65
65
 
66
66
  span_data = self.spans[span_id]
67
- span_data["end_time"] = datetime.utcnow()
67
+ span_data["end_time"] = datetime.now(timezone.utc)
68
68
  span_data["duration_ms"] = (
69
69
  span_data["end_time"] - span_data["start_time"]
70
70
  ).total_seconds() * 1000
@@ -79,7 +79,7 @@ class TraceContext:
79
79
  return
80
80
 
81
81
  log_entry = {
82
- "timestamp": datetime.utcnow().isoformat(),
82
+ "timestamp": datetime.now(timezone.utc).isoformat(),
83
83
  "level": level,
84
84
  "message": message,
85
85
  **fields,
@@ -108,7 +108,7 @@ class TraceContext:
108
108
  error_count += 1
109
109
 
110
110
  start_time = min(all_times) if all_times else self.start_time
111
- end_time = max(all_times) if all_times else datetime.utcnow()
111
+ end_time = max(all_times) if all_times else datetime.now(timezone.utc)
112
112
 
113
113
  return {
114
114
  "trace_id": self.trace_id,
@@ -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
@@ -0,0 +1,19 @@
1
+ fastapi_radar/__init__.py,sha256=RC_pRDx1vkO5_-d_XA4VMJEdVBelwu3CxQrXCAEl8Uc,137
2
+ fastapi_radar/api.py,sha256=uuUQp-E8MBZf9bEklrRLfgXv2c0AYiE9yCI8FBIR8Fk,16136
3
+ fastapi_radar/capture.py,sha256=weWpI2HBb-qp04SZMWXt-Lx3NYo44WBvH65-Yyvv_UI,6623
4
+ fastapi_radar/middleware.py,sha256=qSELsASuZwpg4rsljQCU64fhkAYcOjadfiZICkZO7BA,8439
5
+ fastapi_radar/models.py,sha256=aOxShW8DeYKhehGL9tvBTSWkqqJSgDMKJKNQ1w_OKdE,5092
6
+ fastapi_radar/radar.py,sha256=sBmniNedkd_IWWBOqdgYBLmys2YE9MbGmz8kacDcGcg,15104
7
+ fastapi_radar/tracing.py,sha256=GNayJJaxZR68ZiT3Io9GUyd9SnbFrfXGnRRpQigLDL0,8798
8
+ fastapi_radar/utils.py,sha256=82cNXjbtm7oTaNRwrSy2fPWDVGi8XSk56C_8o7N7oRQ,1516
9
+ fastapi_radar/dashboard/dist/index.html,sha256=qLA_fwKrBIEDOYEuBU0ZlW8EbDy5rec-IaP6RIp67BA,436
10
+ fastapi_radar/dashboard/dist/assets/index-31zorKsE.js,sha256=Hul-8iFpscVAAFsmcQU5Ub2Smo6OCPnGZlCDGnEIItU,837403
11
+ fastapi_radar/dashboard/dist/assets/index-XlGcZj49.css,sha256=z2yLr4jYQoOM7KQ2aw5zrPpMlkr9dRvwBSGaxjzZNnc,35552
12
+ fastapi_radar-0.2.0.dist-info/licenses/LICENSE,sha256=0ga4BB6q-nqx6xlDRhtrgKrYs0HgX02PQyIzNFRK09Q,1067
13
+ tests/__init__.py,sha256=kAWaI50iJRZ4JlAdyt7FICgm8MsloZz0ZlsmhgLXBas,31
14
+ tests/test_async_radar.py,sha256=zsj2r6-q9WjjamLtzrIDWI8fxcBJ0oVOcynfvPlOhRE,1610
15
+ tests/test_radar.py,sha256=3F-_zdemPcgQnjP4kzCa7GhMxNJNYU0SgSWprctyXiA,2374
16
+ fastapi_radar-0.2.0.dist-info/METADATA,sha256=_Qmv_JSleXcpDOIk-6PoisOsK-nNmQiU5IW7ih5VAh0,7637
17
+ fastapi_radar-0.2.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
18
+ fastapi_radar-0.2.0.dist-info/top_level.txt,sha256=M-bALM-KDkiLcATq2aAx-BnG59Nv-GdFBzuzkUhiCa0,20
19
+ fastapi_radar-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,56 @@
1
+ from fastapi import FastAPI
2
+ from fastapi_radar import Radar
3
+ from sqlalchemy import Column, Integer, MetaData, String, Table, select
4
+ from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
5
+
6
+ app = FastAPI()
7
+ engine = create_async_engine("sqlite+aiosqlite:///./app.db")
8
+ async_session: async_sessionmaker[AsyncSession] = async_sessionmaker(
9
+ engine, expire_on_commit=False
10
+ )
11
+
12
+ # 定义一个简单的测试表
13
+ metadata = MetaData()
14
+ users_table = Table(
15
+ "users",
16
+ metadata,
17
+ Column("id", Integer, primary_key=True, autoincrement=True),
18
+ Column("name", String(50), nullable=False),
19
+ )
20
+
21
+
22
+ radar = Radar(app, db_engine=engine)
23
+ radar.create_tables()
24
+
25
+
26
+ @app.on_event("startup")
27
+ async def on_startup() -> None:
28
+ """应用启动时创建测试表并写入示例数据。"""
29
+
30
+ async with engine.begin() as conn:
31
+ await conn.run_sync(metadata.create_all)
32
+
33
+ async with async_session() as session:
34
+ result = await session.execute(select(users_table.c.id).limit(1))
35
+ if result.first() is None:
36
+ await session.execute(
37
+ users_table.insert(),
38
+ [{"name": "Alice"}, {"name": "Bob"}, {"name": "Carol"}],
39
+ )
40
+ await session.commit()
41
+
42
+
43
+ # Your routes work unchanged
44
+ @app.get("/users")
45
+ async def get_users():
46
+ async with async_session() as session:
47
+ result = await session.execute(select(users_table))
48
+ rows = result.mappings().all()
49
+
50
+ return {"users": [dict(row) for row in rows]}
51
+
52
+
53
+ if __name__ == "__main__":
54
+ import uvicorn
55
+
56
+ uvicorn.run(app, host="0.0.0.0", port=8000)
@@ -1,18 +0,0 @@
1
- fastapi_radar/__init__.py,sha256=fedYsDj_d9FfIVMa1eDrNw5-COcCokqHcM8puxLKLRM,137
2
- fastapi_radar/api.py,sha256=JYXYocgWdmjHbrfthpBuqXmIWH8ZNct4qA3WCbinL2o,16099
3
- fastapi_radar/capture.py,sha256=butzGPmjR8f3gdrNqh4iOlXD2WBD8Zh6CcSTDlnYR5o,5186
4
- fastapi_radar/middleware.py,sha256=6FRlZa3XJwOA8UF5NkZ3pOXyK7F9P_ksvixf1CbFllk,8037
5
- fastapi_radar/models.py,sha256=jsYakaDd3HRDdAI7SfXLL5fI_fAfVBBhcYq8ktDPfmk,4865
6
- fastapi_radar/radar.py,sha256=GeD1bUqHX_20oZ0spQylk3-Vusopc9KIUmPoNRNRgQQ,12637
7
- fastapi_radar/tracing.py,sha256=m0AEX4sa-VUu5STezhpa51BZQ7R8Ax9u2zry2JSF3hQ,8743
8
- fastapi_radar/utils.py,sha256=82cNXjbtm7oTaNRwrSy2fPWDVGi8XSk56C_8o7N7oRQ,1516
9
- fastapi_radar/dashboard/dist/index.html,sha256=-Dx0Jw0WDyD29adrACqarkIq_XlUDo7BzqHmt3_mAkc,436
10
- fastapi_radar/dashboard/dist/assets/index-By5DXl8Z.js,sha256=D4jEA0Ep4L7PXXo8rZ_saAQIWhl43aAfIdjI6b4eotQ,833887
11
- fastapi_radar/dashboard/dist/assets/index-XlGcZj49.css,sha256=z2yLr4jYQoOM7KQ2aw5zrPpMlkr9dRvwBSGaxjzZNnc,35552
12
- fastapi_radar-0.1.8.dist-info/licenses/LICENSE,sha256=0ga4BB6q-nqx6xlDRhtrgKrYs0HgX02PQyIzNFRK09Q,1067
13
- tests/__init__.py,sha256=kAWaI50iJRZ4JlAdyt7FICgm8MsloZz0ZlsmhgLXBas,31
14
- tests/test_radar.py,sha256=3F-_zdemPcgQnjP4kzCa7GhMxNJNYU0SgSWprctyXiA,2374
15
- fastapi_radar-0.1.8.dist-info/METADATA,sha256=pwGBPzfpMOI9z1gnzM1mGc_Svqa9d_jKH3PTe03fKVU,6685
16
- fastapi_radar-0.1.8.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
17
- fastapi_radar-0.1.8.dist-info/top_level.txt,sha256=M-bALM-KDkiLcATq2aAx-BnG59Nv-GdFBzuzkUhiCa0,20
18
- fastapi_radar-0.1.8.dist-info/RECORD,,