perfact-api-pd-fastapi 0.6__tar.gz → 1.1__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 (29) hide show
  1. {perfact_api_pd_fastapi-0.6 → perfact_api_pd_fastapi-1.1}/PKG-INFO +2 -1
  2. {perfact_api_pd_fastapi-0.6 → perfact_api_pd_fastapi-1.1}/pyproject.toml +4 -0
  3. {perfact_api_pd_fastapi-0.6 → perfact_api_pd_fastapi-1.1}/src/perfact/api/pd_fastapi/__init__.py +9 -0
  4. perfact_api_pd_fastapi-1.1/src/perfact/api/pd_fastapi/grafana.py +155 -0
  5. {perfact_api_pd_fastapi-0.6 → perfact_api_pd_fastapi-1.1}/src/perfact/api/pd_fastapi/kpi.py +79 -1
  6. {perfact_api_pd_fastapi-0.6 → perfact_api_pd_fastapi-1.1}/src/perfact_api_pd_fastapi.egg-info/PKG-INFO +2 -1
  7. {perfact_api_pd_fastapi-0.6 → perfact_api_pd_fastapi-1.1}/src/perfact_api_pd_fastapi.egg-info/SOURCES.txt +3 -0
  8. perfact_api_pd_fastapi-1.1/src/perfact_api_pd_fastapi.egg-info/entry_points.txt +5 -0
  9. {perfact_api_pd_fastapi-0.6 → perfact_api_pd_fastapi-1.1}/src/perfact_api_pd_fastapi.egg-info/requires.txt +1 -0
  10. perfact_api_pd_fastapi-1.1/src/perfact_api_pd_fastapi.egg-info/scm_file_list.json +22 -0
  11. perfact_api_pd_fastapi-1.1/src/perfact_api_pd_fastapi.egg-info/scm_version.json +8 -0
  12. {perfact_api_pd_fastapi-0.6 → perfact_api_pd_fastapi-1.1}/tests/helpers.py +2 -2
  13. {perfact_api_pd_fastapi-0.6 → perfact_api_pd_fastapi-1.1}/tox.ini +1 -0
  14. perfact_api_pd_fastapi-0.6/src/perfact_api_pd_fastapi.egg-info/entry_points.txt +0 -2
  15. {perfact_api_pd_fastapi-0.6 → perfact_api_pd_fastapi-1.1}/README.md +0 -0
  16. {perfact_api_pd_fastapi-0.6 → perfact_api_pd_fastapi-1.1}/setup.cfg +0 -0
  17. {perfact_api_pd_fastapi-0.6 → perfact_api_pd_fastapi-1.1}/src/perfact/api/pd_fastapi/pdunit.py +0 -0
  18. {perfact_api_pd_fastapi-0.6 → perfact_api_pd_fastapi-1.1}/src/perfact/api/pd_fastapi/pdunitlch.py +0 -0
  19. {perfact_api_pd_fastapi-0.6 → perfact_api_pd_fastapi-1.1}/src/perfact/api/pd_fastapi/performancedata.py +0 -0
  20. {perfact_api_pd_fastapi-0.6 → perfact_api_pd_fastapi-1.1}/src/perfact/api/pd_fastapi/py.typed +0 -0
  21. {perfact_api_pd_fastapi-0.6 → perfact_api_pd_fastapi-1.1}/src/perfact_api_pd_fastapi.egg-info/dependency_links.txt +0 -0
  22. {perfact_api_pd_fastapi-0.6 → perfact_api_pd_fastapi-1.1}/src/perfact_api_pd_fastapi.egg-info/top_level.txt +0 -0
  23. {perfact_api_pd_fastapi-0.6 → perfact_api_pd_fastapi-1.1}/tests/__init__.py +0 -0
  24. {perfact_api_pd_fastapi-0.6 → perfact_api_pd_fastapi-1.1}/tests/conftest.py +0 -0
  25. {perfact_api_pd_fastapi-0.6 → perfact_api_pd_fastapi-1.1}/tests/locustfile.py +0 -0
  26. {perfact_api_pd_fastapi-0.6 → perfact_api_pd_fastapi-1.1}/tests/test_init.py +0 -0
  27. {perfact_api_pd_fastapi-0.6 → perfact_api_pd_fastapi-1.1}/tests/test_kpi.py +0 -0
  28. {perfact_api_pd_fastapi-0.6 → perfact_api_pd_fastapi-1.1}/tests/test_pdunit.py +0 -0
  29. {perfact_api_pd_fastapi-0.6 → perfact_api_pd_fastapi-1.1}/tests/test_performance.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: perfact-api-pd-fastapi
3
- Version: 0.6
3
+ Version: 1.1
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
@@ -13,6 +13,7 @@ Requires-Dist: fastapi[standard-no-fastapi-cloud-cli]
13
13
  Requires-Dist: perfact-api-pp
14
14
  Requires-Dist: perfact-api-pd
15
15
  Requires-Dist: perfact-api-main
16
+ Requires-Dist: perfact-api-it
16
17
 
17
18
  # PerFact API - pd - APIs
18
19
 
@@ -20,6 +20,7 @@ dependencies = [
20
20
  "perfact-api-pp",
21
21
  "perfact-api-pd",
22
22
  "perfact-api-main",
23
+ "perfact-api-it",
23
24
  ]
24
25
  dynamic = ["version"]
25
26
  requires-python = ">=3.10"
@@ -44,3 +45,6 @@ plugins = ["sqlalchemy.ext.mypy.plugin"]
44
45
 
45
46
  [project.entry-points.'perfact.api']
46
47
  pd = 'perfact.api.pd_fastapi:mount'
48
+
49
+ [project.entry-points.'perfact.assignapi']
50
+ pd_assignd = 'perfact.api.pd_fastapi:mount_assignd'
@@ -1,13 +1,16 @@
1
1
  from fastapi import FastAPI
2
2
  from perfact.api.main.dbsession import APIRouter
3
3
 
4
+ from .grafana import mount as grafanamount
4
5
  from .kpi import mount as kpi_mount
6
+ from .kpi import routes_assignd as kpi_routes_assignd
5
7
  from .pdunit import mount as pdunitmount
6
8
  from .pdunitlch import mount as pdunitlch_mount
7
9
  from .performancedata import mount as performancemount
8
10
 
9
11
  routes = APIRouter()
10
12
  routes_pdord = APIRouter()
13
+ routes_grafana = APIRouter()
11
14
 
12
15
 
13
16
  def mount(app: FastAPI):
@@ -15,5 +18,11 @@ def mount(app: FastAPI):
15
18
  pdunitmount(routes)
16
19
  pdunitlch_mount(routes)
17
20
  performancemount(routes_pdord)
21
+ grafanamount(routes_grafana)
18
22
  app.include_router(routes, prefix="/pd")
19
23
  app.include_router(routes_pdord, prefix="/pd/pdord")
24
+ app.include_router(routes_grafana, prefix="/pd/grafana")
25
+
26
+
27
+ def mount_assignd(app: FastAPI):
28
+ app.include_router(kpi_routes_assignd, prefix="/pd")
@@ -0,0 +1,155 @@
1
+ from typing import Any, Sequence
2
+
3
+ from fastapi import HTTPException, status
4
+ from perfact.api.base import VisibilityPolicy
5
+ from perfact.api.main import VisibilityRegistryDep
6
+ from perfact.api.main.auth import Auth, 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 import get_failure_times, pdunit_downtime_grafana_resp
10
+ from perfact.api.pd.services.grafana import (
11
+ QueryRequest,
12
+ Target,
13
+ VariableRequest,
14
+ extract_grafana_timerange,
15
+ )
16
+ from pydantic import BaseModel, Field
17
+ from sqlalchemy import select
18
+
19
+ routes = APIRouter()
20
+
21
+
22
+ def _filter_visible_pdunit_ids(
23
+ session: DBSession,
24
+ auth: Auth,
25
+ policy: VisibilityPolicy,
26
+ pdunit_ids: list[int],
27
+ ) -> list[int]:
28
+ """Return only PdUnit IDs visible to the current user."""
29
+ return list(
30
+ session.scalars(
31
+ select(PdUnit.id)
32
+ .where(PdUnit.id.in_(pdunit_ids))
33
+ .where(policy.filter(auth))
34
+ ).all()
35
+ )
36
+
37
+
38
+ @routes.get("/", tags=["pd.grafana"], summary="Grafana health check")
39
+ def grafana_health() -> dict[str, str]:
40
+ """Default Grafana route to check if the endpoint is available"""
41
+ return {"status": "ok"}
42
+
43
+
44
+ @require_roles("PdKpi")
45
+ @routes.post("/metrics", tags=["pd.grafana"], summary="Grafana metrics discovery")
46
+ def grafana_metrics() -> list[Any]:
47
+ """
48
+ Returns a list of possible metrics which Grafana can use to display data.
49
+ Current available Metrics:
50
+ - pd.kpi.downtime: categorized downtime of workcenters
51
+ """
52
+ return [
53
+ {"label": "PdUnit Downtime", "value": "pd.kpi.downtime"},
54
+ ]
55
+
56
+
57
+ @routes.post("/annotations", tags=["pd.grafana"], summary="Grafana annotations")
58
+ def grafana_annotations() -> list:
59
+ return []
60
+
61
+
62
+ class VariableDefinition(BaseModel):
63
+ text: str = Field(alias="__text")
64
+ value: str | int = Field(alias="__value")
65
+
66
+
67
+ @require_roles("PdKpi", "PdRead")
68
+ @routes.post(
69
+ "/variable",
70
+ tags=["pd.grafana"],
71
+ summary="Grafana variables",
72
+ response_model=Sequence[VariableDefinition],
73
+ )
74
+ def grafana_variables(
75
+ session: DBSession,
76
+ auth: Auth,
77
+ visibility: VisibilityRegistryDep,
78
+ req: VariableRequest,
79
+ ) -> Sequence[Any]:
80
+ """
81
+ Currently available variables:
82
+ - pd.pdunit: all visible pdunits for the requesting user
83
+ """
84
+ if auth is None:
85
+ raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
86
+ if req.payload is None:
87
+ raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST)
88
+
89
+ target = req.payload.get("target", None)
90
+ if target == "pd.pdunit":
91
+ policy = visibility.get(PdUnit)
92
+ return (
93
+ session.execute(
94
+ select(PdUnit.num.label("__text"), PdUnit.id.label("__value")).filter(
95
+ policy.filter(auth)
96
+ )
97
+ )
98
+ .mappings()
99
+ .all()
100
+ )
101
+
102
+ raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST)
103
+
104
+
105
+ @require_roles("PdKpi")
106
+ @routes.post("/query", tags=["pd.grafana"], summary="Grafana query (actually get data)")
107
+ def grafana_query(
108
+ session: DBSession,
109
+ auth: Auth,
110
+ visibility: VisibilityRegistryDep,
111
+ query: QueryRequest,
112
+ ) -> list[dict[str, Any]]:
113
+ """
114
+ Actually query data from the database.
115
+ Supply targets to specify which metric to query.
116
+ All available metrics can be seen in /metrics
117
+ """
118
+ if auth is None:
119
+ raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
120
+ policy = visibility.get(PdUnit)
121
+
122
+ targets = query.targets
123
+ if not targets and (query.target or query.payload):
124
+ targets = [Target(target=query.target, payload=query.payload)]
125
+
126
+ if not targets:
127
+ return []
128
+
129
+ response: list[dict[str, Any]] = []
130
+ for target in targets:
131
+ if target.target not in {None, "pd.kpi.downtime"}:
132
+ continue
133
+
134
+ pdunit_ids = []
135
+ if target.payload is not None:
136
+ pdunit_ids = [int(x) for x in target.payload.get("pdunit", [])]
137
+ pdunit_ids = _filter_visible_pdunit_ids(session, auth, policy, pdunit_ids)
138
+ starttime, endtime = extract_grafana_timerange(query)
139
+
140
+ if (
141
+ not pdunit_ids
142
+ or starttime is None
143
+ or endtime is None
144
+ or starttime >= endtime
145
+ ):
146
+ response.append(pdunit_downtime_grafana_resp([], target.refId))
147
+ continue
148
+
149
+ rows = get_failure_times(session, pdunit_ids, starttime, endtime)
150
+ response.append(pdunit_downtime_grafana_resp(rows, target.refId))
151
+ return response
152
+
153
+
154
+ def mount(app: APIRouter):
155
+ app.include_router(routes)
@@ -1,11 +1,13 @@
1
1
  from datetime import datetime
2
2
  from typing import Annotated, Optional
3
3
 
4
- from fastapi import HTTPException
4
+ from fastapi import HTTPException, Query, status
5
+ from perfact.api.base import VisibilityPolicy
5
6
  from perfact.api.main import Auth, VisibilityRegistryDep
6
7
  from perfact.api.main.auth import require_roles
7
8
  from perfact.api.main.dbsession import APIRouter, DBSession
8
9
  from perfact.api.pd.model.pdunit import PdUnit
10
+ from perfact.api.pd.services import get_failure_times
9
11
  from perfact.api.pd.services.oee.availability import compute_availability
10
12
  from perfact.api.pd.services.oee.breakdown import oee_breakdown_computation
11
13
  from perfact.api.pd.services.oee.prod_metrics import (
@@ -15,10 +17,16 @@ from perfact.api.pd.services.oee.prod_metrics import (
15
17
  from perfact.api.pd.services.oee.prod_records import get_prod_records
16
18
  from perfact.api.pd.services.oee.state_spans import get_state_spans
17
19
  from perfact.api.pd.services.oee.summary import oee_workcenter_time_computation
20
+ from perfact.api.pd.services.oee.trend import (
21
+ compute_trend_for_candidates,
22
+ compute_trend_for_collections,
23
+ get_oee_trend_candidate_days,
24
+ )
18
25
  from pydantic import BaseModel, BeforeValidator
19
26
  from sqlalchemy import select
20
27
 
21
28
  routes = APIRouter()
29
+ routes_assignd = APIRouter()
22
30
 
23
31
 
24
32
  def _require_visible_unit(
@@ -37,6 +45,22 @@ def _require_visible_unit(
37
45
  return unit
38
46
 
39
47
 
48
+ def _filter_visible_pdunit_ids(
49
+ session: DBSession,
50
+ auth: Auth,
51
+ policy: VisibilityPolicy,
52
+ pdunit_ids: list[int],
53
+ ) -> list[int]:
54
+ """Return only PdUnit IDs visible to the current user."""
55
+ return list(
56
+ session.scalars(
57
+ select(PdUnit.id)
58
+ .where(PdUnit.id.in_(pdunit_ids))
59
+ .where(policy.filter(auth))
60
+ ).all()
61
+ )
62
+
63
+
40
64
  def parse_datetime_with_UTC_offset(value: str) -> datetime:
41
65
  try:
42
66
  return datetime.fromisoformat(value)
@@ -217,5 +241,59 @@ def oee_breakdown(
217
241
  return [OEEBreakdownResponse(**row.__dict__) for row in rows]
218
242
 
219
243
 
244
+ @routes_assignd.get(
245
+ "/kpi/oee/aggregation", tags=["pd_assignd"], summary="OEE trend aggregation"
246
+ )
247
+ def oee_trend_aggregation(
248
+ session: DBSession,
249
+ lookback_months: int = 6,
250
+ ) -> dict:
251
+ """
252
+ Calculate and persist daily OEE values into dgval for eligible pdunit/day pairs.
253
+ """
254
+
255
+ candidates = get_oee_trend_candidate_days(session, lookback_months)
256
+
257
+ # compute trend for candidate days
258
+ c, u, s, impacted_days = compute_trend_for_candidates(session, candidates)
259
+
260
+ # create or update pdunitgroups and pdplants
261
+ # if possible for each of the impacted days
262
+ pdgrp_c, pdgrp_u, pdplt_c, pdplt_u = compute_trend_for_collections(
263
+ session, impacted_days
264
+ )
265
+
266
+ return {
267
+ "created": c,
268
+ "updated": u,
269
+ "skipped": s,
270
+ "pdunitgroup_created": pdgrp_c,
271
+ "pdunitgroup_updated": pdgrp_u,
272
+ "pdplant_created": pdplt_c,
273
+ "pdplant_updated": pdplt_u,
274
+ }
275
+
276
+
277
+ @routes.get("/kpi/downtime", tags=["pd"], summary="PdUnit downtime")
278
+ @require_roles("PdKpi")
279
+ def pdunit_downtime_calculation(
280
+ session: DBSession,
281
+ auth: Auth,
282
+ visibility: VisibilityRegistryDep,
283
+ starttime: datetime,
284
+ endtime: datetime,
285
+ pdunit_ids: list[int] = Query(),
286
+ ) -> list:
287
+ if auth is None:
288
+ raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
289
+ if not pdunit_ids:
290
+ return []
291
+ policy = visibility.get(PdUnit)
292
+ pdunit_ids = _filter_visible_pdunit_ids(session, auth, policy, pdunit_ids)
293
+ if not pdunit_ids:
294
+ return []
295
+ return get_failure_times(session, pdunit_ids, starttime, endtime)
296
+
297
+
220
298
  def mount(app: APIRouter):
221
299
  app.include_router(routes)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: perfact-api-pd-fastapi
3
- Version: 0.6
3
+ Version: 1.1
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
@@ -13,6 +13,7 @@ Requires-Dist: fastapi[standard-no-fastapi-cloud-cli]
13
13
  Requires-Dist: perfact-api-pp
14
14
  Requires-Dist: perfact-api-pd
15
15
  Requires-Dist: perfact-api-main
16
+ Requires-Dist: perfact-api-it
16
17
 
17
18
  # PerFact API - pd - APIs
18
19
 
@@ -2,6 +2,7 @@ README.md
2
2
  pyproject.toml
3
3
  tox.ini
4
4
  src/perfact/api/pd_fastapi/__init__.py
5
+ src/perfact/api/pd_fastapi/grafana.py
5
6
  src/perfact/api/pd_fastapi/kpi.py
6
7
  src/perfact/api/pd_fastapi/pdunit.py
7
8
  src/perfact/api/pd_fastapi/pdunitlch.py
@@ -12,6 +13,8 @@ src/perfact_api_pd_fastapi.egg-info/SOURCES.txt
12
13
  src/perfact_api_pd_fastapi.egg-info/dependency_links.txt
13
14
  src/perfact_api_pd_fastapi.egg-info/entry_points.txt
14
15
  src/perfact_api_pd_fastapi.egg-info/requires.txt
16
+ src/perfact_api_pd_fastapi.egg-info/scm_file_list.json
17
+ src/perfact_api_pd_fastapi.egg-info/scm_version.json
15
18
  src/perfact_api_pd_fastapi.egg-info/top_level.txt
16
19
  tests/__init__.py
17
20
  tests/conftest.py
@@ -0,0 +1,5 @@
1
+ [perfact.api]
2
+ pd = perfact.api.pd_fastapi:mount
3
+
4
+ [perfact.assignapi]
5
+ pd_assignd = perfact.api.pd_fastapi:mount_assignd
@@ -2,3 +2,4 @@ fastapi[standard-no-fastapi-cloud-cli]
2
2
  perfact-api-pp
3
3
  perfact-api-pd
4
4
  perfact-api-main
5
+ perfact-api-it
@@ -0,0 +1,22 @@
1
+ {
2
+ "files": [
3
+ "README.md",
4
+ "pyproject.toml",
5
+ "tox.ini",
6
+ "src/perfact/api/pd_fastapi/__init__.py",
7
+ "src/perfact/api/pd_fastapi/kpi.py",
8
+ "src/perfact/api/pd_fastapi/performancedata.py",
9
+ "src/perfact/api/pd_fastapi/grafana.py",
10
+ "src/perfact/api/pd_fastapi/pdunit.py",
11
+ "src/perfact/api/pd_fastapi/py.typed",
12
+ "src/perfact/api/pd_fastapi/pdunitlch.py",
13
+ "tests/__init__.py",
14
+ "tests/conftest.py",
15
+ "tests/test_kpi.py",
16
+ "tests/test_init.py",
17
+ "tests/helpers.py",
18
+ "tests/locustfile.py",
19
+ "tests/test_pdunit.py",
20
+ "tests/test_performance.py"
21
+ ]
22
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "1.1",
3
+ "distance": 0,
4
+ "node": "g6c4583704600c3394229813f5c0430cfc51a0fa8",
5
+ "dirty": false,
6
+ "branch": "HEAD",
7
+ "node_date": "2026-06-25"
8
+ }
@@ -1,4 +1,4 @@
1
- from fastapi.routing import APIRoute, _IncludedRouter
1
+ from fastapi.routing import APIRoute
2
2
 
3
3
 
4
4
  def collect_paths(router, prefix: str = "") -> set[str]:
@@ -11,7 +11,7 @@ def collect_paths(router, prefix: str = "") -> set[str]:
11
11
  for route in router.routes:
12
12
  if isinstance(route, APIRoute):
13
13
  paths.add(prefix + route.path)
14
- elif isinstance(route, _IncludedRouter):
14
+ elif hasattr(route, "original_router") and hasattr(route, "include_context"):
15
15
  sub_prefix = prefix + (route.include_context.prefix or "")
16
16
  paths |= collect_paths(route.original_router, sub_prefix)
17
17
  return paths
@@ -25,6 +25,7 @@ deps =
25
25
  perfact-api-main
26
26
  perfact-api-pd
27
27
  perfact-api-pp
28
+ perfact-api-it
28
29
 
29
30
  commands =
30
31
  ruff format --check
@@ -1,2 +0,0 @@
1
- [perfact.api]
2
- pd = perfact.api.pd_fastapi:mount