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.
- perfact_api_main-0.3/.gitea/workflows/publish.yml +31 -0
- {perfact_api_main-0.2 → perfact_api_main-0.3}/PKG-INFO +1 -1
- perfact_api_main-0.3/src/perfact/api/main/dbsession.py +132 -0
- {perfact_api_main-0.2 → perfact_api_main-0.3}/src/perfact_api_main.egg-info/PKG-INFO +1 -1
- {perfact_api_main-0.2 → perfact_api_main-0.3}/src/perfact_api_main.egg-info/SOURCES.txt +1 -0
- {perfact_api_main-0.2 → perfact_api_main-0.3}/tox.ini +2 -2
- perfact_api_main-0.2/src/perfact/api/main/dbsession.py +0 -61
- {perfact_api_main-0.2 → perfact_api_main-0.3}/.gitea/workflows/check.yml +0 -0
- {perfact_api_main-0.2 → perfact_api_main-0.3}/.gitignore +0 -0
- {perfact_api_main-0.2 → perfact_api_main-0.3}/.vscode/launch.json +0 -0
- {perfact_api_main-0.2 → perfact_api_main-0.3}/.vscode/settings.json +0 -0
- {perfact_api_main-0.2 → perfact_api_main-0.3}/README.md +0 -0
- {perfact_api_main-0.2 → perfact_api_main-0.3}/bandit.yml +0 -0
- {perfact_api_main-0.2 → perfact_api_main-0.3}/pyproject.toml +0 -0
- {perfact_api_main-0.2 → perfact_api_main-0.3}/setup.cfg +0 -0
- {perfact_api_main-0.2 → perfact_api_main-0.3}/src/perfact/api/main/__init__.py +0 -0
- {perfact_api_main-0.2 → perfact_api_main-0.3}/src/perfact/api/main/app.py +0 -0
- {perfact_api_main-0.2 → perfact_api_main-0.3}/src/perfact/api/main/assignworker.py +0 -0
- {perfact_api_main-0.2 → perfact_api_main-0.3}/src/perfact/api/main/auth.py +0 -0
- {perfact_api_main-0.2 → perfact_api_main-0.3}/src/perfact/api/main/config.py +0 -0
- {perfact_api_main-0.2 → perfact_api_main-0.3}/src/perfact/api/main/perfact_generic.py +0 -0
- {perfact_api_main-0.2 → perfact_api_main-0.3}/src/perfact/api/main/py.typed +0 -0
- {perfact_api_main-0.2 → perfact_api_main-0.3}/src/perfact/api/main/utils.py +0 -0
- {perfact_api_main-0.2 → perfact_api_main-0.3}/src/perfact_api_main.egg-info/dependency_links.txt +0 -0
- {perfact_api_main-0.2 → perfact_api_main-0.3}/src/perfact_api_main.egg-info/requires.txt +0 -0
- {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/*
|
|
@@ -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)]
|
|
@@ -21,8 +21,8 @@ deps =
|
|
|
21
21
|
bandit
|
|
22
22
|
mypy
|
|
23
23
|
types-pyyaml
|
|
24
|
-
perfact-api-base-model
|
|
25
|
-
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{perfact_api_main-0.2 → perfact_api_main-0.3}/src/perfact_api_main.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|