fastapi-radar 0.1.5__py3-none-any.whl → 0.1.7__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.
- fastapi_radar/__init__.py +1 -1
- fastapi_radar/api.py +170 -7
- fastapi_radar/capture.py +66 -9
- fastapi_radar/dashboard/dist/assets/index-By5DXl8Z.js +318 -0
- fastapi_radar/dashboard/dist/assets/index-XlGcZj49.css +1 -0
- fastapi_radar/dashboard/dist/index.html +2 -2
- fastapi_radar/middleware.py +78 -1
- fastapi_radar/models.py +106 -15
- fastapi_radar/radar.py +59 -33
- fastapi_radar/tracing.py +258 -0
- fastapi_radar/utils.py +2 -1
- {fastapi_radar-0.1.5.dist-info → fastapi_radar-0.1.7.dist-info}/METADATA +15 -14
- fastapi_radar-0.1.7.dist-info/RECORD +18 -0
- tests/test_radar.py +22 -12
- fastapi_radar/dashboard/dist/assets/index-CxIRSjZZ.js +0 -308
- fastapi_radar/dashboard/dist/assets/index-DCxkDBhr.css +0 -1
- fastapi_radar-0.1.5.dist-info/RECORD +0 -17
- {fastapi_radar-0.1.5.dist-info → fastapi_radar-0.1.7.dist-info}/WHEEL +0 -0
- {fastapi_radar-0.1.5.dist-info → fastapi_radar-0.1.7.dist-info}/licenses/LICENSE +0 -0
- {fastapi_radar-0.1.5.dist-info → fastapi_radar-0.1.7.dist-info}/top_level.txt +0 -0
fastapi_radar/tracing.py
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
"""Tracing core functionality module."""
|
|
2
|
+
|
|
3
|
+
import uuid
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from typing import Optional, Dict, Any, List
|
|
6
|
+
from contextvars import ContextVar
|
|
7
|
+
from sqlalchemy.orm import Session
|
|
8
|
+
|
|
9
|
+
from .models import Trace, Span, SpanRelation
|
|
10
|
+
|
|
11
|
+
# Trace context for the current request
|
|
12
|
+
trace_context: ContextVar[Optional["TraceContext"]] = ContextVar(
|
|
13
|
+
"trace_context", default=None
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TraceContext:
|
|
18
|
+
"""Tracing context that manages trace and span data for a request."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, trace_id: str, service_name: str = "fastapi-app"):
|
|
21
|
+
self.trace_id = trace_id
|
|
22
|
+
self.service_name = service_name
|
|
23
|
+
self.root_span_id: Optional[str] = None
|
|
24
|
+
self.current_span_id: Optional[str] = None
|
|
25
|
+
self.spans: Dict[str, Dict[str, Any]] = {}
|
|
26
|
+
self.start_time = datetime.utcnow()
|
|
27
|
+
|
|
28
|
+
def create_span(
|
|
29
|
+
self,
|
|
30
|
+
operation_name: str,
|
|
31
|
+
parent_span_id: Optional[str] = None,
|
|
32
|
+
span_kind: str = "server",
|
|
33
|
+
tags: Optional[Dict[str, Any]] = None,
|
|
34
|
+
) -> str:
|
|
35
|
+
"""Create a new span."""
|
|
36
|
+
span_id = self._generate_span_id()
|
|
37
|
+
|
|
38
|
+
span_data = {
|
|
39
|
+
"span_id": span_id,
|
|
40
|
+
"trace_id": self.trace_id,
|
|
41
|
+
"parent_span_id": parent_span_id or self.current_span_id,
|
|
42
|
+
"operation_name": operation_name,
|
|
43
|
+
"service_name": self.service_name,
|
|
44
|
+
"span_kind": span_kind,
|
|
45
|
+
"start_time": datetime.utcnow(),
|
|
46
|
+
"tags": tags or {},
|
|
47
|
+
"logs": [],
|
|
48
|
+
"status": "ok",
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
self.spans[span_id] = span_data
|
|
52
|
+
|
|
53
|
+
# Set root span if not already set
|
|
54
|
+
if self.root_span_id is None:
|
|
55
|
+
self.root_span_id = span_id
|
|
56
|
+
|
|
57
|
+
return span_id
|
|
58
|
+
|
|
59
|
+
def finish_span(
|
|
60
|
+
self, span_id: str, status: str = "ok", tags: Optional[Dict[str, Any]] = None
|
|
61
|
+
):
|
|
62
|
+
"""Finish a span."""
|
|
63
|
+
if span_id not in self.spans:
|
|
64
|
+
return
|
|
65
|
+
|
|
66
|
+
span_data = self.spans[span_id]
|
|
67
|
+
span_data["end_time"] = datetime.utcnow()
|
|
68
|
+
span_data["duration_ms"] = (
|
|
69
|
+
span_data["end_time"] - span_data["start_time"]
|
|
70
|
+
).total_seconds() * 1000
|
|
71
|
+
span_data["status"] = status
|
|
72
|
+
|
|
73
|
+
if tags:
|
|
74
|
+
span_data["tags"].update(tags)
|
|
75
|
+
|
|
76
|
+
def add_span_log(self, span_id: str, message: str, level: str = "info", **fields):
|
|
77
|
+
"""Add a log entry to a span."""
|
|
78
|
+
if span_id not in self.spans:
|
|
79
|
+
return
|
|
80
|
+
|
|
81
|
+
log_entry = {
|
|
82
|
+
"timestamp": datetime.utcnow().isoformat(),
|
|
83
|
+
"level": level,
|
|
84
|
+
"message": message,
|
|
85
|
+
**fields,
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
self.spans[span_id]["logs"].append(log_entry)
|
|
89
|
+
|
|
90
|
+
def set_current_span(self, span_id: str):
|
|
91
|
+
"""Set the current active span."""
|
|
92
|
+
self.current_span_id = span_id
|
|
93
|
+
|
|
94
|
+
def get_trace_summary(self) -> Dict[str, Any]:
|
|
95
|
+
"""Return a trace summary for persistence and display."""
|
|
96
|
+
if not self.spans:
|
|
97
|
+
return {}
|
|
98
|
+
|
|
99
|
+
all_times = []
|
|
100
|
+
error_count = 0
|
|
101
|
+
|
|
102
|
+
for span in self.spans.values():
|
|
103
|
+
if span.get("start_time"):
|
|
104
|
+
all_times.append(span["start_time"])
|
|
105
|
+
if span.get("end_time"):
|
|
106
|
+
all_times.append(span["end_time"])
|
|
107
|
+
if span.get("status") == "error":
|
|
108
|
+
error_count += 1
|
|
109
|
+
|
|
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()
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
"trace_id": self.trace_id,
|
|
115
|
+
"service_name": self.service_name,
|
|
116
|
+
"operation_name": self.spans.get(self.root_span_id, {}).get(
|
|
117
|
+
"operation_name", "unknown"
|
|
118
|
+
),
|
|
119
|
+
"start_time": start_time,
|
|
120
|
+
"end_time": end_time,
|
|
121
|
+
"duration_ms": (end_time - start_time).total_seconds() * 1000,
|
|
122
|
+
"span_count": len(self.spans),
|
|
123
|
+
"status": "error" if error_count > 0 else "ok",
|
|
124
|
+
"tags": {},
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
@staticmethod
|
|
128
|
+
def _generate_span_id() -> str:
|
|
129
|
+
"""Generate a 16-character hexadecimal span ID."""
|
|
130
|
+
return uuid.uuid4().hex[:16]
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class TracingManager:
|
|
134
|
+
"""Tracing manager responsible for persistence and querying."""
|
|
135
|
+
|
|
136
|
+
def __init__(self, get_session):
|
|
137
|
+
self.get_session = get_session
|
|
138
|
+
|
|
139
|
+
def save_trace_context(self, trace_ctx: TraceContext):
|
|
140
|
+
"""Persist the trace context into the database."""
|
|
141
|
+
with self.get_session() as session:
|
|
142
|
+
# Save trace
|
|
143
|
+
trace_summary = trace_ctx.get_trace_summary()
|
|
144
|
+
trace = Trace(**trace_summary)
|
|
145
|
+
session.add(trace)
|
|
146
|
+
|
|
147
|
+
# Save spans
|
|
148
|
+
for span_data in trace_ctx.spans.values():
|
|
149
|
+
span = Span(**span_data)
|
|
150
|
+
session.add(span)
|
|
151
|
+
|
|
152
|
+
self._save_span_relations(session, trace_ctx)
|
|
153
|
+
|
|
154
|
+
session.commit()
|
|
155
|
+
|
|
156
|
+
def _save_span_relations(self, session: Session, trace_ctx: TraceContext):
|
|
157
|
+
"""Store parent-child span relations for optimized querying."""
|
|
158
|
+
|
|
159
|
+
def calculate_depth(
|
|
160
|
+
span_id: str, spans: Dict[str, Dict], depth: int = 0
|
|
161
|
+
) -> List[tuple]:
|
|
162
|
+
"""Recursively compute span depth."""
|
|
163
|
+
relations = []
|
|
164
|
+
span = spans.get(span_id)
|
|
165
|
+
if not span:
|
|
166
|
+
return relations
|
|
167
|
+
|
|
168
|
+
# Find all child spans
|
|
169
|
+
for sid, s in spans.items():
|
|
170
|
+
if s.get("parent_span_id") == span_id:
|
|
171
|
+
relations.append((span_id, sid, depth + 1))
|
|
172
|
+
relations.extend(calculate_depth(sid, spans, depth + 1))
|
|
173
|
+
|
|
174
|
+
return relations
|
|
175
|
+
|
|
176
|
+
# Start from the root span
|
|
177
|
+
if trace_ctx.root_span_id:
|
|
178
|
+
relations = calculate_depth(trace_ctx.root_span_id, trace_ctx.spans)
|
|
179
|
+
|
|
180
|
+
for parent_id, child_id, depth in relations:
|
|
181
|
+
relation = SpanRelation(
|
|
182
|
+
trace_id=trace_ctx.trace_id,
|
|
183
|
+
parent_span_id=parent_id,
|
|
184
|
+
child_span_id=child_id,
|
|
185
|
+
depth=depth,
|
|
186
|
+
)
|
|
187
|
+
session.add(relation)
|
|
188
|
+
|
|
189
|
+
def get_waterfall_data(self, trace_id: str) -> List[Dict[str, Any]]:
|
|
190
|
+
"""Return data for the waterfall view."""
|
|
191
|
+
with self.get_session() as session:
|
|
192
|
+
# Query optimized for DuckDB
|
|
193
|
+
from sqlalchemy import text
|
|
194
|
+
|
|
195
|
+
waterfall_query = text(
|
|
196
|
+
"""
|
|
197
|
+
WITH span_timeline AS (
|
|
198
|
+
SELECT
|
|
199
|
+
s.span_id,
|
|
200
|
+
s.parent_span_id,
|
|
201
|
+
s.operation_name,
|
|
202
|
+
s.service_name,
|
|
203
|
+
s.start_time,
|
|
204
|
+
s.end_time,
|
|
205
|
+
s.duration_ms,
|
|
206
|
+
s.status,
|
|
207
|
+
s.tags,
|
|
208
|
+
COALESCE(r.depth, 0) as depth,
|
|
209
|
+
-- Offset relative to trace start
|
|
210
|
+
EXTRACT(EPOCH FROM (
|
|
211
|
+
s.start_time - MIN(s.start_time)
|
|
212
|
+
OVER (PARTITION BY s.trace_id)
|
|
213
|
+
)) * 1000 as offset_ms
|
|
214
|
+
FROM radar_spans s
|
|
215
|
+
LEFT JOIN radar_span_relations r ON s.span_id = r.child_span_id
|
|
216
|
+
WHERE s.trace_id = :trace_id
|
|
217
|
+
)
|
|
218
|
+
SELECT * FROM span_timeline
|
|
219
|
+
ORDER BY offset_ms, depth
|
|
220
|
+
"""
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
result = session.execute(waterfall_query, {"trace_id": trace_id})
|
|
224
|
+
|
|
225
|
+
return [
|
|
226
|
+
{
|
|
227
|
+
"span_id": row.span_id,
|
|
228
|
+
"parent_span_id": row.parent_span_id,
|
|
229
|
+
"operation_name": row.operation_name,
|
|
230
|
+
"service_name": row.service_name,
|
|
231
|
+
"start_time": (
|
|
232
|
+
row.start_time.isoformat() if row.start_time else None
|
|
233
|
+
),
|
|
234
|
+
"end_time": row.end_time.isoformat() if row.end_time else None,
|
|
235
|
+
"duration_ms": row.duration_ms,
|
|
236
|
+
"status": row.status,
|
|
237
|
+
"tags": row.tags,
|
|
238
|
+
"depth": row.depth,
|
|
239
|
+
"offset_ms": float(row.offset_ms) if row.offset_ms else 0.0,
|
|
240
|
+
}
|
|
241
|
+
for row in result
|
|
242
|
+
]
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def get_current_trace_context() -> Optional[TraceContext]:
|
|
246
|
+
"""Get the current trace context."""
|
|
247
|
+
return trace_context.get()
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def set_trace_context(ctx: TraceContext):
|
|
251
|
+
"""Set the current trace context."""
|
|
252
|
+
trace_context.set(ctx)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def create_trace_context(service_name: str = "fastapi-app") -> TraceContext:
|
|
256
|
+
"""Create a new trace context."""
|
|
257
|
+
trace_id = uuid.uuid4().hex
|
|
258
|
+
return TraceContext(trace_id, service_name)
|
fastapi_radar/utils.py
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
"""Utility functions for FastAPI Radar."""
|
|
2
2
|
|
|
3
3
|
from typing import Dict, Optional
|
|
4
|
-
|
|
4
|
+
|
|
5
5
|
from starlette.datastructures import Headers
|
|
6
|
+
from starlette.requests import Request
|
|
6
7
|
|
|
7
8
|
|
|
8
9
|
def serialize_headers(headers: Headers) -> Dict[str, str]:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastapi-radar
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.7
|
|
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
|
|
@@ -13,7 +13,6 @@ Keywords: fastapi,debugging,monitoring,dashboard,development-tools
|
|
|
13
13
|
Classifier: Development Status :: 4 - Beta
|
|
14
14
|
Classifier: Intended Audience :: Developers
|
|
15
15
|
Classifier: Programming Language :: Python :: 3
|
|
16
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
17
16
|
Classifier: Programming Language :: Python :: 3.9
|
|
18
17
|
Classifier: Programming Language :: Python :: 3.10
|
|
19
18
|
Classifier: Programming Language :: Python :: 3.11
|
|
@@ -24,19 +23,21 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
|
24
23
|
Requires-Python: >=3.8
|
|
25
24
|
Description-Content-Type: text/markdown
|
|
26
25
|
License-File: LICENSE
|
|
27
|
-
Requires-Dist: fastapi
|
|
28
|
-
Requires-Dist: sqlalchemy>=
|
|
29
|
-
Requires-Dist: pydantic
|
|
30
|
-
Requires-Dist: starlette
|
|
26
|
+
Requires-Dist: fastapi
|
|
27
|
+
Requires-Dist: sqlalchemy>=2.0
|
|
28
|
+
Requires-Dist: pydantic
|
|
29
|
+
Requires-Dist: starlette
|
|
30
|
+
Requires-Dist: duckdb==1.1.3
|
|
31
|
+
Requires-Dist: duckdb-engine==0.17.0
|
|
31
32
|
Provides-Extra: dev
|
|
32
|
-
Requires-Dist: pytest
|
|
33
|
-
Requires-Dist: pytest-asyncio
|
|
34
|
-
Requires-Dist: uvicorn[standard]
|
|
35
|
-
Requires-Dist: black
|
|
36
|
-
Requires-Dist: isort
|
|
37
|
-
Requires-Dist: flake8
|
|
38
|
-
Requires-Dist: mypy
|
|
39
|
-
Requires-Dist: httpx
|
|
33
|
+
Requires-Dist: pytest; extra == "dev"
|
|
34
|
+
Requires-Dist: pytest-asyncio; extra == "dev"
|
|
35
|
+
Requires-Dist: uvicorn[standard]; extra == "dev"
|
|
36
|
+
Requires-Dist: black; extra == "dev"
|
|
37
|
+
Requires-Dist: isort; extra == "dev"
|
|
38
|
+
Requires-Dist: flake8; extra == "dev"
|
|
39
|
+
Requires-Dist: mypy; extra == "dev"
|
|
40
|
+
Requires-Dist: httpx; extra == "dev"
|
|
40
41
|
Dynamic: author
|
|
41
42
|
Dynamic: home-page
|
|
42
43
|
Dynamic: license-file
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
fastapi_radar/__init__.py,sha256=RsHUtOtkoaufEyEeY9NyVFNLVyPhUppP2RR6iz9mTyY,137
|
|
2
|
+
fastapi_radar/api.py,sha256=JYXYocgWdmjHbrfthpBuqXmIWH8ZNct4qA3WCbinL2o,16099
|
|
3
|
+
fastapi_radar/capture.py,sha256=butzGPmjR8f3gdrNqh4iOlXD2WBD8Zh6CcSTDlnYR5o,5186
|
|
4
|
+
fastapi_radar/middleware.py,sha256=v36cdUWRozkFs1MxG8piRzuazQZG5CHRVp_e7AWobbk,7684
|
|
5
|
+
fastapi_radar/models.py,sha256=jsYakaDd3HRDdAI7SfXLL5fI_fAfVBBhcYq8ktDPfmk,4865
|
|
6
|
+
fastapi_radar/radar.py,sha256=8bVoEY9zekgp4bU50fAftgFYc4CP_3t42J8dS4zduvQ,11218
|
|
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.7.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.7.dist-info/METADATA,sha256=fdCi6WDOrkSyPoxQmH0OgWdFdDNjUUJ_q0E7hb7GLLc,5884
|
|
16
|
+
fastapi_radar-0.1.7.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
17
|
+
fastapi_radar-0.1.7.dist-info/top_level.txt,sha256=M-bALM-KDkiLcATq2aAx-BnG59Nv-GdFBzuzkUhiCa0,20
|
|
18
|
+
fastapi_radar-0.1.7.dist-info/RECORD,,
|
tests/test_radar.py
CHANGED
|
@@ -10,9 +10,12 @@ from fastapi_radar import Radar
|
|
|
10
10
|
def test_radar_initialization():
|
|
11
11
|
"""Test that Radar can be initialized with a FastAPI app."""
|
|
12
12
|
app = FastAPI()
|
|
13
|
+
# Use in-memory SQLite for test database (not for storage)
|
|
13
14
|
engine = create_engine("sqlite:///:memory:")
|
|
15
|
+
# Use in-memory SQLite for storage as well to avoid DuckDB requirement in tests
|
|
16
|
+
storage_engine = create_engine("sqlite:///:memory:")
|
|
14
17
|
|
|
15
|
-
radar = Radar(app, db_engine=engine)
|
|
18
|
+
radar = Radar(app, db_engine=engine, storage_engine=storage_engine)
|
|
16
19
|
assert radar is not None
|
|
17
20
|
assert radar.app == app
|
|
18
21
|
assert radar.db_engine == engine
|
|
@@ -22,8 +25,9 @@ def test_radar_creates_tables():
|
|
|
22
25
|
"""Test that Radar can create necessary database tables."""
|
|
23
26
|
app = FastAPI()
|
|
24
27
|
engine = create_engine("sqlite:///:memory:")
|
|
28
|
+
storage_engine = create_engine("sqlite:///:memory:")
|
|
25
29
|
|
|
26
|
-
radar = Radar(app, db_engine=engine)
|
|
30
|
+
radar = Radar(app, db_engine=engine, storage_engine=storage_engine)
|
|
27
31
|
radar.create_tables()
|
|
28
32
|
|
|
29
33
|
# Tables should be created without errors
|
|
@@ -34,8 +38,9 @@ def test_dashboard_mounted():
|
|
|
34
38
|
"""Test that the dashboard is mounted at the correct path."""
|
|
35
39
|
app = FastAPI()
|
|
36
40
|
engine = create_engine("sqlite:///:memory:")
|
|
41
|
+
storage_engine = create_engine("sqlite:///:memory:")
|
|
37
42
|
|
|
38
|
-
radar = Radar(app, db_engine=engine)
|
|
43
|
+
radar = Radar(app, db_engine=engine, storage_engine=storage_engine)
|
|
39
44
|
radar.create_tables()
|
|
40
45
|
|
|
41
46
|
client = TestClient(app)
|
|
@@ -50,16 +55,21 @@ def test_middleware_captures_requests():
|
|
|
50
55
|
"""Test that middleware captures HTTP requests."""
|
|
51
56
|
app = FastAPI()
|
|
52
57
|
engine = create_engine("sqlite:///:memory:")
|
|
58
|
+
# Use a file-based SQLite for storage to persist tables
|
|
59
|
+
import tempfile
|
|
53
60
|
|
|
54
|
-
|
|
55
|
-
|
|
61
|
+
with tempfile.NamedTemporaryFile(suffix=".db") as temp_db:
|
|
62
|
+
storage_engine = create_engine(f"sqlite:///{temp_db.name}")
|
|
56
63
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
return {"message": "test"}
|
|
64
|
+
radar = Radar(app, db_engine=engine, storage_engine=storage_engine)
|
|
65
|
+
radar.create_tables()
|
|
60
66
|
|
|
61
|
-
|
|
62
|
-
|
|
67
|
+
@app.get("/test")
|
|
68
|
+
async def test_endpoint():
|
|
69
|
+
return {"message": "test"}
|
|
70
|
+
|
|
71
|
+
client = TestClient(app)
|
|
72
|
+
response = client.get("/test")
|
|
63
73
|
|
|
64
|
-
|
|
65
|
-
|
|
74
|
+
assert response.status_code == 200
|
|
75
|
+
assert response.json() == {"message": "test"}
|