fastapi-radar 0.1.8__py3-none-any.whl → 0.3.1__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.
@@ -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-8Om0PGu6.js"></script>
8
8
  <link rel="stylesheet" crossorigin href="/__radar/assets/index-XlGcZj49.css">
9
9
  </head>
10
10
  <body>
@@ -0,0 +1,149 @@
1
+ # ISC License
2
+ #
3
+ # Copyright (c) 2018-2025, Andrea Giammarchi, @WebReflection
4
+ #
5
+ # Permission to use, copy, modify, and/or distribute this software for any
6
+ # purpose with or without fee is hereby granted, provided that the above
7
+ # copyright notice and this permission notice appear in all copies.
8
+ #
9
+ # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
10
+ # REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
11
+ # AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
12
+ # INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
13
+ # LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
14
+ # OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
15
+ # PERFORMANCE OF THIS SOFTWARE.
16
+
17
+ import json as _json
18
+
19
+ class _Known:
20
+ def __init__(self):
21
+ self.key = []
22
+ self.value = []
23
+
24
+ class _String:
25
+ def __init__(self, value):
26
+ self.value = value
27
+
28
+
29
+ def _array_keys(value):
30
+ keys = []
31
+ i = 0
32
+ for _ in value:
33
+ keys.append(i)
34
+ i += 1
35
+ return keys
36
+
37
+ def _object_keys(value):
38
+ keys = []
39
+ for key in value:
40
+ keys.append(key)
41
+ return keys
42
+
43
+ def _is_array(value):
44
+ return isinstance(value, (list, tuple))
45
+
46
+ def _is_object(value):
47
+ return isinstance(value, dict)
48
+
49
+ def _is_string(value):
50
+ return isinstance(value, str)
51
+
52
+ def _index(known, input, value):
53
+ input.append(value)
54
+ index = str(len(input) - 1)
55
+ known.key.append(value)
56
+ known.value.append(index)
57
+ return index
58
+
59
+ def _loop(keys, input, known, output):
60
+ for key in keys:
61
+ value = output[key]
62
+ if isinstance(value, _String):
63
+ _ref(key, input[int(value.value)], input, known, output)
64
+
65
+ return output
66
+
67
+ def _ref(key, value, input, known, output):
68
+ if _is_array(value) and value not in known:
69
+ known.append(value)
70
+ value = _loop(_array_keys(value), input, known, value)
71
+ elif _is_object(value) and value not in known:
72
+ known.append(value)
73
+ value = _loop(_object_keys(value), input, known, value)
74
+
75
+ output[key] = value
76
+
77
+ def _relate(known, input, value):
78
+ if _is_string(value) or _is_array(value) or _is_object(value):
79
+ try:
80
+ return known.value[known.key.index(value)]
81
+ except:
82
+ return _index(known, input, value)
83
+
84
+ return value
85
+
86
+ def _transform(known, input, value):
87
+ if _is_array(value):
88
+ output = []
89
+ for val in value:
90
+ output.append(_relate(known, input, val))
91
+ return output
92
+
93
+ if _is_object(value):
94
+ obj = {}
95
+ for key in value:
96
+ obj[key] = _relate(known, input, value[key])
97
+ return obj
98
+
99
+ return value
100
+
101
+ def _wrap(value):
102
+ if _is_string(value):
103
+ return _String(value)
104
+
105
+ if _is_array(value):
106
+ i = 0
107
+ for val in value:
108
+ value[i] = _wrap(val)
109
+ i += 1
110
+
111
+ elif _is_object(value):
112
+ for key in value:
113
+ value[key] = _wrap(value[key])
114
+
115
+ return value
116
+
117
+ def parse(value, *args, **kwargs):
118
+ json = _json.loads(value, *args, **kwargs)
119
+ wrapped = []
120
+ for value in json:
121
+ wrapped.append(_wrap(value))
122
+
123
+ input = []
124
+ for value in wrapped:
125
+ if isinstance(value, _String):
126
+ input.append(value.value)
127
+ else:
128
+ input.append(value)
129
+
130
+ value = input[0]
131
+
132
+ if _is_array(value):
133
+ return _loop(_array_keys(value), input, [value], value)
134
+
135
+ if _is_object(value):
136
+ return _loop(_object_keys(value), input, [value], value)
137
+
138
+ return value
139
+
140
+
141
+ def stringify(value, *args, **kwargs):
142
+ known = _Known()
143
+ input = []
144
+ output = []
145
+ i = int(_index(known, input, value))
146
+ while i < len(input):
147
+ output.append(_transform(known, input, input[i]))
148
+ i += 1
149
+ return _json.dumps(output, *args, **kwargs)
@@ -7,12 +7,18 @@ 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
13
14
 
14
15
  from .models import CapturedRequest, CapturedException
15
- from .utils import serialize_headers, get_client_ip, truncate_body
16
+ from .utils import (
17
+ serialize_headers,
18
+ get_client_ip,
19
+ truncate_body,
20
+ redact_sensitive_data,
21
+ )
16
22
  from .tracing import (
17
23
  TraceContext,
18
24
  TracingManager,
@@ -98,7 +104,7 @@ class RadarMiddleware(BaseHTTPMiddleware):
98
104
  query_params=dict(request.query_params) if request.query_params else None,
99
105
  headers=serialize_headers(request.headers),
100
106
  body=(
101
- truncate_body(request_body, self.max_body_size)
107
+ redact_sensitive_data(truncate_body(request_body, self.max_body_size))
102
108
  if request_body
103
109
  else None
104
110
  ),
@@ -118,16 +124,27 @@ class RadarMiddleware(BaseHTTPMiddleware):
118
124
 
119
125
  async def capture_response():
120
126
  response_body = ""
127
+ capturing = True
121
128
  async for chunk in original_response.body_iterator:
122
129
  yield chunk
123
- if len(response_body) < self.max_body_size:
130
+ if capturing:
124
131
  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()
132
+ try:
133
+ with self.get_session() as session:
134
+ captured_request.response_body = (
135
+ redact_sensitive_data(
136
+ truncate_body(
137
+ response_body, self.max_body_size
138
+ )
139
+ )
140
+ )
141
+ session.add(captured_request)
142
+ session.commit()
143
+ except SQLAlchemyError:
144
+ # CapturedRequest record has been deleted.
145
+ capturing = False
146
+ else:
147
+ capturing = len(response_body) < self.max_body_size
131
148
 
132
149
  response = StreamingResponse(
133
150
  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,25 @@ 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))
170
+
171
+
172
+ class BackgroundTask(Base):
173
+ __tablename__ = "radar_background_tasks"
174
+
175
+ id = Column(
176
+ Integer, Sequence("radar_background_tasks_id_seq"), primary_key=True, index=True
177
+ )
178
+ task_id = Column(String(36), unique=True, index=True, nullable=False)
179
+ request_id = Column(String(36), index=True, nullable=True)
180
+ name = Column(String(200), nullable=False)
181
+ status = Column(
182
+ String(20), default="pending", index=True
183
+ ) # pending, running, completed, failed
184
+ start_time = Column(DateTime, index=True)
185
+ end_time = Column(DateTime)
186
+ duration_ms = Column(Float)
187
+ error = Column(Text)
188
+ created_at = Column(
189
+ DateTime, default=lambda: datetime.now(timezone.utc), index=True
190
+ )
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
 
@@ -9,6 +11,7 @@ from fastapi import FastAPI
9
11
  from sqlalchemy import create_engine
10
12
  from sqlalchemy.engine import Engine
11
13
  from sqlalchemy.orm import Session, sessionmaker
14
+ from sqlalchemy.pool import StaticPool
12
15
 
13
16
  from .api import create_api_router
14
17
  from .capture import QueryCapture
@@ -16,6 +19,27 @@ from .middleware import RadarMiddleware
16
19
  from .models import Base
17
20
 
18
21
 
22
+ def is_reload_worker() -> bool:
23
+ """Check if we're running in a reload worker process (used by fastapi dev)."""
24
+ if os.environ.get("UVICORN_RELOAD"):
25
+ return True
26
+
27
+ if os.environ.get("WERKZEUG_RUN_MAIN"):
28
+ return True
29
+
30
+ if hasattr(multiprocessing.current_process(), "name"):
31
+ process_name = multiprocessing.current_process().name
32
+ if process_name != "MainProcess" and "SpawnProcess" in process_name:
33
+ return True
34
+
35
+ return False
36
+
37
+
38
+ def is_windows() -> bool:
39
+ """Check if we're running on Windows."""
40
+ return sys.platform.startswith("win")
41
+
42
+
19
43
  class Radar:
20
44
  query_capture: Optional[QueryCapture]
21
45
 
@@ -50,26 +74,26 @@ class Radar:
50
74
  self.db_path = db_path
51
75
  self.query_capture = None
52
76
 
53
- # Exclude radar dashboard paths
54
77
  if dashboard_path not in self.exclude_paths:
55
78
  self.exclude_paths.append(dashboard_path)
56
79
  self.exclude_paths.append("/favicon.ico")
57
80
 
58
- # Setup storage engine
59
81
  if storage_engine:
60
82
  self.storage_engine = storage_engine
61
83
  else:
62
84
  storage_url = os.environ.get("RADAR_STORAGE_URL")
63
85
  if storage_url:
64
- self.storage_engine = create_engine(storage_url)
86
+ if "duckdb" in storage_url:
87
+ self.storage_engine = create_engine(
88
+ storage_url, poolclass=StaticPool
89
+ )
90
+ else:
91
+ self.storage_engine = create_engine(storage_url)
65
92
  else:
66
- # Use DuckDB for analytics-optimized storage
67
- # Import duckdb_engine to register the dialect
68
93
  import duckdb_engine # noqa: F401
69
94
 
70
95
  if self.db_path:
71
96
  try:
72
- # Avoid shadowing the attribute name by using a different variable name
73
97
  provided_path = Path(self.db_path).resolve()
74
98
  if provided_path.suffix.lower() == ".duckdb":
75
99
  radar_db_path = provided_path
@@ -79,7 +103,6 @@ class Radar:
79
103
  provided_path.mkdir(parents=True, exist_ok=True)
80
104
 
81
105
  except Exception as e:
82
- # Fallback to current directory if path creation fails
83
106
  import warnings
84
107
 
85
108
  warnings.warn(
@@ -95,13 +118,33 @@ class Radar:
95
118
  else:
96
119
  radar_db_path = Path.cwd() / "radar.duckdb"
97
120
  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
- )
121
+
122
+ if is_reload_worker():
123
+ import warnings
124
+
125
+ warnings.warn(
126
+ "FastAPI Radar: Detected development mode with auto-reload. "
127
+ "Using in-memory database to avoid file locking issues. "
128
+ "Data will not persist between reloads.",
129
+ UserWarning,
130
+ )
131
+ self.storage_engine = create_engine(
132
+ "duckdb:///:memory:",
133
+ connect_args={
134
+ "read_only": False,
135
+ "config": {"memory_limit": "500mb"},
136
+ },
137
+ poolclass=StaticPool,
138
+ )
139
+ else:
140
+ self.storage_engine = create_engine(
141
+ f"duckdb:///{radar_db_path}",
142
+ connect_args={
143
+ "read_only": False,
144
+ "config": {"memory_limit": "500mb"},
145
+ },
146
+ poolclass=StaticPool,
147
+ )
105
148
 
106
149
  self.SessionLocal = sessionmaker(
107
150
  autocommit=False, autoflush=False, bind=self.storage_engine
@@ -162,7 +205,6 @@ class Radar:
162
205
  dashboard_dir = Path(__file__).parent / "dashboard" / "dist"
163
206
 
164
207
  if not dashboard_dir.exists():
165
- # Create placeholder dashboard for development
166
208
  dashboard_dir.mkdir(parents=True, exist_ok=True)
167
209
  self._create_placeholder_dashboard(dashboard_dir)
168
210
  print("\n" + "=" * 60)
@@ -174,14 +216,11 @@ class Radar:
174
216
  print(" npm run build")
175
217
  print("=" * 60 + "\n")
176
218
 
177
- # Add a catch-all route for the dashboard SPA
178
- # This ensures all sub-routes under /__radar serve the index.html
179
219
  @self.app.get(
180
220
  f"{self.dashboard_path}/{{full_path:path}}",
181
221
  include_in_schema=include_in_schema,
182
222
  )
183
223
  async def serve_dashboard(request: Request, full_path: str = ""):
184
- # Check if it's a request for a static asset
185
224
  if full_path and any(
186
225
  full_path.endswith(ext)
187
226
  for ext in [
@@ -200,7 +239,6 @@ class Radar:
200
239
  if file_path.exists():
201
240
  return FileResponse(file_path)
202
241
 
203
- # For all other routes, serve index.html (SPA behavior)
204
242
  index_path = dashboard_dir / "index.html"
205
243
  if index_path.exists():
206
244
  return FileResponse(index_path)
@@ -296,7 +334,6 @@ class Radar:
296
334
  </div>
297
335
  </div>
298
336
  <script>
299
- // Fetch stats from API
300
337
  async function loadStats() {{
301
338
  try {{
302
339
  const response = await fetch('/__radar/api/stats?hours=1');
@@ -320,9 +357,7 @@ class Radar:
320
357
  }}
321
358
  }}
322
359
 
323
- // Load stats on page load
324
360
  loadStats();
325
- // Refresh stats every 5 seconds
326
361
  setInterval(loadStats, 5000);
327
362
  </script>
328
363
  </body>
@@ -333,19 +368,29 @@ class Radar:
333
368
  )
334
369
 
335
370
  def create_tables(self) -> None:
336
- Base.metadata.create_all(bind=self.storage_engine)
371
+ """Create database tables.
372
+
373
+ With dev mode (fastapi dev), this safely handles
374
+ multiple process attempts to create tables.
375
+ """
376
+ try:
377
+ Base.metadata.create_all(bind=self.storage_engine)
378
+ except Exception as e:
379
+ error_msg = str(e).lower()
380
+ if "already exists" not in error_msg and "lock" not in error_msg:
381
+ raise
337
382
 
338
383
  def drop_tables(self) -> None:
339
384
  Base.metadata.drop_all(bind=self.storage_engine)
340
385
 
341
386
  def cleanup(self, older_than_hours: Optional[int] = None) -> None:
342
- from datetime import datetime, timedelta
387
+ from datetime import datetime, timedelta, timezone
343
388
 
344
389
  from .models import CapturedRequest
345
390
 
346
391
  with self.get_session() as session:
347
392
  hours = older_than_hours or self.retention_hours
348
- cutoff = datetime.utcnow() - timedelta(hours=hours)
393
+ cutoff = datetime.now(timezone.utc) - timedelta(hours=hours)
349
394
 
350
395
  deleted = (
351
396
  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,
fastapi_radar/utils.py CHANGED
@@ -1,5 +1,6 @@
1
1
  """Utility functions for FastAPI Radar."""
2
2
 
3
+ import re
3
4
  from typing import Dict, Optional
4
5
 
5
6
  from starlette.datastructures import Headers
@@ -58,3 +59,26 @@ def format_sql(sql: str, max_length: int = 5000) -> str:
58
59
  sql = sql[:max_length] + "... [truncated]"
59
60
 
60
61
  return sql
62
+
63
+
64
+ def redact_sensitive_data(text: Optional[str]) -> Optional[str]:
65
+ """Redact sensitive data from text (body content)."""
66
+ if not text:
67
+ return text
68
+
69
+ # Patterns for sensitive data
70
+ patterns = [
71
+ (r'"(password|passwd|pwd)"\s*:\s*"[^"]*"', r'"\1": "***REDACTED***"'),
72
+ (
73
+ r'"(token|api_key|apikey|secret|auth)"\s*:\s*"[^"]*"',
74
+ r'"\1": "***REDACTED***"',
75
+ ),
76
+ (r'"(credit_card|card_number|cvv)"\s*:\s*"[^"]*"', r'"\1": "***REDACTED***"'),
77
+ (r"Bearer\s+[A-Za-z0-9\-_\.]+", "Bearer ***REDACTED***"),
78
+ ]
79
+
80
+ result = text
81
+ for pattern, replacement in patterns:
82
+ result = re.sub(pattern, replacement, result, flags=re.IGNORECASE)
83
+
84
+ return result
@@ -1,11 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastapi-radar
3
- Version: 0.1.8
3
+ Version: 0.3.1
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
7
7
  Author-email: Arif Dogan <me@arif.sh>
8
- License-Expression: MIT
8
+ License: MIT
9
9
  Project-URL: Homepage, https://github.com/doganarif/fastapi-radar
10
10
  Project-URL: Bug Reports, https://github.com/doganarif/fastapi-radar/issues
11
11
  Project-URL: Source, https://github.com/doganarif/fastapi-radar
@@ -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"
@@ -38,6 +39,9 @@ Requires-Dist: isort; extra == "dev"
38
39
  Requires-Dist: flake8; extra == "dev"
39
40
  Requires-Dist: mypy; extra == "dev"
40
41
  Requires-Dist: httpx; extra == "dev"
42
+ Provides-Extra: release
43
+ Requires-Dist: build; extra == "release"
44
+ Requires-Dist: twine; extra == "release"
41
45
  Dynamic: author
42
46
  Dynamic: home-page
43
47
  Dynamic: license-file
@@ -163,6 +167,23 @@ radar = Radar(app, db_path="./data")
163
167
 
164
168
  If the specified path cannot be created, FastAPI Radar will fallback to using the current directory with a warning.
165
169
 
170
+ ### Development Mode with Auto-Reload
171
+
172
+ 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:
173
+
174
+ - **No file locking errors** - The dashboard will work seamlessly in development
175
+ - **Data doesn't persist between reloads** - Each reload starts with a fresh database
176
+ - **Production behavior unchanged** - When using `fastapi run` or deploying, the normal file-based database is used
177
+
178
+ ```python
179
+ # With fastapi dev (auto-reload enabled):
180
+ # Automatically uses in-memory database - no configuration needed!
181
+ radar = Radar(app)
182
+ radar.create_tables() # Safe to call - handles multiple processes gracefully
183
+ ```
184
+
185
+ 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.
186
+
166
187
  ## What Gets Captured?
167
188
 
168
189
  - ✅ HTTP requests and responses
@@ -0,0 +1,19 @@
1
+ fastapi_radar/__init__.py,sha256=dGuFHC_3pjoNIB6KsOXgrUA10lwzi3NI2yrcLmz_LeA,208
2
+ fastapi_radar/api.py,sha256=iWm8SUbnXR34JSRE_5lQjVW-5loTcV6MBybC0-sBRqo,22862
3
+ fastapi_radar/background.py,sha256=mTotE-K4H7rYZ1RIDn525XtoN-ZqDAlnG7sOBzvx4TI,4085
4
+ fastapi_radar/capture.py,sha256=weWpI2HBb-qp04SZMWXt-Lx3NYo44WBvH65-Yyvv_UI,6623
5
+ fastapi_radar/middleware.py,sha256=CSEX6bwj5uE9XMSHslNmwl1Ll6Yl8hLJFLGk8csjq2E,8711
6
+ fastapi_radar/models.py,sha256=fyiliKcvDv98q7X-CjWaOlEeX0xnJox3zEyZuwR_aBU,5819
7
+ fastapi_radar/radar.py,sha256=I_nF3YgKKloO9O4ycB27aLo2Y5AdG1xt8t22qVpysJk,13940
8
+ fastapi_radar/tracing.py,sha256=GNayJJaxZR68ZiT3Io9GUyd9SnbFrfXGnRRpQigLDL0,8798
9
+ fastapi_radar/utils.py,sha256=Btmie6I66eyib43jwVqAFZwnIbWDrysly-I8oCkcM_Q,2260
10
+ fastapi_radar/dashboard/dist/index.html,sha256=ACbU2M9oSEONogQcqLPeymIaxMXf2257_drDAHpTM7s,436
11
+ fastapi_radar/dashboard/dist/assets/index-8Om0PGu6.js,sha256=PJJmrtFlm4l3fgH_PIx5USypRSwWzqEpIzALol7b-30,925269
12
+ fastapi_radar/dashboard/dist/assets/index-D51YrvFG.css,sha256=voYJADvNg5vaG4sFp8C-RKIt3011JuvaeVSev3jkKMQ,36262
13
+ fastapi_radar/dashboard/dist/assets/index-p3czTzXB.js,sha256=P37rXeybVEj9sADBkQO7YBptwp747mZzdT4Xamb-9TM,948079
14
+ fastapi_radar/dashboard/node_modules/flatted/python/flatted.py,sha256=UYburBDqkySaTfSpntPCUJRxiBGcplusJM7ECX8FEgA,3860
15
+ fastapi_radar-0.3.1.dist-info/licenses/LICENSE,sha256=0ga4BB6q-nqx6xlDRhtrgKrYs0HgX02PQyIzNFRK09Q,1067
16
+ fastapi_radar-0.3.1.dist-info/METADATA,sha256=ntw4kL9zrUN55lkMjyl2gqWgKcBboM7yTq1YoXPewH4,7732
17
+ fastapi_radar-0.3.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
18
+ fastapi_radar-0.3.1.dist-info/top_level.txt,sha256=FESRhvz7hUtE4X5D2cF-tyWcMUGVWeJK0ep4SRGvmEU,14
19
+ fastapi_radar-0.3.1.dist-info/RECORD,,