arize-phoenix 4.10.1__py3-none-any.whl → 4.10.2rc1__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 arize-phoenix might be problematic. Click here for more details.

Files changed (28) hide show
  1. {arize_phoenix-4.10.1.dist-info → arize_phoenix-4.10.2rc1.dist-info}/METADATA +4 -3
  2. {arize_phoenix-4.10.1.dist-info → arize_phoenix-4.10.2rc1.dist-info}/RECORD +27 -25
  3. phoenix/server/api/context.py +5 -7
  4. phoenix/server/api/dataloaders/__init__.py +2 -0
  5. phoenix/server/api/dataloaders/span_annotations.py +35 -0
  6. phoenix/server/api/openapi/main.py +18 -2
  7. phoenix/server/api/openapi/schema.py +12 -12
  8. phoenix/server/api/routers/v1/__init__.py +36 -83
  9. phoenix/server/api/routers/v1/dataset_examples.py +102 -123
  10. phoenix/server/api/routers/v1/datasets.py +389 -507
  11. phoenix/server/api/routers/v1/evaluations.py +74 -64
  12. phoenix/server/api/routers/v1/experiment_evaluations.py +67 -91
  13. phoenix/server/api/routers/v1/experiment_runs.py +97 -155
  14. phoenix/server/api/routers/v1/experiments.py +131 -181
  15. phoenix/server/api/routers/v1/spans.py +141 -173
  16. phoenix/server/api/routers/v1/traces.py +113 -128
  17. phoenix/server/api/routers/v1/utils.py +94 -0
  18. phoenix/server/api/types/Annotation.py +21 -0
  19. phoenix/server/api/types/Evaluation.py +4 -22
  20. phoenix/server/api/types/Span.py +15 -4
  21. phoenix/server/api/types/SpanAnnotation.py +4 -6
  22. phoenix/server/app.py +149 -174
  23. phoenix/server/thread_server.py +2 -2
  24. phoenix/version.py +1 -1
  25. phoenix/server/openapi/docs.py +0 -221
  26. {arize_phoenix-4.10.1.dist-info → arize_phoenix-4.10.2rc1.dist-info}/WHEEL +0 -0
  27. {arize_phoenix-4.10.1.dist-info → arize_phoenix-4.10.2rc1.dist-info}/licenses/IP_NOTICE +0 -0
  28. {arize_phoenix-4.10.1.dist-info → arize_phoenix-4.10.2rc1.dist-info}/licenses/LICENSE +0 -0
phoenix/server/app.py CHANGED
@@ -19,25 +19,24 @@ from typing import (
19
19
  )
20
20
 
21
21
  import strawberry
22
+ from fastapi import APIRouter, FastAPI
23
+ from fastapi.middleware.gzip import GZipMiddleware
24
+ from fastapi.responses import FileResponse
25
+ from fastapi.utils import is_body_allowed_for_status_code
22
26
  from sqlalchemy.ext.asyncio import (
23
27
  AsyncEngine,
24
28
  AsyncSession,
25
29
  async_sessionmaker,
26
30
  )
27
- from starlette.applications import Starlette
28
- from starlette.datastructures import QueryParams
29
- from starlette.endpoints import HTTPEndpoint
30
31
  from starlette.exceptions import HTTPException
31
32
  from starlette.middleware import Middleware
32
33
  from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
33
34
  from starlette.requests import Request
34
- from starlette.responses import FileResponse, PlainTextResponse, Response
35
- from starlette.routing import Mount, Route
35
+ from starlette.responses import PlainTextResponse, Response
36
36
  from starlette.staticfiles import StaticFiles
37
37
  from starlette.templating import Jinja2Templates
38
38
  from starlette.types import Scope, StatefulLifespan
39
- from starlette.websockets import WebSocket
40
- from strawberry.asgi import GraphQL
39
+ from strawberry.fastapi import GraphQLRouter
41
40
  from strawberry.schema import BaseSchema
42
41
  from typing_extensions import TypeAlias
43
42
 
@@ -72,6 +71,7 @@ from phoenix.server.api.dataloaders import (
72
71
  MinStartOrMaxEndTimeDataLoader,
73
72
  ProjectByNameDataLoader,
74
73
  RecordCountDataLoader,
74
+ SpanAnnotationsDataLoader,
75
75
  SpanDescendantsDataLoader,
76
76
  SpanEvaluationsDataLoader,
77
77
  SpanProjectsDataLoader,
@@ -79,11 +79,10 @@ from phoenix.server.api.dataloaders import (
79
79
  TraceEvaluationsDataLoader,
80
80
  TraceRowIdsDataLoader,
81
81
  )
82
- from phoenix.server.api.openapi.schema import OPENAPI_SCHEMA_GENERATOR
83
- from phoenix.server.api.routers.v1 import V1_ROUTES
82
+ from phoenix.server.api.routers.v1 import REST_API_VERSION
83
+ from phoenix.server.api.routers.v1 import router as v1_router
84
84
  from phoenix.server.api.schema import schema
85
85
  from phoenix.server.grpc_server import GrpcServer
86
- from phoenix.server.openapi.docs import get_swagger_ui_html
87
86
  from phoenix.server.telemetry import initialize_opentelemetry_tracer_provider
88
87
  from phoenix.trace.schemas import Span
89
88
 
@@ -92,6 +91,8 @@ if TYPE_CHECKING:
92
91
 
93
92
  logger = logging.getLogger(__name__)
94
93
 
94
+ router = APIRouter(include_in_schema=False)
95
+
95
96
  templates = Jinja2Templates(directory=SERVER_DIR / "templates")
96
97
 
97
98
 
@@ -156,115 +157,20 @@ class HeadersMiddleware(BaseHTTPMiddleware):
156
157
  ProjectRowId: TypeAlias = int
157
158
 
158
159
 
159
- class GraphQLWithContext(GraphQL): # type: ignore
160
- def __init__(
161
- self,
162
- schema: BaseSchema,
163
- db: Callable[[], AsyncContextManager[AsyncSession]],
164
- model: Model,
165
- export_path: Path,
166
- graphiql: bool = False,
167
- corpus: Optional[Model] = None,
168
- streaming_last_updated_at: Callable[[ProjectRowId], Optional[datetime]] = lambda _: None,
169
- cache_for_dataloaders: Optional[CacheForDataLoaders] = None,
170
- read_only: bool = False,
171
- ) -> None:
172
- self.db = db
173
- self.model = model
174
- self.corpus = corpus
175
- self.export_path = export_path
176
- self.streaming_last_updated_at = streaming_last_updated_at
177
- self.cache_for_dataloaders = cache_for_dataloaders
178
- self.read_only = read_only
179
- super().__init__(schema, graphiql=graphiql)
180
-
181
- async def get_context(
182
- self,
183
- request: Union[Request, WebSocket],
184
- response: Optional[Response] = None,
185
- ) -> Context:
186
- return Context(
187
- request=request,
188
- response=response,
189
- db=self.db,
190
- model=self.model,
191
- corpus=self.corpus,
192
- export_path=self.export_path,
193
- streaming_last_updated_at=self.streaming_last_updated_at,
194
- data_loaders=DataLoaders(
195
- average_experiment_run_latency=AverageExperimentRunLatencyDataLoader(self.db),
196
- dataset_example_revisions=DatasetExampleRevisionsDataLoader(self.db),
197
- dataset_example_spans=DatasetExampleSpansDataLoader(self.db),
198
- document_evaluation_summaries=DocumentEvaluationSummaryDataLoader(
199
- self.db,
200
- cache_map=self.cache_for_dataloaders.document_evaluation_summary
201
- if self.cache_for_dataloaders
202
- else None,
203
- ),
204
- document_evaluations=DocumentEvaluationsDataLoader(self.db),
205
- document_retrieval_metrics=DocumentRetrievalMetricsDataLoader(self.db),
206
- evaluation_summaries=EvaluationSummaryDataLoader(
207
- self.db,
208
- cache_map=self.cache_for_dataloaders.evaluation_summary
209
- if self.cache_for_dataloaders
210
- else None,
211
- ),
212
- experiment_annotation_summaries=ExperimentAnnotationSummaryDataLoader(self.db),
213
- experiment_error_rates=ExperimentErrorRatesDataLoader(self.db),
214
- experiment_run_counts=ExperimentRunCountsDataLoader(self.db),
215
- experiment_sequence_number=ExperimentSequenceNumberDataLoader(self.db),
216
- latency_ms_quantile=LatencyMsQuantileDataLoader(
217
- self.db,
218
- cache_map=self.cache_for_dataloaders.latency_ms_quantile
219
- if self.cache_for_dataloaders
220
- else None,
221
- ),
222
- min_start_or_max_end_times=MinStartOrMaxEndTimeDataLoader(
223
- self.db,
224
- cache_map=self.cache_for_dataloaders.min_start_or_max_end_time
225
- if self.cache_for_dataloaders
226
- else None,
227
- ),
228
- record_counts=RecordCountDataLoader(
229
- self.db,
230
- cache_map=self.cache_for_dataloaders.record_count
231
- if self.cache_for_dataloaders
232
- else None,
233
- ),
234
- span_descendants=SpanDescendantsDataLoader(self.db),
235
- span_evaluations=SpanEvaluationsDataLoader(self.db),
236
- span_projects=SpanProjectsDataLoader(self.db),
237
- token_counts=TokenCountDataLoader(
238
- self.db,
239
- cache_map=self.cache_for_dataloaders.token_count
240
- if self.cache_for_dataloaders
241
- else None,
242
- ),
243
- trace_evaluations=TraceEvaluationsDataLoader(self.db),
244
- trace_row_ids=TraceRowIdsDataLoader(self.db),
245
- project_by_name=ProjectByNameDataLoader(self.db),
246
- ),
247
- cache_for_dataloaders=self.cache_for_dataloaders,
248
- read_only=self.read_only,
249
- )
250
-
251
-
252
- class Download(HTTPEndpoint):
253
- path: Path
254
-
255
- async def get(self, request: Request) -> FileResponse:
256
- params = QueryParams(request.query_params)
257
- file = self.path / (params.get("filename", "") + ".parquet")
258
- if not file.is_file():
259
- raise HTTPException(status_code=404)
260
- return FileResponse(
261
- path=file,
262
- filename=file.name,
263
- media_type="application/x-octet-stream",
264
- )
160
+ @router.get("/exports")
161
+ async def download_exported_file(request: Request, filename: str) -> FileResponse:
162
+ file = request.app.state.export_path / (filename + ".parquet")
163
+ if not file.is_file():
164
+ raise HTTPException(status_code=404)
165
+ return FileResponse(
166
+ path=file,
167
+ filename=file.name,
168
+ media_type="application/x-octet-stream",
169
+ )
265
170
 
266
171
 
267
- async def version(_: Request) -> PlainTextResponse:
172
+ @router.get("/arize_phoenix_version")
173
+ async def version() -> PlainTextResponse:
268
174
  return PlainTextResponse(f"{phoenix.__version__}")
269
175
 
270
176
 
@@ -286,9 +192,9 @@ def _lifespan(
286
192
  enable_prometheus: bool = False,
287
193
  clean_ups: Iterable[Callable[[], None]] = (),
288
194
  read_only: bool = False,
289
- ) -> StatefulLifespan[Starlette]:
195
+ ) -> StatefulLifespan[FastAPI]:
290
196
  @contextlib.asynccontextmanager
291
- async def lifespan(_: Starlette) -> AsyncIterator[Dict[str, Any]]:
197
+ async def lifespan(_: FastAPI) -> AsyncIterator[Dict[str, Any]]:
292
198
  async with bulk_inserter as (
293
199
  queue_span,
294
200
  queue_evaluation,
@@ -310,16 +216,89 @@ def _lifespan(
310
216
  return lifespan
311
217
 
312
218
 
219
+ @router.get("/healthz")
313
220
  async def check_healthz(_: Request) -> PlainTextResponse:
314
221
  return PlainTextResponse("OK")
315
222
 
316
223
 
317
- async def openapi_schema(request: Request) -> Response:
318
- return OPENAPI_SCHEMA_GENERATOR.OpenAPIResponse(request=request)
319
-
224
+ def create_graphql_router(
225
+ *,
226
+ schema: BaseSchema,
227
+ db: Callable[[], AsyncContextManager[AsyncSession]],
228
+ model: Model,
229
+ export_path: Path,
230
+ corpus: Optional[Model] = None,
231
+ streaming_last_updated_at: Callable[[ProjectRowId], Optional[datetime]] = lambda _: None,
232
+ cache_for_dataloaders: Optional[CacheForDataLoaders] = None,
233
+ read_only: bool = False,
234
+ ) -> GraphQLRouter: # type: ignore[type-arg]
235
+ context = Context(
236
+ db=db,
237
+ model=model,
238
+ corpus=corpus,
239
+ export_path=export_path,
240
+ streaming_last_updated_at=streaming_last_updated_at,
241
+ data_loaders=DataLoaders(
242
+ average_experiment_run_latency=AverageExperimentRunLatencyDataLoader(db),
243
+ dataset_example_revisions=DatasetExampleRevisionsDataLoader(db),
244
+ dataset_example_spans=DatasetExampleSpansDataLoader(db),
245
+ document_evaluation_summaries=DocumentEvaluationSummaryDataLoader(
246
+ db,
247
+ cache_map=cache_for_dataloaders.document_evaluation_summary
248
+ if cache_for_dataloaders
249
+ else None,
250
+ ),
251
+ document_evaluations=DocumentEvaluationsDataLoader(db),
252
+ document_retrieval_metrics=DocumentRetrievalMetricsDataLoader(db),
253
+ evaluation_summaries=EvaluationSummaryDataLoader(
254
+ db,
255
+ cache_map=cache_for_dataloaders.evaluation_summary
256
+ if cache_for_dataloaders
257
+ else None,
258
+ ),
259
+ experiment_annotation_summaries=ExperimentAnnotationSummaryDataLoader(db),
260
+ experiment_error_rates=ExperimentErrorRatesDataLoader(db),
261
+ experiment_run_counts=ExperimentRunCountsDataLoader(db),
262
+ experiment_sequence_number=ExperimentSequenceNumberDataLoader(db),
263
+ latency_ms_quantile=LatencyMsQuantileDataLoader(
264
+ db,
265
+ cache_map=cache_for_dataloaders.latency_ms_quantile
266
+ if cache_for_dataloaders
267
+ else None,
268
+ ),
269
+ min_start_or_max_end_times=MinStartOrMaxEndTimeDataLoader(
270
+ db,
271
+ cache_map=cache_for_dataloaders.min_start_or_max_end_time
272
+ if cache_for_dataloaders
273
+ else None,
274
+ ),
275
+ record_counts=RecordCountDataLoader(
276
+ db,
277
+ cache_map=cache_for_dataloaders.record_count if cache_for_dataloaders else None,
278
+ ),
279
+ span_annotations=SpanAnnotationsDataLoader(db),
280
+ span_descendants=SpanDescendantsDataLoader(db),
281
+ span_evaluations=SpanEvaluationsDataLoader(db),
282
+ span_projects=SpanProjectsDataLoader(db),
283
+ token_counts=TokenCountDataLoader(
284
+ db,
285
+ cache_map=cache_for_dataloaders.token_count if cache_for_dataloaders else None,
286
+ ),
287
+ trace_evaluations=TraceEvaluationsDataLoader(db),
288
+ trace_row_ids=TraceRowIdsDataLoader(db),
289
+ project_by_name=ProjectByNameDataLoader(db),
290
+ ),
291
+ cache_for_dataloaders=cache_for_dataloaders,
292
+ read_only=read_only,
293
+ )
320
294
 
321
- async def api_docs(request: Request) -> Response:
322
- return get_swagger_ui_html(openapi_url="/schema", title="arize-phoenix API")
295
+ return GraphQLRouter(
296
+ schema,
297
+ graphiql=True,
298
+ context_getter=lambda: context,
299
+ include_in_schema=False,
300
+ prefix="/graphql",
301
+ )
323
302
 
324
303
 
325
304
  class SessionFactory:
@@ -370,6 +349,18 @@ def instrument_engine_if_enabled(engine: AsyncEngine) -> List[Callable[[], None]
370
349
  return instrumentation_cleanups
371
350
 
372
351
 
352
+ async def plain_text_http_exception_handler(request: Request, exc: HTTPException) -> Response:
353
+ """
354
+ Overrides the default handler for HTTPExceptions to return a plain text
355
+ response instead of a JSON response. For the original source code, see
356
+ https://github.com/tiangolo/fastapi/blob/d3cdd3bbd14109f3b268df7ca496e24bb64593aa/fastapi/exception_handlers.py#L11
357
+ """
358
+ headers = getattr(exc, "headers", None)
359
+ if not is_body_allowed_for_status_code(exc.status_code):
360
+ return Response(status_code=exc.status_code, headers=headers)
361
+ return PlainTextResponse(str(exc.detail), status_code=exc.status_code, headers=headers)
362
+
363
+
373
364
  def create_app(
374
365
  db: SessionFactory,
375
366
  export_path: Path,
@@ -383,7 +374,7 @@ def create_app(
383
374
  initial_evaluations: Optional[Iterable[pb.Evaluation]] = None,
384
375
  serve_ui: bool = True,
385
376
  clean_up_callbacks: List[Callable[[], None]] = [],
386
- ) -> Starlette:
377
+ ) -> FastAPI:
387
378
  clean_ups: List[Callable[[], None]] = clean_up_callbacks # To be called at app shutdown.
388
379
  initial_batch_of_spans: Iterable[Tuple[Span, str]] = (
389
380
  ()
@@ -425,7 +416,7 @@ def create_app(
425
416
 
426
417
  strawberry_extensions.append(_OpenTelemetryExtension)
427
418
 
428
- graphql = GraphQLWithContext(
419
+ graphql_router = create_graphql_router(
429
420
  db=db,
430
421
  schema=strawberry.Schema(
431
422
  query=schema.query,
@@ -436,7 +427,6 @@ def create_app(
436
427
  model=model,
437
428
  corpus=corpus,
438
429
  export_path=export_path,
439
- graphiql=True,
440
430
  streaming_last_updated_at=bulk_inserter.last_updated_at,
441
431
  cache_for_dataloaders=cache_for_dataloaders,
442
432
  read_only=read_only,
@@ -447,7 +437,9 @@ def create_app(
447
437
  prometheus_middlewares = [Middleware(PrometheusMiddleware)]
448
438
  else:
449
439
  prometheus_middlewares = []
450
- app = Starlette(
440
+ app = FastAPI(
441
+ title="Arize-Phoenix REST API",
442
+ version=REST_API_VERSION,
451
443
  lifespan=_lifespan(
452
444
  read_only=read_only,
453
445
  bulk_inserter=bulk_inserter,
@@ -459,56 +451,39 @@ def create_app(
459
451
  Middleware(HeadersMiddleware),
460
452
  *prometheus_middlewares,
461
453
  ],
454
+ exception_handlers={HTTPException: plain_text_http_exception_handler},
462
455
  debug=debug,
463
- routes=V1_ROUTES
464
- + [
465
- Route("/schema", endpoint=openapi_schema, include_in_schema=False),
466
- Route("/arize_phoenix_version", version),
467
- Route("/healthz", check_healthz),
468
- Route(
469
- "/exports",
470
- type(
471
- "DownloadExports",
472
- (Download,),
473
- {"path": export_path},
474
- ),
475
- ),
476
- Route(
477
- "/docs",
478
- api_docs,
479
- ),
480
- Route(
481
- "/graphql",
482
- graphql,
483
- ),
484
- ]
485
- + (
486
- [
487
- Mount(
488
- "/",
489
- app=Static(
490
- directory=SERVER_DIR / "static",
491
- app_config=AppConfig(
492
- has_inferences=model.is_empty is not True,
493
- has_corpus=corpus is not None,
494
- min_dist=umap_params.min_dist,
495
- n_neighbors=umap_params.n_neighbors,
496
- n_samples=umap_params.n_samples,
497
- ),
498
- ),
499
- name="static",
500
- ),
501
- ]
502
- if serve_ui
503
- else []
504
- ),
456
+ swagger_ui_parameters={
457
+ "defaultModelsExpandDepth": -1, # hides the schema section in the Swagger UI
458
+ },
505
459
  )
506
460
  app.state.read_only = read_only
461
+ app.state.export_path = export_path
462
+ app.include_router(v1_router)
463
+ app.include_router(router)
464
+ app.include_router(graphql_router)
465
+ app.add_middleware(GZipMiddleware)
466
+ if serve_ui:
467
+ app.mount(
468
+ "/",
469
+ app=Static(
470
+ directory=SERVER_DIR / "static",
471
+ app_config=AppConfig(
472
+ has_inferences=model.is_empty is not True,
473
+ has_corpus=corpus is not None,
474
+ min_dist=umap_params.min_dist,
475
+ n_neighbors=umap_params.n_neighbors,
476
+ n_samples=umap_params.n_samples,
477
+ ),
478
+ ),
479
+ name="static",
480
+ )
481
+
507
482
  app.state.db = db
508
483
  if tracer_provider:
509
- from opentelemetry.instrumentation.starlette import StarletteInstrumentor
484
+ from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
510
485
 
511
- StarletteInstrumentor().instrument(tracer_provider=tracer_provider)
512
- StarletteInstrumentor.instrument_app(app, tracer_provider=tracer_provider)
513
- clean_ups.append(StarletteInstrumentor().uninstrument)
486
+ FastAPIInstrumentor().instrument(tracer_provider=tracer_provider)
487
+ FastAPIInstrumentor.instrument_app(app, tracer_provider=tracer_provider)
488
+ clean_ups.append(FastAPIInstrumentor().uninstrument)
514
489
  return app
@@ -4,7 +4,7 @@ from threading import Thread
4
4
  from time import sleep, time
5
5
  from typing import Generator
6
6
 
7
- from starlette.applications import Starlette
7
+ from fastapi import FastAPI
8
8
  from uvicorn import Config, Server
9
9
  from uvicorn.config import LoopSetupType
10
10
 
@@ -24,7 +24,7 @@ class ThreadServer(Server):
24
24
 
25
25
  def __init__(
26
26
  self,
27
- app: Starlette,
27
+ app: FastAPI,
28
28
  host: str,
29
29
  port: int,
30
30
  root_path: str,
phoenix/version.py CHANGED
@@ -1 +1 @@
1
- __version__ = "4.10.1"
1
+ __version__ = "4.10.2rc1"
@@ -1,221 +0,0 @@
1
- import json
2
- from typing import Any, Dict, Optional
3
-
4
- from starlette.responses import HTMLResponse
5
-
6
- swagger_ui_default_parameters: Dict[str, Any] = {
7
- "dom_id": "#swagger-ui",
8
- "layout": "BaseLayout",
9
- "deepLinking": True,
10
- "showExtensions": True,
11
- "showCommonExtensions": True,
12
- }
13
-
14
-
15
- def get_swagger_ui_html(
16
- *,
17
- openapi_url: str = "/schema",
18
- title: str,
19
- swagger_js_url: str = "https://cdn.jsdelivr.net/npm/swagger-ui-dist@5.9.0/swagger-ui-bundle.js",
20
- swagger_css_url: str = "https://cdn.jsdelivr.net/npm/swagger-ui-dist@5.9.0/swagger-ui.css",
21
- swagger_favicon_url: str = "/favicon.ico",
22
- oauth2_redirect_url: Optional[str] = None,
23
- init_oauth: Optional[str] = None,
24
- swagger_ui_parameters: Optional[Dict[str, Any]] = None,
25
- ) -> HTMLResponse:
26
- """
27
- Generate and return the HTML that loads Swagger UI for the interactive API
28
- docs (normally served at `/docs`).
29
- """
30
- current_swagger_ui_parameters = swagger_ui_default_parameters.copy()
31
- if swagger_ui_parameters:
32
- current_swagger_ui_parameters.update(swagger_ui_parameters)
33
-
34
- html = f"""
35
- <!DOCTYPE html>
36
- <html>
37
- <head>
38
- <link type="text/css" rel="stylesheet" href="{swagger_css_url}">
39
- <link rel="shortcut icon" href="{swagger_favicon_url}">
40
- <title>{title}</title>
41
- </head>
42
- <body>
43
- <div id="swagger-ui">
44
- </div>
45
- <script src="{swagger_js_url}"></script>
46
- <style type="text/css">
47
- div[id^="operations-private"]{{display:none}} #operations-tag-private{{display:none}}
48
- </style>
49
- <!-- `SwaggerUIBundle` is now available on the page -->
50
- <script>
51
- const ui = SwaggerUIBundle({{
52
- url: '{openapi_url}',
53
- """
54
-
55
- for key, value in current_swagger_ui_parameters.items():
56
- html += f"{json.dumps(key)}: {json.dumps(value)},\n"
57
-
58
- if oauth2_redirect_url:
59
- html += f"oauth2RedirectUrl: window.location.origin + '{oauth2_redirect_url}',"
60
-
61
- html += """
62
- presets: [
63
- SwaggerUIBundle.presets.apis,
64
- SwaggerUIBundle.SwaggerUIStandalonePreset
65
- ],
66
- })"""
67
-
68
- if init_oauth:
69
- html += f"""
70
- ui.initOAuth({json.dumps(init_oauth)})
71
- """
72
-
73
- html += """
74
- </script>
75
- </body>
76
- </html>
77
- """
78
- return HTMLResponse(html)
79
-
80
-
81
- def get_redoc_html(
82
- *,
83
- openapi_url: str,
84
- title: str,
85
- redoc_js_url: str = "https://cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js",
86
- redoc_favicon_url: str = "/favicon.ico",
87
- with_google_fonts: bool = True,
88
- ) -> HTMLResponse:
89
- """
90
- Generate and return the HTML response that loads ReDoc for the alternative
91
- API docs (normally served at `/redoc`).
92
-
93
-
94
- """
95
- html = f"""
96
- <!DOCTYPE html>
97
- <html>
98
- <head>
99
- <title>{title}</title>
100
- <!-- needed for adaptive design -->
101
- <meta charset="utf-8"/>
102
- <meta name="viewport" content="width=device-width, initial-scale=1">
103
- """
104
- if with_google_fonts:
105
- html += """
106
- <link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet">
107
- """ # noqa: E501
108
- html += f"""
109
- <link rel="shortcut icon" href="{redoc_favicon_url}">
110
- <!--
111
- ReDoc doesn't change outer page styles
112
- -->
113
- <style>
114
- body {{
115
- margin: 0;
116
- padding: 0;
117
- }}
118
- </style>
119
- </head>
120
- <body>
121
- <noscript>
122
- ReDoc requires Javascript to function. Please enable it to browse the documentation.
123
- </noscript>
124
- <redoc spec-url="{openapi_url}"></redoc>
125
- <script src="{redoc_js_url}"> </script>
126
- </body>
127
- </html>
128
- """
129
- return HTMLResponse(html)
130
-
131
-
132
- # Not needed now but copy-pasting for future reference
133
- def get_swagger_ui_oauth2_redirect_html() -> HTMLResponse:
134
- """
135
- Generate the HTML response with the OAuth2 redirection for Swagger UI.
136
-
137
- You normally don't need to use or change this.
138
- """
139
- # copied from https://github.com/swagger-api/swagger-ui/blob/v4.14.0/dist/oauth2-redirect.html
140
- html = """
141
- <!doctype html>
142
- <html lang="en-US">
143
- <head>
144
- <title>Swagger UI: OAuth2 Redirect</title>
145
- </head>
146
- <body>
147
- <script>
148
- 'use strict';
149
- function run () {
150
- var oauth2 = window.opener.swaggerUIRedirectOauth2;
151
- var sentState = oauth2.state;
152
- var redirectUrl = oauth2.redirectUrl;
153
- var isValid, qp, arr;
154
-
155
- if (/code|token|error/.test(window.location.hash)) {
156
- qp = window.location.hash.substring(1).replace('?', '&');
157
- } else {
158
- qp = location.search.substring(1);
159
- }
160
-
161
- arr = qp.split("&");
162
- arr.forEach(function (v,i,_arr) { _arr[i] = '"' + v.replace('=', '":"') + '"';});
163
- qp = qp ? JSON.parse('{' + arr.join() + '}',
164
- function (key, value) {
165
- return key === "" ? value : decodeURIComponent(value);
166
- }
167
- ) : {};
168
-
169
- isValid = qp.state === sentState;
170
-
171
- if ((
172
- oauth2.auth.schema.get("flow") === "accessCode" ||
173
- oauth2.auth.schema.get("flow") === "authorizationCode" ||
174
- oauth2.auth.schema.get("flow") === "authorization_code"
175
- ) && !oauth2.auth.code) {
176
- if (!isValid) {
177
- oauth2.errCb({
178
- authId: oauth2.auth.name,
179
- source: "auth",
180
- level: "warning",
181
- message: "Authorization may be unsafe, passed state was changed in server. The passed state wasn't returned from auth server."
182
- });
183
- }
184
-
185
- if (qp.code) {
186
- delete oauth2.state;
187
- oauth2.auth.code = qp.code;
188
- oauth2.callback({auth: oauth2.auth, redirectUrl: redirectUrl});
189
- } else {
190
- let oauthErrorMsg;
191
- if (qp.error) {
192
- oauthErrorMsg = "["+qp.error+"]: " +
193
- (qp.error_description ? qp.error_description+ ". " : "no accessCode received from the server. ") +
194
- (qp.error_uri ? "More info: "+qp.error_uri : "");
195
- }
196
-
197
- oauth2.errCb({
198
- authId: oauth2.auth.name,
199
- source: "auth",
200
- level: "error",
201
- message: oauthErrorMsg || "[Authorization failed]: no accessCode received from the server."
202
- });
203
- }
204
- } else {
205
- oauth2.callback({auth: oauth2.auth, token: qp, isValid: isValid, redirectUrl: redirectUrl});
206
- }
207
- window.close();
208
- }
209
-
210
- if (document.readyState !== 'loading') {
211
- run();
212
- } else {
213
- document.addEventListener('DOMContentLoaded', function () {
214
- run();
215
- });
216
- }
217
- </script>
218
- </body>
219
- </html>
220
- """ # noqa: E501
221
- return HTMLResponse(content=html)