WIP on registration modifications.
This commit is contained in:
parent
cc4bdafa0b
commit
cb74511677
5 changed files with 115 additions and 48 deletions
95
crudkit/registry.py
Normal file
95
crudkit/registry.py
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Any, Dict, Optional, Type, TypeVar, cast
|
||||||
|
|
||||||
|
from flask import Flask
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from crudkit.core.service import CRUDService
|
||||||
|
from crudkit.api.flask_api import generate_crud_blueprint
|
||||||
|
from crudkit.engines import CRUDKitRuntime
|
||||||
|
|
||||||
|
T = TypeVar("T")
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Registered:
|
||||||
|
model: Type[Any]
|
||||||
|
service: CRUDService[Any]
|
||||||
|
blueprint_name: str
|
||||||
|
url_prefix: str
|
||||||
|
|
||||||
|
class CRUDRegistry:
|
||||||
|
"""
|
||||||
|
Binds:
|
||||||
|
- name -> model class
|
||||||
|
- model class -> CRUDService (using CRUDKitRuntime.session_factory)
|
||||||
|
- model class -> Flask blueprint (via generate_crud_blueprint)
|
||||||
|
"""
|
||||||
|
def __init__(self, runtime: CRUDKitRuntime):
|
||||||
|
self._rt = runtime
|
||||||
|
self._models_by_key: Dict[str, Type[Any]] = {}
|
||||||
|
self._services_by_model: Dict[Type[Any], CRUDService[Any]] = {}
|
||||||
|
self._bps_by_model: Dict[Type[Any], Registered] = {}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _key(model_or_name: Type[Any] | str) -> str:
|
||||||
|
return model_or_name.lower() if isinstance(model_or_name, str) else model_or_name.__name__.lower()
|
||||||
|
|
||||||
|
def get_model(self, key: str) -> Optional[Type[Any]]:
|
||||||
|
return self._models_by_key.get(key.lower())
|
||||||
|
|
||||||
|
def get_service(self, model: Type[T]) -> Optional[CRUDService[T]]:
|
||||||
|
return cast(Optional[CRUDService[T]], self._services_by_model.get(model))
|
||||||
|
|
||||||
|
def is_registered(self, model: Type[Any]) -> bool:
|
||||||
|
return model in self._services_by_model
|
||||||
|
|
||||||
|
def register_class(
|
||||||
|
self,
|
||||||
|
app: Flask,
|
||||||
|
model: Type[Any],
|
||||||
|
*,
|
||||||
|
url_prefix: Optional[str] = None,
|
||||||
|
blueprint_name: Optional[str] = None,
|
||||||
|
polymorphic: bool = False,
|
||||||
|
service_kwargs: Optional[dict] = None
|
||||||
|
) -> Registered:
|
||||||
|
"""
|
||||||
|
Register a model:
|
||||||
|
- store name -> class
|
||||||
|
- create a CRUDService bound to a Session from runtime.session_factory
|
||||||
|
- attach backend into from runtime.backend
|
||||||
|
- mount Flask blueprint at /api/<modelname> by default
|
||||||
|
Idempotent for each model.
|
||||||
|
"""
|
||||||
|
key = self._key(model)
|
||||||
|
self._models_by_key.setdefault(key, model)
|
||||||
|
|
||||||
|
svc = self._services_by_model.get(model)
|
||||||
|
if svc is None:
|
||||||
|
SessionMaker = self._rt.session_factory
|
||||||
|
if SessionMaker is None:
|
||||||
|
raise RuntimeError("CRUDKitRuntime.session_factory is not initialized.")
|
||||||
|
session: Session = SessionMaker()
|
||||||
|
svc = CRUDService(
|
||||||
|
model,
|
||||||
|
session=session,
|
||||||
|
polymorphic=polymorphic,
|
||||||
|
backend=self._rt.backend,
|
||||||
|
**(service_kwargs or {}),
|
||||||
|
)
|
||||||
|
self._services_by_model[model] = svc
|
||||||
|
|
||||||
|
reg = self._bps_by_model.get(model)
|
||||||
|
if reg:
|
||||||
|
return reg
|
||||||
|
|
||||||
|
prefix = url_prefix or f"/api/{key}"
|
||||||
|
bp_name = blueprint_name or f"crudkit.{key}"
|
||||||
|
|
||||||
|
bp = generate_crud_blueprint(model, svc)
|
||||||
|
bp.name = bp_name
|
||||||
|
app.register_blueprint(bp, url_prefix=prefix)
|
||||||
|
|
||||||
|
reg = Registered(model=model, service=)
|
||||||
|
|
@ -11,6 +11,7 @@ from crudkit.integrations.flask import init_app
|
||||||
from .config import DevConfig
|
from .config import DevConfig
|
||||||
|
|
||||||
from .routes.index import init_index_routes
|
from .routes.index import init_index_routes
|
||||||
|
from .routes.listing import init_listing_routes
|
||||||
|
|
||||||
def create_app(config_cls=DevConfig) -> Flask:
|
def create_app(config_cls=DevConfig) -> Flask:
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
|
@ -53,6 +54,7 @@ def create_app(config_cls=DevConfig) -> Flask:
|
||||||
app.register_blueprint(bp_reports)
|
app.register_blueprint(bp_reports)
|
||||||
|
|
||||||
init_index_routes(app)
|
init_index_routes(app)
|
||||||
|
init_listing_routes(app)
|
||||||
|
|
||||||
@app.teardown_appcontext
|
@app.teardown_appcontext
|
||||||
def _remove_session(_exc):
|
def _remove_session(_exc):
|
||||||
|
|
|
||||||
|
|
@ -16,54 +16,6 @@ from ..models.work_log import WorkLog
|
||||||
bp_index = Blueprint("index", __name__)
|
bp_index = Blueprint("index", __name__)
|
||||||
|
|
||||||
def init_index_routes(app):
|
def init_index_routes(app):
|
||||||
|
|
||||||
@bp_index.get("/api/pivot/inventory")
|
|
||||||
def pivot_inventory():
|
|
||||||
session = get_session()
|
|
||||||
# qs params: rows, cols, where_status, top_n
|
|
||||||
rows = request.args.get("rows", "device_type.description")
|
|
||||||
cols = request.args.get("cols", "condition")
|
|
||||||
top_n = int(request.args.get("top_n", "40"))
|
|
||||||
|
|
||||||
# Map friendly names to columns
|
|
||||||
COLS = {
|
|
||||||
"device_type.description": DeviceType.description.label("row"),
|
|
||||||
"condition": Inventory.condition.label("row"),
|
|
||||||
"status": Inventory.condition.label("row"), # alias if you use 'status' in UI
|
|
||||||
}
|
|
||||||
ROW = COLS.get(rows)
|
|
||||||
COL = Inventory.condition.label("col") if cols == "condition" else DeviceType.description.label("col")
|
|
||||||
|
|
||||||
stmt = (
|
|
||||||
select(ROW, COL, func.count(Inventory.id).label("n"))
|
|
||||||
.select_from(Inventory).join(DeviceType, Inventory.type_id == DeviceType.id)
|
|
||||||
.group_by(ROW, COL)
|
|
||||||
)
|
|
||||||
data = session.execute(stmt).all() # [(row, col, n), ...]
|
|
||||||
|
|
||||||
# reshape into Chart.js: labels = sorted rows by total, datasets per column value
|
|
||||||
import pandas as pd
|
|
||||||
df = pd.DataFrame(data, columns=["row", "col", "n"])
|
|
||||||
if df.empty:
|
|
||||||
return jsonify({"labels": [], "datasets": []})
|
|
||||||
|
|
||||||
totals = df.groupby("row")["n"].sum().sort_values(ascending=False)
|
|
||||||
keep_rows = totals.head(top_n).index.tolist()
|
|
||||||
df = df[df["row"].isin(keep_rows)]
|
|
||||||
|
|
||||||
labels = keep_rows
|
|
||||||
by_col = df.pivot_table(index="row", columns="col", values="n", aggfunc="sum", fill_value=0)
|
|
||||||
by_col = by_col.reindex(labels) # row order
|
|
||||||
|
|
||||||
payload = {
|
|
||||||
"labels": labels,
|
|
||||||
"datasets": [
|
|
||||||
{"label": str(col), "data": by_col[col].astype(int).tolist()}
|
|
||||||
for col in by_col.columns
|
|
||||||
]
|
|
||||||
}
|
|
||||||
return jsonify(payload)
|
|
||||||
|
|
||||||
@bp_index.get("/")
|
@bp_index.get("/")
|
||||||
def index():
|
def index():
|
||||||
session = get_session()
|
session = get_session()
|
||||||
|
|
|
||||||
13
inventory/routes/listing.py
Normal file
13
inventory/routes/listing.py
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
from flask import Blueprint, render_template
|
||||||
|
|
||||||
|
from ..db import get_session
|
||||||
|
|
||||||
|
bp_listing = Blueprint("listing", __name__)
|
||||||
|
|
||||||
|
def init_listing_routes(app):
|
||||||
|
@bp_listing.get("/listing/<model>")
|
||||||
|
def show_list(model):
|
||||||
|
session = get_session()
|
||||||
|
return render_template("listing.html", model=model)
|
||||||
|
|
||||||
|
app.register_blueprint(bp_listing)
|
||||||
5
inventory/templates/listing.html
Normal file
5
inventory/templates/listing.html
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block main %}
|
||||||
|
Feelin' fine.
|
||||||
|
{% endblock %}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue