Compare commits

...

26 Commits

Author SHA1 Message Date
creyD
7afb8e2fd8 Adjusted files for isort & autopep 2025-04-04 15:55:18 +00:00
vikynoah
badf2b157f Response filter async (#45)
* fix: get_object alter for async response filter

* fix: Alter async response
2025-04-04 17:54:47 +02:00
creyD
c903266ec4 Adjusted files for isort & autopep 2025-04-03 07:45:38 +00:00
vikynoah
910638e3a6 fix: get_object alter for async response filter (#44) 2025-04-03 09:45:09 +02:00
vikynoah
83dca59817 fix: BaseSchemaModelOUT alter (#43) 2025-04-02 09:58:36 +02:00
vikynoah
b80d26586d fix: Response model (#41)
* feat: Filter response fields

* changes

* changes

* import fix

* changes
2025-03-27 09:33:04 +01:00
creyD
c2e2469027 Adjusted files for isort & autopep 2025-03-24 08:26:44 +00:00
renovate[bot]
8363055984 feat(deps): update dependency python to 3.13 (#39)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-24 09:26:14 +01:00
vikynoah
f586ce5c03 feat: Filter response fields (#40)
* feat: Filter response fields

* changes

* changes
2025-03-24 09:25:52 +01:00
creyD
0af8f05edf Adjusted files for isort & autopep 2025-03-06 14:36:12 +00:00
vikynoah
b73c524e8d feat: Delete User from Auth0 on invite delete (#38) 2025-03-06 15:35:42 +01:00
creyD
be260b0ee6 Adjusted files for isort & autopep 2025-02-26 16:11:54 +00:00
renovate[bot]
b0f2815568 feat(deps): update dependency stripe to v11.6.0 (#36)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-26 17:11:23 +01:00
vikynoah
6ac609f3f4 feat: Add CSV file Helper (#37) 2025-02-26 17:11:07 +01:00
creyD
53ed939451 Adjusted files for isort & autopep 2025-02-24 17:27:48 +00:00
c56d14c2fd Merge pull request #35 from vikynoah/invite_bug
fix: add company id to invite user
2025-02-24 18:27:17 +01:00
vikynoah
1e9bcb92b6 fix: add company id to invite user 2025-02-24 11:49:17 +01:00
5e16bd5cbc fix: fixed issue that creyPY couldn't be used without PSQL 2025-02-19 10:27:51 +01:00
creyD
50b444be89 Adjusted files for isort & autopep 2025-02-14 09:10:55 +00:00
e12c86e352 Merge pull request #34 from vikynoah/obj_lifecycle_patch
feat : Add Patch to obj lifecycle
2025-02-14 10:10:21 +01:00
vikynoah
0708a48301 feat : Add Patch to obj lifecycle 2025-02-13 02:05:15 +01:00
34595d52f2 Merge pull request #33 from creyD/renovate/stripe-11.x
feat(deps): update dependency stripe to v11.5.0
2025-02-05 09:38:28 +01:00
renovate[bot]
421725ad10 feat(deps): update dependency stripe to v11.5.0 2025-01-27 22:02:52 +00:00
31c4cbb055 fix: fixed multiple bugs in database handling 2025-01-27 16:26:26 +01:00
410ae12f8e feat: added ssl option to test database 2025-01-27 13:16:55 +01:00
1f224c44bc feat: added sslmode flag 2025-01-27 13:09:16 +01:00
13 changed files with 147 additions and 31 deletions

View File

@@ -38,7 +38,7 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
python-version: '3.13'
- run: python -m pip install --upgrade pip
- run: |
python -m pip install -r requirements.txt
@@ -93,7 +93,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
python-version: '3.13'
- name: Install dependencies
run: |

View File

@@ -1,12 +1,13 @@
from typing import Type, TypeVar, overload
import asyncio
from typing import List, Type, TypeVar, overload
from uuid import UUID
from fastapi import HTTPException
from pydantic import BaseModel
from sqlalchemy.orm import Session
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.future import select
import asyncio
from sqlalchemy.orm import Session
from .models.base import Base
T = TypeVar("T", bound=Base)
@@ -19,13 +20,19 @@ async def get_object_or_404(
db: AsyncSession,
expunge: bool = False,
lookup_column: str = "id",
response_fields: List[str] = [],
) -> T:
pass
@overload
def get_object_or_404(
db_class: Type[T], id: UUID | str, db: Session, expunge: bool = False, lookup_column: str = "id"
db_class: Type[T],
id: UUID | str,
db: Session,
expunge: bool = False,
lookup_column: str = "id",
response_fields: List[str] = [],
) -> T:
pass
@@ -36,20 +43,45 @@ def get_object_or_404(
db: Session | AsyncSession,
expunge: bool = False,
lookup_column: str = "id",
response_fields: List[str] = [],
) -> T:
async def _get_async_object() -> T:
query = select(db_class).filter(getattr(db_class, lookup_column) == id)
result = await db.execute(query)
obj = result.scalar_one_or_none()
if obj is None:
raise HTTPException(status_code=404, detail="The object does not exist.") # type: ignore
if response_fields:
selected_columns = [
getattr(db_class, field) for field in response_fields if hasattr(db_class, field)
]
query = select(*selected_columns).where(getattr(db_class, lookup_column) == id)
result = await db.execute(query)
row = result.first()
if row is None:
raise HTTPException(status_code=404, detail="The object does not exist.")
if hasattr(row, "_mapping"):
obj_dict = dict(row._mapping)
else:
obj_dict = {column.key: getattr(row, column.key) for column in selected_columns}
else:
query = select(db_class).where(getattr(db_class, lookup_column) == id)
result = await db.execute(query)
row = result.scalar_one_or_none()
if row is None:
raise HTTPException(status_code=404, detail="The object does not exist.")
obj_dict = row
if expunge:
await db.expunge(obj)
return obj
await db.expunge(obj_dict)
return obj_dict
def _get_sync_object() -> T:
obj = db.query(db_class).filter(getattr(db_class, lookup_column) == id).one_or_none()
if response_fields:
selected_columns = [
getattr(db_class, field) for field in response_fields if hasattr(db_class, field)
]
query = db.query(*selected_columns).filter(getattr(db_class, lookup_column) == id)
else:
query = db.query(db_class).filter(getattr(db_class, lookup_column) == id)
obj = query.one_or_none()
if obj is None:
raise HTTPException(status_code=404, detail="The object does not exist.") # type: ignore
if expunge:

View File

@@ -1,3 +1,8 @@
from .async_session import * # noqa
from .helpers import * # noqa
from .session import * # noqa
try:
import sqlalchemy
from .async_session import *
from .helpers import *
from .session import *
except ImportError:
print("SQLAlchemy not installed. Database functionality will be disabled.")

View File

@@ -1,15 +1,14 @@
from typing import AsyncGenerator
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
from .common import SQLALCHEMY_DATABASE_URL, name
from .common import SQLALCHEMY_DATABASE_URL, name, ssl_mode
async_engine = create_async_engine(
SQLALCHEMY_DATABASE_URL + name, pool_pre_ping=True, connect_args={"sslmode": "require"}
SQLALCHEMY_DATABASE_URL + name, pool_pre_ping=True, connect_args={"sslmode": ssl_mode}
)
AsyncSessionLocal = sessionmaker(
AsyncSessionLocal = async_sessionmaker(
bind=async_engine,
class_=AsyncSession,
expire_on_commit=False,

View File

@@ -10,4 +10,6 @@ password = os.getenv("POSTGRES_PASSWORD", "root")
port = os.getenv("POSTGRES_PORT", "5432")
name = os.getenv("POSTGRES_DB", "fastapi")
ssl_mode = os.getenv("SSL_MODE", "require")
SQLALCHEMY_DATABASE_URL = f"postgresql+psycopg://{user}:{password}@{host}:{port}/"

View File

@@ -4,10 +4,10 @@ from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.orm.session import Session
from .common import SQLALCHEMY_DATABASE_URL, name
from .common import SQLALCHEMY_DATABASE_URL, name, ssl_mode
engine = create_engine(
SQLALCHEMY_DATABASE_URL + name, pool_pre_ping=True, connect_args={"sslmode": "require"}
SQLALCHEMY_DATABASE_URL + name, pool_pre_ping=True, connect_args={"sslmode": ssl_mode}
)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

View File

@@ -1 +1,2 @@
from .base import * # noqa
from .response_schema import * #noqa

View File

@@ -11,6 +11,6 @@ class BaseSchemaModelIN(BaseModel):
class BaseSchemaModelOUT(BaseSchemaModelIN):
id: UUID
id: UUID | str
created_at: datetime
updated_at: datetime

View File

@@ -0,0 +1,43 @@
from typing import List, Optional, Type
from fastapi import Query
from pydantic import BaseModel, create_model
class ResponseModelDependency:
def __init__(self, model_class: Type[BaseModel]):
self.model_class = model_class
def __call__(self, response_fields: Optional[List[str]] = Query(None)) -> Type[BaseModel]:
def process_result(result, fields=None, async_session=False):
if not fields:
if async_session:
return {k: v for k, v in result.__dict__.items() if not k.startswith("_")}
return result
if hasattr(result, "_fields"):
row_fields = result._fields
return dict(zip(row_fields, result))
elif isinstance(result, tuple):
return dict(zip(fields, result))
elif isinstance(result, dict):
return result
else:
return {field: getattr(result, field) for field in fields if hasattr(result, field)}
if not response_fields:
return self.model_class, None, process_result
all_annotations = {}
for cls in self.model_class.__mro__:
if hasattr(cls, "__annotations__"):
all_annotations.update(cls.__annotations__)
fields = {}
for field in response_fields:
if field in all_annotations:
fields[field] = (all_annotations[field], None)
dynamic_model = create_model(f"Dynamic{self.model_class.__name__}", **fields)
return dynamic_model, response_fields, process_result

View File

@@ -20,17 +20,21 @@ class AbstractTestAPI(unittest.IsolatedAsyncioTestCase):
transport=ASGITransport(app=app), base_url="http://testserver", follow_redirects=True
)
cls.default_headers = headers
print("setting up abstract")
@classmethod
def setup_database(
cls, sync_db_url: str, async_db_url: str, base: Type[Base], btree_gist: bool = False
cls,
sync_db_url: str,
async_db_url: str,
base: Type[Base],
btree_gist: bool = False,
ssl_mode: str = "require",
):
cls.engine_s = create_engine(
sync_db_url,
echo=False,
pool_pre_ping=True,
connect_args={"sslmode": "require"},
connect_args={"sslmode": ssl_mode},
)
if database_exists(cls.engine_s.url):
drop_database(cls.engine_s.url)
@@ -47,7 +51,7 @@ class AbstractTestAPI(unittest.IsolatedAsyncioTestCase):
async_db_url,
echo=False,
pool_pre_ping=True,
connect_args={"sslmode": "require"},
connect_args={"sslmode": ssl_mode},
)
async def get(self, url: str, r_code: int = 200, parse_json=True) -> dict | bytes:
@@ -136,6 +140,7 @@ class AbstractTestAPI(unittest.IsolatedAsyncioTestCase):
pagination: bool = True,
id_field: str = "id",
created_at_check: bool = True,
patch: dict | None = None,
):
# GET LIST
re = await self.get(url)
@@ -160,6 +165,14 @@ class AbstractTestAPI(unittest.IsolatedAsyncioTestCase):
re = await self.get(f"{url}{obj_id}/")
self.assertEqual(re[id_field], obj_id)
# PATCH
if patch:
for key, value in patch.items():
input_obj[key] = value
re = await self.patch(f"{url}{obj_id}/", obj=input_obj)
for key, value in patch.items():
self.assertEqual(re[key], value)
# GET LIST
re = await self.get(url)
if pagination:

View File

@@ -1,5 +1,7 @@
import secrets
import string
import csv
from pathlib import Path
def create_random_password(length: int = 12) -> str:
@@ -14,3 +16,11 @@ def create_random_password(length: int = 12) -> str:
password += [secrets.choice(all_characters) for _ in range(length - 4)]
secrets.SystemRandom().shuffle(password)
return "".join(password)
def data_to_csv(file: Path, data: list) -> None:
with file.open(mode="w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=data[0].keys(), delimiter=";")
writer.writeheader()
writer.writerows(data)

View File

@@ -101,7 +101,7 @@ def request_verification_mail(sub: str) -> None:
return re.json()
def create_user_invite(email: str) -> dict:
def create_user_invite(email: str, company_id: str) -> dict:
re = requests.post(
f"https://{AUTH0_DOMAIN}/api/v2/users",
headers={"Authorization": f"Bearer {get_management_token()}"},
@@ -111,6 +111,7 @@ def create_user_invite(email: str) -> dict:
"password": create_random_password(),
"verify_email": False,
"app_metadata": {"invitedToMyApp": True},
"user_metadata": {"company_ids": [company_id]},
},
timeout=5,
)
@@ -119,6 +120,16 @@ def create_user_invite(email: str) -> dict:
return re.json()
def delete_user_invite(user_id: str) -> None:
re = requests.delete(
f"https://{AUTH0_DOMAIN}/api/v2/users/{user_id}",
headers={"Authorization": f"Bearer {get_management_token()}"},
timeout=5,
)
if re.status_code != 204:
raise HTTPException(re.status_code, re.json())
def password_change_mail(email: str) -> bool:
re = requests.post(
f"https://{AUTH0_DOMAIN}/dbconnections/change_password",

View File

@@ -1 +1 @@
stripe==11.4.1 # Stripe
stripe==11.6.0 # Stripe