arize-phoenix 4.12.1rc1__py3-none-any.whl → 4.14.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.

Potentially problematic release.


This version of arize-phoenix might be problematic. Click here for more details.

Files changed (51) hide show
  1. {arize_phoenix-4.12.1rc1.dist-info → arize_phoenix-4.14.1.dist-info}/METADATA +12 -9
  2. {arize_phoenix-4.12.1rc1.dist-info → arize_phoenix-4.14.1.dist-info}/RECORD +48 -49
  3. phoenix/db/bulk_inserter.py +3 -1
  4. phoenix/experiments/evaluators/base.py +4 -0
  5. phoenix/experiments/evaluators/code_evaluators.py +80 -0
  6. phoenix/experiments/evaluators/llm_evaluators.py +77 -1
  7. phoenix/experiments/evaluators/utils.py +70 -21
  8. phoenix/experiments/functions.py +14 -14
  9. phoenix/server/api/context.py +7 -3
  10. phoenix/server/api/dataloaders/average_experiment_run_latency.py +23 -23
  11. phoenix/server/api/dataloaders/experiment_error_rates.py +30 -10
  12. phoenix/server/api/dataloaders/experiment_run_counts.py +18 -5
  13. phoenix/server/api/input_types/{CreateSpanAnnotationsInput.py → CreateSpanAnnotationInput.py} +4 -2
  14. phoenix/server/api/input_types/{CreateTraceAnnotationsInput.py → CreateTraceAnnotationInput.py} +4 -2
  15. phoenix/server/api/input_types/{PatchAnnotationsInput.py → PatchAnnotationInput.py} +4 -2
  16. phoenix/server/api/mutations/span_annotations_mutations.py +12 -6
  17. phoenix/server/api/mutations/trace_annotations_mutations.py +12 -6
  18. phoenix/server/api/openapi/main.py +2 -18
  19. phoenix/server/api/openapi/schema.py +12 -12
  20. phoenix/server/api/routers/v1/__init__.py +83 -36
  21. phoenix/server/api/routers/v1/dataset_examples.py +123 -102
  22. phoenix/server/api/routers/v1/datasets.py +506 -390
  23. phoenix/server/api/routers/v1/evaluations.py +66 -73
  24. phoenix/server/api/routers/v1/experiment_evaluations.py +91 -68
  25. phoenix/server/api/routers/v1/experiment_runs.py +155 -98
  26. phoenix/server/api/routers/v1/experiments.py +181 -132
  27. phoenix/server/api/routers/v1/spans.py +173 -144
  28. phoenix/server/api/routers/v1/traces.py +128 -115
  29. phoenix/server/api/types/Experiment.py +2 -2
  30. phoenix/server/api/types/Inferences.py +1 -2
  31. phoenix/server/api/types/Model.py +1 -2
  32. phoenix/server/app.py +177 -152
  33. phoenix/server/openapi/docs.py +221 -0
  34. phoenix/server/static/.vite/manifest.json +31 -31
  35. phoenix/server/static/assets/{components-C8sm_r1F.js → components-DeS0YEmv.js} +2 -2
  36. phoenix/server/static/assets/index-CQgXRwU0.js +100 -0
  37. phoenix/server/static/assets/{pages-bN7juCjh.js → pages-hdjlFZhO.js} +275 -198
  38. phoenix/server/static/assets/{vendor-CUDAPm8e.js → vendor-DPvSDRn3.js} +1 -1
  39. phoenix/server/static/assets/{vendor-arizeai-Do2HOmcL.js → vendor-arizeai-CkvPT67c.js} +2 -2
  40. phoenix/server/static/assets/{vendor-codemirror-CrdxOlMs.js → vendor-codemirror-Cqwpwlua.js} +1 -1
  41. phoenix/server/static/assets/{vendor-recharts-PKRvByVe.js → vendor-recharts-5jlNaZuF.js} +1 -1
  42. phoenix/server/thread_server.py +2 -2
  43. phoenix/session/client.py +9 -8
  44. phoenix/trace/dsl/filter.py +40 -25
  45. phoenix/version.py +1 -1
  46. phoenix/server/api/routers/v1/pydantic_compat.py +0 -78
  47. phoenix/server/api/routers/v1/utils.py +0 -95
  48. phoenix/server/static/assets/index-BEKPzgQs.js +0 -100
  49. {arize_phoenix-4.12.1rc1.dist-info → arize_phoenix-4.14.1.dist-info}/WHEEL +0 -0
  50. {arize_phoenix-4.12.1rc1.dist-info → arize_phoenix-4.14.1.dist-info}/licenses/IP_NOTICE +0 -0
  51. {arize_phoenix-4.12.1rc1.dist-info → arize_phoenix-4.14.1.dist-info}/licenses/LICENSE +0 -0
phoenix/server/app.py CHANGED
@@ -21,24 +21,25 @@ from typing import (
21
21
  )
22
22
 
23
23
  import strawberry
24
- from fastapi import APIRouter, FastAPI
25
- from fastapi.middleware.gzip import GZipMiddleware
26
- from fastapi.responses import FileResponse
27
- from fastapi.utils import is_body_allowed_for_status_code
28
24
  from sqlalchemy.ext.asyncio import (
29
25
  AsyncEngine,
30
26
  AsyncSession,
31
27
  async_sessionmaker,
32
28
  )
29
+ from starlette.applications import Starlette
30
+ from starlette.datastructures import QueryParams
31
+ from starlette.endpoints import HTTPEndpoint
33
32
  from starlette.exceptions import HTTPException
34
33
  from starlette.middleware import Middleware
35
34
  from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
36
35
  from starlette.requests import Request
37
- from starlette.responses import PlainTextResponse, Response
36
+ from starlette.responses import FileResponse, PlainTextResponse, Response
37
+ from starlette.routing import Mount, Route
38
38
  from starlette.staticfiles import StaticFiles
39
39
  from starlette.templating import Jinja2Templates
40
40
  from starlette.types import Scope, StatefulLifespan
41
- from strawberry.fastapi import GraphQLRouter
41
+ from starlette.websockets import WebSocket
42
+ from strawberry.asgi import GraphQL
42
43
  from strawberry.schema import BaseSchema
43
44
  from typing_extensions import TypeAlias
44
45
 
@@ -81,10 +82,11 @@ from phoenix.server.api.dataloaders import (
81
82
  TraceEvaluationsDataLoader,
82
83
  TraceRowIdsDataLoader,
83
84
  )
84
- from phoenix.server.api.routers.v1 import REST_API_VERSION
85
- from phoenix.server.api.routers.v1 import router as v1_router
85
+ from phoenix.server.api.openapi.schema import OPENAPI_SCHEMA_GENERATOR
86
+ from phoenix.server.api.routers.v1 import V1_ROUTES
86
87
  from phoenix.server.api.schema import schema
87
88
  from phoenix.server.grpc_server import GrpcServer
89
+ from phoenix.server.openapi.docs import get_swagger_ui_html
88
90
  from phoenix.server.telemetry import initialize_opentelemetry_tracer_provider
89
91
  from phoenix.trace.schemas import Span
90
92
 
@@ -93,8 +95,6 @@ if TYPE_CHECKING:
93
95
 
94
96
  logger = logging.getLogger(__name__)
95
97
 
96
- router = APIRouter(include_in_schema=False)
97
-
98
98
  templates = Jinja2Templates(directory=SERVER_DIR / "templates")
99
99
 
100
100
 
@@ -169,27 +169,122 @@ class HeadersMiddleware(BaseHTTPMiddleware):
169
169
  ) -> Response:
170
170
  response = await call_next(request)
171
171
  response.headers["x-colab-notebook-cache-control"] = "no-cache"
172
- response.headers["Cache-Control"] = "no-store"
173
172
  return response
174
173
 
175
174
 
176
175
  ProjectRowId: TypeAlias = int
177
176
 
178
177
 
179
- @router.get("/exports")
180
- async def download_exported_file(request: Request, filename: str) -> FileResponse:
181
- file = request.app.state.export_path / (filename + ".parquet")
182
- if not file.is_file():
183
- raise HTTPException(status_code=404)
184
- return FileResponse(
185
- path=file,
186
- filename=file.name,
187
- media_type="application/x-octet-stream",
188
- )
178
+ class GraphQLWithContext(GraphQL): # type: ignore
179
+ def __init__(
180
+ self,
181
+ schema: BaseSchema,
182
+ db: Callable[[], AsyncContextManager[AsyncSession]],
183
+ model: Model,
184
+ export_path: Path,
185
+ graphiql: bool = False,
186
+ corpus: Optional[Model] = None,
187
+ streaming_last_updated_at: Callable[[ProjectRowId], Optional[datetime]] = lambda _: None,
188
+ cache_for_dataloaders: Optional[CacheForDataLoaders] = None,
189
+ read_only: bool = False,
190
+ ) -> None:
191
+ self.db = db
192
+ self.model = model
193
+ self.corpus = corpus
194
+ self.export_path = export_path
195
+ self.streaming_last_updated_at = streaming_last_updated_at
196
+ self.cache_for_dataloaders = cache_for_dataloaders
197
+ self.read_only = read_only
198
+ super().__init__(schema, graphiql=graphiql)
199
+
200
+ async def get_context(
201
+ self,
202
+ request: Union[Request, WebSocket],
203
+ response: Optional[Response] = None,
204
+ ) -> Context:
205
+ return Context(
206
+ request=request,
207
+ response=response,
208
+ db=self.db,
209
+ model=self.model,
210
+ corpus=self.corpus,
211
+ export_path=self.export_path,
212
+ streaming_last_updated_at=self.streaming_last_updated_at,
213
+ data_loaders=DataLoaders(
214
+ average_experiment_run_latency=AverageExperimentRunLatencyDataLoader(self.db),
215
+ dataset_example_revisions=DatasetExampleRevisionsDataLoader(self.db),
216
+ dataset_example_spans=DatasetExampleSpansDataLoader(self.db),
217
+ document_evaluation_summaries=DocumentEvaluationSummaryDataLoader(
218
+ self.db,
219
+ cache_map=self.cache_for_dataloaders.document_evaluation_summary
220
+ if self.cache_for_dataloaders
221
+ else None,
222
+ ),
223
+ document_evaluations=DocumentEvaluationsDataLoader(self.db),
224
+ document_retrieval_metrics=DocumentRetrievalMetricsDataLoader(self.db),
225
+ evaluation_summaries=EvaluationSummaryDataLoader(
226
+ self.db,
227
+ cache_map=self.cache_for_dataloaders.evaluation_summary
228
+ if self.cache_for_dataloaders
229
+ else None,
230
+ ),
231
+ experiment_annotation_summaries=ExperimentAnnotationSummaryDataLoader(self.db),
232
+ experiment_error_rates=ExperimentErrorRatesDataLoader(self.db),
233
+ experiment_run_counts=ExperimentRunCountsDataLoader(self.db),
234
+ experiment_sequence_number=ExperimentSequenceNumberDataLoader(self.db),
235
+ latency_ms_quantile=LatencyMsQuantileDataLoader(
236
+ self.db,
237
+ cache_map=self.cache_for_dataloaders.latency_ms_quantile
238
+ if self.cache_for_dataloaders
239
+ else None,
240
+ ),
241
+ min_start_or_max_end_times=MinStartOrMaxEndTimeDataLoader(
242
+ self.db,
243
+ cache_map=self.cache_for_dataloaders.min_start_or_max_end_time
244
+ if self.cache_for_dataloaders
245
+ else None,
246
+ ),
247
+ record_counts=RecordCountDataLoader(
248
+ self.db,
249
+ cache_map=self.cache_for_dataloaders.record_count
250
+ if self.cache_for_dataloaders
251
+ else None,
252
+ ),
253
+ span_descendants=SpanDescendantsDataLoader(self.db),
254
+ span_evaluations=SpanEvaluationsDataLoader(self.db),
255
+ span_projects=SpanProjectsDataLoader(self.db),
256
+ token_counts=TokenCountDataLoader(
257
+ self.db,
258
+ cache_map=self.cache_for_dataloaders.token_count
259
+ if self.cache_for_dataloaders
260
+ else None,
261
+ ),
262
+ trace_evaluations=TraceEvaluationsDataLoader(self.db),
263
+ trace_row_ids=TraceRowIdsDataLoader(self.db),
264
+ project_by_name=ProjectByNameDataLoader(self.db),
265
+ span_annotations=SpanAnnotationsDataLoader(self.db),
266
+ ),
267
+ cache_for_dataloaders=self.cache_for_dataloaders,
268
+ read_only=self.read_only,
269
+ )
270
+
189
271
 
272
+ class Download(HTTPEndpoint):
273
+ path: Path
190
274
 
191
- @router.get("/arize_phoenix_version")
192
- async def version() -> PlainTextResponse:
275
+ async def get(self, request: Request) -> FileResponse:
276
+ params = QueryParams(request.query_params)
277
+ file = self.path / (params.get("filename", "") + ".parquet")
278
+ if not file.is_file():
279
+ raise HTTPException(status_code=404)
280
+ return FileResponse(
281
+ path=file,
282
+ filename=file.name,
283
+ media_type="application/x-octet-stream",
284
+ )
285
+
286
+
287
+ async def version(_: Request) -> PlainTextResponse:
193
288
  return PlainTextResponse(f"{phoenix.__version__}")
194
289
 
195
290
 
@@ -211,9 +306,9 @@ def _lifespan(
211
306
  enable_prometheus: bool = False,
212
307
  clean_ups: Iterable[Callable[[], None]] = (),
213
308
  read_only: bool = False,
214
- ) -> StatefulLifespan[FastAPI]:
309
+ ) -> StatefulLifespan[Starlette]:
215
310
  @contextlib.asynccontextmanager
216
- async def lifespan(_: FastAPI) -> AsyncIterator[Dict[str, Any]]:
311
+ async def lifespan(_: Starlette) -> AsyncIterator[Dict[str, Any]]:
217
312
  async with bulk_inserter as (
218
313
  queue_span,
219
314
  queue_evaluation,
@@ -235,90 +330,16 @@ def _lifespan(
235
330
  return lifespan
236
331
 
237
332
 
238
- @router.get("/healthz")
239
333
  async def check_healthz(_: Request) -> PlainTextResponse:
240
334
  return PlainTextResponse("OK")
241
335
 
242
336
 
243
- def create_graphql_router(
244
- *,
245
- schema: BaseSchema,
246
- db: Callable[[], AsyncContextManager[AsyncSession]],
247
- model: Model,
248
- export_path: Path,
249
- corpus: Optional[Model] = None,
250
- streaming_last_updated_at: Callable[[ProjectRowId], Optional[datetime]] = lambda _: None,
251
- cache_for_dataloaders: Optional[CacheForDataLoaders] = None,
252
- read_only: bool = False,
253
- ) -> GraphQLRouter: # type: ignore[type-arg]
254
- def get_context() -> Context:
255
- return Context(
256
- db=db,
257
- model=model,
258
- corpus=corpus,
259
- export_path=export_path,
260
- streaming_last_updated_at=streaming_last_updated_at,
261
- data_loaders=DataLoaders(
262
- average_experiment_run_latency=AverageExperimentRunLatencyDataLoader(db),
263
- dataset_example_revisions=DatasetExampleRevisionsDataLoader(db),
264
- dataset_example_spans=DatasetExampleSpansDataLoader(db),
265
- document_evaluation_summaries=DocumentEvaluationSummaryDataLoader(
266
- db,
267
- cache_map=cache_for_dataloaders.document_evaluation_summary
268
- if cache_for_dataloaders
269
- else None,
270
- ),
271
- document_evaluations=DocumentEvaluationsDataLoader(db),
272
- document_retrieval_metrics=DocumentRetrievalMetricsDataLoader(db),
273
- evaluation_summaries=EvaluationSummaryDataLoader(
274
- db,
275
- cache_map=cache_for_dataloaders.evaluation_summary
276
- if cache_for_dataloaders
277
- else None,
278
- ),
279
- experiment_annotation_summaries=ExperimentAnnotationSummaryDataLoader(db),
280
- experiment_error_rates=ExperimentErrorRatesDataLoader(db),
281
- experiment_run_counts=ExperimentRunCountsDataLoader(db),
282
- experiment_sequence_number=ExperimentSequenceNumberDataLoader(db),
283
- latency_ms_quantile=LatencyMsQuantileDataLoader(
284
- db,
285
- cache_map=cache_for_dataloaders.latency_ms_quantile
286
- if cache_for_dataloaders
287
- else None,
288
- ),
289
- min_start_or_max_end_times=MinStartOrMaxEndTimeDataLoader(
290
- db,
291
- cache_map=cache_for_dataloaders.min_start_or_max_end_time
292
- if cache_for_dataloaders
293
- else None,
294
- ),
295
- record_counts=RecordCountDataLoader(
296
- db,
297
- cache_map=cache_for_dataloaders.record_count if cache_for_dataloaders else None,
298
- ),
299
- span_annotations=SpanAnnotationsDataLoader(db),
300
- span_descendants=SpanDescendantsDataLoader(db),
301
- span_evaluations=SpanEvaluationsDataLoader(db),
302
- span_projects=SpanProjectsDataLoader(db),
303
- token_counts=TokenCountDataLoader(
304
- db,
305
- cache_map=cache_for_dataloaders.token_count if cache_for_dataloaders else None,
306
- ),
307
- trace_evaluations=TraceEvaluationsDataLoader(db),
308
- trace_row_ids=TraceRowIdsDataLoader(db),
309
- project_by_name=ProjectByNameDataLoader(db),
310
- ),
311
- cache_for_dataloaders=cache_for_dataloaders,
312
- read_only=read_only,
313
- )
337
+ async def openapi_schema(request: Request) -> Response:
338
+ return OPENAPI_SCHEMA_GENERATOR.OpenAPIResponse(request=request)
314
339
 
315
- return GraphQLRouter(
316
- schema,
317
- graphiql=True,
318
- context_getter=get_context,
319
- include_in_schema=False,
320
- prefix="/graphql",
321
- )
340
+
341
+ async def api_docs(request: Request) -> Response:
342
+ return get_swagger_ui_html(openapi_url="/schema", title="arize-phoenix API")
322
343
 
323
344
 
324
345
  class SessionFactory:
@@ -369,18 +390,6 @@ def instrument_engine_if_enabled(engine: AsyncEngine) -> List[Callable[[], None]
369
390
  return instrumentation_cleanups
370
391
 
371
392
 
372
- async def plain_text_http_exception_handler(request: Request, exc: HTTPException) -> Response:
373
- """
374
- Overrides the default handler for HTTPExceptions to return a plain text
375
- response instead of a JSON response. For the original source code, see
376
- https://github.com/tiangolo/fastapi/blob/d3cdd3bbd14109f3b268df7ca496e24bb64593aa/fastapi/exception_handlers.py#L11
377
- """
378
- headers = getattr(exc, "headers", None)
379
- if not is_body_allowed_for_status_code(exc.status_code):
380
- return Response(status_code=exc.status_code, headers=headers)
381
- return PlainTextResponse(str(exc.detail), status_code=exc.status_code, headers=headers)
382
-
383
-
384
393
  def create_app(
385
394
  db: SessionFactory,
386
395
  export_path: Path,
@@ -395,7 +404,7 @@ def create_app(
395
404
  initial_evaluations: Optional[Iterable[pb.Evaluation]] = None,
396
405
  serve_ui: bool = True,
397
406
  clean_up_callbacks: List[Callable[[], None]] = [],
398
- ) -> FastAPI:
407
+ ) -> Starlette:
399
408
  clean_ups: List[Callable[[], None]] = clean_up_callbacks # To be called at app shutdown.
400
409
  initial_batch_of_spans: Iterable[Tuple[Span, str]] = (
401
410
  ()
@@ -438,7 +447,7 @@ def create_app(
438
447
 
439
448
  strawberry_extensions.append(_OpenTelemetryExtension)
440
449
 
441
- graphql_router = create_graphql_router(
450
+ graphql = GraphQLWithContext(
442
451
  db=db,
443
452
  schema=strawberry.Schema(
444
453
  query=schema.query,
@@ -449,6 +458,7 @@ def create_app(
449
458
  model=model,
450
459
  corpus=corpus,
451
460
  export_path=export_path,
461
+ graphiql=True,
452
462
  streaming_last_updated_at=bulk_inserter.last_updated_at,
453
463
  cache_for_dataloaders=cache_for_dataloaders,
454
464
  read_only=read_only,
@@ -459,9 +469,7 @@ def create_app(
459
469
  prometheus_middlewares = [Middleware(PrometheusMiddleware)]
460
470
  else:
461
471
  prometheus_middlewares = []
462
- app = FastAPI(
463
- title="Arize-Phoenix REST API",
464
- version=REST_API_VERSION,
472
+ app = Starlette(
465
473
  lifespan=_lifespan(
466
474
  read_only=read_only,
467
475
  bulk_inserter=bulk_inserter,
@@ -473,41 +481,58 @@ def create_app(
473
481
  Middleware(HeadersMiddleware),
474
482
  *prometheus_middlewares,
475
483
  ],
476
- exception_handlers={HTTPException: plain_text_http_exception_handler},
477
484
  debug=debug,
478
- swagger_ui_parameters={
479
- "defaultModelsExpandDepth": -1, # hides the schema section in the Swagger UI
480
- },
481
- )
482
- app.state.read_only = read_only
483
- app.state.export_path = export_path
484
- app.include_router(v1_router)
485
- app.include_router(router)
486
- app.include_router(graphql_router)
487
- app.add_middleware(GZipMiddleware)
488
- if serve_ui:
489
- app.mount(
490
- "/",
491
- app=Static(
492
- directory=SERVER_DIR / "static",
493
- app_config=AppConfig(
494
- has_inferences=model.is_empty is not True,
495
- has_corpus=corpus is not None,
496
- min_dist=umap_params.min_dist,
497
- n_neighbors=umap_params.n_neighbors,
498
- n_samples=umap_params.n_samples,
499
- is_development=dev,
500
- web_manifest_path=SERVER_DIR / "static" / ".vite" / "manifest.json",
485
+ routes=V1_ROUTES
486
+ + [
487
+ Route("/schema", endpoint=openapi_schema, include_in_schema=False),
488
+ Route("/arize_phoenix_version", version),
489
+ Route("/healthz", check_healthz),
490
+ Route(
491
+ "/exports",
492
+ type(
493
+ "DownloadExports",
494
+ (Download,),
495
+ {"path": export_path},
501
496
  ),
502
497
  ),
503
- name="static",
504
- )
505
-
498
+ Route(
499
+ "/docs",
500
+ api_docs,
501
+ ),
502
+ Route(
503
+ "/graphql",
504
+ graphql,
505
+ ),
506
+ ]
507
+ + (
508
+ [
509
+ Mount(
510
+ "/",
511
+ app=Static(
512
+ directory=SERVER_DIR / "static",
513
+ app_config=AppConfig(
514
+ has_inferences=model.is_empty is not True,
515
+ has_corpus=corpus is not None,
516
+ min_dist=umap_params.min_dist,
517
+ n_neighbors=umap_params.n_neighbors,
518
+ n_samples=umap_params.n_samples,
519
+ is_development=dev,
520
+ web_manifest_path=SERVER_DIR / "static" / ".vite" / "manifest.json",
521
+ ),
522
+ ),
523
+ name="static",
524
+ ),
525
+ ]
526
+ if serve_ui
527
+ else []
528
+ ),
529
+ )
530
+ app.state.read_only = read_only
506
531
  app.state.db = db
507
532
  if tracer_provider:
508
- from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
533
+ from opentelemetry.instrumentation.starlette import StarletteInstrumentor
509
534
 
510
- FastAPIInstrumentor().instrument(tracer_provider=tracer_provider)
511
- FastAPIInstrumentor.instrument_app(app, tracer_provider=tracer_provider)
512
- clean_ups.append(FastAPIInstrumentor().uninstrument)
535
+ StarletteInstrumentor().instrument(tracer_provider=tracer_provider)
536
+ StarletteInstrumentor.instrument_app(app, tracer_provider=tracer_provider)
537
+ clean_ups.append(StarletteInstrumentor().uninstrument)
513
538
  return app
@@ -0,0 +1,221 @@
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)