Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SqlAlchemy dataclass support missing #737

Open
2 tasks done
Goradii opened this issue Apr 2, 2024 · 6 comments
Open
2 tasks done

SqlAlchemy dataclass support missing #737

Goradii opened this issue Apr 2, 2024 · 6 comments

Comments

@Goradii
Copy link

Goradii commented Apr 2, 2024

Checklist

  • The bug is reproducible against the latest release or master.
  • There are no similar issues or pull requests to fix it yet.

Describe the bug

If model mapped as dataclass (MappedAsDataclass), creating new record rises an error at sqladmin._queries:[194|206]
image

Steps to reproduce the bug

No response

Expected behavior

No response

Actual behavior

No response

Debugging material

No response

Environment

  • all

Additional context

No response

@Kumzy
Copy link

Kumzy commented May 6, 2024

Did you find a workaround for this?

@aminalaee
Copy link
Owner

can you please post a minimal code to reproduce this?

@Goradii
Copy link
Author

Goradii commented May 6, 2024

I have a bad hack only:

from sqladmin._queries import Query

async def insert_async_dataclass(self: Query, data: dict[str, Any], request: Request) -> Any:  # noqa: ANN401
    init = {k: v for k, v in data.items() if self.model_view.model.__dataclass_fields__[k].init}
    update = {k: v for k, v in data.items() if k not in init}
    obj = self.model_view.model(**init)

    async with self.model_view.session_maker(expire_on_commit=False) as session:
        await self.model_view.on_model_change(update, obj, True, request)
        obj = await self._set_attributes_async(session, obj, update)
        session.add(obj)
        await session.commit()
        await self.model_view.after_model_change(update, obj, True, request)
        return obj

Query._insert_async = insert_async_dataclass  # noqa: SLF001

@aminalaee
Copy link
Owner

Thanks, but I mean a sample code to reproduce the issue. How do you define your models that this happens?

@Kumzy
Copy link

Kumzy commented May 6, 2024

here is a minimal code example to reproduce it. I was using litestar for that but you can easily replace it with FastAPI

from litestar import Litestar
from sqladmin import ModelView
from sqladmin_litestar_plugin import SQLAdminPlugin
from sqlalchemy import Column, Integer, String
from sqlalchemy.ext.asyncio import create_async_engine
from sqlalchemy.orm import declarative_base
from advanced_alchemy.base import UUIDAuditBase
from sqlalchemy import String
from sqlalchemy.orm import Mapped, mapped_column
import uvicorn

from typing import Any, ClassVar

from uuid import UUID
from datetime import datetime, timezone
from sqlalchemy import DateTime

from sqlalchemy.orm import (
    DeclarativeBase,
    Mapped,
    mapped_column,
    MappedAsDataclass
)

from uuid_utils.compat import uuid4


class Base(MappedAsDataclass, DeclarativeBase, kw_only=True):
    type_annotation_map: ClassVar[dict[Any, Any]] = {
        datetime: DateTime(timezone=True),
    }

    id: Mapped[UUID] = mapped_column(default=uuid4, primary_key=True)

    """Date/time of instance creation."""
    created_at: Mapped[datetime] = mapped_column(
        default=lambda: datetime.now(timezone.utc),
    )

    updated_at: Mapped[datetime] = mapped_column(
        default=lambda: datetime.now(timezone.utc),
        onupdate=lambda: datetime.now(timezone.utc),
    )

engine = create_async_engine("postgresql+asyncpg://app:app@localhost:15432/laby_starter_dataset")


class User(Base):
    """User"""

    __tablename__ = "user"  # type: ignore[assignment]
    __table_args__ = {"comment": "Users of laby"}
    firstname: Mapped[str] = mapped_column(index=False, nullable=False)
    lastname: Mapped[str] = mapped_column(index=False, nullable=False)
    code: Mapped[str] = mapped_column(String(length=10), index=False, unique=True, nullable=False)
    active: Mapped[bool] = mapped_column(index=False, nullable=False, default=True)
    email_pro: Mapped[str | None] = mapped_column(String(length=255), index=False, nullable=False)


class Tier(Base):
    """Tier"""

    __tablename__ = "tier"  # type: ignore[assignment]
    __table_args__ = {"comment": "Tiers"}
    name: Mapped[str] = mapped_column(index=False, unique=True, nullable=False)
    description: Mapped[str | None] = mapped_column(String(length=255), index=False, nullable=True)
    active: Mapped[bool] = mapped_column(index=False, nullable=False, default=True)
    activity: Mapped[str | None] = mapped_column(String(length=255),index=False, nullable=True)


class TierAdmin(ModelView, model=Tier):

    name = "Tier"
    name_plural = "Tiers"
    column_list = [Tier.name, Tier.active, Tier.description]
    column_labels = {Tier.name: "Name", Tier.active: "Status", Tier.description: "Description"}
    icon = 'fa-solid fa-building'
    column_details_exclude_list = [Tier.created_at, Tier.updated_at]
    form_columns = [Tier.id, Tier.name, Tier.active, Tier.description]


class UserAdmin(ModelView, model=User):

    name = "User"
    name_plural = "Users"
    column_list = [User.firstname, User.lastname]
    column_labels = {User.firstname: "Firstname", User.lastname: "Lastname"}
    icon = 'fa-solid fa-user'
    column_details_exclude_list = [User.created_at, User.updated_at]
    form_columns = [User.firstname, User.lastname, User.active, User.code, User.email_pro]


async def on_startup() -> None:
    async with engine.begin() as conn:
        await conn.run_sync(Base.metadata.create_all)  # Create tables


admin = SQLAdminPlugin(views=[UserAdmin, TierAdmin], engine=engine)
app = Litestar(plugins=[admin], on_startup=[on_startup])

uvicorn.run(app)

@aminalaee
Copy link
Owner

The problem is how we are creating a model instance without any arguments here and setting the column/relationship attributes on that object.

It is tricky because when dealing with columns you can just pass kwargs to the object instantiation but for the relationships you have to load them beforehand.

A workaround is to pass init=False to your required model fields, but I guess that's not the desired outcome.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants