perfact-api-pd-fastapi 0.2__tar.gz → 0.3__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 (23) hide show
  1. {perfact_api_pd_fastapi-0.2 → perfact_api_pd_fastapi-0.3}/PKG-INFO +1 -1
  2. perfact_api_pd_fastapi-0.3/src/perfact/api/pd_fastapi/kpi.py +133 -0
  3. perfact_api_pd_fastapi-0.3/src/perfact/api/pd_fastapi/performancedata.py +121 -0
  4. {perfact_api_pd_fastapi-0.2 → perfact_api_pd_fastapi-0.3}/src/perfact_api_pd_fastapi.egg-info/PKG-INFO +1 -1
  5. {perfact_api_pd_fastapi-0.2 → perfact_api_pd_fastapi-0.3}/src/perfact_api_pd_fastapi.egg-info/SOURCES.txt +6 -2
  6. perfact_api_pd_fastapi-0.3/tests/test_init.py +13 -0
  7. perfact_api_pd_fastapi-0.3/tests/test_kpi.py +105 -0
  8. perfact_api_pd_fastapi-0.3/tests/test_pdunit.py +53 -0
  9. perfact_api_pd_fastapi-0.3/tests/test_performance.py +14 -0
  10. {perfact_api_pd_fastapi-0.2 → perfact_api_pd_fastapi-0.3}/tox.ini +1 -0
  11. perfact_api_pd_fastapi-0.2/src/perfact/api/pd_fastapi/kpi.py +0 -226
  12. perfact_api_pd_fastapi-0.2/src/perfact/api/pd_fastapi/performancedata.py +0 -701
  13. {perfact_api_pd_fastapi-0.2 → perfact_api_pd_fastapi-0.3}/README.md +0 -0
  14. {perfact_api_pd_fastapi-0.2 → perfact_api_pd_fastapi-0.3}/pyproject.toml +0 -0
  15. {perfact_api_pd_fastapi-0.2 → perfact_api_pd_fastapi-0.3}/setup.cfg +0 -0
  16. {perfact_api_pd_fastapi-0.2 → perfact_api_pd_fastapi-0.3}/src/perfact/api/pd_fastapi/__init__.py +0 -0
  17. {perfact_api_pd_fastapi-0.2 → perfact_api_pd_fastapi-0.3}/src/perfact/api/pd_fastapi/pdunit.py +0 -0
  18. {perfact_api_pd_fastapi-0.2 → perfact_api_pd_fastapi-0.3}/src/perfact/api/pd_fastapi/py.typed +0 -0
  19. {perfact_api_pd_fastapi-0.2 → perfact_api_pd_fastapi-0.3}/src/perfact_api_pd_fastapi.egg-info/dependency_links.txt +0 -0
  20. {perfact_api_pd_fastapi-0.2 → perfact_api_pd_fastapi-0.3}/src/perfact_api_pd_fastapi.egg-info/entry_points.txt +0 -0
  21. {perfact_api_pd_fastapi-0.2 → perfact_api_pd_fastapi-0.3}/src/perfact_api_pd_fastapi.egg-info/requires.txt +0 -0
  22. {perfact_api_pd_fastapi-0.2 → perfact_api_pd_fastapi-0.3}/src/perfact_api_pd_fastapi.egg-info/top_level.txt +0 -0
  23. {perfact_api_pd_fastapi-0.2/src/perfact/api/pd_fastapi → perfact_api_pd_fastapi-0.3/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.3
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
@@ -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,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.3
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,7 +3,6 @@ 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
8
7
  src/perfact/api/pd_fastapi/performancedata.py
9
8
  src/perfact/api/pd_fastapi/py.typed
@@ -12,4 +11,9 @@ src/perfact_api_pd_fastapi.egg-info/SOURCES.txt
12
11
  src/perfact_api_pd_fastapi.egg-info/dependency_links.txt
13
12
  src/perfact_api_pd_fastapi.egg-info/entry_points.txt
14
13
  src/perfact_api_pd_fastapi.egg-info/requires.txt
15
- src/perfact_api_pd_fastapi.egg-info/top_level.txt
14
+ src/perfact_api_pd_fastapi.egg-info/top_level.txt
15
+ tests/locustfile.py
16
+ tests/test_init.py
17
+ tests/test_kpi.py
18
+ tests/test_pdunit.py
19
+ 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}