-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmain.py
More file actions
126 lines (94 loc) · 3.4 KB
/
Copy pathmain.py
File metadata and controls
126 lines (94 loc) · 3.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
"""FastAPI app: the four peer/dissector views over the analytical store, plus the
layer registry, search, name editing, and static web hosting.
In production, if ``web/dist`` exists it's mounted at ``/`` so the whole tool is one
local URL. In dev, run Vite separately and let it proxy ``/api`` to this server.
"""
import os
from pathlib import Path
from fastapi import FastAPI, HTTPException, Query
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from . import db, queries
from .models import (
Category,
ConnectionStacks,
EndpointDetail,
Graph,
Layer,
Node,
Stats,
)
app = FastAPI(title="net-noder API", version="0.2.0")
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["*"],
allow_headers=["*"],
)
_con = None
def _cursor():
"""Lazily open the meta(rw)+analytical(ro) connection; per-request cursor."""
global _con
if _con is None:
try:
_con = db.get_con()
except Exception as e: # noqa: BLE001
raise HTTPException(
status_code=503,
detail=f"Database unavailable at {db.DB_PATH}. Run ingest first. ({e})",
)
return _con.cursor()
@app.get("/api/health")
def health():
return {"ok": True, "db": str(db.DB_PATH), "exists": db.DB_PATH.exists()}
@app.get("/api/stats", response_model=Stats)
def get_stats():
return queries.stats(_cursor())
@app.get("/api/layers", response_model=list[Layer])
def get_layers():
return queries.layers(_cursor())
@app.get("/api/categories", response_model=list[Category])
def get_categories():
return queries.categories(_cursor())
@app.get("/api/graph", response_model=Graph)
def get_graph(cap: int = Query(queries.MAX_GRAPH_NODES, ge=1, le=queries.MAX_GRAPH_NODES)):
return queries.full_graph(_cursor(), cap)
@app.get("/api/node/{ip}", response_model=EndpointDetail)
def get_node(ip: str):
d = queries.node_detail(_cursor(), ip)
if d is None:
raise HTTPException(404, detail=f"No endpoint {ip}")
return d
@app.get("/api/node/{ip}/neighbors", response_model=Graph)
def get_neighbors(
ip: str,
limit: int = Query(queries.MAX_GRAPH_NODES, ge=1, le=queries.MAX_GRAPH_NODES),
):
return queries.neighbors(_cursor(), ip, limit)
@app.get("/api/connection/stacks", response_model=ConnectionStacks)
def get_connection_stacks(a: str, b: str):
d = queries.connection_stacks(_cursor(), a, b)
if d is None:
raise HTTPException(404, detail=f"No connection {a} <-> {b}")
return d
@app.get("/api/search", response_model=list[Node])
def get_search(q: str, limit: int = Query(20, ge=1, le=100)):
return queries.search(_cursor(), q, limit)
# Mount the built web app last so /api/* routes take precedence. The default points
# at the in-repo build; NETNODER_WEB_DIST lets a packaged/containerised install (where
# the source tree isn't alongside site-packages) point at the copied dist instead.
_DIST = Path(
os.environ.get(
"NETNODER_WEB_DIST",
str(Path(__file__).resolve().parents[2] / "web" / "dist"),
)
)
if _DIST.exists():
app.mount("/", StaticFiles(directory=str(_DIST), html=True), name="web")
def run():
import uvicorn
host = os.environ.get("NETNODER_HOST", "127.0.0.1")
port = int(os.environ.get("NETNODER_PORT", "8000"))
uvicorn.run(app, host=host, port=port)
if __name__ == "__main__":
run()