Compare commits

...

19 Commits

Author SHA1 Message Date
renovate[bot]
c9cad27525 feat(deps): update paulhatch/semantic-version action to v6 2026-03-17 14:10:36 +00:00
5b74ed5620 fix: added name for primary key constraint 2025-07-24 23:10:46 +02:00
bb3a52295d feat: added LowerCaseString field 2025-07-24 22:53:26 +02:00
renovate[bot]
d471b86a25 feat(deps): update dependency stripe to v12.3.0 (#55)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-08 09:46:11 +02:00
creyD
aa99fc6226 Adjusted files for isort & autopep 2025-06-26 12:50:57 +00:00
vikynoah
30a5e417eb feat: User Password change ticket (#54) 2025-06-26 14:49:56 +02:00
creyD
1f5ba9210f Adjusted files for isort & autopep 2025-06-03 08:01:59 +00:00
vikynoah
f805b3f508 feat: Added Email Sending Service (#52)
* feat: Added Email Sending Service

* changes

* changes
2025-06-03 10:00:49 +02:00
renovate[bot]
8a882cdaae feat(deps): update dependency stripe to v12.2.0 (#51)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-02 16:31:03 +02:00
renovate[bot]
40176aa3e9 feat(deps): update dependency stripe to v12.1.0 (#50)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-06 11:09:31 +02:00
vikynoah
be66bbebbf fix: remove optional schema and alter base out (#49)
* fix: remove optional schema and alter base out

* changes
2025-04-15 20:32:02 +02:00
renovate[bot]
79dde8008a feat(deps): update dependency stripe to v12 (#42)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-09 14:01:08 +02:00
creyD
adb017d6ce Adjusted files for isort & autopep 2025-04-09 12:01:00 +00:00
vikynoah
e160cc5fea feat: Response filter async (#47)
* fix: get_object alter for async response filter

* fix: Alter async response

* feat: Decorator for schema out
2025-04-09 14:00:30 +02:00
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
14 changed files with 122 additions and 55 deletions

View File

@@ -76,7 +76,7 @@ jobs:
fi
- name: Git Version
uses: PaulHatch/semantic-version@v5.4.0
uses: PaulHatch/semantic-version@v6.0.2
id: git_version
with:
tag_prefix: ""

2
.gitignore vendored
View File

@@ -158,3 +158,5 @@ cython_debug/
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
.DS_*

View File

@@ -1,3 +1,4 @@
from .fields import * # noqa
from .groups import * # noqa
from .i18n import * # noqa
from .stripe import * # noqa

17
creyPY/const/fields.py Normal file
View File

@@ -0,0 +1,17 @@
from sqlalchemy import types
class LowerCaseString(types.TypeDecorator):
"""Converts strings to lower case on the way in."""
impl = types.String
cache_ok = True
def process_bind_param(self, value, dialect):
if value is None:
return value
return value.lower()
@property
def python_type(self):
return str

View File

@@ -1,12 +1,13 @@
from typing import Type, TypeVar, overload, List
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)
@@ -50,16 +51,26 @@ def get_object_or_404(
selected_columns = [
getattr(db_class, field) for field in response_fields if hasattr(db_class, field)
]
query = select(*selected_columns).select_from(db_class)
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).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
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:
if response_fields:

View File

@@ -1,7 +1,7 @@
import uuid
from datetime import datetime
from sqlalchemy import Column, DateTime, String
from sqlalchemy import Column, DateTime, PrimaryKeyConstraint, String
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.ext.declarative import declared_attr
from sqlalchemy.orm import as_declarative
@@ -23,6 +23,11 @@ class Base(AutoAnnotateMixin, AutoInitMixin):
# TODO: Add automated foreign key resolution
# Add name to primary key constraint to ensure alembic can pick it up later
@declared_attr
def __table_args__(cls):
return (PrimaryKeyConstraint("id", name=f"pk_{cls.__tablename__}"),)
# Generate __tablename__ automatically
@declared_attr
def __tablename__(cls) -> str:

View File

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

View File

@@ -1,40 +0,0 @@
from typing import List, Optional, Type
from pydantic import BaseModel, create_model
from fastapi import Query
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):
if not fields:
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

@@ -0,0 +1,19 @@
from typing import Optional, Type, Union, get_args, get_origin, get_type_hints
from pydantic import BaseModel, create_model
def optional_fields(cls: Type[BaseModel]) -> Type[BaseModel]:
fields = {}
for name, hint in get_type_hints(cls).items():
if name.startswith("_"):
continue
if get_origin(hint) is not Union or type(None) not in get_args(hint):
hint = Optional[hint]
fields[name] = (hint, None)
new_model = create_model(cls.__name__, __base__=cls, **fields)
return new_model

View File

@@ -1,2 +1,3 @@
from .auth0 import * # noqa
from .stripe import * # noqa
from .aws import * # noqa

View File

@@ -145,3 +145,21 @@ def password_change_mail(email: str) -> bool:
if re.status_code != 200:
raise HTTPException(re.status_code, re.json())
return True
def user_password_change_ticket(user_id: str) -> str:
re = requests.post(
f"https://{AUTH0_DOMAIN}/api/v2/tickets/password-change",
headers={"Authorization": f"Bearer {get_management_token()}"},
json={
"user_id": user_id,
"client_id": AUTH0_CLIENT_ID,
"ttl_sec": 0,
"mark_email_as_verified": False,
"includeEmailInRedirect": False,
},
timeout=5,
)
if re.status_code != 201:
raise HTTPException(re.status_code, re.json())
return re.json()["ticket"]

View File

@@ -0,0 +1 @@
from .email import * # noqa

View File

@@ -0,0 +1,32 @@
import os
import boto3
from botocore.exceptions import ClientError
AWS_CLIENT_ID = os.getenv("AWS_CLIENT_ID")
AWS_CLIENT_SECRET = os.getenv("AWS_CLIENT_SECRET")
AWS_SENDER_EMAIL = os.getenv("AWS_SENDER_EMAIL")
AWS_REGION = os.getenv("AWS_REGION", "eu-central-1")
async def send_email_ses(recipient_email, subject, html_body):
ses_client = boto3.client(
"ses",
aws_access_key_id=AWS_CLIENT_ID,
aws_secret_access_key=AWS_CLIENT_SECRET,
region_name=AWS_REGION,
)
email_message = {
"Source": AWS_SENDER_EMAIL,
"Destination": {"ToAddresses": [recipient_email]},
"Message": {
"Subject": {"Data": subject, "Charset": "UTF-8"},
"Body": {"Html": {"Data": html_body, "Charset": "UTF-8"}},
},
}
try:
response = ses_client.send_email(**email_message)
return response["MessageId"]
except ClientError as e:
return None

View File

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