diff --git a/crudkit/registry.py b/crudkit/registry.py new file mode 100644 index 0000000..8dc200d --- /dev/null +++ b/crudkit/registry.py @@ -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/ 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=) diff --git a/inventory/__init__.py b/inventory/__init__.py index 3ab61dd..4eb2929 100644 --- a/inventory/__init__.py +++ b/inventory/__init__.py @@ -11,6 +11,7 @@ from crudkit.integrations.flask import init_app from .config import DevConfig from .routes.index import init_index_routes +from .routes.listing import init_listing_routes def create_app(config_cls=DevConfig) -> Flask: app = Flask(__name__) @@ -53,6 +54,7 @@ def create_app(config_cls=DevConfig) -> Flask: app.register_blueprint(bp_reports) init_index_routes(app) + init_listing_routes(app) @app.teardown_appcontext def _remove_session(_exc): diff --git a/inventory/routes/index.py b/inventory/routes/index.py index cc04000..1d7c83f 100644 --- a/inventory/routes/index.py +++ b/inventory/routes/index.py @@ -16,54 +16,6 @@ from ..models.work_log import WorkLog bp_index = Blueprint("index", __name__) 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("/") def index(): session = get_session() diff --git a/inventory/routes/listing.py b/inventory/routes/listing.py new file mode 100644 index 0000000..4bb6ab7 --- /dev/null +++ b/inventory/routes/listing.py @@ -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/") + def show_list(model): + session = get_session() + return render_template("listing.html", model=model) + + app.register_blueprint(bp_listing) \ No newline at end of file diff --git a/inventory/templates/listing.html b/inventory/templates/listing.html new file mode 100644 index 0000000..8948643 --- /dev/null +++ b/inventory/templates/listing.html @@ -0,0 +1,5 @@ +{% extends 'base.html' %} + +{% block main %} +Feelin' fine. +{% endblock %} \ No newline at end of file