mirror of
https://github.com/checktheroads/hyperglass
synced 2024-05-11 05:55:08 +00:00
add fastapi docs config model
This commit is contained in:
@@ -4,7 +4,7 @@
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
# Third Party Imports
|
# Third Party Imports
|
||||||
from pydantic import BaseSettings
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
def clean_name(_name):
|
def clean_name(_name):
|
||||||
@@ -25,7 +25,7 @@ def clean_name(_name):
|
|||||||
return _scrubbed.lower()
|
return _scrubbed.lower()
|
||||||
|
|
||||||
|
|
||||||
class HyperglassModel(BaseSettings):
|
class HyperglassModel(BaseModel):
|
||||||
"""Base model for all hyperglass configuration models."""
|
"""Base model for all hyperglass configuration models."""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
@@ -51,3 +51,29 @@ class HyperglassModelExtra(HyperglassModel):
|
|||||||
"""Default pydantic configuration."""
|
"""Default pydantic configuration."""
|
||||||
|
|
||||||
extra = "allow"
|
extra = "allow"
|
||||||
|
|
||||||
|
|
||||||
|
class AnyUri(str):
|
||||||
|
"""Custom field type for HTTP URI, e.g. /example."""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def __get_validators__(cls):
|
||||||
|
"""Pydantic custim field method."""
|
||||||
|
yield cls.validate
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def validate(cls, value):
|
||||||
|
"""Ensure URI string contains a leading forward-slash."""
|
||||||
|
uri_regex = re.compile(r"^(\/.*)$")
|
||||||
|
if not isinstance(value, str):
|
||||||
|
raise TypeError("AnyUri type must be a string")
|
||||||
|
match = uri_regex.fullmatch(value)
|
||||||
|
if not match:
|
||||||
|
raise ValueError(
|
||||||
|
"Invalid format. A URI must begin with a forward slash, e.g. '/example'"
|
||||||
|
)
|
||||||
|
return cls(match.group())
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
"""Stringify custom field representation."""
|
||||||
|
return f"AnyUri({super().__repr__()})"
|
||||||
|
@@ -18,6 +18,7 @@ from pydantic import validator
|
|||||||
|
|
||||||
# Project Imports
|
# Project Imports
|
||||||
from hyperglass.configuration.models._utils import HyperglassModel
|
from hyperglass.configuration.models._utils import HyperglassModel
|
||||||
|
from hyperglass.configuration.models.docs import Docs
|
||||||
from hyperglass.configuration.models.opengraph import OpenGraph
|
from hyperglass.configuration.models.opengraph import OpenGraph
|
||||||
|
|
||||||
|
|
||||||
@@ -46,6 +47,7 @@ class General(HyperglassModel):
|
|||||||
"isp",
|
"isp",
|
||||||
]
|
]
|
||||||
opengraph: OpenGraph = OpenGraph()
|
opengraph: OpenGraph = OpenGraph()
|
||||||
|
docs: Docs = Docs()
|
||||||
google_analytics: StrictStr = ""
|
google_analytics: StrictStr = ""
|
||||||
redis_host: StrictStr = "localhost"
|
redis_host: StrictStr = "localhost"
|
||||||
redis_port: StrictInt = 6379
|
redis_port: StrictInt = 6379
|
||||||
@@ -54,6 +56,7 @@ class General(HyperglassModel):
|
|||||||
listen_address: Optional[Union[IPvAnyAddress, StrictStr]]
|
listen_address: Optional[Union[IPvAnyAddress, StrictStr]]
|
||||||
listen_port: StrictInt = 8001
|
listen_port: StrictInt = 8001
|
||||||
log_file: Optional[FilePath]
|
log_file: Optional[FilePath]
|
||||||
|
cors_origins: List[StrictStr] = []
|
||||||
|
|
||||||
@validator("listen_address", pre=True, always=True)
|
@validator("listen_address", pre=True, always=True)
|
||||||
def validate_listen_address(cls, value, values):
|
def validate_listen_address(cls, value, values):
|
||||||
|
@@ -1,7 +1,6 @@
|
|||||||
"""Hyperglass Front End."""
|
"""Hyperglass Front End."""
|
||||||
|
|
||||||
# Standard Library Imports
|
# Standard Library Imports
|
||||||
import asyncio
|
|
||||||
import os
|
import os
|
||||||
import tempfile
|
import tempfile
|
||||||
import time
|
import time
|
||||||
@@ -17,12 +16,14 @@ from prometheus_client import Counter
|
|||||||
from prometheus_client import generate_latest
|
from prometheus_client import generate_latest
|
||||||
from prometheus_client import multiprocess
|
from prometheus_client import multiprocess
|
||||||
from starlette.exceptions import HTTPException as StarletteHTTPException
|
from starlette.exceptions import HTTPException as StarletteHTTPException
|
||||||
|
from starlette.middleware.cors import CORSMiddleware
|
||||||
from starlette.requests import Request
|
from starlette.requests import Request
|
||||||
from starlette.responses import PlainTextResponse
|
from starlette.responses import PlainTextResponse
|
||||||
from starlette.responses import UJSONResponse
|
from starlette.responses import UJSONResponse
|
||||||
from starlette.staticfiles import StaticFiles
|
from starlette.staticfiles import StaticFiles
|
||||||
|
|
||||||
# Project Imports
|
# Project Imports
|
||||||
|
from hyperglass import __version__
|
||||||
from hyperglass.configuration import frontend_params
|
from hyperglass.configuration import frontend_params
|
||||||
from hyperglass.configuration import params
|
from hyperglass.configuration import params
|
||||||
from hyperglass.exceptions import AuthError
|
from hyperglass.exceptions import AuthError
|
||||||
@@ -37,6 +38,7 @@ from hyperglass.execution.execute import Execute
|
|||||||
from hyperglass.models.query import Query
|
from hyperglass.models.query import Query
|
||||||
from hyperglass.util import check_python
|
from hyperglass.util import check_python
|
||||||
from hyperglass.util import log
|
from hyperglass.util import log
|
||||||
|
from hyperglass.util import write_env
|
||||||
|
|
||||||
# Verify Python version meets minimum requirement
|
# Verify Python version meets minimum requirement
|
||||||
try:
|
try:
|
||||||
@@ -57,14 +59,41 @@ IMAGES_DIR = STATIC_DIR / "images"
|
|||||||
NEXT_DIR = UI_DIR / "_next"
|
NEXT_DIR = UI_DIR / "_next"
|
||||||
log.debug(f"Static Files: {STATIC_DIR}")
|
log.debug(f"Static Files: {STATIC_DIR}")
|
||||||
|
|
||||||
|
docs_mode_map = {"swagger": "docs_url", "redoc": "redoc_url"}
|
||||||
|
|
||||||
|
docs_config = {"docs_url": None, "redoc_url": None}
|
||||||
|
|
||||||
|
if params.general.docs.enable:
|
||||||
|
if params.general.docs.mode == "swagger":
|
||||||
|
docs_config["docs_url"] = params.general.docs.uri
|
||||||
|
docs_config["redoc_url"] = None
|
||||||
|
elif params.general.docs.mode == "redoc":
|
||||||
|
docs_config["docs_url"] = None
|
||||||
|
docs_config["redoc_url"] = params.general.docs.uri
|
||||||
|
|
||||||
|
|
||||||
# Main App Definition
|
# Main App Definition
|
||||||
app = FastAPI()
|
app = FastAPI(
|
||||||
|
debug=params.general.debug,
|
||||||
|
title=params.general.site_title,
|
||||||
|
description=params.general.site_description,
|
||||||
|
version=__version__,
|
||||||
|
**docs_config,
|
||||||
|
)
|
||||||
app.mount("/ui", StaticFiles(directory=UI_DIR), name="ui")
|
app.mount("/ui", StaticFiles(directory=UI_DIR), name="ui")
|
||||||
app.mount("/_next", StaticFiles(directory=NEXT_DIR), name="_next")
|
app.mount("/_next", StaticFiles(directory=NEXT_DIR), name="_next")
|
||||||
app.mount("/images", StaticFiles(directory=IMAGES_DIR), name="images")
|
app.mount("/images", StaticFiles(directory=IMAGES_DIR), name="images")
|
||||||
app.mount("/ui/images", StaticFiles(directory=IMAGES_DIR), name="ui/images")
|
app.mount("/ui/images", StaticFiles(directory=IMAGES_DIR), name="ui/images")
|
||||||
|
|
||||||
APP_PARAMS = {
|
# CORS Configuration
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=params.general.cors_origins,
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
|
||||||
|
ASGI_PARAMS = {
|
||||||
"host": str(params.general.listen_address),
|
"host": str(params.general.listen_address),
|
||||||
"port": params.general.listen_port,
|
"port": params.general.listen_port,
|
||||||
"debug": params.general.debug,
|
"debug": params.general.debug,
|
||||||
@@ -72,7 +101,7 @@ APP_PARAMS = {
|
|||||||
|
|
||||||
# Redis Config
|
# Redis Config
|
||||||
redis_config = {
|
redis_config = {
|
||||||
"host": params.general.redis_host,
|
"host": str(params.general.redis_host),
|
||||||
"port": params.general.redis_port,
|
"port": params.general.redis_port,
|
||||||
"decode_responses": True,
|
"decode_responses": True,
|
||||||
}
|
}
|
||||||
@@ -80,6 +109,7 @@ redis_config = {
|
|||||||
r_cache = aredis.StrictRedis(db=params.features.cache.redis_id, **redis_config)
|
r_cache = aredis.StrictRedis(db=params.features.cache.redis_id, **redis_config)
|
||||||
|
|
||||||
|
|
||||||
|
@app.on_event("startup")
|
||||||
async def check_redis():
|
async def check_redis():
|
||||||
"""Ensure Redis is running before starting server.
|
"""Ensure Redis is running before starting server.
|
||||||
|
|
||||||
@@ -89,19 +119,29 @@ async def check_redis():
|
|||||||
Returns:
|
Returns:
|
||||||
{bool} -- True if Redis is running.
|
{bool} -- True if Redis is running.
|
||||||
"""
|
"""
|
||||||
|
redis_host = redis_config["host"]
|
||||||
|
redis_port = redis_config["port"]
|
||||||
try:
|
try:
|
||||||
await r_cache.echo("hyperglass test")
|
await r_cache.echo("hyperglass test")
|
||||||
# await r_limiter.echo("hyperglass test")
|
|
||||||
except Exception:
|
except Exception:
|
||||||
raise HyperglassError(
|
raise HyperglassError(
|
||||||
f"Redis isn't running at: {redis_config['host']}:{redis_config['port']}",
|
f"Redis isn't running at: {redis_host}:{redis_port}", alert="danger"
|
||||||
alert="danger",
|
|
||||||
) from None
|
) from None
|
||||||
|
log.debug(f"Redis is running at: {redis_host}:{redis_port}")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
# Verify Redis is running
|
@app.on_event("startup")
|
||||||
asyncio.run(check_redis())
|
async def write_env_variables():
|
||||||
|
"""Write environment varibles for Next.js/Node.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
{bool} -- True if successful
|
||||||
|
"""
|
||||||
|
result = await write_env({"NODE_ENV": "production", "_HYPERGLASS_URL_": "/"})
|
||||||
|
if result:
|
||||||
|
log.debug(result)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
# Prometheus Config
|
# Prometheus Config
|
||||||
@@ -141,20 +181,16 @@ async def metrics(request):
|
|||||||
|
|
||||||
@app.exception_handler(StarletteHTTPException)
|
@app.exception_handler(StarletteHTTPException)
|
||||||
async def http_exception_handler(request, exc):
|
async def http_exception_handler(request, exc):
|
||||||
"""Handle application errors."""
|
"""Handle web server errors."""
|
||||||
return UJSONResponse(
|
return UJSONResponse(
|
||||||
{
|
{"output": exc.detail, "alert": "danger", "keywords": []},
|
||||||
"output": exc.detail.get("message", params.messages.general),
|
|
||||||
"alert": exc.detail.get("alert", "error"),
|
|
||||||
"keywords": exc.detail.get("keywords", []),
|
|
||||||
},
|
|
||||||
status_code=exc.status_code,
|
status_code=exc.status_code,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.exception_handler(HyperglassError)
|
@app.exception_handler(HyperglassError)
|
||||||
async def http_exception_handler(request, exc):
|
async def http_exception_handler(request, exc):
|
||||||
"""Handle request validation errors."""
|
"""Handle application errors."""
|
||||||
return UJSONResponse(
|
return UJSONResponse(
|
||||||
{"output": exc.message, "alert": exc.alert, "keywords": exc.keywords},
|
{"output": exc.message, "alert": exc.alert, "keywords": exc.keywords},
|
||||||
status_code=400,
|
status_code=400,
|
||||||
|
@@ -4,9 +4,9 @@
|
|||||||
def start():
|
def start():
|
||||||
"""Start the web server with Uvicorn ASGI."""
|
"""Start the web server with Uvicorn ASGI."""
|
||||||
import uvicorn
|
import uvicorn
|
||||||
from hyperglass.hyperglass import app, APP_PARAMS
|
from hyperglass.hyperglass import app, ASGI_PARAMS
|
||||||
|
|
||||||
uvicorn.run(app, **APP_PARAMS)
|
uvicorn.run(app, **ASGI_PARAMS)
|
||||||
|
|
||||||
|
|
||||||
app = start()
|
app = start()
|
||||||
|
13
manage.py
13
manage.py
@@ -611,21 +611,22 @@ def build_ui():
|
|||||||
def dev_server(host, port, build):
|
def dev_server(host, port, build):
|
||||||
"""Renders theme and web build, then starts dev web server"""
|
"""Renders theme and web build, then starts dev web server"""
|
||||||
try:
|
try:
|
||||||
from hyperglass.hyperglass import app, APP_PARAMS
|
from hyperglass.hyperglass import app, ASGI_PARAMS
|
||||||
except ImportError as import_error:
|
except ImportError as import_error:
|
||||||
raise click.ClickException(
|
raise click.ClickException(
|
||||||
click.style("✗ Error importing hyperglass: ", fg="red", bold=True)
|
click.style("✗ Error importing hyperglass: ", fg="red", bold=True)
|
||||||
+ click.style(import_error, fg="blue")
|
+ click.style(import_error, fg="blue")
|
||||||
)
|
)
|
||||||
|
asgi_params = ASGI_PARAMS.copy()
|
||||||
if host is not None:
|
if host is not None:
|
||||||
APP_PARAMS["host"] = host
|
asgi_params["host"] = host
|
||||||
if port is not None:
|
if port is not None:
|
||||||
APP_PARAMS["port"] = port
|
asgi_params["port"] = port
|
||||||
|
|
||||||
write_env_variables(
|
write_env_variables(
|
||||||
{
|
{
|
||||||
"NODE_ENV": "development",
|
"NODE_ENV": "development",
|
||||||
"_HYPERGLASS_URL_": f'http://{APP_PARAMS["host"]}:{APP_PARAMS["port"]}/',
|
"_HYPERGLASS_URL_": f'http://{asgi_params["host"]}:{asgi_params["port"]}/',
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
if build:
|
if build:
|
||||||
@@ -637,9 +638,9 @@ def dev_server(host, port, build):
|
|||||||
+ click.style(e, fg="white")
|
+ click.style(e, fg="white")
|
||||||
) from None
|
) from None
|
||||||
if build_complete:
|
if build_complete:
|
||||||
start_dev_server(app, APP_PARAMS)
|
start_dev_server(app, asgi_params)
|
||||||
if not build:
|
if not build:
|
||||||
start_dev_server(app, APP_PARAMS)
|
start_dev_server(app, asgi_params)
|
||||||
|
|
||||||
|
|
||||||
@hg.command("migrate-configs", help="Copy YAML examples to usable config files")
|
@hg.command("migrate-configs", help="Copy YAML examples to usable config files")
|
||||||
|
Reference in New Issue
Block a user