Redoing all the database logic entirely. I didn't understand FastAPI going into this and let grok do weird stuff. We did weird stuff together, really.
This commit is contained in:
@@ -7,6 +7,7 @@ import time
|
|||||||
from persistent.mapping import PersistentMapping
|
from persistent.mapping import PersistentMapping
|
||||||
from persistent.list import PersistentList
|
from persistent.list import PersistentList
|
||||||
from packetserver.common.util import is_valid_ax25_callsign
|
from packetserver.common.util import is_valid_ax25_callsign
|
||||||
|
from .database import get_db, get_transaction
|
||||||
|
|
||||||
ph = PasswordHasher()
|
ph = PasswordHasher()
|
||||||
|
|
||||||
@@ -50,12 +51,13 @@ class HttpUser(Persistent):
|
|||||||
# rf enabled checks..
|
# rf enabled checks..
|
||||||
#
|
#
|
||||||
|
|
||||||
def is_rf_enabled(self, connection) -> bool:
|
def is_rf_enabled(self) -> bool:
|
||||||
"""
|
"""
|
||||||
Check if RF gateway is enabled (i.e., callsign NOT in global blacklist).
|
Check if RF gateway is enabled (i.e., callsign NOT in global blacklist).
|
||||||
Requires an open ZODB connection.
|
Requires an open ZODB connection.
|
||||||
"""
|
"""
|
||||||
root = connection.root()
|
with get_transaction() as storage:
|
||||||
|
root = storage.root()
|
||||||
blacklist = root.get('config', {}).get('blacklist', [])
|
blacklist = root.get('config', {}).get('blacklist', [])
|
||||||
return self.username not in blacklist
|
return self.username not in blacklist
|
||||||
|
|
||||||
@@ -67,7 +69,8 @@ class HttpUser(Persistent):
|
|||||||
"""
|
"""
|
||||||
from packetserver.common.util import is_valid_ax25_callsign # our validator
|
from packetserver.common.util import is_valid_ax25_callsign # our validator
|
||||||
|
|
||||||
root = connection.root()
|
with get_transaction() as storage:
|
||||||
|
root = storage.root()
|
||||||
config = root.setdefault('config', PersistentMapping())
|
config = root.setdefault('config', PersistentMapping())
|
||||||
blacklist = config.setdefault('blacklist', PersistentList())
|
blacklist = config.setdefault('blacklist', PersistentList())
|
||||||
|
|
||||||
@@ -86,7 +89,6 @@ class HttpUser(Persistent):
|
|||||||
|
|
||||||
config._p_changed = True
|
config._p_changed = True
|
||||||
root._p_changed = True
|
root._p_changed = True
|
||||||
# Caller should commit the transaction
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# Password handling (unchanged)
|
# Password handling (unchanged)
|
||||||
|
|||||||
18
packetserver/http/config.py
Normal file
18
packetserver/http/config.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||||
|
|
||||||
|
class Settings(BaseSettings):
|
||||||
|
"""
|
||||||
|
Application settings loaded from environment variables and .env files.
|
||||||
|
"""
|
||||||
|
# Define your settings fields with type hints and optional default values
|
||||||
|
name: str = "PacketServer"
|
||||||
|
zeo_file: str
|
||||||
|
operator: str | None = None
|
||||||
|
debug_mode: bool = False
|
||||||
|
log_level: str = "info"
|
||||||
|
|
||||||
|
# Configure how settings are loaded
|
||||||
|
model_config = SettingsConfigDict(
|
||||||
|
case_sensitive=False, # Make environment variable names case-sensitive
|
||||||
|
env_prefix="PS_APP_" # Use a prefix for environment variables (e.g., MY_APP_DATABASE_URL)
|
||||||
|
)
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
from .config import Settings
|
||||||
|
from os.path import isfile
|
||||||
|
import ZEO
|
||||||
|
import ZODB
|
||||||
|
from fastapi import Depends
|
||||||
|
from typing import Annotated, ContextManager
|
||||||
|
|
||||||
|
settings = Settings()
|
||||||
|
|
||||||
|
def get_zeo_address(zeo_address_file: str) -> tuple[str,int]:
|
||||||
|
if not isfile(zeo_address_file):
|
||||||
|
raise FileNotFoundError(f"ZEO address file is not a file: '{zeo_address_file}'")
|
||||||
|
|
||||||
|
contents = open(zeo_address_file, 'r').read().strip().split(":")
|
||||||
|
|
||||||
|
if len(contents) != 2:
|
||||||
|
raise ValueError(f"Invalid ZEO address file: {zeo_address_file}")
|
||||||
|
|
||||||
|
host = contents[0]
|
||||||
|
try:
|
||||||
|
port = int(contents[1])
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError(f"Invalid ZEO address file: {zeo_address_file}")
|
||||||
|
return host,port
|
||||||
|
|
||||||
|
def get_db() -> ZODB.DB:
|
||||||
|
return ZEO.DB(get_zeo_address(settings.zeo_file))
|
||||||
|
|
||||||
|
def get_transaction() -> ContextManager:
|
||||||
|
return ZEO.DB(get_zeo_address(settings.zeo_file)).transaction()
|
||||||
|
|
||||||
|
DbDependency = Annotated[ZODB.DB, Depends(get_db)]
|
||||||
|
TransactionDependency = Annotated[ContextManager, Depends(get_transaction)]
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from fastapi import Depends, HTTPException, status
|
|||||||
from fastapi.security import HTTPBasic, HTTPBasicCredentials
|
from fastapi.security import HTTPBasic, HTTPBasicCredentials
|
||||||
|
|
||||||
from .auth import HttpUser
|
from .auth import HttpUser
|
||||||
|
from .database import get_transaction
|
||||||
|
|
||||||
security = HTTPBasic()
|
security = HTTPBasic()
|
||||||
|
|
||||||
@@ -13,9 +13,8 @@ async def get_current_http_user(credentials: HTTPBasicCredentials = Depends(secu
|
|||||||
Authenticate via Basic Auth using HttpUser from ZODB.
|
Authenticate via Basic Auth using HttpUser from ZODB.
|
||||||
Injected by the standalone runner (get_db_connection available).
|
Injected by the standalone runner (get_db_connection available).
|
||||||
"""
|
"""
|
||||||
from packetserver.runners.http_server import get_db_connection # provided by runner
|
|
||||||
|
|
||||||
conn = get_db_connection()
|
with get_transaction() as conn:
|
||||||
root = conn.root()
|
root = conn.root()
|
||||||
|
|
||||||
http_users = root.get("httpUsers")
|
http_users = root.get("httpUsers")
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from datetime import datetime
|
|||||||
import transaction
|
import transaction
|
||||||
from persistent.list import PersistentList
|
from persistent.list import PersistentList
|
||||||
|
|
||||||
|
from ..database import DbDependency, TransactionDependency
|
||||||
from ..dependencies import get_current_http_user
|
from ..dependencies import get_current_http_user
|
||||||
from ..auth import HttpUser
|
from ..auth import HttpUser
|
||||||
from ..server import templates
|
from ..server import templates
|
||||||
@@ -20,9 +21,8 @@ html_router = APIRouter(tags=["bulletins-html"])
|
|||||||
|
|
||||||
# --- API Endpoints ---
|
# --- API Endpoints ---
|
||||||
|
|
||||||
async def list_bulletins(limit: int = 50, since: Optional[datetime] = None) -> dict:
|
async def list_bulletins(, limit: int = 50, since: Optional[datetime] = None) -> dict:
|
||||||
from packetserver.runners.http_server import get_db_connection
|
with trans as conn:
|
||||||
conn = get_db_connection()
|
|
||||||
root = conn.root()
|
root = conn.root()
|
||||||
bulletins_list: List[Bulletin] = root.get("bulletins", [])
|
bulletins_list: List[Bulletin] = root.get("bulletins", [])
|
||||||
|
|
||||||
@@ -55,8 +55,7 @@ async def api_list_bulletins(
|
|||||||
return await list_bulletins(limit=limit, since=since)
|
return await list_bulletins(limit=limit, since=since)
|
||||||
|
|
||||||
async def get_one_bulletin(bid: int) -> dict:
|
async def get_one_bulletin(bid: int) -> dict:
|
||||||
from packetserver.runners.http_server import get_db_connection
|
with get_transaction() as conn:
|
||||||
conn = get_db_connection()
|
|
||||||
root = conn.root()
|
root = conn.root()
|
||||||
bulletins_list: List[Bulletin] = root.get("bulletins", [])
|
bulletins_list: List[Bulletin] = root.get("bulletins", [])
|
||||||
|
|
||||||
|
|||||||
@@ -18,50 +18,6 @@ import ZODB.DB
|
|||||||
import logging
|
import logging
|
||||||
from packetserver.http.server import app
|
from packetserver.http.server import app
|
||||||
|
|
||||||
# Global DB and connection for reuse in the FastAPI dependency
|
|
||||||
_db = None
|
|
||||||
_connection = None
|
|
||||||
|
|
||||||
|
|
||||||
def open_database(db_arg: str) -> ZODB.DB:
|
|
||||||
"""
|
|
||||||
Open a ZODB database from either a local FileStorage path or ZEO address.
|
|
||||||
"""
|
|
||||||
if ":" in db_arg:
|
|
||||||
parts = db_arg.split(":")
|
|
||||||
if len(parts) == 2 and parts[1].isdigit():
|
|
||||||
import ZEO
|
|
||||||
host = parts[0]
|
|
||||||
port = int(parts[1])
|
|
||||||
storage = ZEO.client((host, port)) # correct modern ZEO client function
|
|
||||||
return ZODB.DB(storage)
|
|
||||||
|
|
||||||
# Local FileStorage fallback
|
|
||||||
storage = ZODB.FileStorage.FileStorage(db_arg)
|
|
||||||
return ZODB.DB(storage)
|
|
||||||
|
|
||||||
|
|
||||||
def get_db_connection():
|
|
||||||
"""Helper used in http/server.py dependency (get_current_http_user)"""
|
|
||||||
global _connection
|
|
||||||
if _connection is None or getattr(_connection, "opened", None) is None:
|
|
||||||
if _db is None:
|
|
||||||
raise RuntimeError("Database not opened – run the runner properly")
|
|
||||||
_connection = _db.open()
|
|
||||||
return _connection
|
|
||||||
|
|
||||||
def get_db():
|
|
||||||
"""Helper used in http/server.py dependency (get_current_http_user)"""
|
|
||||||
if _db is None:
|
|
||||||
raise RuntimeError("Database not opened – run the runner properly")
|
|
||||||
return _db
|
|
||||||
|
|
||||||
|
|
||||||
# Monkey-patch the dependency helper so server.py can use it without changes
|
|
||||||
from packetserver.http import server
|
|
||||||
server.get_db_connection = get_db_connection # replaces any previous definition
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser = argparse.ArgumentParser(description="Run the PacketServer HTTP API server")
|
parser = argparse.ArgumentParser(description="Run the PacketServer HTTP API server")
|
||||||
parser.add_argument("--db", required=True, help="DB path (local /path/to/Data.fs) or ZEO (host:port)")
|
parser.add_argument("--db", required=True, help="DB path (local /path/to/Data.fs) or ZEO (host:port)")
|
||||||
@@ -71,30 +27,12 @@ def main():
|
|||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
global _db
|
|
||||||
try:
|
|
||||||
_db = open_database(args.db)
|
|
||||||
print(f"Opened database: {args.db}")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Failed to open database {args.db}: {e}")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# Open initial connection (will be reused/closed on shutdown)
|
|
||||||
get_db_connection()
|
|
||||||
|
|
||||||
try:
|
|
||||||
uvicorn.run(
|
uvicorn.run(
|
||||||
"packetserver.http.server:app",
|
"packetserver.http.server:app",
|
||||||
host=args.host,
|
host=args.host,
|
||||||
port=args.port,
|
port=args.port,
|
||||||
reload=args.reload,
|
reload=args.reload,
|
||||||
)
|
)
|
||||||
finally:
|
|
||||||
if _connection and not _connection.closed:
|
|
||||||
_connection.close()
|
|
||||||
if _db:
|
|
||||||
_db.close()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
@@ -14,3 +14,4 @@ jinja2
|
|||||||
python-multipart
|
python-multipart
|
||||||
argon2-cffi
|
argon2-cffi
|
||||||
pydantic
|
pydantic
|
||||||
|
pydantic_settings
|
||||||
Reference in New Issue
Block a user