Add WebAPI

This commit is contained in:
2023-07-11 18:26:26 +03:00
parent 9b00552912
commit c428479110
18 changed files with 319 additions and 31 deletions

View File

@@ -0,0 +1,2 @@
from .app import web_app
from .app import _stop

View File

@@ -0,0 +1,129 @@
import asyncio
from asyncio import CancelledError
import uvicorn
from fastapi import FastAPI, Request, HTTPException
from fastapi.exceptions import RequestValidationError
from starlette import status
from starlette.exceptions import HTTPException as StarletteHTTPException
from starlette.responses import JSONResponse
from uvicorn.config import LOGGING_CONFIG
import core.utils
from . import utils
# from .models import SecretKey
web_app = FastAPI()
log = core.utils.get_logger("web")
uvserver = None
data_pool = []
data_run = [True]
LOGGING_CONFIG["formatters"]["default"]['fmt'] = core.utils.log_format
LOGGING_CONFIG["formatters"]["access"]["fmt"] = core.utils.log_format_access
LOGGING_CONFIG["formatters"].update({
"file_default": {
"fmt": core.utils.log_format
},
"file_access": {
"fmt": core.utils.log_format_access
}
})
LOGGING_CONFIG["handlers"]["default"]['stream'] = "ext://sys.stdout"
LOGGING_CONFIG["handlers"].update({
"file_default": {
"class": "logging.handlers.RotatingFileHandler",
"filename": "webserver.log"
},
"file_access": {
"class": "logging.handlers.RotatingFileHandler",
"filename": "webserver.log"
}
})
LOGGING_CONFIG["loggers"]["uvicorn"]["handlers"].append("file_default")
LOGGING_CONFIG["loggers"]["uvicorn.access"]["handlers"].append("file_access")
def response(data=None, code=status.HTTP_200_OK, error_code=0, error_message=None):
if 200 >= code <= 300:
return JSONResponse(content={"result": data, "error": None}, status_code=code)
return JSONResponse(
content={"error": {"code": error_code if error_code else code, "message": f"{error_message}"}, "result": None},
status_code=code)
@web_app.get("/")
async def index():
log.debug("Request IndexPage;")
return response("Index page")
@web_app.get("/method/{method}")
async def _method(method, secret_key: str = None):
# log.debug(f"Request method; kwargs: {kwargs}")
is_auth = secret_key == config.WebAPI["secret_key"]
spl = method.split(".")
if len(spl) != 2:
raise StarletteHTTPException(405)
api_class, api_method = spl
match api_class:
case "events":
match api_method, is_auth:
case "get", False:
return response(data_pool)
raise StarletteHTTPException(404)
async def _stop():
await asyncio.sleep(1)
uvserver.should_exit = True
data_run[0] = False
@web_app.get("/stop")
async def stop(secret_key: str):
log.debug(f"Request stop; secret key: {secret_key}")
if secret_key == config.WebAPI["secret_key"]:
log.info("Stopping Web server")
asyncio.create_task(_stop())
return response("Web server stopped")
@web_app.exception_handler(HTTPException)
async def default_exception_handler(request: Request, exc: HTTPException):
return response(
code=status.HTTP_500_INTERNAL_SERVER_ERROR,
error_code=exc.status_code, error_message=f"Internal Server Error: {exc.status_code}"
)
@web_app.exception_handler(StarletteHTTPException)
async def http_exception_handler(request: Request, exc: StarletteHTTPException):
code = exc.status_code
if code == status.HTTP_405_METHOD_NOT_ALLOWED:
return response(code=code, error_message="Method Not Allowed")
if code == status.HTTP_404_NOT_FOUND:
return response(code=code, error_message="Method not Found")
return response(code=code, error_message="Unhandled error..")
@web_app.exception_handler(RequestValidationError)
async def request_validation_exception_handler(request: Request, exc: RequestValidationError):
code = status.HTTP_422_UNPROCESSABLE_ENTITY
return response(code=code, error_message="Request Validation Error")
utils.hack_fastapi()
if __name__ == '__main__':
try:
uvconfig = uvicorn.Config(web_app,
host=config.WebAPI["server_ip"],
port=config.WebAPI["server_port"],
loop="asyncio")
uvserver = uvicorn.Server(uvconfig)
uvserver.run()
except KeyboardInterrupt or CancelledError:
pass

View File

View File

@@ -0,0 +1,87 @@
import asyncio
import sys
import click
from uvicorn.server import Server, logger
from uvicorn.lifespan import on
def ev_log_started_message(self, listeners) -> None:
cfg = self.config
if cfg.fd is not None:
sock = listeners[0]
logger.info(i18n.web_start.format(sock.getsockname()))
elif cfg.uds is not None:
logger.info(i18n.web_start.format(cfg.uds))
else:
addr_format = "%s://%s:%d"
host = "0.0.0.0" if cfg.host is None else cfg.host
if ":" in host:
addr_format = "%s://[%s]:%d"
port = cfg.port
if port == 0:
port = listeners[0].getsockname()[1]
protocol_name = "https" if cfg.ssl else "http"
message = i18n.web_start.format(addr_format)
color_message = (i18n.web_start.format(click.style(addr_format, bold=True)))
logger.info(message, protocol_name, host, port, extra={"color_message": color_message})
async def ev_shutdown(self, sockets=None) -> None:
logger.debug("Shutting down")
for server in self.servers:
server.close()
for sock in sockets or []:
sock.close()
for server in self.servers:
await server.wait_closed()
for connection in list(self.server_state.connections):
connection.shutdown()
await asyncio.sleep(0.1)
try:
await asyncio.wait_for(self._wait_tasks_to_complete(), timeout=self.config.timeout_graceful_shutdown)
except asyncio.TimeoutError:
logger.error("Cancel %s running task(s), timeout graceful shutdown exceeded",len(self.server_state.tasks))
for t in self.server_state.tasks:
if sys.version_info < (3, 9):
t.cancel()
else:
t.cancel(msg="Task cancelled, timeout graceful shutdown exceeded")
if not self.force_exit:
await self.lifespan.shutdown()
async def on_startup(self) -> None:
self.logger.debug("Waiting for application startup.")
loop = asyncio.get_event_loop()
main_lifespan_task = loop.create_task(self.main()) # noqa: F841
startup_event = {"type": "lifespan.startup"}
await self.receive_queue.put(startup_event)
await self.startup_event.wait()
if self.startup_failed or (self.error_occured and self.config.lifespan == "on"):
self.logger.error("Application startup failed. Exiting.")
self.should_exit = True
else:
self.logger.debug("Application startup complete.")
async def on_shutdown(self) -> None:
if self.error_occured:
return
self.logger.debug("Waiting for application shutdown.")
shutdown_event = {"type": "lifespan.shutdown"}
await self.receive_queue.put(shutdown_event)
await self.shutdown_event.wait()
if self.shutdown_failed or (self.error_occured and self.config.lifespan == "on"):
self.logger.error("Application shutdown failed. Exiting.")
self.should_exit = True
else:
self.logger.debug("Application shutdown complete.")
def hack_fastapi():
Server.shutdown = ev_shutdown
Server._log_started_message = ev_log_started_message
on.LifespanOn.startup = on_startup
on.LifespanOn.shutdown = on_shutdown