start project

This commit is contained in:
Hemmat
2026-08-07 21:17:17 +03:30
commit ea1e1eead6
121 changed files with 8108 additions and 0 deletions
View File
+39
View File
@@ -0,0 +1,39 @@
"""Vite manifest reader — maps a source entry to its hashed dist path (Ch05)."""
from __future__ import annotations
import json
from pathlib import Path
from flask import Flask
class ManifestNotFound(RuntimeError):
"""app/static/dist/manifest.json is missing — `npm run build` hasn't been run."""
def _load_manifest(static_dist_dir: Path) -> dict:
for candidate in (static_dist_dir / ".vite" / "manifest.json", static_dist_dir / "manifest.json"):
if candidate.exists():
return json.loads(candidate.read_text())
raise ManifestNotFound(f"No Vite manifest under {static_dist_dir} — run `npm run build`.")
def register_asset_helper(app: Flask) -> None:
"""Register `asset(name)` as a Jinja global (Chapter 05)."""
static_dist_dir = Path(app.static_folder) / "dist"
@app.context_processor
def inject_asset_helper():
def asset(name: str) -> str:
try:
manifest = _load_manifest(static_dist_dir)
except ManifestNotFound:
if app.debug:
return f"/static/dist/{name}" # tolerate an unbuilt dev checkout
raise
entry = manifest.get(name)
if entry is None:
raise KeyError(f"{name!r} not in Vite manifest.")
return f"/static/dist/{entry['file']}"
return {"asset": asset}
+32
View File
@@ -0,0 +1,32 @@
"""Shared from/to date-range parsing (Chapter 11: consistent across every
chart/KPI endpoint; never defaults to "all time" per Chapter 03 rule 7).
"""
from __future__ import annotations
from datetime import date, datetime, timedelta
from flask import Request
DEFAULT_RANGE_DAYS = 7 # within Ch11's stated "7 or 30 day" default allowance
def parse_date_range(request: Request) -> tuple[date, date]:
"""Parse `?from=&to=` (ISO 8601 dates), defaulting to the last 7 days."""
to_raw = request.args.get("to")
from_raw = request.args.get("from")
to_date = date.fromisoformat(to_raw) if to_raw else date.today()
from_date = date.fromisoformat(from_raw) if from_raw else to_date - timedelta(days=DEFAULT_RANGE_DAYS - 1)
if from_date > to_date:
from_date, to_date = to_date, from_date
return from_date, to_date
def day_bounds(from_date: date, to_date: date) -> tuple[datetime, datetime]:
"""Inclusive [from_date, to_date] -> half-open [start, end) datetime range.
Shared by overview/queries.py, seo/queries.py, and security/queries.py
(consolidated here rather than each keeping its own local copy).
"""
start = datetime.combine(from_date, datetime.min.time())
end = datetime.combine(to_date, datetime.min.time()) + timedelta(days=1)
return start, end
+18
View File
@@ -0,0 +1,18 @@
"""Shared JSON envelope helpers (Chapter 11).
Every existing /api/... endpoint already hand-builds this exact shape via
jsonify(data=..., meta=...) — audited for compliance in docs/api-contract-
final.md. This module exists so NEW endpoints (e.g. this chapter's
unauthorized_handler) have one call site instead of re-typing the shape.
"""
from __future__ import annotations
from flask import jsonify
def api_ok(data, meta: dict | None = None):
return jsonify(data=data, meta=meta or {})
def api_error(code: str, message: str, status: int):
return jsonify(error={"code": code, "message": message}), status
+20
View File
@@ -0,0 +1,20 @@
"""Shared HTMX full-page-vs-fragment response helper (Chapter 04).
Every blueprint serving a dashboard tab (Ch08/09/10) uses this instead of
duplicating the `if request.headers.get("HX-Request")` check, so the
convention is identical everywhere.
"""
from __future__ import annotations
from flask import Request, render_template
def is_htmx(request: Request) -> bool:
"""True if this request was triggered by an hx-* attribute."""
return request.headers.get("HX-Request", "").lower() == "true"
def render_htmx_aware(request: Request, *, full_template: str, partial_template: str, **ctx):
"""Render `full_template` on first load, `partial_template` on HTMX swaps."""
template = partial_template if is_htmx(request) else full_template
return render_template(template, **ctx)
+14
View File
@@ -0,0 +1,14 @@
"""Shared HTTP status-code bucketing — used by the aggregator (per-IP
rollup) and available for any endpoint needing the same 2xx/3xx/4xx/5xx
buckets, so there's one bucketing rule, not several copies."""
from __future__ import annotations
def status_bucket(status_code: int) -> str:
if status_code < 300:
return "2xx"
if status_code < 400:
return "3xx"
if status_code < 500:
return "4xx"
return "5xx"
+14
View File
@@ -0,0 +1,14 @@
"""Shared page/per_page parsing (Chapter 11: consistent across every
Grid.js-backed endpoint)."""
from __future__ import annotations
from flask import Request
DEFAULT_PER_PAGE = 20
MAX_PER_PAGE = 100
def parse_pagination(request: Request) -> tuple[int, int]:
page = max(1, request.args.get("page", 1, type=int))
per_page = request.args.get("per_page", DEFAULT_PER_PAGE, type=int)
return page, max(1, min(per_page, MAX_PER_PAGE))
+20
View File
@@ -0,0 +1,20 @@
"""Shared upload-path helper (Chapter 04) — used by the uploads blueprint,
the optional CLI processor, and the automatic background-thread processor,
so there's one definition of where a given LogFile's raw upload lives on
disk. Pulled out of the uploads blueprint so services/ doesn't have to
import from blueprints/ (the wrong direction) to reach it.
"""
from __future__ import annotations
from pathlib import Path
from flask import current_app
from app.models.log_file import LogFile
def upload_path_for(log_file: LogFile) -> Path:
"""Deterministic on-disk path for a LogFile row — avoids a new DB column."""
ext = Path(log_file.filename).suffix.lower()
upload_dir = Path(current_app.config["UPLOAD_DIR"])
return upload_dir / f"{log_file.id}{ext}"