Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions LICENSE
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

7 changes: 6 additions & 1 deletion copier.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -121,4 +121,9 @@ _tasks:
when: "{{ project_type in ['api-monolith', 'api-microservice'] }}"
- command: uvx python -c "import os; os.makedirs('migrations/versions', exist_ok=True)"
when: "{{ project_type in ['api-monolith', 'api-microservice', 'agent'] }}"
- "uv sync"
- "uv sync --group code-quality"
- "git init && git checkout -b master && git add --all"
- command: uvx python -c "import subprocess; result = subprocess.run(['uv', 'run', 'pre-commit', 'run', '--all-files']); exit(0)"
- "git add --all"
- command: uvx python -c "import subprocess; subprocess.run(['git', 'commit', '-m', 'Initial commit'])"

2 changes: 1 addition & 1 deletion python-ai-kit/.dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,4 @@ Makefile
.*

*.xml
*.db
*.db
2 changes: 1 addition & 1 deletion python-ai-kit/.gitattributes
Original file line number Diff line number Diff line change
@@ -1 +1 @@
*.sh text eol=lf
*.sh text eol=lf
2 changes: 1 addition & 1 deletion python-ai-kit/.github/workflows/ci.yml.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,4 @@ jobs:
- name: Run formatter
run: uv run ruff format --check
- name: Run type checker
run: uv run ty check
run: uv run ty check
5 changes: 4 additions & 1 deletion python-ai-kit/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,9 @@ __pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
celerybeat-schedule
celerybeat-schedule-wal
celerybeat-schedule-shm
*-shm
*-wal

Expand Down Expand Up @@ -156,4 +159,4 @@ volumes
*.pem

# Streamlit secrets
.streamlit/
.streamlit/
5 changes: 3 additions & 2 deletions python-ai-kit/.pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,14 @@ repos:
hooks:
- id: ty
name: ty check
entry: .venv/bin/ty check .
entry: uv run ty check .
language: system
pass_filenames: false

- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-merge-conflict
args: [--assume-in-merge]
args: [--assume-in-merge]
2 changes: 1 addition & 1 deletion python-ai-kit/.python-version.jinja
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{{default_python_version}}
{{default_python_version}}
2 changes: 1 addition & 1 deletion python-ai-kit/LICENSE
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
SOFTWARE.
18 changes: 9 additions & 9 deletions python-ai-kit/app/config.py.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -22,27 +22,27 @@ class Settings(BaseSettings):
)

# CORE SETTINGS
fernet_decryptor: FernetDecryptorField = Field("MASTER_KEY")
fernet_decryptor: FernetDecryptorField = Field(FernetDecryptorField("MASTER_KEY"))
environment: EnvironmentType = EnvironmentType.LOCAL

# API SETTINGS
api_name: str = f"{{project_name}} API"
api_name: str = "{{project_name}} API"
api_v1: str = "/api/v1"
api_latest: str = api_v1
paging_limit: int = 100
cors_origins: list[AnyHttpUrl] = []
cors_allow_all: bool = False

{% if project_type in ["api-monolith", "api-microservice"] %}

# DATABASE SETTINGS
db_host: str = "localhost"
db_port: int = 5432
db_name: str = "{{project_name}}"
db_user: str = "{{project_name}}"
db_password: SecretStr = SecretStr("{{project_name}}")
{% endif %}

{% if project_type == "agent" %}

# AGENT SETTINGS
debug_mode: bool = False
api_key: str
Expand All @@ -66,14 +66,14 @@ class Settings(BaseSettings):
return [url.strip() for url in v.split(',') if url.strip()]
return v
{% endif %}

{% if "celery" in plugins %}

# CELERY SETTINGS
CELERY_BROKER_URL: str
CELERY_RESULT_BACKEND: str
{% endif %}

{% if "sentry" in plugins %}

# Sentry
SENTRY_ENABLED: bool = False
SENTRY_DSN: str | None = None
Expand All @@ -99,12 +99,12 @@ class Settings(BaseSettings):
return v.get_decrypted_value(validation_info.data["fernet_decryptor"])
return v


{%if project_type == "mcp-server" %}

# MCP SETTINGS
mcp_server_name: str = f"MCP Server"
{% endif %}

{% endif %}
{% if project_type in ["api-monolith", "api-microservice"] %}
@property
def db_uri(self) -> str:
Expand All @@ -113,8 +113,8 @@ class Settings(BaseSettings):
f"{self.db_user}:{self.db_password.get_secret_value()}"
f"@{self.db_host}:{self.db_port}/{self.db_name}"
)
{% endif %}

{% endif %}
# 0. pytest ini_options
# 1. environment variables
# 2. .env
Expand Down
12 changes: 5 additions & 7 deletions python-ai-kit/app/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
from uuid import UUID

from fastapi import Depends
from sqlalchemy import Engine, UUID as SqlUUID, Text, create_engine, inspect
from sqlalchemy import UUID as SQL_UUID
from sqlalchemy import Engine, Text, create_engine, inspect
from sqlalchemy.orm import (
DeclarativeBase,
Session,
Expand All @@ -28,7 +29,7 @@ def _prepare_sessionmaker(engine: Engine) -> sessionmaker:
return sessionmaker(autocommit=False, autoflush=False, bind=engine)


class BaseDbModel(DeclarativeBase, metaclass=AutoRelMeta):
class BaseDbModel(DeclarativeBase, metaclass=AutoRelMeta):
@declared_attr
def __tablename__(self) -> str:
return self.__name__.lower()
Expand All @@ -39,15 +40,12 @@ def id_str(self) -> str:

def __repr__(self) -> str:
mapper = inspect(self.__class__)
fields = [
f"{col.key}={repr(getattr(self, col.key, None))}"
for col in mapper.columns
]
fields = [f"{col.key}={repr(getattr(self, col.key, None))}" for col in mapper.columns]
return f"<{self.__class__.__name__}({', '.join(fields)})>"

type_annotation_map = {
str: Text,
UUID: SqlUUID,
UUID: SQL_UUID,
}


Expand Down
2 changes: 1 addition & 1 deletion python-ai-kit/app/integrations/celery/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
from .core import create_celery

__all__ = ["create_celery"]
__all__ = ["create_celery"]
7 changes: 3 additions & 4 deletions python-ai-kit/app/integrations/celery/core.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
from app.config import settings
from celery import Celery
from celery import current_app as current_celery_app
from celery.schedules import crontab

from app.config import settings


def create_celery() -> Celery:
celery_app: Celery = current_celery_app # type: ignore[assignment]
Expand All @@ -20,11 +19,11 @@ def create_celery() -> Celery:
result_expires=3 * 24 * 3600,
)

celery_app.autodiscover_tasks(["app.integrations.celery.tasks.dummy_task"])
celery_app.autodiscover_tasks(["app.integrations.celery"])

celery_app.conf.beat_schedule = {
"dummy-task": {
"task": "app.integrations.celery.tasks.dummy_task",
"task": "app.integrations.celery.tasks.dummy_task.dummy_task",
"schedule": crontab(minute="*/1"),
},
}
Expand Down
2 changes: 1 addition & 1 deletion python-ai-kit/app/integrations/celery/tasks/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
from .dummy_task import dummy_task

__all__ = ["dummy_task"]
__all__ = ["dummy_task"]
2 changes: 1 addition & 1 deletion python-ai-kit/app/integrations/sentry.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from app.config import settings


def init_sentry():
def init_sentry() -> None:
if settings.SENTRY_ENABLED:
sentry_sdk.init(
dsn=settings.SENTRY_DSN,
Expand Down
2 changes: 1 addition & 1 deletion python-ai-kit/app/integrations/sqladmin/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
from .views import add_admin_views

__all__ = ["add_admin_views"]
__all__ = ["add_admin_views"]
70 changes: 41 additions & 29 deletions python-ai-kit/app/integrations/sqladmin/base_view.py
Original file line number Diff line number Diff line change
@@ -1,54 +1,66 @@
from typing import Any

from fastapi import Request
from sqladmin import ModelView
from pydantic import BaseModel
from sqladmin.models import ModelViewMeta

from app.database import BaseDbModel
from app.integrations.sqladmin.view_models import ColumnConfig, FormConfig, _get_model_fields
from app.integrations.sqladmin.view_models import (
ColumnConfig,
FormConfig,
_get_model_fields,
)
from sqladmin import ModelView
from sqladmin.models import ModelViewMeta


class BaseAdminMeta(ModelViewMeta):
def __new__(mcls, name: str, bases: tuple, attrs: dict, **kwargs: Any) -> ModelViewMeta:
def __new__(
mcls,
name: str,
bases: tuple,
attrs: dict,
**kwargs: Any,
) -> ModelViewMeta:
cls = super().__new__(mcls, name, bases, attrs, **kwargs)

if 'create_schema' in kwargs:
cls._create_schema = kwargs['create_schema']
if 'update_schema' in kwargs:
cls._update_schema = kwargs['update_schema']
if "create_schema" in kwargs:
cls._create_schema = kwargs["create_schema"]

if "update_schema" in kwargs:
cls._update_schema = kwargs["update_schema"]

if 'column' in kwargs:
ColumnConfig(**kwargs['column']).apply_to_class(cls)
if "column" in kwargs:
ColumnConfig(**kwargs["column"]).apply_to_class(cls)
else:
if hasattr(cls, 'model'):
if hasattr(cls, "model"):
cls.column_searchable_list = _get_model_fields(cls)
cls.column_sortable_list = _get_model_fields(cls)

if auto_exclude_fields := mcls._get_fields_with_default_factory(cls):
cls.form_excluded_columns = auto_exclude_fields

if 'form' in kwargs:
FormConfig(**kwargs['form']).apply_to_class(cls)
if "form" in kwargs:
FormConfig(**kwargs["form"]).apply_to_class(cls)

return cls

@staticmethod
def _get_fields_with_default_factory(cls) -> list[str]:
def _get_fields_with_default_factory(cls: Any) -> list[str]:
exclude_fields = []
for schema_attr in ['_create_schema', '_update_schema']:

for schema_attr in ["_create_schema", "_update_schema"]:
if schema := getattr(cls, schema_attr, None):
for field_name, field_info in schema.model_fields.items():
if getattr(field_info, 'default_factory', None):
if getattr(field_info, "default_factory", None):
exclude_fields.append(field_name)

return list(set(exclude_fields))


class BaseAdminView(ModelView, metaclass=BaseAdminMeta):
_create_schema: type[BaseModel]
_update_schema: type[BaseModel]

column_list: str | list[str] = "__all__"

# by default metaclass excludes fields from schemas with default_factory
Expand All @@ -57,16 +69,16 @@ class BaseAdminView(ModelView, metaclass=BaseAdminMeta):
# add form_include_pk=True to your target class to override this behavior

async def on_model_change(
self,
data: dict[str, Any],
model: BaseDbModel,
is_created: bool,
request: Request
self,
data: dict[str, Any],
model: BaseDbModel,
is_created: bool,
request: Request,
) -> None:
schema = self._create_schema if is_created else self._update_schema
validated_data = schema.model_validate(data)

update_dict = validated_data.model_dump(exclude_none=True)

for field_name, field_value in update_dict.items():
setattr(model, field_name, field_value)
setattr(model, field_name, field_value)
Loading