forked from mirrors/thatmattlove-hyperglass
initial migration to litestar
This commit is contained in:
parent
1ef376fb38
commit
d2e1486b5a
19 changed files with 356 additions and 439 deletions
|
|
@ -1,253 +1,62 @@
|
||||||
"""hyperglass REST API & Web UI."""
|
"""hyperglass API."""
|
||||||
|
|
||||||
# Standard Library
|
|
||||||
import sys
|
|
||||||
from typing import List
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
# Third Party
|
# Third Party
|
||||||
from fastapi import FastAPI
|
from litestar import Litestar
|
||||||
from fastapi.responses import JSONResponse
|
from litestar.openapi import OpenAPIConfig
|
||||||
from fastapi.exceptions import ValidationException, RequestValidationError
|
from litestar.exceptions import HTTPException, ValidationException
|
||||||
from fastapi.staticfiles import StaticFiles
|
from litestar.static_files import create_static_files_router
|
||||||
from starlette.exceptions import HTTPException as StarletteHTTPException
|
|
||||||
from fastapi.openapi.utils import get_openapi
|
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
|
||||||
from fastapi.middleware.gzip import GZipMiddleware
|
|
||||||
|
|
||||||
# Project
|
# Project
|
||||||
from hyperglass.log import log
|
|
||||||
from hyperglass.util import cpu_count
|
|
||||||
from hyperglass.state import use_state
|
from hyperglass.state import use_state
|
||||||
from hyperglass.constants import __version__
|
from hyperglass.constants import __version__
|
||||||
from hyperglass.models.ui import UIParameters
|
|
||||||
from hyperglass.api.events import on_startup, on_shutdown
|
|
||||||
from hyperglass.api.routes import docs, info, query, router, queries, routers, ui_props
|
|
||||||
from hyperglass.exceptions import HyperglassError
|
from hyperglass.exceptions import HyperglassError
|
||||||
from hyperglass.api.error_handlers import (
|
|
||||||
app_handler,
|
# Local
|
||||||
http_handler,
|
from .events import check_redis
|
||||||
default_handler,
|
from .routes import info, query, device, devices, queries
|
||||||
validation_handler,
|
from .middleware import COMPRESSION_CONFIG, create_cors_config
|
||||||
)
|
from .error_handlers import app_handler, http_handler, default_handler, validation_handler
|
||||||
from hyperglass.models.api.response import (
|
|
||||||
QueryError,
|
__all__ = ("app",)
|
||||||
InfoResponse,
|
|
||||||
QueryResponse,
|
|
||||||
RoutersResponse,
|
|
||||||
SupportedQueryResponse,
|
|
||||||
)
|
|
||||||
|
|
||||||
STATE = use_state()
|
STATE = use_state()
|
||||||
|
|
||||||
WORKING_DIR = Path(__file__).parent
|
|
||||||
EXAMPLES_DIR = WORKING_DIR / "examples"
|
|
||||||
|
|
||||||
UI_DIR = STATE.settings.static_path / "ui"
|
UI_DIR = STATE.settings.static_path / "ui"
|
||||||
IMAGES_DIR = STATE.settings.static_path / "images"
|
IMAGES_DIR = STATE.settings.static_path / "images"
|
||||||
|
|
||||||
EXAMPLE_DEVICES_PY = EXAMPLES_DIR / "devices.py"
|
|
||||||
EXAMPLE_QUERIES_PY = EXAMPLES_DIR / "queries.py"
|
|
||||||
EXAMPLE_QUERY_PY = EXAMPLES_DIR / "query.py"
|
|
||||||
EXAMPLE_DEVICES_CURL = EXAMPLES_DIR / "devices.sh"
|
|
||||||
EXAMPLE_QUERIES_CURL = EXAMPLES_DIR / "queries.sh"
|
|
||||||
EXAMPLE_QUERY_CURL = EXAMPLES_DIR / "query.sh"
|
|
||||||
|
|
||||||
ASGI_PARAMS = {
|
OPEN_API = OpenAPIConfig(
|
||||||
"host": str(STATE.settings.host),
|
|
||||||
"port": STATE.settings.port,
|
|
||||||
"debug": STATE.settings.debug,
|
|
||||||
"workers": cpu_count(2),
|
|
||||||
}
|
|
||||||
DOCS_PARAMS = {}
|
|
||||||
if STATE.params.docs.enable:
|
|
||||||
DOCS_PARAMS.update({"openapi_url": "/openapi.json"})
|
|
||||||
if STATE.params.docs.mode == "redoc":
|
|
||||||
DOCS_PARAMS.update({"docs_url": None, "redoc_url": STATE.params.docs.path})
|
|
||||||
elif STATE.params.docs.mode == "swagger":
|
|
||||||
DOCS_PARAMS.update({"docs_url": STATE.params.docs.path, "redoc_url": None})
|
|
||||||
|
|
||||||
for directory in (UI_DIR, IMAGES_DIR):
|
|
||||||
if not directory.exists():
|
|
||||||
log.warning("Directory '{d}' does not exist, creating...", d=str(directory))
|
|
||||||
directory.mkdir()
|
|
||||||
|
|
||||||
# Main App Definition
|
|
||||||
app = FastAPI(
|
|
||||||
debug=STATE.settings.debug,
|
|
||||||
title=STATE.params.site_title,
|
|
||||||
description=STATE.params.site_description,
|
|
||||||
version=__version__,
|
|
||||||
default_response_class=JSONResponse,
|
|
||||||
**DOCS_PARAMS,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Add Event Handlers
|
|
||||||
for startup in on_startup:
|
|
||||||
app.add_event_handler("startup", startup)
|
|
||||||
|
|
||||||
for shutdown in on_shutdown:
|
|
||||||
app.add_event_handler("shutdown", shutdown)
|
|
||||||
|
|
||||||
# HTTP Error Handler
|
|
||||||
app.add_exception_handler(StarletteHTTPException, http_handler)
|
|
||||||
|
|
||||||
# Backend Application Error Handler
|
|
||||||
app.add_exception_handler(HyperglassError, app_handler)
|
|
||||||
|
|
||||||
# Request Validation Error Handler
|
|
||||||
app.add_exception_handler(RequestValidationError, validation_handler)
|
|
||||||
|
|
||||||
# App Validation Error Handler
|
|
||||||
app.add_exception_handler(ValidationException, validation_handler)
|
|
||||||
|
|
||||||
# Uncaught Error Handler
|
|
||||||
app.add_exception_handler(Exception, default_handler)
|
|
||||||
|
|
||||||
|
|
||||||
def _custom_openapi():
|
|
||||||
"""Generate custom OpenAPI config."""
|
|
||||||
openapi_schema = get_openapi(
|
|
||||||
title=STATE.params.docs.title.format(site_title=STATE.params.site_title),
|
title=STATE.params.docs.title.format(site_title=STATE.params.site_title),
|
||||||
version=__version__,
|
version=__version__,
|
||||||
description=STATE.params.docs.description,
|
description=STATE.params.docs.description,
|
||||||
routes=app.routes,
|
path=STATE.params.docs.path,
|
||||||
)
|
root_schema_site="elements",
|
||||||
openapi_schema["info"]["x-logo"] = {"url": "/images/light" + STATE.params.web.logo.light.suffix}
|
|
||||||
|
|
||||||
query_samples = []
|
|
||||||
queries_samples = []
|
|
||||||
devices_samples = []
|
|
||||||
|
|
||||||
with EXAMPLE_QUERY_CURL.open("r") as e:
|
|
||||||
example = e.read()
|
|
||||||
query_samples.append({"lang": "cURL", "source": example % str(STATE.params.docs.base_url)})
|
|
||||||
|
|
||||||
with EXAMPLE_QUERY_PY.open("r") as e:
|
|
||||||
example = e.read()
|
|
||||||
query_samples.append(
|
|
||||||
{"lang": "Python", "source": example % str(STATE.params.docs.base_url)}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
with EXAMPLE_DEVICES_CURL.open("r") as e:
|
|
||||||
example = e.read()
|
|
||||||
queries_samples.append(
|
|
||||||
{"lang": "cURL", "source": example % str(STATE.params.docs.base_url)}
|
|
||||||
)
|
|
||||||
with EXAMPLE_DEVICES_PY.open("r") as e:
|
|
||||||
example = e.read()
|
|
||||||
queries_samples.append(
|
|
||||||
{"lang": "Python", "source": example % str(STATE.params.docs.base_url)}
|
|
||||||
)
|
|
||||||
|
|
||||||
with EXAMPLE_QUERIES_CURL.open("r") as e:
|
app = Litestar(
|
||||||
example = e.read()
|
route_handlers=[
|
||||||
devices_samples.append(
|
device,
|
||||||
{"lang": "cURL", "source": example % str(STATE.params.docs.base_url)}
|
devices,
|
||||||
)
|
queries,
|
||||||
|
info,
|
||||||
with EXAMPLE_QUERIES_PY.open("r") as e:
|
query,
|
||||||
example = e.read()
|
create_static_files_router(
|
||||||
devices_samples.append(
|
path="/images", directories=[IMAGES_DIR], name="images", include_in_schema=False
|
||||||
{"lang": "Python", "source": example % str(STATE.params.docs.base_url)}
|
),
|
||||||
)
|
create_static_files_router(
|
||||||
|
path="/", directories=[UI_DIR], name="ui", html_mode=True, include_in_schema=False
|
||||||
openapi_schema["paths"]["/api/query/"]["post"]["x-code-samples"] = query_samples
|
),
|
||||||
openapi_schema["paths"]["/api/devices"]["get"]["x-code-samples"] = devices_samples
|
],
|
||||||
openapi_schema["paths"]["/api/queries"]["get"]["x-code-samples"] = queries_samples
|
exception_handlers={
|
||||||
|
HTTPException: http_handler,
|
||||||
app.openapi_schema = openapi_schema
|
HyperglassError: app_handler,
|
||||||
return app.openapi_schema
|
ValidationException: validation_handler,
|
||||||
|
Exception: default_handler,
|
||||||
|
|
||||||
CORS_ORIGINS = STATE.params.cors_origins.copy()
|
|
||||||
if STATE.settings.dev_mode:
|
|
||||||
CORS_ORIGINS = [*CORS_ORIGINS, STATE.settings.dev_url, "http://localhost:3000"]
|
|
||||||
|
|
||||||
# CORS Configuration
|
|
||||||
app.add_middleware(
|
|
||||||
CORSMiddleware,
|
|
||||||
allow_origins=CORS_ORIGINS,
|
|
||||||
allow_methods=["GET", "POST", "OPTIONS"],
|
|
||||||
allow_headers=["*"],
|
|
||||||
)
|
|
||||||
|
|
||||||
# GZIP Middleware
|
|
||||||
app.add_middleware(GZipMiddleware)
|
|
||||||
|
|
||||||
app.add_api_route(
|
|
||||||
path="/api/info",
|
|
||||||
endpoint=info,
|
|
||||||
methods=["GET"],
|
|
||||||
response_model=InfoResponse,
|
|
||||||
response_class=JSONResponse,
|
|
||||||
summary=STATE.params.docs.info.summary,
|
|
||||||
description=STATE.params.docs.info.description,
|
|
||||||
tags=[STATE.params.docs.info.title],
|
|
||||||
)
|
|
||||||
|
|
||||||
app.add_api_route(
|
|
||||||
path="/api/devices",
|
|
||||||
endpoint=routers,
|
|
||||||
methods=["GET"],
|
|
||||||
response_model=List[RoutersResponse],
|
|
||||||
response_class=JSONResponse,
|
|
||||||
summary=STATE.params.docs.devices.summary,
|
|
||||||
description=STATE.params.docs.devices.description,
|
|
||||||
tags=[STATE.params.docs.devices.title],
|
|
||||||
)
|
|
||||||
|
|
||||||
app.add_api_route(
|
|
||||||
path="/api/devices/{id}",
|
|
||||||
endpoint=router,
|
|
||||||
methods=["GET"],
|
|
||||||
response_model=RoutersResponse,
|
|
||||||
response_class=JSONResponse,
|
|
||||||
summary=STATE.params.docs.devices.summary,
|
|
||||||
description=STATE.params.docs.devices.description,
|
|
||||||
tags=[STATE.params.docs.devices.title],
|
|
||||||
)
|
|
||||||
|
|
||||||
app.add_api_route(
|
|
||||||
path="/api/queries",
|
|
||||||
endpoint=queries,
|
|
||||||
methods=["GET"],
|
|
||||||
response_class=JSONResponse,
|
|
||||||
response_model=List[SupportedQueryResponse],
|
|
||||||
summary=STATE.params.docs.queries.summary,
|
|
||||||
description=STATE.params.docs.queries.description,
|
|
||||||
tags=[STATE.params.docs.queries.title],
|
|
||||||
)
|
|
||||||
|
|
||||||
app.add_api_route(
|
|
||||||
path="/api/query",
|
|
||||||
endpoint=query,
|
|
||||||
methods=["POST"],
|
|
||||||
summary=STATE.params.docs.query.summary,
|
|
||||||
description=STATE.params.docs.query.description,
|
|
||||||
responses={
|
|
||||||
400: {"model": QueryError, "description": "Request Content Error"},
|
|
||||||
422: {"model": QueryError, "description": "Request Format Error"},
|
|
||||||
500: {"model": QueryError, "description": "Server Error"},
|
|
||||||
},
|
},
|
||||||
response_model=QueryResponse,
|
on_startup=[check_redis],
|
||||||
tags=[STATE.params.docs.query.title],
|
debug=STATE.settings.debug,
|
||||||
response_class=JSONResponse,
|
cors_config=create_cors_config(state=STATE),
|
||||||
|
compression_config=COMPRESSION_CONFIG,
|
||||||
|
openapi_config=OPEN_API if STATE.params.docs.enable else None,
|
||||||
)
|
)
|
||||||
|
|
||||||
app.add_api_route(
|
|
||||||
path="/ui/props/",
|
|
||||||
endpoint=ui_props,
|
|
||||||
methods=["GET", "OPTIONS"],
|
|
||||||
response_class=JSONResponse,
|
|
||||||
response_model=UIParameters,
|
|
||||||
response_model_by_alias=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
if STATE.params.docs.enable:
|
|
||||||
app.add_api_route(path=STATE.params.docs.path, endpoint=docs, include_in_schema=False)
|
|
||||||
app.openapi = _custom_openapi
|
|
||||||
|
|
||||||
app.mount("/images", StaticFiles(directory=IMAGES_DIR), name="images")
|
|
||||||
app.mount("/", StaticFiles(directory=UI_DIR, html=True), name="ui")
|
|
||||||
|
|
|
||||||
|
|
@ -1,49 +1,55 @@
|
||||||
"""API Error Handlers."""
|
"""API Error Handlers."""
|
||||||
|
|
||||||
# Third Party
|
# Third Party
|
||||||
from fastapi import Request
|
from litestar import Request, Response
|
||||||
from starlette.responses import JSONResponse
|
|
||||||
|
|
||||||
# Project
|
# Project
|
||||||
from hyperglass.log import log
|
from hyperglass.log import log
|
||||||
from hyperglass.state import use_state
|
from hyperglass.state import use_state
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
"default_handler",
|
||||||
|
"http_handler",
|
||||||
|
"app_handler",
|
||||||
|
"validation_handler",
|
||||||
|
)
|
||||||
|
|
||||||
async def default_handler(request: Request, exc: BaseException) -> JSONResponse:
|
|
||||||
|
def default_handler(request: Request, exc: BaseException) -> Response:
|
||||||
"""Handle uncaught errors."""
|
"""Handle uncaught errors."""
|
||||||
state = use_state()
|
state = use_state()
|
||||||
log.critical(
|
log.critical(
|
||||||
"{method} {path} {detail!s}", method=request.method, path=request.url.path, detail=exc
|
"{method} {path} {detail!s}", method=request.method, path=request.url.path, detail=exc
|
||||||
)
|
)
|
||||||
return JSONResponse(
|
return Response(
|
||||||
{"output": state.params.messages.general, "level": "danger", "keywords": []},
|
{"output": state.params.messages.general, "level": "danger", "keywords": []},
|
||||||
status_code=500,
|
status_code=500,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def http_handler(request: Request, exc: BaseException) -> JSONResponse:
|
def http_handler(request: Request, exc: BaseException) -> Response:
|
||||||
"""Handle web server errors."""
|
"""Handle web server errors."""
|
||||||
log.critical(
|
log.critical(
|
||||||
"{method} {path} {detail}", method=request.method, path=request.url.path, detail=exc.detail
|
"{method} {path} {detail}", method=request.method, path=request.url.path, detail=exc.detail
|
||||||
)
|
)
|
||||||
return JSONResponse(
|
return Response(
|
||||||
{"output": exc.detail, "level": "danger", "keywords": []},
|
{"output": exc.detail, "level": "danger", "keywords": []},
|
||||||
status_code=exc.status_code,
|
status_code=exc.status_code,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def app_handler(request: Request, exc: BaseException) -> JSONResponse:
|
def app_handler(request: Request, exc: BaseException) -> Response:
|
||||||
"""Handle application errors."""
|
"""Handle application errors."""
|
||||||
log.critical(
|
log.critical(
|
||||||
"{method} {path} {detail}", method=request.method, path=request.url.path, detail=exc.message
|
"{method} {path} {detail}", method=request.method, path=request.url.path, detail=exc.message
|
||||||
)
|
)
|
||||||
return JSONResponse(
|
return Response(
|
||||||
{"output": exc.message, "level": exc.level, "keywords": exc.keywords},
|
{"output": exc.message, "level": exc.level, "keywords": exc.keywords},
|
||||||
status_code=exc.status_code,
|
status_code=exc.status_code,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def validation_handler(request: Request, exc: BaseException) -> JSONResponse:
|
def validation_handler(request: Request, exc: BaseException) -> Response:
|
||||||
"""Handle Pydantic validation errors raised by FastAPI."""
|
"""Handle Pydantic validation errors raised by FastAPI."""
|
||||||
error = exc.errors()[0]
|
error = exc.errors()[0]
|
||||||
log.critical(
|
log.critical(
|
||||||
|
|
@ -52,7 +58,7 @@ async def validation_handler(request: Request, exc: BaseException) -> JSONRespon
|
||||||
path=request.url.path,
|
path=request.url.path,
|
||||||
detail=error["msg"],
|
detail=error["msg"],
|
||||||
)
|
)
|
||||||
return JSONResponse(
|
return Response(
|
||||||
{"output": error["msg"], "level": "error", "keywords": error["loc"]},
|
{"output": error["msg"], "level": "error", "keywords": error["loc"]},
|
||||||
status_code=422,
|
status_code=422,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,18 @@
|
||||||
"""API Events."""
|
"""API Events."""
|
||||||
|
|
||||||
|
# Standard Library
|
||||||
|
import typing as t
|
||||||
|
|
||||||
|
# Third Party
|
||||||
|
from litestar import Litestar
|
||||||
|
|
||||||
# Project
|
# Project
|
||||||
from hyperglass.state import use_state
|
from hyperglass.state import use_state
|
||||||
|
|
||||||
|
__all__ = ("check_redis",)
|
||||||
|
|
||||||
def check_redis() -> None:
|
|
||||||
|
async def check_redis(_: Litestar) -> t.NoReturn:
|
||||||
"""Ensure Redis is running before starting server."""
|
"""Ensure Redis is running before starting server."""
|
||||||
cache = use_state("cache")
|
cache = use_state("cache")
|
||||||
cache.check()
|
cache.check()
|
||||||
|
|
||||||
|
|
||||||
on_startup = (check_redis,)
|
|
||||||
on_shutdown = ()
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
# Third Party
|
|
||||||
import httpx
|
|
||||||
|
|
||||||
request = httpx.get("%s/api/devices")
|
|
||||||
|
|
||||||
print(request.json())
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
curl %s/api/devices
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
# Third Party
|
|
||||||
import httpx
|
|
||||||
|
|
||||||
request = httpx.get("%s/api/queries")
|
|
||||||
|
|
||||||
print(request.json())
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
curl %s/api/queries
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
# Third Party
|
|
||||||
import httpx
|
|
||||||
|
|
||||||
query = {
|
|
||||||
"query_location": "router01",
|
|
||||||
"query_type": "bgp_route",
|
|
||||||
"query_vrf": "default",
|
|
||||||
"query_target": "1.1.1.0/24",
|
|
||||||
}
|
|
||||||
|
|
||||||
request = httpx.post("%s/api/query/", data=query)
|
|
||||||
|
|
||||||
print(request.json().get("output"))
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
curl -X POST %s/api/query/ -d \
|
|
||||||
'{
|
|
||||||
"query_location": "router01",
|
|
||||||
"query_type": "bgp_route",
|
|
||||||
"query_vrf": "default",
|
|
||||||
"query_target": "1.1.1.0/24"
|
|
||||||
}'
|
|
||||||
34
hyperglass/api/middleware.py
Normal file
34
hyperglass/api/middleware.py
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
"""hyperglass API middleware."""
|
||||||
|
|
||||||
|
# Standard Library
|
||||||
|
import typing as t
|
||||||
|
|
||||||
|
# Third Party
|
||||||
|
from litestar.config.cors import CORSConfig
|
||||||
|
from litestar.config.compression import CompressionConfig
|
||||||
|
|
||||||
|
if t.TYPE_CHECKING:
|
||||||
|
# Project
|
||||||
|
from hyperglass.state import HyperglassState
|
||||||
|
|
||||||
|
__all__ = ("create_cors_config", "COMPRESSION_CONFIG")
|
||||||
|
|
||||||
|
COMPRESSION_CONFIG = CompressionConfig(backend="brotli", brotli_gzip_fallback=True)
|
||||||
|
|
||||||
|
REQUEST_LOG_MESSAGE = "REQ"
|
||||||
|
RESPONSE_LOG_MESSAGE = "RES"
|
||||||
|
REQUEST_LOG_FIELDS = ("method", "path", "path_params", "query")
|
||||||
|
RESPONSE_LOG_FIELDS = ("status_code",)
|
||||||
|
|
||||||
|
|
||||||
|
def create_cors_config(state: "HyperglassState") -> CORSConfig:
|
||||||
|
"""Create CORS configuration from parameters."""
|
||||||
|
origins = state.params.cors_origins.copy()
|
||||||
|
if state.settings.dev_mode:
|
||||||
|
origins = [*origins, state.settings.dev_url, "http://localhost:3000"]
|
||||||
|
|
||||||
|
return CORSConfig(
|
||||||
|
allow_origins=origins,
|
||||||
|
allow_methods=["GET", "POST", "OPTIONS"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
|
@ -3,69 +3,77 @@
|
||||||
# Standard Library
|
# Standard Library
|
||||||
import time
|
import time
|
||||||
import typing as t
|
import typing as t
|
||||||
from datetime import datetime
|
from datetime import UTC, datetime
|
||||||
|
|
||||||
# Third Party
|
# Third Party
|
||||||
from fastapi import Depends, Request, HTTPException, BackgroundTasks
|
from litestar import Request, Response, get, post
|
||||||
from fastapi.openapi.docs import get_redoc_html, get_swagger_ui_html
|
from litestar.di import Provide
|
||||||
|
from litestar.background_tasks import BackgroundTask
|
||||||
|
|
||||||
# Project
|
# Project
|
||||||
from hyperglass.log import log
|
from hyperglass.log import log
|
||||||
from hyperglass.state import HyperglassState, use_state
|
from hyperglass.state import HyperglassState
|
||||||
from hyperglass.constants import __version__
|
|
||||||
from hyperglass.models.ui import UIParameters
|
|
||||||
from hyperglass.exceptions import HyperglassError
|
from hyperglass.exceptions import HyperglassError
|
||||||
from hyperglass.models.api import Query
|
from hyperglass.models.api import Query
|
||||||
from hyperglass.models.data import OutputDataModel
|
from hyperglass.models.data import OutputDataModel
|
||||||
from hyperglass.util.typing import is_type
|
from hyperglass.util.typing import is_type
|
||||||
from hyperglass.execution.main import execute
|
from hyperglass.execution.main import execute
|
||||||
from hyperglass.models.config.params import Params
|
from hyperglass.models.api.response import QueryResponse
|
||||||
from hyperglass.models.config.devices import Devices
|
from hyperglass.models.config.params import Params, APIParams
|
||||||
|
from hyperglass.models.config.devices import Devices, APIDevice
|
||||||
|
|
||||||
# Local
|
# Local
|
||||||
|
from .state import get_state, get_params, get_devices
|
||||||
from .tasks import send_webhook
|
from .tasks import send_webhook
|
||||||
from .fake_output import fake_output
|
from .fake_output import fake_output
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
def get_state(attr: t.Optional[str] = None):
|
"device",
|
||||||
"""Get hyperglass state as a FastAPI dependency."""
|
"devices",
|
||||||
return use_state(attr)
|
"queries",
|
||||||
|
"info",
|
||||||
|
"query",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_params():
|
@get("/api/devices/{id:str}", dependencies={"devices": Provide(get_devices)})
|
||||||
"""Get hyperglass params as FastAPI dependency."""
|
async def device(devices: Devices, id: str) -> APIDevice:
|
||||||
return use_state("params")
|
"""Retrieve a device by ID."""
|
||||||
|
return devices[id].export_api()
|
||||||
|
|
||||||
|
|
||||||
def get_devices():
|
@get("/api/devices", dependencies={"devices": Provide(get_devices)})
|
||||||
"""Get hyperglass devices as FastAPI dependency."""
|
async def devices(devices: Devices) -> t.List[APIDevice]:
|
||||||
return use_state("devices")
|
"""Retrieve all devices."""
|
||||||
|
return devices.export_api()
|
||||||
|
|
||||||
|
|
||||||
def get_ui_params():
|
@get("/api/queries", dependencies={"devices": Provide(get_devices)})
|
||||||
"""Get hyperglass ui_params as FastAPI dependency."""
|
async def queries(devices: Devices) -> t.List[str]:
|
||||||
return use_state("ui_params")
|
"""Retrieve all directive names."""
|
||||||
|
return devices.directive_names()
|
||||||
|
|
||||||
|
|
||||||
async def query(
|
@get("/api/info", dependencies={"params": Provide(get_params)})
|
||||||
query_data: Query,
|
async def info(params: Params) -> APIParams:
|
||||||
request: Request,
|
"""Retrieve looking glass parameters."""
|
||||||
background_tasks: BackgroundTasks,
|
return params.export_api()
|
||||||
state: "HyperglassState" = Depends(get_state),
|
|
||||||
):
|
|
||||||
|
@post("/api/query", dependencies={"_state": Provide(get_state)})
|
||||||
|
async def query(_state: HyperglassState, request: Request, data: Query) -> QueryResponse:
|
||||||
"""Ingest request data pass it to the backend application to perform the query."""
|
"""Ingest request data pass it to the backend application to perform the query."""
|
||||||
|
|
||||||
timestamp = datetime.utcnow()
|
timestamp = datetime.now(UTC)
|
||||||
background_tasks.add_task(send_webhook, query_data, request, timestamp)
|
|
||||||
|
|
||||||
# Initialize cache
|
# Initialize cache
|
||||||
cache = state.redis
|
cache = _state.redis
|
||||||
|
|
||||||
# Use hashed query_data string as key for for k/v cache store so
|
# Use hashed `data` string as key for for k/v cache store so
|
||||||
# each command output value is unique.
|
# each command output value is unique.
|
||||||
cache_key = f"hyperglass.query.{query_data.digest()}"
|
cache_key = f"hyperglass.query.{data.digest()}"
|
||||||
|
|
||||||
log.info("{!r} starting query execution", query_data)
|
log.info("{!r} starting query execution", data)
|
||||||
|
|
||||||
cache_response = cache.get_map(cache_key, "output")
|
cache_response = cache.get_map(cache_key, "output")
|
||||||
json_output = False
|
json_output = False
|
||||||
|
|
@ -73,38 +81,38 @@ async def query(
|
||||||
runtime = 65535
|
runtime = 65535
|
||||||
|
|
||||||
if cache_response:
|
if cache_response:
|
||||||
log.debug("{!r} cache hit (cache key {!r})", query_data, cache_key)
|
log.debug("{!r} cache hit (cache key {!r})", data, cache_key)
|
||||||
|
|
||||||
# If a cached response exists, reset the expiration time.
|
# If a cached response exists, reset the expiration time.
|
||||||
cache.expire(cache_key, expire_in=state.params.cache.timeout)
|
cache.expire(cache_key, expire_in=_state.params.cache.timeout)
|
||||||
|
|
||||||
cached = True
|
cached = True
|
||||||
runtime = 0
|
runtime = 0
|
||||||
timestamp = cache.get_map(cache_key, "timestamp")
|
timestamp = cache.get_map(cache_key, "timestamp")
|
||||||
|
|
||||||
elif not cache_response:
|
elif not cache_response:
|
||||||
log.debug("{!r} cache miss (cache key {!r})", query_data, cache_key)
|
log.debug("{!r} cache miss (cache key {!r})", data, cache_key)
|
||||||
|
|
||||||
timestamp = query_data.timestamp
|
timestamp = data.timestamp
|
||||||
|
|
||||||
starttime = time.time()
|
starttime = time.time()
|
||||||
|
|
||||||
if state.params.fake_output:
|
if _state.params.fake_output:
|
||||||
# Return fake, static data for development purposes, if enabled.
|
# Return fake, static data for development purposes, if enabled.
|
||||||
output = await fake_output(
|
output = await fake_output(
|
||||||
query_type=query_data.query_type,
|
query_type=data.query_type,
|
||||||
structured=query_data.device.structured_output or False,
|
structured=data.device.structured_output or False,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# Pass request to execution module
|
# Pass request to execution module
|
||||||
output = await execute(query_data)
|
output = await execute(data)
|
||||||
|
|
||||||
endtime = time.time()
|
endtime = time.time()
|
||||||
elapsedtime = round(endtime - starttime, 4)
|
elapsedtime = round(endtime - starttime, 4)
|
||||||
log.debug("{!r} runtime: {!s} seconds", query_data, elapsedtime)
|
log.debug("{!r} runtime: {!s} seconds", data, elapsedtime)
|
||||||
|
|
||||||
if output is None:
|
if output is None:
|
||||||
raise HyperglassError(message=state.params.messages.general, alert="danger")
|
raise HyperglassError(message=_state.params.messages.general, alert="danger")
|
||||||
|
|
||||||
json_output = is_type(output, OutputDataModel)
|
json_output = is_type(output, OutputDataModel)
|
||||||
|
|
||||||
|
|
@ -115,9 +123,9 @@ async def query(
|
||||||
|
|
||||||
cache.set_map_item(cache_key, "output", raw_output)
|
cache.set_map_item(cache_key, "output", raw_output)
|
||||||
cache.set_map_item(cache_key, "timestamp", timestamp)
|
cache.set_map_item(cache_key, "timestamp", timestamp)
|
||||||
cache.expire(cache_key, expire_in=state.params.cache.timeout)
|
cache.expire(cache_key, expire_in=_state.params.cache.timeout)
|
||||||
|
|
||||||
log.debug("{!r} cached for {!s} seconds", query_data, state.params.cache.timeout)
|
log.debug("{!r} cached for {!s} seconds", data, _state.params.cache.timeout)
|
||||||
|
|
||||||
runtime = int(round(elapsedtime, 0))
|
runtime = int(round(elapsedtime, 0))
|
||||||
|
|
||||||
|
|
@ -130,60 +138,27 @@ async def query(
|
||||||
if json_output:
|
if json_output:
|
||||||
response_format = "application/json"
|
response_format = "application/json"
|
||||||
|
|
||||||
log.success("{!r} execution completed", query_data)
|
log.success("{!r} execution completed", data)
|
||||||
|
|
||||||
return {
|
response = {
|
||||||
"output": cache_response,
|
"output": cache_response,
|
||||||
"id": cache_key,
|
"id": cache_key,
|
||||||
"cached": cached,
|
"cached": cached,
|
||||||
"runtime": runtime,
|
"runtime": runtime,
|
||||||
"timestamp": timestamp,
|
"timestamp": timestamp,
|
||||||
"format": response_format,
|
"format": response_format,
|
||||||
"random": query_data.random(),
|
"random": data.random(),
|
||||||
"level": "success",
|
"level": "success",
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return Response(
|
||||||
async def docs(params: "Params" = Depends(get_params)):
|
response,
|
||||||
"""Serve custom docs."""
|
background=BackgroundTask(
|
||||||
if params.docs.enable:
|
send_webhook,
|
||||||
docs_func_map = {"swagger": get_swagger_ui_html, "redoc": get_redoc_html}
|
params=_state.params,
|
||||||
docs_func = docs_func_map[params.docs.mode]
|
data=data,
|
||||||
return docs_func(
|
request=request,
|
||||||
openapi_url=params.docs.openapi_url, title=params.site_title + " - API Docs"
|
timestamp=timestamp,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
raise HTTPException(detail="Not found", status_code=404)
|
|
||||||
|
|
||||||
|
|
||||||
async def router(id: str, devices: "Devices" = Depends(get_devices)):
|
|
||||||
"""Get a device's API-facing attributes."""
|
|
||||||
return devices[id].export_api()
|
|
||||||
|
|
||||||
|
|
||||||
async def routers(devices: "Devices" = Depends(get_devices)):
|
|
||||||
"""Serve list of configured routers and attributes."""
|
|
||||||
return devices.export_api()
|
|
||||||
|
|
||||||
|
|
||||||
async def queries(params: "Params" = Depends(get_params)):
|
|
||||||
"""Serve list of enabled query types."""
|
|
||||||
return params.queries.list
|
|
||||||
|
|
||||||
|
|
||||||
async def info(params: "Params" = Depends(get_params)):
|
|
||||||
"""Serve general information about this instance of hyperglass."""
|
|
||||||
return {
|
|
||||||
"name": params.site_title,
|
|
||||||
"organization": params.org_name,
|
|
||||||
"primary_asn": int(params.primary_asn),
|
|
||||||
"version": __version__,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
async def ui_props(ui_params: "UIParameters" = Depends(get_ui_params)):
|
|
||||||
"""Serve UI configration."""
|
|
||||||
return ui_params
|
|
||||||
|
|
||||||
|
|
||||||
endpoints = [query, docs, routers, info, ui_props]
|
|
||||||
|
|
|
||||||
27
hyperglass/api/state.py
Normal file
27
hyperglass/api/state.py
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
"""hyperglass state dependencies."""
|
||||||
|
|
||||||
|
# Standard Library
|
||||||
|
import typing as t
|
||||||
|
|
||||||
|
# Project
|
||||||
|
from hyperglass.state import use_state
|
||||||
|
|
||||||
|
|
||||||
|
async def get_state(attr: t.Optional[str] = None):
|
||||||
|
"""Get hyperglass state as a FastAPI dependency."""
|
||||||
|
return use_state(attr)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_params():
|
||||||
|
"""Get hyperglass params as FastAPI dependency."""
|
||||||
|
return use_state("params")
|
||||||
|
|
||||||
|
|
||||||
|
async def get_devices():
|
||||||
|
"""Get hyperglass devices as FastAPI dependency."""
|
||||||
|
return use_state("devices")
|
||||||
|
|
||||||
|
|
||||||
|
async def get_ui_params():
|
||||||
|
"""Get hyperglass ui_params as FastAPI dependency."""
|
||||||
|
return use_state("ui_params")
|
||||||
|
|
@ -1,55 +1,26 @@
|
||||||
"""Tasks to be executed from web API."""
|
"""Tasks to be executed from web API."""
|
||||||
|
|
||||||
# Standard Library
|
# Standard Library
|
||||||
from typing import Dict, Union
|
import typing as t
|
||||||
from pathlib import Path
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
# Third Party
|
# Third Party
|
||||||
from httpx import Headers
|
from httpx import Headers
|
||||||
from starlette.requests import Request
|
from litestar import Request
|
||||||
|
|
||||||
# Project
|
# Project
|
||||||
from hyperglass.log import log
|
from hyperglass.log import log
|
||||||
from hyperglass.state import use_state
|
|
||||||
from hyperglass.external import Webhook, bgptools
|
from hyperglass.external import Webhook, bgptools
|
||||||
from hyperglass.models.api import Query
|
from hyperglass.models.api import Query
|
||||||
|
|
||||||
__all__ = (
|
if t.TYPE_CHECKING:
|
||||||
"import_public_key",
|
# Project
|
||||||
"process_headers",
|
from hyperglass.models.config.params import Params
|
||||||
"send_webhook",
|
|
||||||
)
|
__all__ = ("send_webhook",)
|
||||||
|
|
||||||
|
|
||||||
def import_public_key(app_path: Union[Path, str], device_name: str, keystring: str) -> bool:
|
async def process_headers(headers: Headers) -> t.Dict[str, t.Any]:
|
||||||
"""Import a public key for hyperglass-agent."""
|
|
||||||
if not isinstance(app_path, Path):
|
|
||||||
app_path = Path(app_path)
|
|
||||||
|
|
||||||
cert_dir = app_path / "certs"
|
|
||||||
|
|
||||||
if not cert_dir.exists():
|
|
||||||
cert_dir.mkdir()
|
|
||||||
|
|
||||||
if not cert_dir.exists():
|
|
||||||
raise RuntimeError(f"Failed to create certs directory at {str(cert_dir)}")
|
|
||||||
|
|
||||||
filename = f"{device_name}.pem"
|
|
||||||
cert_file = cert_dir / filename
|
|
||||||
|
|
||||||
with cert_file.open("w+") as file:
|
|
||||||
file.write(str(keystring))
|
|
||||||
|
|
||||||
with cert_file.open("r") as file:
|
|
||||||
read_file = file.read().strip()
|
|
||||||
if not keystring == read_file:
|
|
||||||
raise RuntimeError("Wrote key, but written file did not match input key")
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
async def process_headers(headers: Headers) -> Dict:
|
|
||||||
"""Filter out unwanted headers and return as a dictionary."""
|
"""Filter out unwanted headers and return as a dictionary."""
|
||||||
headers = dict(headers)
|
headers = dict(headers)
|
||||||
header_keys = (
|
header_keys = (
|
||||||
|
|
@ -64,12 +35,12 @@ async def process_headers(headers: Headers) -> Dict:
|
||||||
|
|
||||||
|
|
||||||
async def send_webhook(
|
async def send_webhook(
|
||||||
query_data: Query,
|
params: "Params",
|
||||||
|
data: Query,
|
||||||
request: Request,
|
request: Request,
|
||||||
timestamp: datetime,
|
timestamp: datetime,
|
||||||
):
|
) -> t.NoReturn:
|
||||||
"""If webhooks are enabled, get request info and send a webhook."""
|
"""If webhooks are enabled, get request info and send a webhook."""
|
||||||
params = use_state("params")
|
|
||||||
try:
|
try:
|
||||||
if params.logging.http is not None:
|
if params.logging.http is not None:
|
||||||
headers = await process_headers(headers=request.headers)
|
headers = await process_headers(headers=request.headers)
|
||||||
|
|
@ -84,10 +55,9 @@ async def send_webhook(
|
||||||
network_info = await bgptools.network_info(host)
|
network_info = await bgptools.network_info(host)
|
||||||
|
|
||||||
async with Webhook(params.logging.http) as hook:
|
async with Webhook(params.logging.http) as hook:
|
||||||
|
|
||||||
await hook.send(
|
await hook.send(
|
||||||
query={
|
query={
|
||||||
**query_data.dict(),
|
**data.dict(),
|
||||||
"headers": headers,
|
"headers": headers,
|
||||||
"source": host,
|
"source": host,
|
||||||
"network": network_info.get(host, {}),
|
"network": network_info.get(host, {}),
|
||||||
|
|
@ -95,4 +65,4 @@ async def send_webhook(
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
log.error("Error sending webhook to {}: {}", params.logging.http.provider, str(err))
|
log.error("Error sending webhook to {}: {!s}", params.logging.http.provider, err)
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,14 @@ from .http_client import HttpConfiguration
|
||||||
ALL_DEVICE_TYPES = {*DRIVER_MAP.keys(), *CLASS_MAPPER.keys()}
|
ALL_DEVICE_TYPES = {*DRIVER_MAP.keys(), *CLASS_MAPPER.keys()}
|
||||||
|
|
||||||
|
|
||||||
|
class APIDevice(t.TypedDict):
|
||||||
|
"""API Response Model for a device."""
|
||||||
|
|
||||||
|
id: str
|
||||||
|
name: str
|
||||||
|
group: t.Union[str, None]
|
||||||
|
|
||||||
|
|
||||||
class DirectiveOptions(HyperglassModel, extra="ignore"):
|
class DirectiveOptions(HyperglassModel, extra="ignore"):
|
||||||
"""Per-device directive options."""
|
"""Per-device directive options."""
|
||||||
|
|
||||||
|
|
@ -99,7 +107,7 @@ class Device(HyperglassModelWithId, extra="allow"):
|
||||||
|
|
||||||
return {"id": device_id, "name": display_name, "display_name": None, **values}
|
return {"id": device_id, "name": display_name, "display_name": None, **values}
|
||||||
|
|
||||||
def export_api(self) -> t.Dict[str, t.Any]:
|
def export_api(self) -> APIDevice:
|
||||||
"""Export API-facing device fields."""
|
"""Export API-facing device fields."""
|
||||||
return {
|
return {
|
||||||
"id": self.id,
|
"id": self.id,
|
||||||
|
|
@ -122,6 +130,11 @@ class Device(HyperglassModelWithId, extra="allow"):
|
||||||
"""Get all directive IDs associated with the device."""
|
"""Get all directive IDs associated with the device."""
|
||||||
return [directive.id for directive in self.directives]
|
return [directive.id for directive in self.directives]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def directive_names(self) -> t.List[str]:
|
||||||
|
"""Get all directive names associated with the device."""
|
||||||
|
return list({directive.name for directive in self.directives})
|
||||||
|
|
||||||
def has_directives(self, *directive_ids: str) -> bool:
|
def has_directives(self, *directive_ids: str) -> bool:
|
||||||
"""Determine if a directive is used on this device."""
|
"""Determine if a directive is used on this device."""
|
||||||
for directive_id in directive_ids:
|
for directive_id in directive_ids:
|
||||||
|
|
@ -304,7 +317,7 @@ class Devices(MultiModel, model=Device, unique_by="id"):
|
||||||
with_id = (Device._with_id(item) for item in items)
|
with_id = (Device._with_id(item) for item in items)
|
||||||
super().__init__(*with_id)
|
super().__init__(*with_id)
|
||||||
|
|
||||||
def export_api(self: "Devices") -> t.List[t.Dict[str, t.Any]]:
|
def export_api(self: "Devices") -> t.List[APIDevice]:
|
||||||
"""Export API-facing device fields."""
|
"""Export API-facing device fields."""
|
||||||
return [d.export_api() for d in self]
|
return [d.export_api() for d in self]
|
||||||
|
|
||||||
|
|
@ -332,6 +345,10 @@ class Devices(MultiModel, model=Device, unique_by="id"):
|
||||||
# Convert the directive set to a tuple.
|
# Convert the directive set to a tuple.
|
||||||
return {k: tuple(v) for k, v in result.items()}
|
return {k: tuple(v) for k, v in result.items()}
|
||||||
|
|
||||||
|
def directive_names(self) -> t.List[str]:
|
||||||
|
"""Get all directive names for all devices."""
|
||||||
|
return list({directive.name for device in self for directive in device.directives})
|
||||||
|
|
||||||
def frontend(self: "Devices") -> t.List[t.Dict[str, t.Any]]:
|
def frontend(self: "Devices") -> t.List[t.Dict[str, t.Any]]:
|
||||||
"""Export grouped devices for UIParameters."""
|
"""Export grouped devices for UIParameters."""
|
||||||
groups = {device.group for device in self}
|
groups = {device.group for device in self}
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@ class Docs(HyperglassModel):
|
||||||
description="Base URL used in request samples.",
|
description="Base URL used in request samples.",
|
||||||
)
|
)
|
||||||
path: AnyUri = Field(
|
path: AnyUri = Field(
|
||||||
"/api/docs",
|
"/docs",
|
||||||
title="URI",
|
title="URI",
|
||||||
description="HTTP URI/path where API documentation can be accessed.",
|
description="HTTP URI/path where API documentation can be accessed.",
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,15 @@
|
||||||
|
|
||||||
# Standard Library
|
# Standard Library
|
||||||
import typing as t
|
import typing as t
|
||||||
from pathlib import Path
|
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
# Third Party
|
# Third Party
|
||||||
from pydantic import Field, ConfigDict, ValidationInfo, field_validator, HttpUrl
|
from pydantic import Field, HttpUrl, ConfigDict, ValidationInfo, field_validator
|
||||||
|
|
||||||
# Project
|
# Project
|
||||||
from hyperglass.settings import Settings
|
from hyperglass.settings import Settings
|
||||||
|
from hyperglass.constants import __version__
|
||||||
|
|
||||||
# Local
|
# Local
|
||||||
from .web import Web
|
from .web import Web
|
||||||
|
|
@ -23,6 +24,15 @@ from .structured import Structured
|
||||||
Localhost = t.Literal["localhost"]
|
Localhost = t.Literal["localhost"]
|
||||||
|
|
||||||
|
|
||||||
|
class APIParams(t.TypedDict):
|
||||||
|
"""/api/info response model."""
|
||||||
|
|
||||||
|
name: str
|
||||||
|
organization: str
|
||||||
|
primary_asn: int
|
||||||
|
version: str
|
||||||
|
|
||||||
|
|
||||||
class ParamsPublic(HyperglassModel):
|
class ParamsPublic(HyperglassModel):
|
||||||
"""Public configuration parameters."""
|
"""Public configuration parameters."""
|
||||||
|
|
||||||
|
|
@ -124,6 +134,15 @@ class Params(ParamsPublic, HyperglassModel):
|
||||||
"""Get all validated external common plugins as Path objects."""
|
"""Get all validated external common plugins as Path objects."""
|
||||||
return tuple(Path(p) for p in self.plugins)
|
return tuple(Path(p) for p in self.plugins)
|
||||||
|
|
||||||
|
def export_api(self) -> APIParams:
|
||||||
|
"""Export API-specific parameters."""
|
||||||
|
return {
|
||||||
|
"name": self.site_title,
|
||||||
|
"organization": self.org_name,
|
||||||
|
"primary_asn": int(self.primary_asn),
|
||||||
|
"version": __version__,
|
||||||
|
}
|
||||||
|
|
||||||
def frontend(self) -> t.Dict[str, t.Any]:
|
def frontend(self) -> t.Dict[str, t.Any]:
|
||||||
"""Export UI-specific parameters."""
|
"""Export UI-specific parameters."""
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ dependencies = [
|
||||||
"PyYAML>=6.0",
|
"PyYAML>=6.0",
|
||||||
"aiofiles>=23.2.1",
|
"aiofiles>=23.2.1",
|
||||||
"distro==1.8.0",
|
"distro==1.8.0",
|
||||||
"fastapi>=0.110.0",
|
|
||||||
"favicons==0.2.2",
|
"favicons==0.2.2",
|
||||||
"gunicorn>=21.2.0",
|
"gunicorn>=21.2.0",
|
||||||
"httpx==0.24.0",
|
"httpx==0.24.0",
|
||||||
|
|
@ -30,6 +29,7 @@ dependencies = [
|
||||||
"toml>=0.10.2",
|
"toml>=0.10.2",
|
||||||
"pydantic-settings>=2.2.1",
|
"pydantic-settings>=2.2.1",
|
||||||
"pydantic-extra-types>=2.6.0",
|
"pydantic-extra-types>=2.6.0",
|
||||||
|
"litestar[standard,brotli]>=2.7.0",
|
||||||
]
|
]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">= 3.11"
|
requires-python = ">= 3.11"
|
||||||
|
|
@ -75,7 +75,7 @@ import_heading_stdlib = "Standard Library"
|
||||||
import_heading_thirdparty = "Third Party"
|
import_heading_thirdparty = "Third Party"
|
||||||
include_trailing_comma = true
|
include_trailing_comma = true
|
||||||
indent = ' '
|
indent = ' '
|
||||||
known_third_party = ["starlette", "fastapi", "inquirer"]
|
known_third_party = ["litestar", "inquirer"]
|
||||||
length_sort = true
|
length_sort = true
|
||||||
line_length = 100
|
line_length = 100
|
||||||
multi_line_output = 3
|
multi_line_output = 3
|
||||||
|
|
|
||||||
|
|
@ -14,11 +14,16 @@ annotated-types==0.6.0
|
||||||
# via pydantic
|
# via pydantic
|
||||||
anyio==4.3.0
|
anyio==4.3.0
|
||||||
# via httpcore
|
# via httpcore
|
||||||
# via starlette
|
# via litestar
|
||||||
|
# via watchfiles
|
||||||
|
async-timeout==4.0.3
|
||||||
|
# via redis
|
||||||
bandit==1.7.7
|
bandit==1.7.7
|
||||||
bcrypt==4.1.2
|
bcrypt==4.1.2
|
||||||
# via paramiko
|
# via paramiko
|
||||||
black==24.2.0
|
black==24.2.0
|
||||||
|
brotli==1.1.0
|
||||||
|
# via litestar
|
||||||
certifi==2024.2.2
|
certifi==2024.2.2
|
||||||
# via httpcore
|
# via httpcore
|
||||||
# via httpx
|
# via httpx
|
||||||
|
|
@ -31,6 +36,8 @@ chardet==5.2.0
|
||||||
# via reportlab
|
# via reportlab
|
||||||
click==8.1.7
|
click==8.1.7
|
||||||
# via black
|
# via black
|
||||||
|
# via litestar
|
||||||
|
# via rich-click
|
||||||
# via typer
|
# via typer
|
||||||
# via uvicorn
|
# via uvicorn
|
||||||
colorama==0.4.6
|
colorama==0.4.6
|
||||||
|
|
@ -43,8 +50,12 @@ distlib==0.3.8
|
||||||
# via virtualenv
|
# via virtualenv
|
||||||
distro==1.8.0
|
distro==1.8.0
|
||||||
# via hyperglass
|
# via hyperglass
|
||||||
fastapi==0.110.0
|
editorconfig==0.12.4
|
||||||
# via hyperglass
|
# via jsbeautifier
|
||||||
|
faker==24.4.0
|
||||||
|
# via polyfactory
|
||||||
|
fast-query-parsers==1.0.3
|
||||||
|
# via litestar
|
||||||
favicons==0.2.2
|
favicons==0.2.2
|
||||||
# via hyperglass
|
# via hyperglass
|
||||||
filelock==3.13.1
|
filelock==3.13.1
|
||||||
|
|
@ -62,8 +73,11 @@ h11==0.14.0
|
||||||
# via uvicorn
|
# via uvicorn
|
||||||
httpcore==0.17.3
|
httpcore==0.17.3
|
||||||
# via httpx
|
# via httpx
|
||||||
|
httptools==0.6.1
|
||||||
|
# via uvicorn
|
||||||
httpx==0.24.0
|
httpx==0.24.0
|
||||||
# via hyperglass
|
# via hyperglass
|
||||||
|
# via litestar
|
||||||
identify==2.5.35
|
identify==2.5.35
|
||||||
# via pre-commit
|
# via pre-commit
|
||||||
idna==3.6
|
idna==3.6
|
||||||
|
|
@ -72,16 +86,28 @@ idna==3.6
|
||||||
iniconfig==2.0.0
|
iniconfig==2.0.0
|
||||||
# via pytest
|
# via pytest
|
||||||
isort==5.13.2
|
isort==5.13.2
|
||||||
|
jinja2==3.1.3
|
||||||
|
# via litestar
|
||||||
|
jsbeautifier==1.15.1
|
||||||
|
# via litestar
|
||||||
|
litestar==2.7.0
|
||||||
|
# via hyperglass
|
||||||
loguru==0.7.0
|
loguru==0.7.0
|
||||||
# via hyperglass
|
# via hyperglass
|
||||||
lxml==5.1.0
|
lxml==5.1.0
|
||||||
# via svglib
|
# via svglib
|
||||||
markdown-it-py==3.0.0
|
markdown-it-py==3.0.0
|
||||||
# via rich
|
# via rich
|
||||||
|
markupsafe==2.1.5
|
||||||
|
# via jinja2
|
||||||
mccabe==0.7.0
|
mccabe==0.7.0
|
||||||
# via flake8
|
# via flake8
|
||||||
mdurl==0.1.2
|
mdurl==0.1.2
|
||||||
# via markdown-it-py
|
# via markdown-it-py
|
||||||
|
msgspec==0.18.6
|
||||||
|
# via litestar
|
||||||
|
multidict==6.0.5
|
||||||
|
# via litestar
|
||||||
mypy-extensions==1.0.0
|
mypy-extensions==1.0.0
|
||||||
# via black
|
# via black
|
||||||
netmiko==4.1.2
|
netmiko==4.1.2
|
||||||
|
|
@ -112,6 +138,8 @@ platformdirs==4.2.0
|
||||||
# via virtualenv
|
# via virtualenv
|
||||||
pluggy==1.4.0
|
pluggy==1.4.0
|
||||||
# via pytest
|
# via pytest
|
||||||
|
polyfactory==2.15.0
|
||||||
|
# via litestar
|
||||||
pre-commit==3.6.2
|
pre-commit==3.6.2
|
||||||
psutil==5.9.4
|
psutil==5.9.4
|
||||||
# via hyperglass
|
# via hyperglass
|
||||||
|
|
@ -125,7 +153,6 @@ pycodestyle==2.11.1
|
||||||
pycparser==2.21
|
pycparser==2.21
|
||||||
# via cffi
|
# via cffi
|
||||||
pydantic==2.6.3
|
pydantic==2.6.3
|
||||||
# via fastapi
|
|
||||||
# via hyperglass
|
# via hyperglass
|
||||||
# via pydantic-extra-types
|
# via pydantic-extra-types
|
||||||
# via pydantic-settings
|
# via pydantic-settings
|
||||||
|
|
@ -150,13 +177,18 @@ pytest==8.0.1
|
||||||
# via pytest-dependency
|
# via pytest-dependency
|
||||||
pytest-asyncio==0.23.5
|
pytest-asyncio==0.23.5
|
||||||
pytest-dependency==0.6.0
|
pytest-dependency==0.6.0
|
||||||
|
python-dateutil==2.9.0.post0
|
||||||
|
# via faker
|
||||||
python-dotenv==1.0.1
|
python-dotenv==1.0.1
|
||||||
# via pydantic-settings
|
# via pydantic-settings
|
||||||
|
# via uvicorn
|
||||||
pyyaml==6.0.1
|
pyyaml==6.0.1
|
||||||
# via bandit
|
# via bandit
|
||||||
# via hyperglass
|
# via hyperglass
|
||||||
|
# via litestar
|
||||||
# via netmiko
|
# via netmiko
|
||||||
# via pre-commit
|
# via pre-commit
|
||||||
|
# via uvicorn
|
||||||
redis==4.5.4
|
redis==4.5.4
|
||||||
# via hyperglass
|
# via hyperglass
|
||||||
reportlab==4.1.0
|
reportlab==4.1.0
|
||||||
|
|
@ -166,6 +198,10 @@ rich==13.7.0
|
||||||
# via bandit
|
# via bandit
|
||||||
# via favicons
|
# via favicons
|
||||||
# via hyperglass
|
# via hyperglass
|
||||||
|
# via litestar
|
||||||
|
# via rich-click
|
||||||
|
rich-click==1.7.4
|
||||||
|
# via litestar
|
||||||
rlpycairo==0.3.0
|
rlpycairo==0.3.0
|
||||||
# via favicons
|
# via favicons
|
||||||
ruff==0.2.2
|
ruff==0.2.2
|
||||||
|
|
@ -176,14 +212,14 @@ setuptools==69.1.0
|
||||||
# via nodeenv
|
# via nodeenv
|
||||||
# via pytest-dependency
|
# via pytest-dependency
|
||||||
six==1.16.0
|
six==1.16.0
|
||||||
|
# via jsbeautifier
|
||||||
|
# via python-dateutil
|
||||||
# via textfsm
|
# via textfsm
|
||||||
sniffio==1.3.0
|
sniffio==1.3.0
|
||||||
# via anyio
|
# via anyio
|
||||||
# via httpcore
|
# via httpcore
|
||||||
# via httpx
|
# via httpx
|
||||||
stackprinter==0.2.11
|
stackprinter==0.2.11
|
||||||
starlette==0.36.3
|
|
||||||
# via fastapi
|
|
||||||
stevedore==5.1.0
|
stevedore==5.1.0
|
||||||
# via bandit
|
# via bandit
|
||||||
svglib==1.5.1
|
svglib==1.5.1
|
||||||
|
|
@ -205,18 +241,27 @@ typer==0.9.0
|
||||||
# via favicons
|
# via favicons
|
||||||
# via hyperglass
|
# via hyperglass
|
||||||
typing-extensions==4.9.0
|
typing-extensions==4.9.0
|
||||||
# via fastapi
|
# via litestar
|
||||||
|
# via polyfactory
|
||||||
# via pydantic
|
# via pydantic
|
||||||
# via pydantic-core
|
# via pydantic-core
|
||||||
|
# via rich-click
|
||||||
# via typer
|
# via typer
|
||||||
uvicorn==0.21.1
|
uvicorn==0.21.1
|
||||||
# via hyperglass
|
# via hyperglass
|
||||||
uvloop==0.17.0
|
# via litestar
|
||||||
|
uvloop==0.19.0
|
||||||
# via hyperglass
|
# via hyperglass
|
||||||
|
# via litestar
|
||||||
|
# via uvicorn
|
||||||
virtualenv==20.25.0
|
virtualenv==20.25.0
|
||||||
# via pre-commit
|
# via pre-commit
|
||||||
|
watchfiles==0.21.0
|
||||||
|
# via uvicorn
|
||||||
webencodings==0.5.1
|
webencodings==0.5.1
|
||||||
# via cssselect2
|
# via cssselect2
|
||||||
# via tinycss2
|
# via tinycss2
|
||||||
|
websockets==12.0
|
||||||
|
# via uvicorn
|
||||||
xmltodict==0.13.0
|
xmltodict==0.13.0
|
||||||
# via hyperglass
|
# via hyperglass
|
||||||
|
|
|
||||||
|
|
@ -14,9 +14,14 @@ annotated-types==0.6.0
|
||||||
# via pydantic
|
# via pydantic
|
||||||
anyio==4.3.0
|
anyio==4.3.0
|
||||||
# via httpcore
|
# via httpcore
|
||||||
# via starlette
|
# via litestar
|
||||||
|
# via watchfiles
|
||||||
|
async-timeout==4.0.3
|
||||||
|
# via redis
|
||||||
bcrypt==4.1.2
|
bcrypt==4.1.2
|
||||||
# via paramiko
|
# via paramiko
|
||||||
|
brotli==1.1.0
|
||||||
|
# via litestar
|
||||||
certifi==2024.2.2
|
certifi==2024.2.2
|
||||||
# via httpcore
|
# via httpcore
|
||||||
# via httpx
|
# via httpx
|
||||||
|
|
@ -26,6 +31,8 @@ cffi==1.16.0
|
||||||
chardet==5.2.0
|
chardet==5.2.0
|
||||||
# via reportlab
|
# via reportlab
|
||||||
click==8.1.7
|
click==8.1.7
|
||||||
|
# via litestar
|
||||||
|
# via rich-click
|
||||||
# via typer
|
# via typer
|
||||||
# via uvicorn
|
# via uvicorn
|
||||||
cryptography==42.0.3
|
cryptography==42.0.3
|
||||||
|
|
@ -34,8 +41,12 @@ cssselect2==0.7.0
|
||||||
# via svglib
|
# via svglib
|
||||||
distro==1.8.0
|
distro==1.8.0
|
||||||
# via hyperglass
|
# via hyperglass
|
||||||
fastapi==0.110.0
|
editorconfig==0.12.4
|
||||||
# via hyperglass
|
# via jsbeautifier
|
||||||
|
faker==24.4.0
|
||||||
|
# via polyfactory
|
||||||
|
fast-query-parsers==1.0.3
|
||||||
|
# via litestar
|
||||||
favicons==0.2.2
|
favicons==0.2.2
|
||||||
# via hyperglass
|
# via hyperglass
|
||||||
freetype-py==2.4.0
|
freetype-py==2.4.0
|
||||||
|
|
@ -49,19 +60,34 @@ h11==0.14.0
|
||||||
# via uvicorn
|
# via uvicorn
|
||||||
httpcore==0.17.3
|
httpcore==0.17.3
|
||||||
# via httpx
|
# via httpx
|
||||||
|
httptools==0.6.1
|
||||||
|
# via uvicorn
|
||||||
httpx==0.24.0
|
httpx==0.24.0
|
||||||
# via hyperglass
|
# via hyperglass
|
||||||
|
# via litestar
|
||||||
idna==3.6
|
idna==3.6
|
||||||
# via anyio
|
# via anyio
|
||||||
# via httpx
|
# via httpx
|
||||||
|
jinja2==3.1.3
|
||||||
|
# via litestar
|
||||||
|
jsbeautifier==1.15.1
|
||||||
|
# via litestar
|
||||||
|
litestar==2.7.0
|
||||||
|
# via hyperglass
|
||||||
loguru==0.7.0
|
loguru==0.7.0
|
||||||
# via hyperglass
|
# via hyperglass
|
||||||
lxml==5.1.0
|
lxml==5.1.0
|
||||||
# via svglib
|
# via svglib
|
||||||
markdown-it-py==3.0.0
|
markdown-it-py==3.0.0
|
||||||
# via rich
|
# via rich
|
||||||
|
markupsafe==2.1.5
|
||||||
|
# via jinja2
|
||||||
mdurl==0.1.2
|
mdurl==0.1.2
|
||||||
# via markdown-it-py
|
# via markdown-it-py
|
||||||
|
msgspec==0.18.6
|
||||||
|
# via litestar
|
||||||
|
multidict==6.0.5
|
||||||
|
# via litestar
|
||||||
netmiko==4.1.2
|
netmiko==4.1.2
|
||||||
# via hyperglass
|
# via hyperglass
|
||||||
ntc-templates==4.3.0
|
ntc-templates==4.3.0
|
||||||
|
|
@ -76,6 +102,8 @@ pillow==10.2.0
|
||||||
# via favicons
|
# via favicons
|
||||||
# via hyperglass
|
# via hyperglass
|
||||||
# via reportlab
|
# via reportlab
|
||||||
|
polyfactory==2.15.0
|
||||||
|
# via litestar
|
||||||
psutil==5.9.4
|
psutil==5.9.4
|
||||||
# via hyperglass
|
# via hyperglass
|
||||||
py-cpuinfo==9.0.0
|
py-cpuinfo==9.0.0
|
||||||
|
|
@ -85,7 +113,6 @@ pycairo==1.26.0
|
||||||
pycparser==2.21
|
pycparser==2.21
|
||||||
# via cffi
|
# via cffi
|
||||||
pydantic==2.6.3
|
pydantic==2.6.3
|
||||||
# via fastapi
|
|
||||||
# via hyperglass
|
# via hyperglass
|
||||||
# via pydantic-extra-types
|
# via pydantic-extra-types
|
||||||
# via pydantic-settings
|
# via pydantic-settings
|
||||||
|
|
@ -103,11 +130,16 @@ pynacl==1.5.0
|
||||||
# via paramiko
|
# via paramiko
|
||||||
pyserial==3.5
|
pyserial==3.5
|
||||||
# via netmiko
|
# via netmiko
|
||||||
|
python-dateutil==2.9.0.post0
|
||||||
|
# via faker
|
||||||
python-dotenv==1.0.1
|
python-dotenv==1.0.1
|
||||||
# via pydantic-settings
|
# via pydantic-settings
|
||||||
|
# via uvicorn
|
||||||
pyyaml==6.0.1
|
pyyaml==6.0.1
|
||||||
# via hyperglass
|
# via hyperglass
|
||||||
|
# via litestar
|
||||||
# via netmiko
|
# via netmiko
|
||||||
|
# via uvicorn
|
||||||
redis==4.5.4
|
redis==4.5.4
|
||||||
# via hyperglass
|
# via hyperglass
|
||||||
reportlab==4.1.0
|
reportlab==4.1.0
|
||||||
|
|
@ -116,6 +148,10 @@ reportlab==4.1.0
|
||||||
rich==13.7.0
|
rich==13.7.0
|
||||||
# via favicons
|
# via favicons
|
||||||
# via hyperglass
|
# via hyperglass
|
||||||
|
# via litestar
|
||||||
|
# via rich-click
|
||||||
|
rich-click==1.7.4
|
||||||
|
# via litestar
|
||||||
rlpycairo==0.3.0
|
rlpycairo==0.3.0
|
||||||
# via favicons
|
# via favicons
|
||||||
scp==0.14.5
|
scp==0.14.5
|
||||||
|
|
@ -123,13 +159,13 @@ scp==0.14.5
|
||||||
setuptools==69.1.0
|
setuptools==69.1.0
|
||||||
# via netmiko
|
# via netmiko
|
||||||
six==1.16.0
|
six==1.16.0
|
||||||
|
# via jsbeautifier
|
||||||
|
# via python-dateutil
|
||||||
# via textfsm
|
# via textfsm
|
||||||
sniffio==1.3.0
|
sniffio==1.3.0
|
||||||
# via anyio
|
# via anyio
|
||||||
# via httpcore
|
# via httpcore
|
||||||
# via httpx
|
# via httpx
|
||||||
starlette==0.36.3
|
|
||||||
# via fastapi
|
|
||||||
svglib==1.5.1
|
svglib==1.5.1
|
||||||
# via favicons
|
# via favicons
|
||||||
tenacity==8.2.3
|
tenacity==8.2.3
|
||||||
|
|
@ -146,16 +182,25 @@ typer==0.9.0
|
||||||
# via favicons
|
# via favicons
|
||||||
# via hyperglass
|
# via hyperglass
|
||||||
typing-extensions==4.9.0
|
typing-extensions==4.9.0
|
||||||
# via fastapi
|
# via litestar
|
||||||
|
# via polyfactory
|
||||||
# via pydantic
|
# via pydantic
|
||||||
# via pydantic-core
|
# via pydantic-core
|
||||||
|
# via rich-click
|
||||||
# via typer
|
# via typer
|
||||||
uvicorn==0.21.1
|
uvicorn==0.21.1
|
||||||
# via hyperglass
|
# via hyperglass
|
||||||
uvloop==0.17.0
|
# via litestar
|
||||||
|
uvloop==0.19.0
|
||||||
# via hyperglass
|
# via hyperglass
|
||||||
|
# via litestar
|
||||||
|
# via uvicorn
|
||||||
|
watchfiles==0.21.0
|
||||||
|
# via uvicorn
|
||||||
webencodings==0.5.1
|
webencodings==0.5.1
|
||||||
# via cssselect2
|
# via cssselect2
|
||||||
# via tinycss2
|
# via tinycss2
|
||||||
|
websockets==12.0
|
||||||
|
# via uvicorn
|
||||||
xmltodict==0.13.0
|
xmltodict==0.13.0
|
||||||
# via hyperglass
|
# via hyperglass
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue