Initial commit: site monorepo with API, web, and infra.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,183 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from datetime import UTC, datetime
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import DateTime, select
|
||||
|
||||
from app.core.database import session_scope
|
||||
from app.modules.zootech.catalog_models import (
|
||||
ZootechDailyComponentNormAdjustment,
|
||||
ZootechDailyIngredientReplacement,
|
||||
ZootechDailyIngredientSkip,
|
||||
ZootechDailyTripSkip,
|
||||
ZootechDailyUnloadingGroupSkip,
|
||||
ZootechFeedDispenser,
|
||||
ZootechFeedingLocation,
|
||||
ZootechFeedingPeriod,
|
||||
ZootechFeedingPoint,
|
||||
ZootechFeedMixer,
|
||||
ZootechPeriodRecipe,
|
||||
ZootechTrip,
|
||||
ZootechUnloadingGroup,
|
||||
)
|
||||
from app.modules.zootech.models import ZootechComponent, ZootechIngredient, ZootechRecipe
|
||||
|
||||
TABLE_MODEL_MAP = {
|
||||
"component": ZootechComponent,
|
||||
"recipe": ZootechRecipe,
|
||||
"ingredient": ZootechIngredient,
|
||||
"unloading_group": ZootechUnloadingGroup,
|
||||
"feed_mixer": ZootechFeedMixer,
|
||||
"feed_dispenser": ZootechFeedDispenser,
|
||||
"feeding_location": ZootechFeedingLocation,
|
||||
"feeding_period": ZootechFeedingPeriod,
|
||||
"feeding_point": ZootechFeedingPoint,
|
||||
"period_recipes": ZootechPeriodRecipe,
|
||||
"daily_trip_skip": ZootechDailyTripSkip,
|
||||
"daily_ingredient_skip": ZootechDailyIngredientSkip,
|
||||
"daily_unloading_group_skip": ZootechDailyUnloadingGroupSkip,
|
||||
"daily_ingredient_replacement": ZootechDailyIngredientReplacement,
|
||||
"daily_component_norm_adjustment": ZootechDailyComponentNormAdjustment,
|
||||
"trip": ZootechTrip,
|
||||
}
|
||||
|
||||
|
||||
def _coerce_for_model(model, key: str, value: Any) -> Any:
|
||||
if value is None:
|
||||
return value
|
||||
column = model.__table__.columns.get(key)
|
||||
if column is None:
|
||||
return value
|
||||
if isinstance(column.type, DateTime) and isinstance(value, str):
|
||||
try:
|
||||
return datetime.fromisoformat(value.replace("Z", "+00:00"))
|
||||
except ValueError:
|
||||
return datetime.now(UTC)
|
||||
return value
|
||||
|
||||
|
||||
def _parse_period_recipe_id(record_id: str) -> tuple[str, str]:
|
||||
if ":" in record_id:
|
||||
period_id, recipe_id = record_id.split(":", 1)
|
||||
return period_id, recipe_id
|
||||
return record_id, record_id
|
||||
|
||||
|
||||
def load_catalog_row(enterprise_id: str, table_name: str, record_id: str) -> dict[str, Any] | None:
|
||||
model = TABLE_MODEL_MAP.get(table_name)
|
||||
if not model:
|
||||
return None
|
||||
with session_scope() as db:
|
||||
if model is ZootechPeriodRecipe:
|
||||
period_id, recipe_id = _parse_period_recipe_id(record_id)
|
||||
row = db.scalar(
|
||||
select(model).where(
|
||||
model.enterprise_id == enterprise_id,
|
||||
model.period_id == period_id,
|
||||
model.recipe_id == recipe_id,
|
||||
)
|
||||
)
|
||||
else:
|
||||
row = db.scalar(
|
||||
select(model).where(model.enterprise_id == enterprise_id, model.id == record_id)
|
||||
)
|
||||
if not row:
|
||||
return None
|
||||
return _row_to_dict(row, table_name)
|
||||
|
||||
|
||||
def apply_catalog_change(
|
||||
enterprise_id: str,
|
||||
table_name: str,
|
||||
record_id: str,
|
||||
action: str,
|
||||
payload: dict[str, Any],
|
||||
version: int,
|
||||
content_hash: str,
|
||||
) -> None:
|
||||
model = TABLE_MODEL_MAP.get(table_name)
|
||||
if not model:
|
||||
return
|
||||
with session_scope() as db:
|
||||
if model is ZootechPeriodRecipe:
|
||||
period_id, recipe_id = _parse_period_recipe_id(record_id)
|
||||
row = db.scalar(
|
||||
select(model).where(
|
||||
model.enterprise_id == enterprise_id,
|
||||
model.period_id == period_id,
|
||||
model.recipe_id == recipe_id,
|
||||
)
|
||||
)
|
||||
else:
|
||||
row = db.scalar(
|
||||
select(model).where(model.enterprise_id == enterprise_id, model.id == record_id)
|
||||
)
|
||||
if action == "delete":
|
||||
if row:
|
||||
row.is_deleted = True
|
||||
row.version = version
|
||||
row.content_hash = content_hash
|
||||
row.updated_at = datetime.now(UTC)
|
||||
return
|
||||
data = dict(payload)
|
||||
data["enterprise_id"] = enterprise_id
|
||||
data["version"] = version
|
||||
data["content_hash"] = content_hash
|
||||
data["is_deleted"] = False
|
||||
if model is ZootechPeriodRecipe:
|
||||
data["period_id"] = data.get("period_id") or _parse_period_recipe_id(record_id)[0]
|
||||
data["recipe_id"] = data.get("recipe_id") or _parse_period_recipe_id(record_id)[1]
|
||||
else:
|
||||
data["id"] = record_id
|
||||
if row:
|
||||
_apply_fields(row, data, model)
|
||||
row.updated_at = datetime.now(UTC)
|
||||
else:
|
||||
allowed = {c.key for c in model.__table__.columns}
|
||||
filtered = {
|
||||
k: _coerce_for_model(model, k, v) for k, v in data.items() if k in allowed
|
||||
}
|
||||
extra = {k: v for k, v in data.items() if k not in allowed}
|
||||
if extra and "payload_json" in allowed:
|
||||
filtered["payload_json"] = json.dumps(extra, ensure_ascii=False)
|
||||
db.add(model(**filtered))
|
||||
|
||||
|
||||
def _apply_fields(row, data: dict[str, Any], model) -> None:
|
||||
allowed = {c.key for c in model.__table__.columns}
|
||||
extra: dict[str, Any] = {}
|
||||
for key, value in data.items():
|
||||
if key in allowed and key not in ("enterprise_id", "created_at"):
|
||||
setattr(row, key, _coerce_for_model(model, key, value))
|
||||
elif key not in ("enterprise_id", "created_at", "id"):
|
||||
extra[key] = value
|
||||
if model is ZootechRecipe and "ingredients" in data:
|
||||
row.payload_json = json.dumps(data.get("ingredients"), ensure_ascii=False)
|
||||
elif extra and "payload_json" in allowed:
|
||||
row.payload_json = json.dumps(extra, ensure_ascii=False)
|
||||
|
||||
|
||||
def _row_to_dict(row, table_name: str) -> dict[str, Any]:
|
||||
result = {}
|
||||
for col in row.__table__.columns:
|
||||
val = getattr(row, col.key)
|
||||
if isinstance(val, datetime):
|
||||
val = val.isoformat()
|
||||
result[col.key] = val
|
||||
if isinstance(row, ZootechRecipe) and row.payload_json:
|
||||
try:
|
||||
parsed = json.loads(row.payload_json)
|
||||
if isinstance(parsed, list):
|
||||
result["ingredients"] = parsed
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
if table_name == "period_recipes":
|
||||
result["id"] = f"{row.period_id}:{row.recipe_id}"
|
||||
elif hasattr(row, "payload_json") and row.payload_json and table_name != "recipe":
|
||||
try:
|
||||
result.update(json.loads(row.payload_json))
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
return result
|
||||
Reference in New Issue
Block a user