perfact-api-pd-fastapi 0.2__tar.gz → 0.4__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.2 → perfact_api_pd_fastapi-0.4}/PKG-INFO +1 -1
- {perfact_api_pd_fastapi-0.2 → perfact_api_pd_fastapi-0.4}/src/perfact/api/pd_fastapi/__init__.py +2 -0
- perfact_api_pd_fastapi-0.4/src/perfact/api/pd_fastapi/kpi.py +133 -0
- perfact_api_pd_fastapi-0.4/src/perfact/api/pd_fastapi/pdunitlch.py +143 -0
- perfact_api_pd_fastapi-0.4/src/perfact/api/pd_fastapi/performancedata.py +121 -0
- {perfact_api_pd_fastapi-0.2 → perfact_api_pd_fastapi-0.4}/src/perfact_api_pd_fastapi.egg-info/PKG-INFO +1 -1
- {perfact_api_pd_fastapi-0.2 → perfact_api_pd_fastapi-0.4}/src/perfact_api_pd_fastapi.egg-info/SOURCES.txt +7 -2
- perfact_api_pd_fastapi-0.4/tests/test_init.py +13 -0
- perfact_api_pd_fastapi-0.4/tests/test_kpi.py +105 -0
- perfact_api_pd_fastapi-0.4/tests/test_pdunit.py +53 -0
- perfact_api_pd_fastapi-0.4/tests/test_performance.py +14 -0
- {perfact_api_pd_fastapi-0.2 → perfact_api_pd_fastapi-0.4}/tox.ini +1 -0
- perfact_api_pd_fastapi-0.2/src/perfact/api/pd_fastapi/kpi.py +0 -226
- perfact_api_pd_fastapi-0.2/src/perfact/api/pd_fastapi/performancedata.py +0 -701
- {perfact_api_pd_fastapi-0.2 → perfact_api_pd_fastapi-0.4}/README.md +0 -0
- {perfact_api_pd_fastapi-0.2 → perfact_api_pd_fastapi-0.4}/pyproject.toml +0 -0
- {perfact_api_pd_fastapi-0.2 → perfact_api_pd_fastapi-0.4}/setup.cfg +0 -0
- {perfact_api_pd_fastapi-0.2 → perfact_api_pd_fastapi-0.4}/src/perfact/api/pd_fastapi/pdunit.py +0 -0
- {perfact_api_pd_fastapi-0.2 → perfact_api_pd_fastapi-0.4}/src/perfact/api/pd_fastapi/py.typed +0 -0
- {perfact_api_pd_fastapi-0.2 → perfact_api_pd_fastapi-0.4}/src/perfact_api_pd_fastapi.egg-info/dependency_links.txt +0 -0
- {perfact_api_pd_fastapi-0.2 → perfact_api_pd_fastapi-0.4}/src/perfact_api_pd_fastapi.egg-info/entry_points.txt +0 -0
- {perfact_api_pd_fastapi-0.2 → perfact_api_pd_fastapi-0.4}/src/perfact_api_pd_fastapi.egg-info/requires.txt +0 -0
- {perfact_api_pd_fastapi-0.2 → perfact_api_pd_fastapi-0.4}/src/perfact_api_pd_fastapi.egg-info/top_level.txt +0 -0
- {perfact_api_pd_fastapi-0.2/src/perfact/api/pd_fastapi → perfact_api_pd_fastapi-0.4/tests}/locustfile.py +0 -0
{perfact_api_pd_fastapi-0.2 → perfact_api_pd_fastapi-0.4}/src/perfact/api/pd_fastapi/__init__.py
RENAMED
|
@@ -3,6 +3,7 @@ from perfact.api.main.dbsession import APIRouter
|
|
|
3
3
|
|
|
4
4
|
from .kpi import mount as kpi_mount
|
|
5
5
|
from .pdunit import mount as pdunitmount
|
|
6
|
+
from .pdunitlch import mount as pdunitlch_mount
|
|
6
7
|
from .performancedata import mount as performancemount
|
|
7
8
|
|
|
8
9
|
routes = APIRouter()
|
|
@@ -12,6 +13,7 @@ routes_pdord = APIRouter()
|
|
|
12
13
|
def mount(app: FastAPI):
|
|
13
14
|
kpi_mount(routes)
|
|
14
15
|
pdunitmount(routes)
|
|
16
|
+
pdunitlch_mount(routes)
|
|
15
17
|
performancemount(routes_pdord)
|
|
16
18
|
app.include_router(routes, prefix="/pd")
|
|
17
19
|
app.include_router(routes_pdord, prefix="/pd/pdord")
|
|
@@ -0,0 +1,133 @@
|
|
|
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)
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
from typing import Optional, Sequence
|
|
3
|
+
|
|
4
|
+
from fastapi import HTTPException
|
|
5
|
+
from perfact.api.base.model import PydanticBase
|
|
6
|
+
from perfact.api.main import Auth, DBSession, require_roles
|
|
7
|
+
from perfact.api.main.dbsession import APIRouter
|
|
8
|
+
from perfact.api.pd.model import PdOrdLc, PdOrdLch, PdOrdLct, PdUnitLc, PdUnitLct
|
|
9
|
+
from perfact.api.pd.services.pdunitlch import UnitLchHistoryEntry, get_unit_lch_history
|
|
10
|
+
from pydantic import BaseModel, ConfigDict
|
|
11
|
+
from sqlalchemy import select
|
|
12
|
+
|
|
13
|
+
routes = APIRouter()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class PdUnitLcGet(PydanticBase):
|
|
17
|
+
name: str
|
|
18
|
+
seqnum: Optional[int]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class PdUnitLctGet(PydanticBase):
|
|
22
|
+
name: str
|
|
23
|
+
from_pdunitlc_id: int
|
|
24
|
+
to_pdunitlc_id: int
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class PdOrdLcGet(PydanticBase):
|
|
28
|
+
name: str
|
|
29
|
+
isactive: bool
|
|
30
|
+
isexecution: bool
|
|
31
|
+
isfinal: bool
|
|
32
|
+
issetup: bool
|
|
33
|
+
isdowntime: bool
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class PdOrdLctGet(PydanticBase):
|
|
37
|
+
name: str
|
|
38
|
+
from_pdordlc_id: int
|
|
39
|
+
to_pdordlc_id: int
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class PdOrdLchGet(PydanticBase):
|
|
43
|
+
pdunit_id: Optional[int]
|
|
44
|
+
pdordlct_id: int
|
|
45
|
+
pdord_id: int
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class PdUnitLchEntryGet(BaseModel):
|
|
49
|
+
model_config = ConfigDict(from_attributes=True)
|
|
50
|
+
createtime: datetime.datetime
|
|
51
|
+
pdunitlct_name: str
|
|
52
|
+
pdunitlc_name: str
|
|
53
|
+
pdord_num: Optional[str]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@routes.get(
|
|
57
|
+
"/pdunitlc",
|
|
58
|
+
tags=["pd"],
|
|
59
|
+
summary="Get all PdUnitLc",
|
|
60
|
+
response_model=Sequence[PdUnitLcGet],
|
|
61
|
+
)
|
|
62
|
+
@require_roles("PdRead")
|
|
63
|
+
def pdunitlc_get_all(session: DBSession, auth: Auth) -> Sequence[PdUnitLc]:
|
|
64
|
+
return session.execute(select(PdUnitLc)).scalars().all()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@routes.get(
|
|
68
|
+
"/pdunitlct",
|
|
69
|
+
tags=["pd"],
|
|
70
|
+
summary="Get all PdUnitLct",
|
|
71
|
+
response_model=Sequence[PdUnitLctGet],
|
|
72
|
+
)
|
|
73
|
+
@require_roles("PdRead")
|
|
74
|
+
def pdunitlct_get_all(session: DBSession, auth: Auth) -> Sequence[PdUnitLct]:
|
|
75
|
+
return session.execute(select(PdUnitLct)).scalars().all()
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@routes.get(
|
|
79
|
+
"/pdordlc",
|
|
80
|
+
tags=["pd"],
|
|
81
|
+
summary="Get all PdOrdLc",
|
|
82
|
+
response_model=Sequence[PdOrdLcGet],
|
|
83
|
+
)
|
|
84
|
+
@require_roles("PdRead")
|
|
85
|
+
def pdordlc_get_all(session: DBSession, auth: Auth) -> Sequence[PdOrdLc]:
|
|
86
|
+
return session.execute(select(PdOrdLc)).scalars().all()
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@routes.get(
|
|
90
|
+
"/pdordlct",
|
|
91
|
+
tags=["pd"],
|
|
92
|
+
summary="Get all PdOrdLct",
|
|
93
|
+
response_model=Sequence[PdOrdLctGet],
|
|
94
|
+
)
|
|
95
|
+
@require_roles("PdRead")
|
|
96
|
+
def pdordlct_get_all(session: DBSession, auth: Auth) -> Sequence[PdOrdLct]:
|
|
97
|
+
return session.execute(select(PdOrdLct)).scalars().all()
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@routes.get(
|
|
101
|
+
"/pdordlch",
|
|
102
|
+
tags=["pd"],
|
|
103
|
+
summary="Get all PdOrdLch",
|
|
104
|
+
response_model=Sequence[PdOrdLchGet],
|
|
105
|
+
)
|
|
106
|
+
@require_roles("PdRead")
|
|
107
|
+
def pdordlch_get_all(session: DBSession, auth: Auth) -> Sequence[PdOrdLch]:
|
|
108
|
+
return session.execute(select(PdOrdLch)).scalars().all()
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@routes.get(
|
|
112
|
+
"/pdunitlch",
|
|
113
|
+
tags=["pd"],
|
|
114
|
+
summary="Get PdUnitLch history for a production unit",
|
|
115
|
+
response_model=list[PdUnitLchEntryGet],
|
|
116
|
+
)
|
|
117
|
+
@require_roles("PdRead")
|
|
118
|
+
def pdunitlch_get(
|
|
119
|
+
session: DBSession,
|
|
120
|
+
auth: Auth,
|
|
121
|
+
pdunit_id: Optional[int] = None,
|
|
122
|
+
pdunit_num: Optional[str] = None,
|
|
123
|
+
duration_s: Optional[int] = None,
|
|
124
|
+
starttime: Optional[datetime.datetime] = None,
|
|
125
|
+
endtime: Optional[datetime.datetime] = None,
|
|
126
|
+
) -> list[UnitLchHistoryEntry]:
|
|
127
|
+
try:
|
|
128
|
+
return get_unit_lch_history(
|
|
129
|
+
session,
|
|
130
|
+
pdunit_id=pdunit_id,
|
|
131
|
+
pdunit_num=pdunit_num,
|
|
132
|
+
starttime=starttime,
|
|
133
|
+
endtime=endtime,
|
|
134
|
+
duration_s=duration_s,
|
|
135
|
+
)
|
|
136
|
+
except ValueError as exc:
|
|
137
|
+
raise HTTPException(status_code=422, detail=str(exc)) from exc
|
|
138
|
+
except LookupError as exc:
|
|
139
|
+
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def mount(app: APIRouter) -> None:
|
|
143
|
+
app.include_router(routes)
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from typing import Dict, Optional, Sequence
|
|
3
|
+
|
|
4
|
+
from perfact.api.main.auth import Auth, require_roles
|
|
5
|
+
from perfact.api.main.dbsession import APIRouter, DBSession
|
|
6
|
+
from perfact.api.pd.services.pdunitperformance_orm import (
|
|
7
|
+
get_time_performance_data_v2,
|
|
8
|
+
get_time_performance_data_v3,
|
|
9
|
+
get_time_performance_data_v4,
|
|
10
|
+
)
|
|
11
|
+
from perfact.api.pd.services.pdunitperformance_plainsql import (
|
|
12
|
+
get_time_performance_data as get_time_performance_data_plain,
|
|
13
|
+
)
|
|
14
|
+
from pydantic import BaseModel
|
|
15
|
+
|
|
16
|
+
routes = APIRouter()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class PdUnitPerformanceGetTime(BaseModel):
|
|
20
|
+
estimated_start: Optional[datetime]
|
|
21
|
+
estimated_end: Optional[datetime]
|
|
22
|
+
delta_start_seconds: Optional[float]
|
|
23
|
+
delta_end_seconds: Optional[float]
|
|
24
|
+
delta_planned_duration_seconds: Optional[float]
|
|
25
|
+
estimated_duration_seconds: Optional[float]
|
|
26
|
+
delta_duration_seconds: Optional[float]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class PdUnitPerformanceGetStatus(BaseModel):
|
|
30
|
+
running_time_seconds: Optional[float]
|
|
31
|
+
remaining_time_seconds: Optional[float]
|
|
32
|
+
downtime_seconds: Optional[float]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class PdUnitPerformanceGetPerformanceTime(BaseModel):
|
|
36
|
+
planned_seconds: Optional[float]
|
|
37
|
+
current_seconds: Optional[float]
|
|
38
|
+
delta_seconds: Optional[float]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class PdUnitPerformanceGetPerformanceTimeSetup(BaseModel):
|
|
42
|
+
current_seconds: Optional[float]
|
|
43
|
+
delta_seconds: Optional[float]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class PdUnitPerformanceGetPerformance(BaseModel):
|
|
47
|
+
cycle_time: Optional[PdUnitPerformanceGetPerformanceTime]
|
|
48
|
+
setup_time: Optional[PdUnitPerformanceGetPerformanceTimeSetup]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class PdUnitPerformanceGet(BaseModel):
|
|
52
|
+
time_management: Optional[PdUnitPerformanceGetTime]
|
|
53
|
+
operation_status: Optional[PdUnitPerformanceGetStatus]
|
|
54
|
+
performance: Optional[PdUnitPerformanceGetPerformance]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@routes.post(
|
|
58
|
+
"/get_time_performance_data_v1",
|
|
59
|
+
tags=["pd"],
|
|
60
|
+
summary="get_time_performance_data - variant 1 (Pure SQL)",
|
|
61
|
+
response_model=Dict[str, PdUnitPerformanceGet],
|
|
62
|
+
)
|
|
63
|
+
@require_roles("PdRead")
|
|
64
|
+
def get_time_performance_data_1(
|
|
65
|
+
session: DBSession, auth: Auth, pdord_ids: Sequence[int]
|
|
66
|
+
):
|
|
67
|
+
if not pdord_ids:
|
|
68
|
+
return {}
|
|
69
|
+
|
|
70
|
+
return get_time_performance_data_plain(session, pdord_ids)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@routes.post(
|
|
74
|
+
"/get_time_performance_data_v2",
|
|
75
|
+
tags=["pd"],
|
|
76
|
+
summary="get_time_performance_data_ variant 2 (ORM)",
|
|
77
|
+
response_model=Dict[str, PdUnitPerformanceGet],
|
|
78
|
+
)
|
|
79
|
+
@require_roles("PdRead")
|
|
80
|
+
def get_time_performance_data_2(
|
|
81
|
+
session: DBSession, auth: Auth, pdord_ids: Sequence[int]
|
|
82
|
+
):
|
|
83
|
+
if not pdord_ids:
|
|
84
|
+
return {}
|
|
85
|
+
return get_time_performance_data_v2(session, pdord_ids)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@routes.post(
|
|
89
|
+
"/get_time_performance_data_v3",
|
|
90
|
+
tags=["pd"],
|
|
91
|
+
summary="get_time_performance_data_ variant 3 (Pure Python SQL)",
|
|
92
|
+
response_model=Dict[str, PdUnitPerformanceGet],
|
|
93
|
+
)
|
|
94
|
+
@require_roles("PdRead")
|
|
95
|
+
def get_time_performance_data_3(
|
|
96
|
+
session: DBSession, auth: Auth, pdord_ids: Sequence[int]
|
|
97
|
+
):
|
|
98
|
+
if not pdord_ids:
|
|
99
|
+
return {}
|
|
100
|
+
|
|
101
|
+
return get_time_performance_data_v3(session, pdord_ids)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@routes.post(
|
|
105
|
+
"/get_time_performance_data_v4",
|
|
106
|
+
tags=["pd"],
|
|
107
|
+
summary="get_time_performance_data_ variant 4 (Pure Python ORM)",
|
|
108
|
+
response_model=Dict[str, PdUnitPerformanceGet],
|
|
109
|
+
)
|
|
110
|
+
@require_roles("PdRead")
|
|
111
|
+
def get_time_performance_data_4(
|
|
112
|
+
session: DBSession, auth: Auth, pdord_ids: Sequence[int]
|
|
113
|
+
):
|
|
114
|
+
if not pdord_ids:
|
|
115
|
+
return {}
|
|
116
|
+
|
|
117
|
+
return get_time_performance_data_v4(session, pdord_ids)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def mount(app: APIRouter):
|
|
121
|
+
app.include_router(routes)
|
|
@@ -3,8 +3,8 @@ pyproject.toml
|
|
|
3
3
|
tox.ini
|
|
4
4
|
src/perfact/api/pd_fastapi/__init__.py
|
|
5
5
|
src/perfact/api/pd_fastapi/kpi.py
|
|
6
|
-
src/perfact/api/pd_fastapi/locustfile.py
|
|
7
6
|
src/perfact/api/pd_fastapi/pdunit.py
|
|
7
|
+
src/perfact/api/pd_fastapi/pdunitlch.py
|
|
8
8
|
src/perfact/api/pd_fastapi/performancedata.py
|
|
9
9
|
src/perfact/api/pd_fastapi/py.typed
|
|
10
10
|
src/perfact_api_pd_fastapi.egg-info/PKG-INFO
|
|
@@ -12,4 +12,9 @@ src/perfact_api_pd_fastapi.egg-info/SOURCES.txt
|
|
|
12
12
|
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
|
-
src/perfact_api_pd_fastapi.egg-info/top_level.txt
|
|
15
|
+
src/perfact_api_pd_fastapi.egg-info/top_level.txt
|
|
16
|
+
tests/locustfile.py
|
|
17
|
+
tests/test_init.py
|
|
18
|
+
tests/test_kpi.py
|
|
19
|
+
tests/test_pdunit.py
|
|
20
|
+
tests/test_performance.py
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from fastapi import FastAPI
|
|
2
|
+
from perfact.api.pd_fastapi import mount
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def test_mount_adds_pd_and_pdord_routes() -> None:
|
|
6
|
+
app = FastAPI()
|
|
7
|
+
|
|
8
|
+
mount(app)
|
|
9
|
+
|
|
10
|
+
paths = {route.path for route in app.routes}
|
|
11
|
+
assert "/pd/kpi/oee" in paths
|
|
12
|
+
assert "/pd/pdunit" in paths
|
|
13
|
+
assert "/pd/pdord/get_time_performance_data_v1" in paths
|
|
@@ -0,0 +1,105 @@
|
|
|
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
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from unittest.mock import Mock
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
from fastapi import HTTPException
|
|
6
|
+
from perfact.api.main.dbsession import APIRouter
|
|
7
|
+
from perfact.api.pd.model import PdUnit
|
|
8
|
+
from perfact.api.pd_fastapi import pdunit
|
|
9
|
+
from sqlalchemy import literal
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class _Policy:
|
|
13
|
+
def __init__(self, predicate):
|
|
14
|
+
self._predicate = predicate
|
|
15
|
+
self.auth_calls = []
|
|
16
|
+
|
|
17
|
+
def filter(self, auth):
|
|
18
|
+
self.auth_calls.append(auth)
|
|
19
|
+
return self._predicate
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class _Visibility:
|
|
23
|
+
def __init__(self, policy):
|
|
24
|
+
self.policy = policy
|
|
25
|
+
self.model_calls = []
|
|
26
|
+
|
|
27
|
+
def get(self, model):
|
|
28
|
+
self.model_calls.append(model)
|
|
29
|
+
return self.policy
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_pdunit_get_all_requires_authentication() -> None:
|
|
33
|
+
policy = _Policy(literal(True))
|
|
34
|
+
visibility = _Visibility(policy)
|
|
35
|
+
session = Mock()
|
|
36
|
+
|
|
37
|
+
with pytest.raises(HTTPException, match="Not authenticated") as exc_info:
|
|
38
|
+
asyncio.run(
|
|
39
|
+
pdunit.pdunit_get_all(session=session, auth=None, visibility=visibility)
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
assert exc_info.value.status_code == 401
|
|
43
|
+
assert visibility.model_calls == [PdUnit]
|
|
44
|
+
session.execute.assert_not_called()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_mount_includes_pdunit_route() -> None:
|
|
48
|
+
app_router = APIRouter()
|
|
49
|
+
|
|
50
|
+
pdunit.mount(app_router)
|
|
51
|
+
|
|
52
|
+
paths = {route.path for route in app_router.routes}
|
|
53
|
+
assert "/pdunit" in paths
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from fastapi import APIRouter
|
|
2
|
+
from perfact.api.pd_fastapi import performancedata as perf
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def test_mount_includes_performance_routes() -> None:
|
|
6
|
+
app_router = APIRouter()
|
|
7
|
+
|
|
8
|
+
perf.mount(app_router)
|
|
9
|
+
|
|
10
|
+
paths = {route.path for route in app_router.routes}
|
|
11
|
+
assert "/get_time_performance_data_v1" in paths
|
|
12
|
+
assert "/get_time_performance_data_v2" in paths
|
|
13
|
+
assert "/get_time_performance_data_v3" in paths
|
|
14
|
+
assert "/get_time_performance_data_v4" in paths
|
|
@@ -32,3 +32,4 @@ commands =
|
|
|
32
32
|
bandit --configfile {toxinidir}/../bandit.yml -r src
|
|
33
33
|
mypy src
|
|
34
34
|
# pytest --doctest-modules --cov-branch --cov=src --cov-report=term-missing {posargs:src}
|
|
35
|
+
pytest --cov-branch --cov=perfact.api.pd_fastapi --cov-report=term-missing --cov-fail-under=100 {posargs:src}
|