perfact-api-pd-fastapi 0.5__tar.gz → 0.6__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {perfact_api_pd_fastapi-0.5 → perfact_api_pd_fastapi-0.6}/PKG-INFO +1 -1
- perfact_api_pd_fastapi-0.6/src/perfact/api/pd_fastapi/kpi.py +221 -0
- {perfact_api_pd_fastapi-0.5 → perfact_api_pd_fastapi-0.6}/src/perfact_api_pd_fastapi.egg-info/PKG-INFO +1 -1
- {perfact_api_pd_fastapi-0.5 → perfact_api_pd_fastapi-0.6}/src/perfact_api_pd_fastapi.egg-info/SOURCES.txt +3 -0
- perfact_api_pd_fastapi-0.6/tests/__init__.py +0 -0
- perfact_api_pd_fastapi-0.6/tests/conftest.py +0 -0
- perfact_api_pd_fastapi-0.6/tests/helpers.py +17 -0
- {perfact_api_pd_fastapi-0.5 → perfact_api_pd_fastapi-0.6}/tests/test_init.py +3 -1
- perfact_api_pd_fastapi-0.6/tests/test_kpi.py +333 -0
- {perfact_api_pd_fastapi-0.5 → perfact_api_pd_fastapi-0.6}/tests/test_pdunit.py +4 -2
- {perfact_api_pd_fastapi-0.5 → perfact_api_pd_fastapi-0.6}/tests/test_performance.py +4 -2
- perfact_api_pd_fastapi-0.5/src/perfact/api/pd_fastapi/kpi.py +0 -133
- perfact_api_pd_fastapi-0.5/tests/test_kpi.py +0 -105
- {perfact_api_pd_fastapi-0.5 → perfact_api_pd_fastapi-0.6}/README.md +0 -0
- {perfact_api_pd_fastapi-0.5 → perfact_api_pd_fastapi-0.6}/pyproject.toml +0 -0
- {perfact_api_pd_fastapi-0.5 → perfact_api_pd_fastapi-0.6}/setup.cfg +0 -0
- {perfact_api_pd_fastapi-0.5 → perfact_api_pd_fastapi-0.6}/src/perfact/api/pd_fastapi/__init__.py +0 -0
- {perfact_api_pd_fastapi-0.5 → perfact_api_pd_fastapi-0.6}/src/perfact/api/pd_fastapi/pdunit.py +0 -0
- {perfact_api_pd_fastapi-0.5 → perfact_api_pd_fastapi-0.6}/src/perfact/api/pd_fastapi/pdunitlch.py +0 -0
- {perfact_api_pd_fastapi-0.5 → perfact_api_pd_fastapi-0.6}/src/perfact/api/pd_fastapi/performancedata.py +0 -0
- {perfact_api_pd_fastapi-0.5 → perfact_api_pd_fastapi-0.6}/src/perfact/api/pd_fastapi/py.typed +0 -0
- {perfact_api_pd_fastapi-0.5 → perfact_api_pd_fastapi-0.6}/src/perfact_api_pd_fastapi.egg-info/dependency_links.txt +0 -0
- {perfact_api_pd_fastapi-0.5 → perfact_api_pd_fastapi-0.6}/src/perfact_api_pd_fastapi.egg-info/entry_points.txt +0 -0
- {perfact_api_pd_fastapi-0.5 → perfact_api_pd_fastapi-0.6}/src/perfact_api_pd_fastapi.egg-info/requires.txt +0 -0
- {perfact_api_pd_fastapi-0.5 → perfact_api_pd_fastapi-0.6}/src/perfact_api_pd_fastapi.egg-info/top_level.txt +0 -0
- {perfact_api_pd_fastapi-0.5 → perfact_api_pd_fastapi-0.6}/tests/locustfile.py +0 -0
- {perfact_api_pd_fastapi-0.5 → perfact_api_pd_fastapi-0.6}/tox.ini +0 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from typing import Annotated, Optional
|
|
3
|
+
|
|
4
|
+
from fastapi import HTTPException
|
|
5
|
+
from perfact.api.main import Auth, VisibilityRegistryDep
|
|
6
|
+
from perfact.api.main.auth import require_roles
|
|
7
|
+
from perfact.api.main.dbsession import APIRouter, DBSession
|
|
8
|
+
from perfact.api.pd.model.pdunit import PdUnit
|
|
9
|
+
from perfact.api.pd.services.oee.availability import compute_availability
|
|
10
|
+
from perfact.api.pd.services.oee.breakdown import oee_breakdown_computation
|
|
11
|
+
from perfact.api.pd.services.oee.prod_metrics import (
|
|
12
|
+
compute_performance,
|
|
13
|
+
compute_quality,
|
|
14
|
+
)
|
|
15
|
+
from perfact.api.pd.services.oee.prod_records import get_prod_records
|
|
16
|
+
from perfact.api.pd.services.oee.state_spans import get_state_spans
|
|
17
|
+
from perfact.api.pd.services.oee.summary import oee_workcenter_time_computation
|
|
18
|
+
from pydantic import BaseModel, BeforeValidator
|
|
19
|
+
from sqlalchemy import select
|
|
20
|
+
|
|
21
|
+
routes = APIRouter()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _require_visible_unit(
|
|
25
|
+
session: DBSession,
|
|
26
|
+
auth: Auth,
|
|
27
|
+
visibility: VisibilityRegistryDep,
|
|
28
|
+
pdunit_id: int,
|
|
29
|
+
) -> PdUnit:
|
|
30
|
+
"""Fetch PdUnit with visibility policy applied. Raise 404 if not found or hidden."""
|
|
31
|
+
policy = visibility.get(PdUnit)
|
|
32
|
+
unit = session.scalar(
|
|
33
|
+
select(PdUnit).where(PdUnit.id == pdunit_id).where(policy.filter(auth))
|
|
34
|
+
)
|
|
35
|
+
if unit is None:
|
|
36
|
+
raise HTTPException(status_code=404)
|
|
37
|
+
return unit
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def parse_datetime_with_UTC_offset(value: str) -> datetime:
|
|
41
|
+
try:
|
|
42
|
+
return datetime.fromisoformat(value)
|
|
43
|
+
except ValueError:
|
|
44
|
+
raise ValueError(
|
|
45
|
+
f"Invalid datetime format. \
|
|
46
|
+
Expected: yyyy-mm-ddThh:mm:ss.sssss+HH:00, got: {value}"
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
UTCOffsetDatetime = Annotated[datetime, BeforeValidator(parse_datetime_with_UTC_offset)]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@routes.get("/kpi/oee/availability", tags=["pd"], summary="Route for OEE availability")
|
|
54
|
+
@require_roles("PdKpi")
|
|
55
|
+
def oee_availability(
|
|
56
|
+
session: DBSession,
|
|
57
|
+
auth: Auth,
|
|
58
|
+
visibility: VisibilityRegistryDep,
|
|
59
|
+
pdunit_id: int,
|
|
60
|
+
# ex w timezone "2025-05-20T13:54:15.555103+03:00"
|
|
61
|
+
# ex wo timezone "2025-05-20T13:54:15.555103"
|
|
62
|
+
starttime: datetime,
|
|
63
|
+
# ex w timezone "2025-05-22T11:53:13.561411+03:00"
|
|
64
|
+
# ex wo timezone "2025-05-22T11:53:13.561411"
|
|
65
|
+
stoptime: datetime,
|
|
66
|
+
) -> float:
|
|
67
|
+
"""
|
|
68
|
+
Route for OEE availability
|
|
69
|
+
Uses the availability_computation function
|
|
70
|
+
Returns single floating point value
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
unit = _require_visible_unit(session, auth, visibility, pdunit_id)
|
|
74
|
+
spans = get_state_spans(session, pdunit_id, starttime, stoptime)
|
|
75
|
+
shifts = list(session.scalars(unit.shifts_in_range_stmt(starttime, stoptime)).all())
|
|
76
|
+
return compute_availability(spans, shifts, starttime, stoptime)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@routes.get("/kpi/oee/performance", tags=["pd"], summary="Route for OEE performance")
|
|
80
|
+
@require_roles("PdKpi")
|
|
81
|
+
def oee_performance(
|
|
82
|
+
session: DBSession,
|
|
83
|
+
auth: Auth,
|
|
84
|
+
visibility: VisibilityRegistryDep,
|
|
85
|
+
pdunit_id: int,
|
|
86
|
+
# ex w timezone "2025-05-20T13:54:15.555103+03:00"
|
|
87
|
+
# ex wo timezone "2025-05-20T13:54:15.555103"
|
|
88
|
+
starttime: datetime,
|
|
89
|
+
# ex w timezone "2025-05-22T11:53:13.561411+03:00"
|
|
90
|
+
# ex wo timezone "2025-05-22T11:53:13.561411"
|
|
91
|
+
stoptime: datetime,
|
|
92
|
+
) -> float:
|
|
93
|
+
"""
|
|
94
|
+
Route for OEE availability
|
|
95
|
+
Uses the performance_computation function
|
|
96
|
+
Returns single floating point value
|
|
97
|
+
"""
|
|
98
|
+
_require_visible_unit(session, auth, visibility, pdunit_id)
|
|
99
|
+
records = get_prod_records(session, pdunit_id, starttime, stoptime)
|
|
100
|
+
spans = get_state_spans(session, pdunit_id, starttime, stoptime)
|
|
101
|
+
isexecution_spans = [s for s in spans if s.isexecution]
|
|
102
|
+
return compute_performance(
|
|
103
|
+
records, starttime, stoptime, isexecution_spans=isexecution_spans
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@routes.get("/kpi/oee/quality", tags=["pd"], summary="Route for OEE quality")
|
|
108
|
+
# response_model=Sequence[PdordlchGet])
|
|
109
|
+
@require_roles("PdKpi")
|
|
110
|
+
def oee_quality(
|
|
111
|
+
session: DBSession,
|
|
112
|
+
auth: Auth,
|
|
113
|
+
visibility: VisibilityRegistryDep,
|
|
114
|
+
pdunit_id: int,
|
|
115
|
+
# ex w timezone "2025-05-20T13:54:15.555103+03:00"
|
|
116
|
+
# ex wo timezone "2025-05-20T13:54:15.555103"
|
|
117
|
+
starttime: datetime,
|
|
118
|
+
# ex w timezone "2025-05-22T11:53:13.561411+03:00"
|
|
119
|
+
# ex wo timezone "2025-05-22T11:53:13.561411"
|
|
120
|
+
stoptime: datetime,
|
|
121
|
+
) -> float | None:
|
|
122
|
+
"""
|
|
123
|
+
Route for OEE quality
|
|
124
|
+
Uses the quality_computation function
|
|
125
|
+
Returns single floating point value
|
|
126
|
+
"""
|
|
127
|
+
_require_visible_unit(session, auth, visibility, pdunit_id)
|
|
128
|
+
records = get_prod_records(session, pdunit_id, starttime, stoptime)
|
|
129
|
+
return compute_quality(records, starttime, stoptime)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class OEECalculationResponse(BaseModel):
|
|
133
|
+
"""Response model for GET /kpi/oee.
|
|
134
|
+
|
|
135
|
+
Includes A/P/Q/OEE values plus workcenter-level time breakdown and
|
|
136
|
+
quantity aggregates across all operations in the requested window.
|
|
137
|
+
See ADR 0003.
|
|
138
|
+
"""
|
|
139
|
+
|
|
140
|
+
availability: float
|
|
141
|
+
performance: float
|
|
142
|
+
quality: float | None
|
|
143
|
+
oee: float | None
|
|
144
|
+
planned_machine_time_seconds: float
|
|
145
|
+
unplanned_downtime_seconds: float
|
|
146
|
+
setup_time_deviation_seconds: float
|
|
147
|
+
productive_time_seconds: float
|
|
148
|
+
pdordprd_quantitytotal: float
|
|
149
|
+
pdordprd_quantity: float
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@routes.get("/kpi/oee", tags=["pd"], summary="OEE calculation")
|
|
153
|
+
@require_roles("PdKpi")
|
|
154
|
+
def oee_calculation(
|
|
155
|
+
session: DBSession,
|
|
156
|
+
auth: Auth,
|
|
157
|
+
visibility: VisibilityRegistryDep,
|
|
158
|
+
pdunit_id: int,
|
|
159
|
+
starttime: datetime,
|
|
160
|
+
stoptime: datetime,
|
|
161
|
+
) -> OEECalculationResponse:
|
|
162
|
+
"""OEE with time breakdown and quantity aggregates for one work centre."""
|
|
163
|
+
unit = _require_visible_unit(session, auth, visibility, pdunit_id)
|
|
164
|
+
spans = get_state_spans(session, pdunit_id, starttime, stoptime)
|
|
165
|
+
shifts = list(session.scalars(unit.shifts_in_range_stmt(starttime, stoptime)).all())
|
|
166
|
+
availability = compute_availability(spans, shifts, starttime, stoptime)
|
|
167
|
+
prod_records = get_prod_records(session, pdunit_id, starttime, stoptime)
|
|
168
|
+
isexecution_spans = [s for s in spans if s.isexecution]
|
|
169
|
+
performance = compute_performance(
|
|
170
|
+
prod_records, starttime, stoptime, isexecution_spans=isexecution_spans
|
|
171
|
+
)
|
|
172
|
+
quality = compute_quality(prod_records, starttime, stoptime)
|
|
173
|
+
oee = availability * performance * quality if quality is not None else None
|
|
174
|
+
summary = oee_workcenter_time_computation(session, pdunit_id, starttime, stoptime)
|
|
175
|
+
return OEECalculationResponse(
|
|
176
|
+
availability=availability,
|
|
177
|
+
performance=performance,
|
|
178
|
+
quality=quality,
|
|
179
|
+
oee=oee,
|
|
180
|
+
**summary.__dict__,
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class OEEBreakdownResponse(BaseModel):
|
|
185
|
+
pdord_id: int
|
|
186
|
+
pdord_num: str
|
|
187
|
+
planned_machine_time_seconds: float
|
|
188
|
+
unplanned_downtime_seconds: float
|
|
189
|
+
setup_time_deviation_seconds: float
|
|
190
|
+
productive_time_seconds: float
|
|
191
|
+
availability: float
|
|
192
|
+
performance: float
|
|
193
|
+
quality: float | None
|
|
194
|
+
oee: float | None
|
|
195
|
+
pdordprd_quantitytotal: float
|
|
196
|
+
pdordprd_quantity: float
|
|
197
|
+
equipment: list[str]
|
|
198
|
+
mtart_num: Optional[str]
|
|
199
|
+
mtart_name: Optional[str]
|
|
200
|
+
operation_start: Optional[datetime]
|
|
201
|
+
operation_end: Optional[datetime]
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
@routes.get("/kpi/oee/breakdown", tags=["pd"], summary="Per-operation OEE breakdown")
|
|
205
|
+
@require_roles("PdKpi")
|
|
206
|
+
def oee_breakdown(
|
|
207
|
+
session: DBSession,
|
|
208
|
+
auth: Auth,
|
|
209
|
+
visibility: VisibilityRegistryDep,
|
|
210
|
+
pdunit_id: int,
|
|
211
|
+
starttime: datetime,
|
|
212
|
+
stoptime: datetime,
|
|
213
|
+
) -> list[OEEBreakdownResponse]:
|
|
214
|
+
"""Return one OEE row per operation that had active time on the given unit."""
|
|
215
|
+
_require_visible_unit(session, auth, visibility, pdunit_id)
|
|
216
|
+
rows = oee_breakdown_computation(session, pdunit_id, starttime, stoptime)
|
|
217
|
+
return [OEEBreakdownResponse(**row.__dict__) for row in rows]
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def mount(app: APIRouter):
|
|
221
|
+
app.include_router(routes)
|
|
@@ -13,6 +13,9 @@ src/perfact_api_pd_fastapi.egg-info/dependency_links.txt
|
|
|
13
13
|
src/perfact_api_pd_fastapi.egg-info/entry_points.txt
|
|
14
14
|
src/perfact_api_pd_fastapi.egg-info/requires.txt
|
|
15
15
|
src/perfact_api_pd_fastapi.egg-info/top_level.txt
|
|
16
|
+
tests/__init__.py
|
|
17
|
+
tests/conftest.py
|
|
18
|
+
tests/helpers.py
|
|
16
19
|
tests/locustfile.py
|
|
17
20
|
tests/test_init.py
|
|
18
21
|
tests/test_kpi.py
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from fastapi.routing import APIRoute, _IncludedRouter
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def collect_paths(router, prefix: str = "") -> set[str]:
|
|
5
|
+
"""Recursively collect route paths from a router, handling _IncludedRouter wrappers.
|
|
6
|
+
|
|
7
|
+
FastAPI ≥ 0.115 lazily wraps included routers as _IncludedRouter instead of
|
|
8
|
+
immediately flattening them into APIRoute instances in the parent routes list.
|
|
9
|
+
"""
|
|
10
|
+
paths: set[str] = set()
|
|
11
|
+
for route in router.routes:
|
|
12
|
+
if isinstance(route, APIRoute):
|
|
13
|
+
paths.add(prefix + route.path)
|
|
14
|
+
elif isinstance(route, _IncludedRouter):
|
|
15
|
+
sub_prefix = prefix + (route.include_context.prefix or "")
|
|
16
|
+
paths |= collect_paths(route.original_router, sub_prefix)
|
|
17
|
+
return paths
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
from fastapi import FastAPI
|
|
2
2
|
from perfact.api.pd_fastapi import mount
|
|
3
3
|
|
|
4
|
+
from .helpers import collect_paths
|
|
5
|
+
|
|
4
6
|
|
|
5
7
|
def test_mount_adds_pd_and_pdord_routes() -> None:
|
|
6
8
|
app = FastAPI()
|
|
7
9
|
|
|
8
10
|
mount(app)
|
|
9
11
|
|
|
10
|
-
paths =
|
|
12
|
+
paths = collect_paths(app)
|
|
11
13
|
assert "/pd/kpi/oee" in paths
|
|
12
14
|
assert "/pd/pdunit" in paths
|
|
13
15
|
assert "/pd/pdord/get_time_performance_data_v1" in paths
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from datetime import datetime, timezone
|
|
3
|
+
from unittest.mock import Mock
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
from perfact.api.main.dbsession import APIRouter
|
|
7
|
+
from perfact.api.pd.services.oee.breakdown import OEEBreakdownRow
|
|
8
|
+
from perfact.api.pd.services.oee.state_spans import StateSpan
|
|
9
|
+
from perfact.api.pd.services.oee.summary import OEEWorkcenterSummary
|
|
10
|
+
from perfact.api.pd_fastapi import kpi
|
|
11
|
+
from psycopg.types.range import Range as PsycopgRange
|
|
12
|
+
from sqlalchemy.orm import Session
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_parse_datetime_with_utc_offset_accepts_isoformat() -> None:
|
|
16
|
+
value = "2025-05-20T13:54:15.555103+03:00"
|
|
17
|
+
|
|
18
|
+
parsed = kpi.parse_datetime_with_UTC_offset(value)
|
|
19
|
+
|
|
20
|
+
assert parsed == datetime.fromisoformat(value)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_parse_datetime_with_utc_offset_rejects_invalid_input() -> None:
|
|
24
|
+
with pytest.raises(ValueError, match="Invalid datetime format"):
|
|
25
|
+
kpi.parse_datetime_with_UTC_offset("not-a-datetime")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_oee_performance_route_delegates_to_compute_performance(
|
|
29
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
30
|
+
) -> None:
|
|
31
|
+
"""oee_performance delegates to get_prod_records, get_state_spans,
|
|
32
|
+
and compute_performance."""
|
|
33
|
+
session: Session = Mock()
|
|
34
|
+
auth = Mock()
|
|
35
|
+
visibility = Mock()
|
|
36
|
+
pdunit_id = 7
|
|
37
|
+
starttime = datetime(2025, 1, 1, tzinfo=timezone.utc)
|
|
38
|
+
stoptime = datetime(2025, 1, 2, tzinfo=timezone.utc)
|
|
39
|
+
expected = 0.82
|
|
40
|
+
|
|
41
|
+
monkeypatch.setattr(kpi, "_require_visible_unit", lambda *_: Mock())
|
|
42
|
+
monkeypatch.setattr(kpi, "get_prod_records", lambda *_: [])
|
|
43
|
+
monkeypatch.setattr(kpi, "get_state_spans", lambda *_: [])
|
|
44
|
+
monkeypatch.setattr(
|
|
45
|
+
kpi,
|
|
46
|
+
"compute_performance",
|
|
47
|
+
lambda records, start, stop, isexecution_spans=None: expected,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
coro = kpi.oee_performance(
|
|
51
|
+
session, auth, visibility, pdunit_id, starttime, stoptime
|
|
52
|
+
)
|
|
53
|
+
result: float = asyncio.run(coro) # type: ignore[arg-type]
|
|
54
|
+
|
|
55
|
+
assert result == expected
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_oee_quality_route_delegates_to_compute_quality(
|
|
59
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
60
|
+
) -> None:
|
|
61
|
+
"""oee_quality delegates to get_prod_records and compute_quality."""
|
|
62
|
+
session: Session = Mock()
|
|
63
|
+
auth = Mock()
|
|
64
|
+
visibility = Mock()
|
|
65
|
+
pdunit_id = 7
|
|
66
|
+
starttime = datetime(2025, 1, 1, tzinfo=timezone.utc)
|
|
67
|
+
stoptime = datetime(2025, 1, 2, tzinfo=timezone.utc)
|
|
68
|
+
expected = 0.97
|
|
69
|
+
|
|
70
|
+
monkeypatch.setattr(kpi, "_require_visible_unit", lambda *_: Mock())
|
|
71
|
+
monkeypatch.setattr(kpi, "get_prod_records", lambda *_: [])
|
|
72
|
+
monkeypatch.setattr(kpi, "compute_quality", lambda records, start, stop: expected)
|
|
73
|
+
|
|
74
|
+
coro = kpi.oee_quality(session, auth, visibility, pdunit_id, starttime, stoptime)
|
|
75
|
+
result: float | None = asyncio.run(coro) # type: ignore[arg-type]
|
|
76
|
+
|
|
77
|
+
assert result == expected
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_oee_availability_delegates_to_compute_availability(
|
|
81
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
82
|
+
) -> None:
|
|
83
|
+
"""oee_availability delegates to get_state_spans and compute_availability."""
|
|
84
|
+
session: Session = Mock()
|
|
85
|
+
session.scalars.return_value.all.return_value = [] # type: ignore[attr-defined]
|
|
86
|
+
auth = Mock()
|
|
87
|
+
visibility = Mock()
|
|
88
|
+
pdunit_id = 7
|
|
89
|
+
starttime = datetime(2025, 1, 1, tzinfo=timezone.utc)
|
|
90
|
+
stoptime = datetime(2025, 1, 2, tzinfo=timezone.utc)
|
|
91
|
+
expected = 0.91
|
|
92
|
+
captured: list[tuple] = []
|
|
93
|
+
|
|
94
|
+
def fake_get_state_spans(s, uid, start, stop):
|
|
95
|
+
captured.append(("get_state_spans", uid, start, stop))
|
|
96
|
+
return []
|
|
97
|
+
|
|
98
|
+
def fake_compute_availability(spans, shifts, start, stop):
|
|
99
|
+
captured.append(("compute_availability", start, stop))
|
|
100
|
+
return expected
|
|
101
|
+
|
|
102
|
+
monkeypatch.setattr(kpi, "_require_visible_unit", lambda *_: Mock())
|
|
103
|
+
monkeypatch.setattr(kpi, "get_state_spans", fake_get_state_spans)
|
|
104
|
+
monkeypatch.setattr(kpi, "compute_availability", fake_compute_availability)
|
|
105
|
+
|
|
106
|
+
coro = kpi.oee_availability(
|
|
107
|
+
session, auth, visibility, pdunit_id, starttime, stoptime
|
|
108
|
+
)
|
|
109
|
+
result: float = asyncio.run(coro) # type: ignore[arg-type]
|
|
110
|
+
|
|
111
|
+
assert result == expected
|
|
112
|
+
assert ("get_state_spans", pdunit_id, starttime, stoptime) in captured
|
|
113
|
+
assert ("compute_availability", starttime, stoptime) in captured
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def test_oee_calculation_returns_combined_values(
|
|
117
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
118
|
+
) -> None:
|
|
119
|
+
"""Route returns OEECalculationResponse combining A/P/Q and workcenter time
|
|
120
|
+
summary.
|
|
121
|
+
"""
|
|
122
|
+
session: Session = Mock()
|
|
123
|
+
pdunit_id = 3
|
|
124
|
+
starttime = datetime(2025, 2, 1, tzinfo=timezone.utc)
|
|
125
|
+
stoptime = datetime(2025, 2, 2, tzinfo=timezone.utc)
|
|
126
|
+
fake_summary = OEEWorkcenterSummary(
|
|
127
|
+
planned_machine_time_seconds=100.0,
|
|
128
|
+
unplanned_downtime_seconds=50.0,
|
|
129
|
+
setup_time_deviation_seconds=10.0,
|
|
130
|
+
productive_time_seconds=200.0,
|
|
131
|
+
pdordprd_quantitytotal=10.0,
|
|
132
|
+
pdordprd_quantity=9.0,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
auth = Mock()
|
|
136
|
+
visibility = Mock()
|
|
137
|
+
session.scalars.return_value.all.return_value = [] # type: ignore[attr-defined]
|
|
138
|
+
monkeypatch.setattr(kpi, "_require_visible_unit", lambda *_: Mock())
|
|
139
|
+
monkeypatch.setattr(kpi, "get_state_spans", lambda *_: [])
|
|
140
|
+
monkeypatch.setattr(kpi, "compute_availability", lambda *_, **__: 0.5)
|
|
141
|
+
monkeypatch.setattr(kpi, "get_prod_records", lambda *_: [])
|
|
142
|
+
monkeypatch.setattr(kpi, "compute_performance", lambda *_, **__: 0.8)
|
|
143
|
+
monkeypatch.setattr(kpi, "compute_quality", lambda *_: 0.9)
|
|
144
|
+
monkeypatch.setattr(kpi, "oee_workcenter_time_computation", lambda *_: fake_summary)
|
|
145
|
+
|
|
146
|
+
result: kpi.OEECalculationResponse = asyncio.run(
|
|
147
|
+
kpi.oee_calculation(session, auth, visibility, pdunit_id, starttime, stoptime) # type: ignore[arg-type]
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
assert result.availability == 0.5
|
|
151
|
+
assert result.performance == 0.8
|
|
152
|
+
assert result.quality == 0.9
|
|
153
|
+
assert result.oee == pytest.approx(0.36)
|
|
154
|
+
assert result.planned_machine_time_seconds == 100.0
|
|
155
|
+
assert result.unplanned_downtime_seconds == 50.0
|
|
156
|
+
assert result.setup_time_deviation_seconds == 10.0
|
|
157
|
+
assert result.productive_time_seconds == 200.0
|
|
158
|
+
assert result.pdordprd_quantitytotal == 10.0
|
|
159
|
+
assert result.pdordprd_quantity == 9.0
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def test_oee_performance_route_passes_only_isexecution_spans(
|
|
163
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
164
|
+
) -> None:
|
|
165
|
+
"""oee_performance passes only isexecution spans to compute_performance.
|
|
166
|
+
|
|
167
|
+
Non-execution spans (e.g. setup) must be excluded so that production records
|
|
168
|
+
recorded during setup are not included in the performance numerator/denominator.
|
|
169
|
+
"""
|
|
170
|
+
session: Session = Mock()
|
|
171
|
+
auth = Mock()
|
|
172
|
+
visibility = Mock()
|
|
173
|
+
pdunit_id = 7
|
|
174
|
+
starttime = datetime(2025, 1, 1, tzinfo=timezone.utc)
|
|
175
|
+
stoptime = datetime(2025, 1, 2, tzinfo=timezone.utc)
|
|
176
|
+
|
|
177
|
+
dummy_range: PsycopgRange[datetime] = PsycopgRange(None, None)
|
|
178
|
+
exec_span = StateSpan(
|
|
179
|
+
pdord_id=1, range=dummy_range, isactive=True, isexecution=True
|
|
180
|
+
)
|
|
181
|
+
setup_span = StateSpan(
|
|
182
|
+
pdord_id=1, range=dummy_range, isactive=True, isexecution=False
|
|
183
|
+
)
|
|
184
|
+
captured_isexecution_spans: list[StateSpan] = []
|
|
185
|
+
|
|
186
|
+
monkeypatch.setattr(kpi, "_require_visible_unit", lambda *_: Mock())
|
|
187
|
+
monkeypatch.setattr(kpi, "get_prod_records", lambda *_: [])
|
|
188
|
+
monkeypatch.setattr(kpi, "get_state_spans", lambda *_: [exec_span, setup_span])
|
|
189
|
+
|
|
190
|
+
def fake_compute_performance(records, start, stop, isexecution_spans=None):
|
|
191
|
+
captured_isexecution_spans.extend(isexecution_spans or [])
|
|
192
|
+
return 0.82
|
|
193
|
+
|
|
194
|
+
monkeypatch.setattr(kpi, "compute_performance", fake_compute_performance)
|
|
195
|
+
|
|
196
|
+
coro = kpi.oee_performance(
|
|
197
|
+
session, auth, visibility, pdunit_id, starttime, stoptime
|
|
198
|
+
)
|
|
199
|
+
asyncio.run(coro) # type: ignore[arg-type]
|
|
200
|
+
|
|
201
|
+
assert exec_span in captured_isexecution_spans
|
|
202
|
+
assert setup_span not in captured_isexecution_spans
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def test_oee_calculation_performance_uses_only_isexecution_spans(
|
|
206
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
207
|
+
) -> None:
|
|
208
|
+
"""oee_calculation passes only isexecution spans to compute_performance.
|
|
209
|
+
|
|
210
|
+
Non-execution spans must be excluded so the work-centre performance matches
|
|
211
|
+
the per-operation breakdown performance for single-operation scenarios.
|
|
212
|
+
"""
|
|
213
|
+
session: Session = Mock()
|
|
214
|
+
session.scalars.return_value.all.return_value = [] # type: ignore[attr-defined]
|
|
215
|
+
auth = Mock()
|
|
216
|
+
visibility = Mock()
|
|
217
|
+
pdunit_id = 3
|
|
218
|
+
starttime = datetime(2025, 2, 1, tzinfo=timezone.utc)
|
|
219
|
+
stoptime = datetime(2025, 2, 2, tzinfo=timezone.utc)
|
|
220
|
+
|
|
221
|
+
dummy_range: PsycopgRange[datetime] = PsycopgRange(None, None)
|
|
222
|
+
exec_span = StateSpan(
|
|
223
|
+
pdord_id=1, range=dummy_range, isactive=True, isexecution=True
|
|
224
|
+
)
|
|
225
|
+
setup_span = StateSpan(
|
|
226
|
+
pdord_id=1, range=dummy_range, isactive=True, isexecution=False
|
|
227
|
+
)
|
|
228
|
+
captured_isexecution_spans: list[StateSpan] = []
|
|
229
|
+
|
|
230
|
+
monkeypatch.setattr(kpi, "_require_visible_unit", lambda *_: Mock())
|
|
231
|
+
monkeypatch.setattr(kpi, "get_state_spans", lambda *_: [exec_span, setup_span])
|
|
232
|
+
monkeypatch.setattr(kpi, "compute_availability", lambda *_, **__: 0.9)
|
|
233
|
+
monkeypatch.setattr(kpi, "get_prod_records", lambda *_: [])
|
|
234
|
+
|
|
235
|
+
def fake_compute_performance(records, start, stop, isexecution_spans=None):
|
|
236
|
+
captured_isexecution_spans.extend(isexecution_spans or [])
|
|
237
|
+
return 0.8
|
|
238
|
+
|
|
239
|
+
monkeypatch.setattr(kpi, "compute_performance", fake_compute_performance)
|
|
240
|
+
monkeypatch.setattr(kpi, "compute_quality", lambda *_: 0.9)
|
|
241
|
+
monkeypatch.setattr(
|
|
242
|
+
kpi,
|
|
243
|
+
"oee_workcenter_time_computation",
|
|
244
|
+
lambda *_: OEEWorkcenterSummary(
|
|
245
|
+
planned_machine_time_seconds=0.0,
|
|
246
|
+
unplanned_downtime_seconds=0.0,
|
|
247
|
+
setup_time_deviation_seconds=0.0,
|
|
248
|
+
productive_time_seconds=0.0,
|
|
249
|
+
pdordprd_quantitytotal=0.0,
|
|
250
|
+
pdordprd_quantity=0.0,
|
|
251
|
+
),
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
coro = kpi.oee_calculation(
|
|
255
|
+
session, auth, visibility, pdunit_id, starttime, stoptime
|
|
256
|
+
)
|
|
257
|
+
asyncio.run(coro) # type: ignore[arg-type]
|
|
258
|
+
|
|
259
|
+
assert exec_span in captured_isexecution_spans
|
|
260
|
+
assert setup_span not in captured_isexecution_spans
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def test_mount_includes_kpi_routes() -> None:
|
|
264
|
+
from .helpers import collect_paths
|
|
265
|
+
|
|
266
|
+
app_router = APIRouter()
|
|
267
|
+
|
|
268
|
+
kpi.mount(app_router)
|
|
269
|
+
|
|
270
|
+
paths = collect_paths(app_router)
|
|
271
|
+
assert "/kpi/oee/availability" in paths
|
|
272
|
+
assert "/kpi/oee/performance" in paths
|
|
273
|
+
assert "/kpi/oee/quality" in paths
|
|
274
|
+
assert "/kpi/oee" in paths
|
|
275
|
+
assert "/kpi/oee/breakdown" in paths
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def test_oee_breakdown_route_delegates_to_service(
|
|
279
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
280
|
+
) -> None:
|
|
281
|
+
"""Route delegates to oee_breakdown_computation and returns its result."""
|
|
282
|
+
session: Session = Mock()
|
|
283
|
+
pdunit_id = 5
|
|
284
|
+
starttime = datetime(2025, 3, 1, tzinfo=timezone.utc)
|
|
285
|
+
stoptime = datetime(2025, 3, 2, tzinfo=timezone.utc)
|
|
286
|
+
fake_row = OEEBreakdownRow(
|
|
287
|
+
pdord_id=1,
|
|
288
|
+
pdord_num="OP-001",
|
|
289
|
+
planned_machine_time_seconds=600.0,
|
|
290
|
+
unplanned_downtime_seconds=120.0,
|
|
291
|
+
setup_time_deviation_seconds=60.0,
|
|
292
|
+
productive_time_seconds=3000.0,
|
|
293
|
+
availability=0.9,
|
|
294
|
+
performance=0.85,
|
|
295
|
+
quality=0.95,
|
|
296
|
+
oee=0.9 * 0.85 * 0.95,
|
|
297
|
+
pdordprd_quantitytotal=10.0,
|
|
298
|
+
pdordprd_quantity=9.0,
|
|
299
|
+
equipment=["TOOL-1"],
|
|
300
|
+
mtart_num="MAT-001",
|
|
301
|
+
mtart_name="Material One",
|
|
302
|
+
operation_start=datetime(2025, 3, 1, 6, tzinfo=timezone.utc),
|
|
303
|
+
operation_end=datetime(2025, 3, 1, 18, tzinfo=timezone.utc),
|
|
304
|
+
)
|
|
305
|
+
captured: list[tuple] = []
|
|
306
|
+
|
|
307
|
+
def fake_breakdown(s, u, start, stop):
|
|
308
|
+
captured.append((s, u, start, stop))
|
|
309
|
+
return [fake_row]
|
|
310
|
+
|
|
311
|
+
auth = Mock()
|
|
312
|
+
visibility = Mock()
|
|
313
|
+
monkeypatch.setattr(kpi, "_require_visible_unit", lambda *_: Mock())
|
|
314
|
+
monkeypatch.setattr(kpi, "oee_breakdown_computation", fake_breakdown)
|
|
315
|
+
|
|
316
|
+
result: list[kpi.OEEBreakdownResponse] = asyncio.run(
|
|
317
|
+
kpi.oee_breakdown(session, auth, visibility, pdunit_id, starttime, stoptime) # type: ignore[arg-type]
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
assert captured == [(session, pdunit_id, starttime, stoptime)]
|
|
321
|
+
assert len(result) == 1
|
|
322
|
+
assert result[0].pdord_id == 1
|
|
323
|
+
assert result[0].pdord_num == "OP-001"
|
|
324
|
+
assert result[0].availability == 0.9
|
|
325
|
+
assert result[0].performance == 0.85
|
|
326
|
+
assert result[0].quality == 0.95
|
|
327
|
+
assert result[0].pdordprd_quantitytotal == 10.0
|
|
328
|
+
assert result[0].pdordprd_quantity == 9.0
|
|
329
|
+
assert result[0].equipment == ["TOOL-1"]
|
|
330
|
+
assert result[0].mtart_num == "MAT-001"
|
|
331
|
+
assert result[0].mtart_name == "Material One"
|
|
332
|
+
assert result[0].operation_start == datetime(2025, 3, 1, 6, tzinfo=timezone.utc)
|
|
333
|
+
assert result[0].operation_end == datetime(2025, 3, 1, 18, tzinfo=timezone.utc)
|
|
@@ -36,7 +36,7 @@ def test_pdunit_get_all_requires_authentication() -> None:
|
|
|
36
36
|
|
|
37
37
|
with pytest.raises(HTTPException, match="Not authenticated") as exc_info:
|
|
38
38
|
asyncio.run(
|
|
39
|
-
pdunit.pdunit_get_all(session=session, auth=None, visibility=visibility)
|
|
39
|
+
pdunit.pdunit_get_all(session=session, auth=None, visibility=visibility) # type: ignore[arg-type]
|
|
40
40
|
)
|
|
41
41
|
|
|
42
42
|
assert exc_info.value.status_code == 401
|
|
@@ -45,9 +45,11 @@ def test_pdunit_get_all_requires_authentication() -> None:
|
|
|
45
45
|
|
|
46
46
|
|
|
47
47
|
def test_mount_includes_pdunit_route() -> None:
|
|
48
|
+
from .helpers import collect_paths
|
|
49
|
+
|
|
48
50
|
app_router = APIRouter()
|
|
49
51
|
|
|
50
52
|
pdunit.mount(app_router)
|
|
51
53
|
|
|
52
|
-
paths =
|
|
54
|
+
paths = collect_paths(app_router)
|
|
53
55
|
assert "/pdunit" in paths
|
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
from
|
|
1
|
+
from perfact.api.main.dbsession import APIRouter
|
|
2
2
|
from perfact.api.pd_fastapi import performancedata as perf
|
|
3
3
|
|
|
4
|
+
from .helpers import collect_paths
|
|
5
|
+
|
|
4
6
|
|
|
5
7
|
def test_mount_includes_performance_routes() -> None:
|
|
6
8
|
app_router = APIRouter()
|
|
7
9
|
|
|
8
10
|
perf.mount(app_router)
|
|
9
11
|
|
|
10
|
-
paths =
|
|
12
|
+
paths = collect_paths(app_router)
|
|
11
13
|
assert "/get_time_performance_data_v1" in paths
|
|
12
14
|
assert "/get_time_performance_data_v2" in paths
|
|
13
15
|
assert "/get_time_performance_data_v3" in paths
|
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
from datetime import datetime
|
|
2
|
-
from typing import Annotated
|
|
3
|
-
|
|
4
|
-
from perfact.api.main.auth import require_roles
|
|
5
|
-
from perfact.api.main.dbsession import APIRouter, DBSession
|
|
6
|
-
from perfact.api.pd.model import PdOrdPrd
|
|
7
|
-
from pydantic import BeforeValidator
|
|
8
|
-
|
|
9
|
-
routes = APIRouter()
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def parse_datetime_with_UTC_offset(value: str) -> datetime:
|
|
13
|
-
try:
|
|
14
|
-
return datetime.fromisoformat(value)
|
|
15
|
-
except ValueError:
|
|
16
|
-
raise ValueError(
|
|
17
|
-
f"Invalid datetime format. \
|
|
18
|
-
Expected: yyyy-mm-ddThh:mm:ss.sssss+HH:00, got: {value}"
|
|
19
|
-
)
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
UTCOffsetDatetime = Annotated[datetime, BeforeValidator(parse_datetime_with_UTC_offset)]
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
@routes.get("/kpi/oee/availability", tags=["pd"], summary="Route for OEE availability")
|
|
26
|
-
@require_roles("PdKpi")
|
|
27
|
-
def oee_availability(
|
|
28
|
-
session: DBSession,
|
|
29
|
-
pdunit_id: int,
|
|
30
|
-
# ex w timezone "2025-05-20T13:54:15.555103+03:00"
|
|
31
|
-
# ex wo timezone "2025-05-20T13:54:15.555103"
|
|
32
|
-
starttime: datetime,
|
|
33
|
-
# ex w timezone "2025-05-22T11:53:13.561411+03:00"
|
|
34
|
-
# ex wo timezone "2025-05-22T11:53:13.561411"
|
|
35
|
-
stoptime: datetime,
|
|
36
|
-
) -> float:
|
|
37
|
-
"""
|
|
38
|
-
Route for OEE availability
|
|
39
|
-
Uses the availability_computation function
|
|
40
|
-
Returns single floating point value
|
|
41
|
-
"""
|
|
42
|
-
|
|
43
|
-
result = PdOrdPrd.oee_availability_computation(
|
|
44
|
-
session, pdunit_id, starttime, stoptime
|
|
45
|
-
)
|
|
46
|
-
|
|
47
|
-
return result
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
@routes.get("/kpi/oee/performance", tags=["pd"], summary="Route for OEE performance")
|
|
51
|
-
@require_roles("PdKpi")
|
|
52
|
-
def oee_performance(
|
|
53
|
-
session: DBSession,
|
|
54
|
-
pdunit_id: int,
|
|
55
|
-
# ex w timezone "2025-05-20T13:54:15.555103+03:00"
|
|
56
|
-
# ex wo timezone "2025-05-20T13:54:15.555103"
|
|
57
|
-
starttime: datetime,
|
|
58
|
-
# ex w timezone "2025-05-22T11:53:13.561411+03:00"
|
|
59
|
-
# ex wo timezone "2025-05-22T11:53:13.561411"
|
|
60
|
-
stoptime: datetime,
|
|
61
|
-
) -> float:
|
|
62
|
-
"""
|
|
63
|
-
Route for OEE availability
|
|
64
|
-
Uses the performance_computation function
|
|
65
|
-
Returns single floating point value
|
|
66
|
-
"""
|
|
67
|
-
result = PdOrdPrd.oee_performance_computation(
|
|
68
|
-
session, pdunit_id, starttime, stoptime
|
|
69
|
-
)
|
|
70
|
-
|
|
71
|
-
return result
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
@routes.get("/kpi/oee/quality", tags=["pd"], summary="Route for OEE quality")
|
|
75
|
-
# response_model=Sequence[PdordlchGet])
|
|
76
|
-
@require_roles("PdKpi")
|
|
77
|
-
def oee_quality(
|
|
78
|
-
session: DBSession,
|
|
79
|
-
pdunit_id: int,
|
|
80
|
-
# ex w timezone "2025-05-20T13:54:15.555103+03:00"
|
|
81
|
-
# ex wo timezone "2025-05-20T13:54:15.555103"
|
|
82
|
-
starttime: datetime,
|
|
83
|
-
# ex w timezone "2025-05-22T11:53:13.561411+03:00"
|
|
84
|
-
# ex wo timezone "2025-05-22T11:53:13.561411"
|
|
85
|
-
stoptime: datetime,
|
|
86
|
-
) -> float | None:
|
|
87
|
-
"""
|
|
88
|
-
Route for OEE quality
|
|
89
|
-
Uses the quality_computation function
|
|
90
|
-
Returns single floating point value
|
|
91
|
-
"""
|
|
92
|
-
result = PdOrdPrd.oee_quality_computation(session, pdunit_id, starttime, stoptime)
|
|
93
|
-
|
|
94
|
-
return result
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
@routes.get("/kpi/oee", tags=["pd"], summary="OEE calculation")
|
|
98
|
-
# response_model=Sequence[PdordlchGet])
|
|
99
|
-
@require_roles("PdKpi")
|
|
100
|
-
def oee_calculation(
|
|
101
|
-
session: DBSession,
|
|
102
|
-
pdunit_id: int,
|
|
103
|
-
# ex w timezone "2025-05-20T13:54:15.555103+03:00"
|
|
104
|
-
# ex wo timezone "2025-05-20T13:54:15.555103"
|
|
105
|
-
starttime: datetime,
|
|
106
|
-
# ex w timezone "2025-05-22T11:53:13.561411+03:00"
|
|
107
|
-
# ex wo timezone "2025-05-22T11:53:13.561411"
|
|
108
|
-
stoptime: datetime,
|
|
109
|
-
) -> dict:
|
|
110
|
-
"""
|
|
111
|
-
OEE route for complete calculations: availability, performance, quality
|
|
112
|
-
Returns dict/json type response containing
|
|
113
|
-
all results under their specific category
|
|
114
|
-
"""
|
|
115
|
-
availability = PdOrdPrd.oee_availability_computation(
|
|
116
|
-
session, pdunit_id, starttime, stoptime
|
|
117
|
-
)
|
|
118
|
-
performance = PdOrdPrd.oee_performance_computation(
|
|
119
|
-
session, pdunit_id, starttime, stoptime
|
|
120
|
-
)
|
|
121
|
-
quality = PdOrdPrd.oee_quality_computation(session, pdunit_id, starttime, stoptime)
|
|
122
|
-
oee = availability * performance * quality if quality is not None else None
|
|
123
|
-
|
|
124
|
-
return {
|
|
125
|
-
"availability": availability,
|
|
126
|
-
"performance": performance,
|
|
127
|
-
"quality": quality,
|
|
128
|
-
"oee": oee,
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
def mount(app: APIRouter):
|
|
133
|
-
app.include_router(routes)
|
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
import asyncio
|
|
2
|
-
from datetime import datetime, timezone
|
|
3
|
-
|
|
4
|
-
import pytest
|
|
5
|
-
from perfact.api.main.dbsession import APIRouter
|
|
6
|
-
from perfact.api.pd_fastapi import kpi
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def test_parse_datetime_with_utc_offset_accepts_isoformat() -> None:
|
|
10
|
-
value = "2025-05-20T13:54:15.555103+03:00"
|
|
11
|
-
|
|
12
|
-
parsed = kpi.parse_datetime_with_UTC_offset(value)
|
|
13
|
-
|
|
14
|
-
assert parsed == datetime.fromisoformat(value)
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
def test_parse_datetime_with_utc_offset_rejects_invalid_input() -> None:
|
|
18
|
-
with pytest.raises(ValueError, match="Invalid datetime format"):
|
|
19
|
-
kpi.parse_datetime_with_UTC_offset("not-a-datetime")
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
@pytest.mark.parametrize(
|
|
23
|
-
("route_func", "computation_name", "expected"),
|
|
24
|
-
[
|
|
25
|
-
(kpi.oee_availability, "oee_availability_computation", 0.91),
|
|
26
|
-
(kpi.oee_performance, "oee_performance_computation", 0.82),
|
|
27
|
-
(kpi.oee_quality, "oee_quality_computation", 0.97),
|
|
28
|
-
],
|
|
29
|
-
)
|
|
30
|
-
def test_single_kpi_routes_delegate_to_model(
|
|
31
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
32
|
-
route_func,
|
|
33
|
-
computation_name: str,
|
|
34
|
-
expected: float,
|
|
35
|
-
) -> None:
|
|
36
|
-
session = object()
|
|
37
|
-
pdunit_id = 7
|
|
38
|
-
starttime = datetime(2025, 1, 1, tzinfo=timezone.utc)
|
|
39
|
-
stoptime = datetime(2025, 1, 2, tzinfo=timezone.utc)
|
|
40
|
-
captured: list[tuple[object, int, datetime, datetime]] = []
|
|
41
|
-
|
|
42
|
-
def fake_computation(
|
|
43
|
-
local_session, local_pdunit_id, local_starttime, local_stoptime
|
|
44
|
-
):
|
|
45
|
-
captured.append(
|
|
46
|
-
(local_session, local_pdunit_id, local_starttime, local_stoptime)
|
|
47
|
-
)
|
|
48
|
-
return expected
|
|
49
|
-
|
|
50
|
-
monkeypatch.setattr(
|
|
51
|
-
kpi.PdOrdPrd,
|
|
52
|
-
computation_name,
|
|
53
|
-
staticmethod(fake_computation),
|
|
54
|
-
)
|
|
55
|
-
|
|
56
|
-
result = asyncio.run(route_func(session, pdunit_id, starttime, stoptime))
|
|
57
|
-
|
|
58
|
-
assert result == expected
|
|
59
|
-
assert captured == [(session, pdunit_id, starttime, stoptime)]
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
def test_oee_calculation_returns_combined_values(
|
|
63
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
64
|
-
) -> None:
|
|
65
|
-
session = object()
|
|
66
|
-
pdunit_id = 3
|
|
67
|
-
starttime = datetime(2025, 2, 1, tzinfo=timezone.utc)
|
|
68
|
-
stoptime = datetime(2025, 2, 2, tzinfo=timezone.utc)
|
|
69
|
-
|
|
70
|
-
monkeypatch.setattr(
|
|
71
|
-
kpi.PdOrdPrd,
|
|
72
|
-
"oee_availability_computation",
|
|
73
|
-
staticmethod(lambda *_: 0.5),
|
|
74
|
-
)
|
|
75
|
-
monkeypatch.setattr(
|
|
76
|
-
kpi.PdOrdPrd,
|
|
77
|
-
"oee_performance_computation",
|
|
78
|
-
staticmethod(lambda *_: 0.8),
|
|
79
|
-
)
|
|
80
|
-
monkeypatch.setattr(
|
|
81
|
-
kpi.PdOrdPrd,
|
|
82
|
-
"oee_quality_computation",
|
|
83
|
-
staticmethod(lambda *_: 0.9),
|
|
84
|
-
)
|
|
85
|
-
|
|
86
|
-
result = asyncio.run(kpi.oee_calculation(session, pdunit_id, starttime, stoptime))
|
|
87
|
-
|
|
88
|
-
assert result == {
|
|
89
|
-
"availability": 0.5,
|
|
90
|
-
"performance": 0.8,
|
|
91
|
-
"quality": 0.9,
|
|
92
|
-
"oee": 0.36000000000000004,
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
def test_mount_includes_kpi_routes() -> None:
|
|
97
|
-
app_router = APIRouter()
|
|
98
|
-
|
|
99
|
-
kpi.mount(app_router)
|
|
100
|
-
|
|
101
|
-
paths = {route.path for route in app_router.routes}
|
|
102
|
-
assert "/kpi/oee/availability" in paths
|
|
103
|
-
assert "/kpi/oee/performance" in paths
|
|
104
|
-
assert "/kpi/oee/quality" in paths
|
|
105
|
-
assert "/kpi/oee" in paths
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{perfact_api_pd_fastapi-0.5 → perfact_api_pd_fastapi-0.6}/src/perfact/api/pd_fastapi/__init__.py
RENAMED
|
File without changes
|
{perfact_api_pd_fastapi-0.5 → perfact_api_pd_fastapi-0.6}/src/perfact/api/pd_fastapi/pdunit.py
RENAMED
|
File without changes
|
{perfact_api_pd_fastapi-0.5 → perfact_api_pd_fastapi-0.6}/src/perfact/api/pd_fastapi/pdunitlch.py
RENAMED
|
File without changes
|
|
File without changes
|
{perfact_api_pd_fastapi-0.5 → perfact_api_pd_fastapi-0.6}/src/perfact/api/pd_fastapi/py.typed
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|