Интегрирован wesp в сайт
CI / quality (push) Canceled after 0s

This commit is contained in:
влад
2026-07-17 12:57:18 +03:00
parent 5dfa06ddbe
commit 355c0ef9f1
883 changed files with 194576 additions and 177 deletions
@@ -0,0 +1,213 @@
"""Согласованность POST /api/recipes/calculate с app.services.recipe_calculator."""
from __future__ import annotations
import os
import tempfile
import unittest
from app import create_app, db
from app.models import Component
from app.services.recipe_calculator import calculate_recipe
from config import TestingConfig
class RecipeCalculatePipelineConfig(TestingConfig):
_TMP_DIR = tempfile.mkdtemp(prefix="wesp-calc-pipeline-")
SQLALCHEMY_DATABASE_URI = f"sqlite:///{os.path.join(_TMP_DIR, 'calc_pipeline.db')}"
SQLALCHEMY_BINDS = {"reports": f"sqlite:///{os.path.join(_TMP_DIR, 'reports_pipeline.db')}"}
AUTH_LOGIN = "calc-pipeline-admin"
AUTH_PASSWORD = "calc-pipeline-secret"
def _format_like_api(result: dict) -> dict:
"""Те же правила округления, что в calculate_recipe_endpoint."""
out = {
"ingredients": [dict(i) for i in result.get("ingredients", [])],
"totals": dict(result.get("totals", {})),
"unloadingGroups": [dict(g) for g in result.get("unloadingGroups", [])],
"unloadingTotals": dict(result.get("unloadingTotals", {})),
}
def to_float2(x):
try:
return round(float(x), 2)
except (TypeError, ValueError):
return 0.0
def truncate2(x):
try:
v = float(x)
return float(int(v * 100)) / 100.0
except (TypeError, ValueError):
return 0.0
for ing in out["ingredients"]:
for key in ("weightPerHead", "tripWeight", "totalWeight", "dryMatterPerHead"):
if key in ing and ing[key] is not None:
raw_val = ing[key]
v = truncate2(raw_val) if key == "weightPerHead" else to_float2(raw_val)
ing[key] = f"{v:.2f}" if key == "weightPerHead" else v
for key in (
"totalWeight",
"totalTripWeight",
"totalDryMatterPerHead",
"totalWeightPerHead",
):
if key in out["totals"] and out["totals"][key] is not None:
out["totals"][key] = to_float2(out["totals"][key])
return out
class RecipeCalculatePipelineTests(unittest.TestCase):
def setUp(self) -> None:
self.app = create_app(RecipeCalculatePipelineConfig)
self.client = self.app.test_client()
self.ctx = self.app.app_context()
self.ctx.push()
db.create_all()
self.client.post(
"/api/auth/login",
json={"login": "calc-pipeline-admin", "password": "calc-pipeline-secret"},
)
self.comp_grain = "pipe-comp-grain"
self.comp_protein = "pipe-comp-protein"
db.session.add_all(
[
Component(
id=self.comp_grain,
name="Зерно",
type="grain",
dry_matter=80.0,
protein=0.0,
energy=0.0,
price=0.0,
),
Component(
id=self.comp_protein,
name="Белок",
type="protein",
dry_matter=90.0,
protein=0.0,
energy=0.0,
price=0.0,
),
]
)
db.session.commit()
def tearDown(self) -> None:
db.session.remove()
db.drop_all()
self.ctx.pop()
def _post_calculate(self, payload: dict) -> dict:
resp = self.client.post("/api/recipes/calculate", json=payload)
self.assertEqual(resp.status_code, 200, resp.get_data(as_text=True))
return resp.get_json()
def _expected_from_service(self, payload: dict) -> dict:
heads_count = int(
payload.get("headsCount")
or payload.get("heads_count")
or payload.get("headsPerTrip")
or 0
)
trip_percent = float(payload.get("tripPercent") or payload.get("trip_percent") or 100)
ingredients = payload.get("ingredients", []) or []
unloading_groups = payload.get("unloadingGroups") or payload.get("unloading_groups") or []
calculate_from_dry_matter = bool(
payload.get("calculateFromDryMatter")
if payload.get("calculateFromDryMatter") is not None
else payload.get("calculate_from_dry_matter", False)
)
normalized_ingredients = []
for ing in ingredients:
ing = dict(ing)
if "component_id" not in ing and "componentId" in ing:
ing["component_id"] = ing.get("componentId")
if "dryMatterPerHead" not in ing and "dry_matter_per_head" in ing:
ing["dryMatterPerHead"] = ing.get("dry_matter_per_head")
if "dryMatter" not in ing and "dry_matter" in ing:
ing["dryMatter"] = ing.get("dry_matter")
if "weightPerHead" not in ing and "weight_per_head" in ing:
ing["weightPerHead"] = ing.get("weight_per_head")
normalized_ingredients.append(ing)
normalized_groups = []
for g in unloading_groups:
g = dict(g)
if "distributionType" not in g and "distribution_type" in g:
g["distributionType"] = g.get("distribution_type")
normalized_groups.append(g)
component_ids = [i.get("component_id") for i in normalized_ingredients if i.get("component_id")]
component_dry_matter_map = {}
if component_ids:
comps = db.session.query(Component).filter(Component.id.in_(component_ids)).all()
component_dry_matter_map = {c.id: c.dry_matter for c in comps}
raw = calculate_recipe(
ingredients=normalized_ingredients,
heads_count=heads_count,
trip_percent=trip_percent,
unloading_groups=normalized_groups,
component_dry_matter_map=component_dry_matter_map or None,
calculate_from_dry_matter=calculate_from_dry_matter,
)
return _format_like_api(raw)
def test_api_matches_service_weight_mode_with_groups(self) -> None:
payload = {
"headsCount": 137,
"tripPercent": 75.0,
"ingredients": [
{"weightPerHead": 2.5, "dryMatter": 50.0, "componentId": self.comp_grain},
{"weightPerHead": 1.0, "dryMatter": 0, "componentId": self.comp_protein},
],
"unloadingGroups": [
{"distributionType": "heads", "value": 67},
{"distributionType": "heads", "value": 68},
],
}
api = self._post_calculate(payload)
expected = self._expected_from_service(payload)
self.assertEqual(api["totals"], expected["totals"])
self.assertEqual(api["unloadingGroups"], expected["unloadingGroups"])
self.assertEqual(api["unloadingTotals"], expected["unloadingTotals"])
self.assertEqual(len(api["ingredients"]), 2)
self.assertEqual(api["ingredients"][0]["weightPerHead"], expected["ingredients"][0]["weightPerHead"])
self.assertEqual(api["ingredients"][1]["dryMatterPerHead"], expected["ingredients"][1]["dryMatterPerHead"])
def test_api_matches_service_dry_matter_mode(self) -> None:
payload = {
"heads_count": 50,
"trip_percent": 100,
"calculate_from_dry_matter": True,
"ingredients": [
{"dry_matter_per_head": 0.8, "dry_matter": 0, "component_id": self.comp_grain},
{"dry_matter_per_head": 0.45, "dryMatter": 90.0, "component_id": self.comp_protein},
],
"unloading_groups": [{"distribution_type": "percent", "value": 100}],
}
api = self._post_calculate(payload)
expected = self._expected_from_service(payload)
self.assertEqual(api["totals"], expected["totals"])
self.assertEqual(api["ingredients"][0]["weightPerHead"], expected["ingredients"][0]["weightPerHead"])
self.assertEqual(api["ingredients"][1]["weightPerHead"], expected["ingredients"][1]["weightPerHead"])
self.assertEqual(api["unloadingGroups"][0]["calculatedWeight"], expected["unloadingGroups"][0]["calculatedWeight"])
def test_api_rejects_invalid_numeric_params(self) -> None:
resp = self.client.post(
"/api/recipes/calculate",
json={"headsCount": "abc", "ingredients": []},
)
self.assertEqual(resp.status_code, 400)
self.assertIn("error", resp.get_json())
if __name__ == "__main__":
unittest.main()