Skip to content

Commit

Permalink
feat: add http serve (#6)
Browse files Browse the repository at this point in the history
Signed-off-by: Keming <[email protected]>
  • Loading branch information
kemingy authored Jan 24, 2024
1 parent 8692e95 commit a070551
Show file tree
Hide file tree
Showing 4 changed files with 93 additions and 4 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,7 @@ print(openapi.to_dict())
# get the OpenAPI spec bytes
with open("openapi.json", "wb") as f:
f.write(openapi.to_json())

# serve as a HTTP server
openapi.serve_as_http_daemon(port=8000, run_in_background=True)
```
53 changes: 53 additions & 0 deletions defspec/server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
from __future__ import annotations

import sys
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from threading import Thread

from defspec.template import RenderTemplate

if sys.version_info >= (3, 11):
from typing import Self
else:
from typing_extensions import Self


class OpenAPIHandler(BaseHTTPRequestHandler):
spec: bytes

@classmethod
def set_openapi_handler(cls, spec: bytes) -> Self:
cls.spec = spec
return cls

def do_GET(self):
print(self.path)
if self.path == "/openapi/spec.json":
return self.send_spec()
for template in RenderTemplate:
if self.path == f"/openapi/{template.name.lower()}":
return self.send_ui(template.value)
self.send_response(404, "Not Found")
self.end_headers()

def send_spec(self):
self.send_response(200)
self.send_header("Content-Type", "application/json; charset=utf-8")
self.end_headers()
self.wfile.write(self.spec)

def send_ui(self, template: str):
content = template.format(spec_url="/openapi/spec.json")
self.send_response(200)
self.send_header("Content-Type", "text/html; charset=utf-8")
self.end_headers()
self.wfile.write(content.encode())


def serve_openapi_http_daemon(host: str, port: int, daemon: bool, spec: bytes):
server = ThreadingHTTPServer((host, port), OpenAPIHandler.set_openapi_handler(spec))
if daemon:
thread = Thread(target=server.serve_forever, daemon=True)
thread.start()
return
server.serve_forever()
35 changes: 34 additions & 1 deletion defspec/spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

import msgspec

from defspec.server import serve_openapi_http_daemon

if sys.version_info >= (3, 11):
from typing import Self
else:
Expand Down Expand Up @@ -76,6 +78,20 @@ class OpenAPIComponent(msgspec.Struct, kw_only=True, omit_defaults=True):
]


__MSGSPEC_STRUCT_DOC__ = inspect.getdoc(msgspec.Struct)


def get_def_doc(obj: Type) -> str:
"""Get the docstring of a type."""
doc = inspect.getdoc(obj)
if doc is None:
return ""
# `inspect.getdoc` will traverse the __mro__ of a type, so we need to check
if doc == __MSGSPEC_STRUCT_DOC__:
return ""
return doc


class OpenAPI(msgspec.Struct, kw_only=True):
"""OpenAPI specification.
Expand Down Expand Up @@ -145,7 +161,7 @@ def register_route(
name=param_type.__name__,
located_in=param_location,
schema=schema,
description=inspect.getdoc(param_type) or "",
description=get_def_doc(param_type),
)
)

Expand All @@ -156,3 +172,20 @@ def to_json(self) -> bytes:
def to_dict(self) -> dict:
"""Convert to a dict."""
return msgspec.to_builtins(self)

def serve_as_http_daemon(
self, host: str = "127.0.0.1", port: int = 8080, run_in_background: bool = False
):
"""Serve the OpenAPI specification and UI as a HTTP server.
- `/openapi/spec.json`: the OpenAPI specification
- `/openapi/swagger`: the Swagger UI
- `/openapi/redoc`: the ReDoc UI
- `/openapi/scalar`: the Scalar UI
Args:
host: host to serve
port: port to serve
run_in_background: whether to run in a daemon thread
"""
serve_openapi_http_daemon(host, port, run_in_background, self.to_json())
6 changes: 3 additions & 3 deletions examples/falcon_openapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,17 @@
from defspec import OpenAPI, RenderTemplate


class JSONRequest(msgspec.Struct):
class JSONRequest(msgspec.Struct, frozen=True):
title: str
timeout: float


class Query(msgspec.Struct):
class Query(msgspec.Struct, frozen=True):
limit: int
offset: int


class JSONResponse(msgspec.Struct):
class JSONResponse(msgspec.Struct, frozen=True):
elapsed: float
queries: list[Query]

Expand Down

0 comments on commit a070551

Please sign in to comment.