fastapi-radar 0.1.7__py3-none-any.whl → 0.3.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-8Om0PGu6.js"></script>
8
8
  <link rel="stylesheet" crossorigin href="/__radar/assets/index-XlGcZj49.css">
9
9
  </head>
10
10
  <body>
@@ -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
  ),
@@ -109,24 +115,39 @@ class RadarMiddleware(BaseHTTPMiddleware):
109
115
  exception_occurred = False
110
116
 
111
117
  try:
112
- response = await call_next(request)
118
+ response = original_response = await call_next(request)
113
119
 
114
120
  captured_request.status_code = response.status_code
115
121
  captured_request.response_headers = serialize_headers(response.headers)
116
122
 
117
- if self.capture_response_body and not isinstance(
118
- response, StreamingResponse
119
- ):
120
- response_body = b""
121
- async for chunk in response.body_iterator:
122
- response_body += chunk
123
-
124
- captured_request.response_body = truncate_body(
125
- response_body.decode("utf-8", errors="ignore"), self.max_body_size
126
- )
127
-
128
- response = Response(
129
- content=response_body,
123
+ if self.capture_response_body:
124
+
125
+ async def capture_response():
126
+ response_body = ""
127
+ capturing = True
128
+ async for chunk in original_response.body_iterator:
129
+ yield chunk
130
+ if capturing:
131
+ response_body += chunk.decode("utf-8", errors="ignore")
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
148
+
149
+ response = StreamingResponse(
150
+ content=capture_response(),
130
151
  status_code=response.status_code,
131
152
  headers=dict(response.headers),
132
153
  media_type=response.media_type,
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
 
@@ -34,6 +58,7 @@ class Radar:
34
58
  enable_tracing: bool = True,
35
59
  service_name: str = "fastapi-app",
36
60
  include_in_schema: bool = True,
61
+ db_path: Optional[str] = None,
37
62
  ):
38
63
  self.app = app
39
64
  self.db_engine = db_engine
@@ -46,33 +71,80 @@ class Radar:
46
71
  self.theme = theme
47
72
  self.enable_tracing = enable_tracing
48
73
  self.service_name = service_name
74
+ self.db_path = db_path
49
75
  self.query_capture = None
50
76
 
51
- # Exclude radar dashboard paths
52
77
  if dashboard_path not in self.exclude_paths:
53
78
  self.exclude_paths.append(dashboard_path)
54
79
  self.exclude_paths.append("/favicon.ico")
55
80
 
56
- # Setup storage engine
57
81
  if storage_engine:
58
82
  self.storage_engine = storage_engine
59
83
  else:
60
84
  storage_url = os.environ.get("RADAR_STORAGE_URL")
61
85
  if storage_url:
62
- 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)
63
92
  else:
64
- # Use DuckDB for analytics-optimized storage
65
- # Import duckdb_engine to register the dialect
66
93
  import duckdb_engine # noqa: F401
67
94
 
68
- radar_db_path = Path.cwd() / "radar.duckdb"
69
- self.storage_engine = create_engine(
70
- f"duckdb:///{radar_db_path}",
71
- connect_args={
72
- "read_only": False,
73
- "config": {"memory_limit": "500mb"},
74
- },
75
- )
95
+ if self.db_path:
96
+ try:
97
+ provided_path = Path(self.db_path).resolve()
98
+ if provided_path.suffix.lower() == ".duckdb":
99
+ radar_db_path = provided_path
100
+ radar_db_path.parent.mkdir(parents=True, exist_ok=True)
101
+ else:
102
+ radar_db_path = provided_path / "radar.duckdb"
103
+ provided_path.mkdir(parents=True, exist_ok=True)
104
+
105
+ except Exception as e:
106
+ import warnings
107
+
108
+ warnings.warn(
109
+ (
110
+ f"Failed to create database path '{self.db_path}': {e}. "
111
+ f"Using current directory."
112
+ ),
113
+ UserWarning,
114
+ )
115
+
116
+ radar_db_path = Path.cwd() / "radar.duckdb"
117
+ radar_db_path.parent.mkdir(parents=True, exist_ok=True)
118
+ else:
119
+ radar_db_path = Path.cwd() / "radar.duckdb"
120
+ radar_db_path.parent.mkdir(parents=True, exist_ok=True)
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
+ )
76
148
 
77
149
  self.SessionLocal = sessionmaker(
78
150
  autocommit=False, autoflush=False, bind=self.storage_engine
@@ -133,7 +205,6 @@ class Radar:
133
205
  dashboard_dir = Path(__file__).parent / "dashboard" / "dist"
134
206
 
135
207
  if not dashboard_dir.exists():
136
- # Create placeholder dashboard for development
137
208
  dashboard_dir.mkdir(parents=True, exist_ok=True)
138
209
  self._create_placeholder_dashboard(dashboard_dir)
139
210
  print("\n" + "=" * 60)
@@ -145,14 +216,11 @@ class Radar:
145
216
  print(" npm run build")
146
217
  print("=" * 60 + "\n")
147
218
 
148
- # Add a catch-all route for the dashboard SPA
149
- # This ensures all sub-routes under /__radar serve the index.html
150
219
  @self.app.get(
151
220
  f"{self.dashboard_path}/{{full_path:path}}",
152
221
  include_in_schema=include_in_schema,
153
222
  )
154
223
  async def serve_dashboard(request: Request, full_path: str = ""):
155
- # Check if it's a request for a static asset
156
224
  if full_path and any(
157
225
  full_path.endswith(ext)
158
226
  for ext in [
@@ -171,7 +239,6 @@ class Radar:
171
239
  if file_path.exists():
172
240
  return FileResponse(file_path)
173
241
 
174
- # For all other routes, serve index.html (SPA behavior)
175
242
  index_path = dashboard_dir / "index.html"
176
243
  if index_path.exists():
177
244
  return FileResponse(index_path)
@@ -267,7 +334,6 @@ class Radar:
267
334
  </div>
268
335
  </div>
269
336
  <script>
270
- // Fetch stats from API
271
337
  async function loadStats() {{
272
338
  try {{
273
339
  const response = await fetch('/__radar/api/stats?hours=1');
@@ -291,9 +357,7 @@ class Radar:
291
357
  }}
292
358
  }}
293
359
 
294
- // Load stats on page load
295
360
  loadStats();
296
- // Refresh stats every 5 seconds
297
361
  setInterval(loadStats, 5000);
298
362
  </script>
299
363
  </body>
@@ -304,19 +368,29 @@ class Radar:
304
368
  )
305
369
 
306
370
  def create_tables(self) -> None:
307
- 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
308
382
 
309
383
  def drop_tables(self) -> None:
310
384
  Base.metadata.drop_all(bind=self.storage_engine)
311
385
 
312
386
  def cleanup(self, older_than_hours: Optional[int] = None) -> None:
313
- from datetime import datetime, timedelta
387
+ from datetime import datetime, timedelta, timezone
314
388
 
315
389
  from .models import CapturedRequest
316
390
 
317
391
  with self.get_session() as session:
318
392
  hours = older_than_hours or self.retention_hours
319
- cutoff = datetime.utcnow() - timedelta(hours=hours)
393
+ cutoff = datetime.now(timezone.utc) - timedelta(hours=hours)
320
394
 
321
395
  deleted = (
322
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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastapi-radar
3
- Version: 0.1.7
3
+ Version: 0.3.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"
@@ -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
@@ -139,9 +143,47 @@ radar = Radar(
139
143
  capture_sql_bindings=True, # Capture SQL query parameters
140
144
  exclude_paths=["/health"], # Paths to exclude from monitoring
141
145
  theme="auto", # Dashboard theme: "light", "dark", or "auto"
146
+ db_path="/path/to/db", # Custom path for radar.duckdb file (default: current directory)
142
147
  )
143
148
  ```
144
149
 
150
+ ### Custom Database Location
151
+
152
+ By default, FastAPI Radar stores its monitoring data in a `radar.duckdb` file in your current working directory. You can customize this location using the `db_path` parameter:
153
+
154
+ ```python
155
+ # Store in a specific directory
156
+ radar = Radar(app, db_path="/var/data/monitoring")
157
+ # Creates: /var/data/monitoring/radar.duckdb
158
+
159
+ # Store with a specific filename
160
+ radar = Radar(app, db_path="/var/data/my_app_monitoring.duckdb")
161
+ # Creates: /var/data/my_app_monitoring.duckdb
162
+
163
+ # Use a relative path
164
+ radar = Radar(app, db_path="./data")
165
+ # Creates: ./data/radar.duckdb
166
+ ```
167
+
168
+ If the specified path cannot be created, FastAPI Radar will fallback to using the current directory with a warning.
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
+
145
187
  ## What Gets Captured?
146
188
 
147
189
  - ✅ HTTP requests and responses
@@ -0,0 +1,21 @@
1
+ fastapi_radar/__init__.py,sha256=SCCnfsp3PuaoHJhm3Kxvi2578-ztdTQaIjljZxfINAw,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-0.3.0.dist-info/licenses/LICENSE,sha256=0ga4BB6q-nqx6xlDRhtrgKrYs0HgX02PQyIzNFRK09Q,1067
15
+ tests/__init__.py,sha256=kAWaI50iJRZ4JlAdyt7FICgm8MsloZz0ZlsmhgLXBas,31
16
+ tests/test_async_radar.py,sha256=zsj2r6-q9WjjamLtzrIDWI8fxcBJ0oVOcynfvPlOhRE,1610
17
+ tests/test_radar.py,sha256=3F-_zdemPcgQnjP4kzCa7GhMxNJNYU0SgSWprctyXiA,2374
18
+ fastapi_radar-0.3.0.dist-info/METADATA,sha256=__ZM6RCDwt0JwUjXvFrn6iFoSs9letL3DZmZZ_9RcJc,7743
19
+ fastapi_radar-0.3.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
20
+ fastapi_radar-0.3.0.dist-info/top_level.txt,sha256=M-bALM-KDkiLcATq2aAx-BnG59Nv-GdFBzuzkUhiCa0,20
21
+ fastapi_radar-0.3.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)