perfact-api-pd-fastapi 1.0__tar.gz → 1.2__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-1.0 → perfact_api_pd_fastapi-1.2}/PKG-INFO +2 -1
  2. {perfact_api_pd_fastapi-1.0 → perfact_api_pd_fastapi-1.2}/pyproject.toml +1 -0
  3. {perfact_api_pd_fastapi-1.0 → perfact_api_pd_fastapi-1.2}/src/perfact/api/pd_fastapi/__init__.py +4 -0
  4. perfact_api_pd_fastapi-1.2/src/perfact/api/pd_fastapi/grafana.py +155 -0
  5. {perfact_api_pd_fastapi-1.0 → perfact_api_pd_fastapi-1.2}/src/perfact/api/pd_fastapi/kpi.py +40 -1
  6. {perfact_api_pd_fastapi-1.0 → perfact_api_pd_fastapi-1.2}/src/perfact_api_pd_fastapi.egg-info/PKG-INFO +2 -1
  7. {perfact_api_pd_fastapi-1.0 → perfact_api_pd_fastapi-1.2}/src/perfact_api_pd_fastapi.egg-info/SOURCES.txt +1 -0
  8. {perfact_api_pd_fastapi-1.0 → perfact_api_pd_fastapi-1.2}/src/perfact_api_pd_fastapi.egg-info/requires.txt +1 -0
  9. {perfact_api_pd_fastapi-1.0 → perfact_api_pd_fastapi-1.2}/src/perfact_api_pd_fastapi.egg-info/scm_file_list.json +8 -7
  10. perfact_api_pd_fastapi-1.2/src/perfact_api_pd_fastapi.egg-info/scm_version.json +8 -0
  11. {perfact_api_pd_fastapi-1.0 → perfact_api_pd_fastapi-1.2}/tests/helpers.py +2 -2
  12. {perfact_api_pd_fastapi-1.0 → perfact_api_pd_fastapi-1.2}/tox.ini +1 -0
  13. perfact_api_pd_fastapi-1.0/src/perfact_api_pd_fastapi.egg-info/scm_version.json +0 -8
  14. {perfact_api_pd_fastapi-1.0 → perfact_api_pd_fastapi-1.2}/README.md +0 -0
  15. {perfact_api_pd_fastapi-1.0 → perfact_api_pd_fastapi-1.2}/setup.cfg +0 -0
  16. {perfact_api_pd_fastapi-1.0 → perfact_api_pd_fastapi-1.2}/src/perfact/api/pd_fastapi/pdunit.py +0 -0
  17. {perfact_api_pd_fastapi-1.0 → perfact_api_pd_fastapi-1.2}/src/perfact/api/pd_fastapi/pdunitlch.py +0 -0
  18. {perfact_api_pd_fastapi-1.0 → perfact_api_pd_fastapi-1.2}/src/perfact/api/pd_fastapi/performancedata.py +0 -0
  19. {perfact_api_pd_fastapi-1.0 → perfact_api_pd_fastapi-1.2}/src/perfact/api/pd_fastapi/py.typed +0 -0
  20. {perfact_api_pd_fastapi-1.0 → perfact_api_pd_fastapi-1.2}/src/perfact_api_pd_fastapi.egg-info/dependency_links.txt +0 -0
  21. {perfact_api_pd_fastapi-1.0 → perfact_api_pd_fastapi-1.2}/src/perfact_api_pd_fastapi.egg-info/entry_points.txt +0 -0
  22. {perfact_api_pd_fastapi-1.0 → perfact_api_pd_fastapi-1.2}/src/perfact_api_pd_fastapi.egg-info/top_level.txt +0 -0
  23. {perfact_api_pd_fastapi-1.0 → perfact_api_pd_fastapi-1.2}/tests/__init__.py +0 -0
  24. {perfact_api_pd_fastapi-1.0 → perfact_api_pd_fastapi-1.2}/tests/conftest.py +0 -0
  25. {perfact_api_pd_fastapi-1.0 → perfact_api_pd_fastapi-1.2}/tests/locustfile.py +0 -0
  26. {perfact_api_pd_fastapi-1.0 → perfact_api_pd_fastapi-1.2}/tests/test_init.py +0 -0
  27. {perfact_api_pd_fastapi-1.0 → perfact_api_pd_fastapi-1.2}/tests/test_kpi.py +0 -0
  28. {perfact_api_pd_fastapi-1.0 → perfact_api_pd_fastapi-1.2}/tests/test_pdunit.py +0 -0
  29. {perfact_api_pd_fastapi-1.0 → perfact_api_pd_fastapi-1.2}/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: 1.0
3
+ Version: 1.2
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"
@@ -1,6 +1,7 @@
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
5
6
  from .kpi import routes_assignd as kpi_routes_assignd
6
7
  from .pdunit import mount as pdunitmount
@@ -9,6 +10,7 @@ from .performancedata import mount as performancemount
9
10
 
10
11
  routes = APIRouter()
11
12
  routes_pdord = APIRouter()
13
+ routes_grafana = APIRouter()
12
14
 
13
15
 
14
16
  def mount(app: FastAPI):
@@ -16,8 +18,10 @@ def mount(app: FastAPI):
16
18
  pdunitmount(routes)
17
19
  pdunitlch_mount(routes)
18
20
  performancemount(routes_pdord)
21
+ grafanamount(routes_grafana)
19
22
  app.include_router(routes, prefix="/pd")
20
23
  app.include_router(routes_pdord, prefix="/pd/pdord")
24
+ app.include_router(routes_grafana, prefix="/pd/grafana")
21
25
 
22
26
 
23
27
  def mount_assignd(app: FastAPI):
@@ -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 (
@@ -43,6 +45,22 @@ def _require_visible_unit(
43
45
  return unit
44
46
 
45
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
+
46
64
  def parse_datetime_with_UTC_offset(value: str) -> datetime:
47
65
  try:
48
66
  return datetime.fromisoformat(value)
@@ -256,5 +274,26 @@ def oee_trend_aggregation(
256
274
  }
257
275
 
258
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
+
259
298
  def mount(app: APIRouter):
260
299
  app.include_router(routes)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: perfact-api-pd-fastapi
3
- Version: 1.0
3
+ Version: 1.2
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
@@ -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
@@ -3,19 +3,20 @@
3
3
  "README.md",
4
4
  "pyproject.toml",
5
5
  "tox.ini",
6
- "src/perfact/api/pd_fastapi/__init__.py",
7
- "src/perfact/api/pd_fastapi/kpi.py",
8
6
  "src/perfact/api/pd_fastapi/performancedata.py",
9
- "src/perfact/api/pd_fastapi/pdunit.py",
7
+ "src/perfact/api/pd_fastapi/grafana.py",
10
8
  "src/perfact/api/pd_fastapi/py.typed",
9
+ "src/perfact/api/pd_fastapi/kpi.py",
10
+ "src/perfact/api/pd_fastapi/__init__.py",
11
11
  "src/perfact/api/pd_fastapi/pdunitlch.py",
12
- "tests/__init__.py",
13
- "tests/conftest.py",
14
- "tests/test_kpi.py",
15
- "tests/test_init.py",
12
+ "src/perfact/api/pd_fastapi/pdunit.py",
16
13
  "tests/helpers.py",
17
14
  "tests/locustfile.py",
15
+ "tests/test_init.py",
16
+ "tests/test_kpi.py",
17
+ "tests/__init__.py",
18
18
  "tests/test_pdunit.py",
19
+ "tests/conftest.py",
19
20
  "tests/test_performance.py"
20
21
  ]
21
22
  }
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "1.2",
3
+ "distance": 0,
4
+ "node": "g23f9e71ace0327b4a1e5f9488a191b8ffe17585a",
5
+ "dirty": false,
6
+ "branch": "HEAD",
7
+ "node_date": "2026-06-26"
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,8 +0,0 @@
1
- {
2
- "tag": "1.0",
3
- "distance": 0,
4
- "node": "gfb2c87d0ac4741b17796b2fd015fa54fc09cd710",
5
- "dirty": false,
6
- "branch": "HEAD",
7
- "node_date": "2026-06-24"
8
- }