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.
Files changed (24) hide show
  1. {perfact_api_pd_fastapi-0.2 → perfact_api_pd_fastapi-0.4}/PKG-INFO +1 -1
  2. {perfact_api_pd_fastapi-0.2 → perfact_api_pd_fastapi-0.4}/src/perfact/api/pd_fastapi/__init__.py +2 -0
  3. perfact_api_pd_fastapi-0.4/src/perfact/api/pd_fastapi/kpi.py +133 -0
  4. perfact_api_pd_fastapi-0.4/src/perfact/api/pd_fastapi/pdunitlch.py +143 -0
  5. perfact_api_pd_fastapi-0.4/src/perfact/api/pd_fastapi/performancedata.py +121 -0
  6. {perfact_api_pd_fastapi-0.2 → perfact_api_pd_fastapi-0.4}/src/perfact_api_pd_fastapi.egg-info/PKG-INFO +1 -1
  7. {perfact_api_pd_fastapi-0.2 → perfact_api_pd_fastapi-0.4}/src/perfact_api_pd_fastapi.egg-info/SOURCES.txt +7 -2
  8. perfact_api_pd_fastapi-0.4/tests/test_init.py +13 -0
  9. perfact_api_pd_fastapi-0.4/tests/test_kpi.py +105 -0
  10. perfact_api_pd_fastapi-0.4/tests/test_pdunit.py +53 -0
  11. perfact_api_pd_fastapi-0.4/tests/test_performance.py +14 -0
  12. {perfact_api_pd_fastapi-0.2 → perfact_api_pd_fastapi-0.4}/tox.ini +1 -0
  13. perfact_api_pd_fastapi-0.2/src/perfact/api/pd_fastapi/kpi.py +0 -226
  14. perfact_api_pd_fastapi-0.2/src/perfact/api/pd_fastapi/performancedata.py +0 -701
  15. {perfact_api_pd_fastapi-0.2 → perfact_api_pd_fastapi-0.4}/README.md +0 -0
  16. {perfact_api_pd_fastapi-0.2 → perfact_api_pd_fastapi-0.4}/pyproject.toml +0 -0
  17. {perfact_api_pd_fastapi-0.2 → perfact_api_pd_fastapi-0.4}/setup.cfg +0 -0
  18. {perfact_api_pd_fastapi-0.2 → perfact_api_pd_fastapi-0.4}/src/perfact/api/pd_fastapi/pdunit.py +0 -0
  19. {perfact_api_pd_fastapi-0.2 → perfact_api_pd_fastapi-0.4}/src/perfact/api/pd_fastapi/py.typed +0 -0
  20. {perfact_api_pd_fastapi-0.2 → perfact_api_pd_fastapi-0.4}/src/perfact_api_pd_fastapi.egg-info/dependency_links.txt +0 -0
  21. {perfact_api_pd_fastapi-0.2 → perfact_api_pd_fastapi-0.4}/src/perfact_api_pd_fastapi.egg-info/entry_points.txt +0 -0
  22. {perfact_api_pd_fastapi-0.2 → perfact_api_pd_fastapi-0.4}/src/perfact_api_pd_fastapi.egg-info/requires.txt +0 -0
  23. {perfact_api_pd_fastapi-0.2 → perfact_api_pd_fastapi-0.4}/src/perfact_api_pd_fastapi.egg-info/top_level.txt +0 -0
  24. {perfact_api_pd_fastapi-0.2/src/perfact/api/pd_fastapi → perfact_api_pd_fastapi-0.4/tests}/locustfile.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: perfact-api-pd-fastapi
3
- Version: 0.2
3
+ Version: 0.4
4
4
  Summary: PerFact API - SQLAlchemy+FastAPI pd apis package
5
5
  Author-email: Viktor Dick <viktor.dick@perfact.de>
6
6
  License-Expression: GPL-2.0-or-later
@@ -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)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: perfact-api-pd-fastapi
3
- Version: 0.2
3
+ Version: 0.4
4
4
  Summary: PerFact API - SQLAlchemy+FastAPI pd apis package
5
5
  Author-email: Viktor Dick <viktor.dick@perfact.de>
6
6
  License-Expression: GPL-2.0-or-later
@@ -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}