Metadata-Version: 2.1
Name: sqldantic
Version: 0.1.0
Summary: sqlalchemy and pydantic integration.
Home-page: https://github.com/aachurin/sqldantic
License: Unlicense
Author: Andrey Churin
Author-email: aachurin@gmail.com
Requires-Python: >=3.10,<4.0
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Science/Research
Classifier: Intended Audience :: System Administrators
Classifier: License :: OSI Approved
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Database
Classifier: Topic :: Database :: Database Engines/Servers
Classifier: Topic :: Internet
Classifier: Topic :: Internet :: WWW/HTTP
Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
Classifier: Typing :: Typed
Requires-Dist: pydantic (>=2.6.1,<3.0.0)
Requires-Dist: sqlalchemy (>=2.0.25,<3.0.0)
Project-URL: Repository, https://github.com/aachurin/sqldantic
Description-Content-Type: text/markdown

# sqldantic
SQLalchemy + pyDANTIC

### Example:
```python
from __future__ import annotations

import enum
import ipaddress 

from pydantic import BaseModel
from sqlalchemy import String, ForeignKey
from sqlalchemy.orm import Mapped
from sqlalchemy.dialects.postgresql import JSONB
from sqldantic import DeclarativeBase, Field, Relationship, Typed


class Base(DeclarativeBase):
    # see https://docs.sqlalchemy.org/en/20/orm/declarative_styles.html
    pass


class OS(str, enum.Enum):
    linux = "linux"
    windows = "windows"
    macos = "macos"
    

class Info(BaseModel):
    os: OS
    tags: set[str]
    

class ClusterBase(Base):
    name: Mapped[str]
    hosts: Mapped[list[Host]] = Relationship(back_populates="cluster")


class Cluster(ClusterBase, table=True):
    id: Mapped[int] = Field(primary_key=True)


class HostBase(Base):
    hostname: Mapped[str] = Field(index=True)
    address: Mapped[ipaddress.IPv4Address] = Field(Typed(String()))
    info: Mapped[Info] = Field(Typed(JSONB))
    cluster: Mapped[Cluster] = Relationship(back_populates="hosts")
    
    
class Host(HostBase, table=True):
    id: Mapped[int] = Field(primary_key=True)
    cluster_id: Mapped[int] = Field(ForeignKey("cluster.id"))

```

### Descripion
Any subclass of `DeclarativeBase` is Pydantic Model.

Any subclass of `DeclarativeBase` with `table=True` is Sqlalchemy Model.

Both `Mapped[...]` and "unmapped" formats are supported, but Sqlalchemy needs `Mapped` for mypy type checking, 
so `Mapped` is preferred. 

Sometimes it's impossible to use generic Sqlalchemy types:

```python
class Foo(Base, table=True):
    ...
    address: Mapped[ipaddress.IPv4Address]
    info: Mapped[Info]
    ...
```

Both `address` and `info` are not supported by Sqlalchemy. 

In such case, you can use special `Typed` type, which can handle any pydantic-supported types:

```python
class Foo(Base, table=True):
    ...
    address: Mapped[ipaddress.IPv4Address] = Field(Typed(String))
    # means validate value as an IPv4Address object, but store it as a String
    info: Mapped[Info] = Field(Typed(JSONB))
    # means validate value as an Info object, but store it as a JSONB
    ...
```

