perfact-api-main 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 (26) hide show
  1. perfact_api_main-0.3/.gitea/workflows/publish.yml +31 -0
  2. {perfact_api_main-0.2 → perfact_api_main-0.3}/PKG-INFO +1 -1
  3. perfact_api_main-0.3/src/perfact/api/main/dbsession.py +132 -0
  4. {perfact_api_main-0.2 → perfact_api_main-0.3}/src/perfact_api_main.egg-info/PKG-INFO +1 -1
  5. {perfact_api_main-0.2 → perfact_api_main-0.3}/src/perfact_api_main.egg-info/SOURCES.txt +1 -0
  6. {perfact_api_main-0.2 → perfact_api_main-0.3}/tox.ini +2 -2
  7. perfact_api_main-0.2/src/perfact/api/main/dbsession.py +0 -61
  8. {perfact_api_main-0.2 → perfact_api_main-0.3}/.gitea/workflows/check.yml +0 -0
  9. {perfact_api_main-0.2 → perfact_api_main-0.3}/.gitignore +0 -0
  10. {perfact_api_main-0.2 → perfact_api_main-0.3}/.vscode/launch.json +0 -0
  11. {perfact_api_main-0.2 → perfact_api_main-0.3}/.vscode/settings.json +0 -0
  12. {perfact_api_main-0.2 → perfact_api_main-0.3}/README.md +0 -0
  13. {perfact_api_main-0.2 → perfact_api_main-0.3}/bandit.yml +0 -0
  14. {perfact_api_main-0.2 → perfact_api_main-0.3}/pyproject.toml +0 -0
  15. {perfact_api_main-0.2 → perfact_api_main-0.3}/setup.cfg +0 -0
  16. {perfact_api_main-0.2 → perfact_api_main-0.3}/src/perfact/api/main/__init__.py +0 -0
  17. {perfact_api_main-0.2 → perfact_api_main-0.3}/src/perfact/api/main/app.py +0 -0
  18. {perfact_api_main-0.2 → perfact_api_main-0.3}/src/perfact/api/main/assignworker.py +0 -0
  19. {perfact_api_main-0.2 → perfact_api_main-0.3}/src/perfact/api/main/auth.py +0 -0
  20. {perfact_api_main-0.2 → perfact_api_main-0.3}/src/perfact/api/main/config.py +0 -0
  21. {perfact_api_main-0.2 → perfact_api_main-0.3}/src/perfact/api/main/perfact_generic.py +0 -0
  22. {perfact_api_main-0.2 → perfact_api_main-0.3}/src/perfact/api/main/py.typed +0 -0
  23. {perfact_api_main-0.2 → perfact_api_main-0.3}/src/perfact/api/main/utils.py +0 -0
  24. {perfact_api_main-0.2 → perfact_api_main-0.3}/src/perfact_api_main.egg-info/dependency_links.txt +0 -0
  25. {perfact_api_main-0.2 → perfact_api_main-0.3}/src/perfact_api_main.egg-info/requires.txt +0 -0
  26. {perfact_api_main-0.2 → perfact_api_main-0.3}/src/perfact_api_main.egg-info/top_level.txt +0 -0
@@ -0,0 +1,31 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "[0-9]*"
7
+
8
+ jobs:
9
+ publish:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+ with:
14
+ fetch-depth: 0
15
+
16
+ - name: Set up Python
17
+ uses: actions/setup-python@v5
18
+ with:
19
+ python-version: "3.x"
20
+
21
+ - name: Install build dependencies
22
+ run: python -m pip install --upgrade build twine
23
+
24
+ - name: Build package
25
+ run: python -m build
26
+
27
+ - name: Publish to PyPI
28
+ env:
29
+ TWINE_USERNAME: __token__
30
+ TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
31
+ run: twine upload dist/*
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: perfact-api-main
3
- Version: 0.2
3
+ Version: 0.3
4
4
  Summary: PerFact API - FastAPI main package (middleware, auth, entrypoints)
5
5
  Author-email: Viktor Dick <viktor.dick@perfact.de>
6
6
  License-Expression: GPL-2.0-or-later
@@ -0,0 +1,132 @@
1
+ import asyncio
2
+ import logging
3
+ from typing import Annotated, Callable
4
+
5
+ from fastapi import APIRouter as FastAPIRouter
6
+ from fastapi import Depends, Request, Response
7
+ from fastapi.routing import APIRoute
8
+ from pydantic_settings import BaseSettings
9
+ from sqlalchemy import create_engine
10
+ from sqlalchemy.exc import DBAPIError, OperationalError
11
+ from sqlalchemy.orm import Session
12
+ from sqlalchemy.pool import NullPool
13
+ from starlette.middleware.base import BaseHTTPMiddleware
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class Settings(BaseSettings):
19
+ connstr: str = "postgresql+psycopg://zope@/perfactema"
20
+ sql_debug: bool = False
21
+ pooling: bool = True # Set to False for testing
22
+
23
+
24
+ settings = Settings()
25
+
26
+
27
+ def set_connstr(connstr):
28
+ """
29
+ To be called before the app starts up,
30
+ set the connection string from there.
31
+ """
32
+ if connstr:
33
+ settings.connstr = connstr
34
+
35
+
36
+ def skip_db_retry(func):
37
+ """
38
+ Decorator to explicitly opt out of the db retry functionality
39
+ """
40
+ func.skip_db_retry = True
41
+ return func
42
+
43
+
44
+ class RetryRoute(APIRoute):
45
+ """
46
+ Custom route class that automatically retries database transactoins
47
+ on errors, we can explicitly opt out via @skil_db_retry decorator
48
+ """
49
+
50
+ def get_route_handler(self) -> Callable:
51
+ original_route_handler = super().get_route_handler()
52
+
53
+ async def custom_route_handler(request: Request) -> Response:
54
+ # Opt out check
55
+ if getattr(self.endpoint, "skip_db_retry", False):
56
+ return await original_route_handler(request)
57
+
58
+ max_retries = 3
59
+ backoff_seconds = 0.5
60
+
61
+ for attempt in range(1, max_retries + 1):
62
+ try:
63
+ response = await original_route_handler(request)
64
+
65
+ if hasattr(request.state, "db"):
66
+ request.state.db.flush()
67
+
68
+ return response
69
+
70
+ except (OperationalError, DBAPIError) as e:
71
+ if hasattr(request.state, "db"):
72
+ request.state.db.rollback()
73
+
74
+ logger.warning(
75
+ f"Attempting retry {attempt}/{max_retries} for error {e}"
76
+ )
77
+
78
+ if attempt == max_retries:
79
+ raise
80
+
81
+ await asyncio.sleep(backoff_seconds * attempt)
82
+
83
+ raise RuntimeError("Unreachable route: handler failed")
84
+
85
+ return custom_route_handler
86
+
87
+
88
+ class DBSessionMiddleware(BaseHTTPMiddleware):
89
+ """
90
+ Start a DB session for each request. Commit it at the end if there is no error,
91
+ otherwise roll back and return a generic 500 error. Note that this does not mean
92
+ that a request is not allowed to do its own commits in between.
93
+ """
94
+
95
+ async def dispatch(self, request, call_next):
96
+ if not hasattr(self, "engine"):
97
+ self.engine = create_engine(
98
+ settings.connstr,
99
+ pool_pre_ping=True,
100
+ echo=settings.sql_debug,
101
+ poolclass=NullPool if not settings.pooling else None,
102
+ )
103
+ response = Response("Internal server error", status_code=500)
104
+ try:
105
+ request.state.db = Session(self.engine)
106
+ response = await call_next(request)
107
+ request.state.db.commit()
108
+ except Exception:
109
+ request.state.db.rollback()
110
+ raise
111
+ finally:
112
+ request.state.db.close()
113
+ return response
114
+
115
+
116
+ class APIRouter(FastAPIRouter):
117
+ """
118
+ Standard API Router that applies the db logic to all endpoints
119
+ APIRouter needs to be imported in the target file
120
+ for the retry functionality to work
121
+ """
122
+
123
+ def __init__(self, *args, **kwargs):
124
+ kwargs.setdefault("route_class", RetryRoute)
125
+ super().__init__(*args, **kwargs)
126
+
127
+
128
+ def _get_session(request: Request):
129
+ return request.state.db
130
+
131
+
132
+ DBSession = Annotated[Session, Depends(_get_session)]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: perfact-api-main
3
- Version: 0.2
3
+ Version: 0.3
4
4
  Summary: PerFact API - FastAPI main package (middleware, auth, entrypoints)
5
5
  Author-email: Viktor Dick <viktor.dick@perfact.de>
6
6
  License-Expression: GPL-2.0-or-later
@@ -4,6 +4,7 @@ bandit.yml
4
4
  pyproject.toml
5
5
  tox.ini
6
6
  .gitea/workflows/check.yml
7
+ .gitea/workflows/publish.yml
7
8
  .vscode/launch.json
8
9
  .vscode/settings.json
9
10
  src/perfact/api/main/__init__.py
@@ -21,8 +21,8 @@ deps =
21
21
  bandit
22
22
  mypy
23
23
  types-pyyaml
24
- perfact-api-base-model @ git+ssh://git@git.perfact.de:3022/PythonPackages/perfact-api-base-model
25
- perfact-api-app-model @ git+ssh://git@git.perfact.de:3022/PythonPackages/perfact-api-app-model
24
+ perfact-api-base-model
25
+ perfact-api-app-model
26
26
 
27
27
  commands =
28
28
  ruff format --check
@@ -1,61 +0,0 @@
1
- from typing import Annotated
2
-
3
- from fastapi import Depends, Request, Response
4
- from pydantic_settings import BaseSettings
5
- from sqlalchemy import create_engine
6
- from sqlalchemy.orm import Session
7
- from sqlalchemy.pool import NullPool
8
- from starlette.middleware.base import BaseHTTPMiddleware
9
-
10
-
11
- class Settings(BaseSettings):
12
- connstr: str = "postgresql+psycopg://zope@/perfactema"
13
- sql_debug: bool = False
14
- pooling: bool = True # Set to False for testing
15
-
16
-
17
- settings = Settings()
18
-
19
-
20
- def set_connstr(connstr):
21
- """
22
- To be called before the app starts up,
23
- set the connection string from there.
24
- """
25
- if connstr:
26
- settings.connstr = connstr
27
-
28
-
29
- class DBSessionMiddleware(BaseHTTPMiddleware):
30
- """
31
- Start a DB session for each request. Commit it at the end if there is no error,
32
- otherwise roll back and return a generic 500 error. Note that this does not mean
33
- that a request is not allowed to do its own commits in between.
34
- """
35
-
36
- async def dispatch(self, request, call_next):
37
- if not hasattr(self, "engine"):
38
- self.engine = create_engine(
39
- settings.connstr,
40
- pool_pre_ping=True,
41
- echo=settings.sql_debug,
42
- poolclass=NullPool if not settings.pooling else None,
43
- )
44
- response = Response("Internal server error", status_code=500)
45
- try:
46
- request.state.db = Session(self.engine)
47
- response = await call_next(request)
48
- request.state.db.commit()
49
- except Exception:
50
- request.state.db.rollback()
51
- raise
52
- finally:
53
- request.state.db.close()
54
- return response
55
-
56
-
57
- def _get_session(request: Request):
58
- return request.state.db
59
-
60
-
61
- DBSession = Annotated[Session, Depends(_get_session)]
File without changes
File without changes