Metadata-Version: 2.5
Name: simple-dao-core
Version: 0.1.1
Summary: Reusable async DAO core for SQLAlchemy
License-Expression: MIT
License-File: LICENSE
Keywords: async,asyncio,crud,dao,database,orm,sqlalchemy
Classifier: Development Status :: 3 - Alpha
Classifier: Framework :: AsyncIO
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Database
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.10
Requires-Dist: sqlalchemy[asyncio]<3.0,>=2.0
Provides-Extra: dev
Requires-Dist: aiosqlite>=0.20; extra == 'dev'
Requires-Dist: build>=1.2; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: twine>=5.0; extra == 'dev'
Description-Content-Type: text/markdown

# Simple DAO Core

**Simple DAO Core** — небольшая библиотека для упрощения работы с базой данных через асинхронный SQLAlchemy ORM.

Библиотека предоставляет базовый DAO-класс (`DAOCore`), который реализует основные CRUD-операции и позволяет создавать DAO для разных моделей без дублирования кода.

## Возможности

- Асинхронное взаимодействие с базой данных.
- Создание записей (`add`).
- Получение одной записи (`find_one`).
- Получение списка записей с фильтрацией (`find_all`).
- Получение всех записей (`get_all_records`).
- Обновление записей (`update`).
- Удаление записей (`delete`).
- Повторное использование базового DAO для разных моделей.

## Установка

```bash
pip install simple-dao-core
```

Для работы также понадобится SQLAlchemy и асинхронный драйвер, подходящий для используемой базы данных.

Например, для SQLite:

```bash
pip install aiosqlite
```

Для PostgreSQL:

```bash
pip install asyncpg
```

## Структура проекта

Пример структуры приложения:

```text
my_project/
├── database/
│   ├── __init__.py
│   └── database_core.py
├── models/
│   ├── __init__.py
│   └── user.py
├── dao/
│   ├── __init__.py
│   └── user.py
└── main.py
```

## 1. Настройка базы данных

Создадим файл `database/database_core.py`.

В нём будут находиться движок SQLAlchemy, фабрика асинхронных сессий и базовый класс для моделей.

```python
from datetime import datetime

from sqlalchemy import func
from sqlalchemy.ext.asyncio import (
    create_async_engine,
    AsyncAttrs,
    async_sessionmaker,
    AsyncSession,
)
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column


database_url = "sqlite+aiosqlite:///./database.db"

engine = create_async_engine(
    url=database_url,
    echo=False,
)

async_session_maker = async_sessionmaker(
    engine,
    class_=AsyncSession,
    expire_on_commit=False,
)


class BaseCore(AsyncAttrs, DeclarativeBase):
    created_at: Mapped[datetime] = mapped_column(
        server_default=func.now(),
    )

    updated_at: Mapped[datetime] = mapped_column(
        server_default=func.now(),
        onupdate=func.now(),
    )


async def init_db():
    async with engine.begin() as conn:
        await conn.run_sync(BaseCore.metadata.create_all)


async def close_db():
    await engine.dispose()
```

`BaseCore` автоматически добавляет к моделям поля `created_at` и `updated_at`.

`async_session_maker` будет использоваться DAO-классами для открытия сессий.

> Для PostgreSQL строка подключения может выглядеть так: `postgresql+asyncpg://user:password@localhost:5432/my_database`. В реальном проекте храните параметры подключения в переменных окружения, а не в исходном коде.

## 2. Создание модели

Создадим файл `models/user.py`.

```python
from sqlalchemy import String
from sqlalchemy.orm import Mapped, mapped_column

from database.database_core import BaseCore


class User(BaseCore):
    __tablename__ = "users"

    id: Mapped[int] = mapped_column(
        primary_key=True,
        index=True,
    )

    name: Mapped[str] = mapped_column(
        String(100),
        nullable=False,
    )

    email: Mapped[str] = mapped_column(
        String(255),
        unique=True,
        nullable=False,
    )
```

Теперь модель `User` содержит следующие поля:

| Поле | Описание |
|---|---|
| `id` | Первичный ключ |
| `name` | Имя пользователя |
| `email` | Уникальный адрес электронной почты |
| `created_at` | Время создания записи |
| `updated_at` | Время последнего обновления |

## 3. Создание DAO

Создадим файл `dao/user.py`.

```python
from simple_dao_core import DAOCore

from database.database_core import async_session_maker
from models.user import User


class UserDAO(DAOCore):
    model = User
    session_factory = async_session_maker
```

На этом настройка завершена.

`UserDAO` наследует все методы `DAOCore`. Не нужно повторно реализовывать создание, поиск, обновление и удаление записей.

Для другой модели можно создать отдельный DAO, указав соответствующие `model` и `session_factory`.

## 4. Примеры использования

Все методы DAO являются асинхронными, поэтому вызываются с помощью `await` внутри асинхронной функции.

### Создание записи — `add()`

```python
user = await UserDAO.add(
    name="Alex",
    email="alex@example.com",
)

print(user.id)
print(user.name)
print(user.email)
```

Метод создаёт новую запись в таблице `users` и возвращает экземпляр модели.

### Получение одной записи — `find_one()`

```python
user = await UserDAO.find_one(id=1)

if user:
    print(user.name)
    print(user.email)
else:
    print("Пользователь не найден")
```

Можно передавать несколько параметров фильтрации:

```python
user = await UserDAO.find_one(
    name="Alex",
    email="alex@example.com",
)
```

Метод возвращает найденный объект или `None`. Если запрос соответствует нескольким записям, `scalar_one_or_none()` выбросит исключение, поэтому этот метод лучше использовать с уникальными критериями поиска.

### Получение списка записей — `find_all()`

```python
users = await UserDAO.find_all()

for user in users:
    print(user.id, user.name, user.email)
```

С фильтрацией:

```python
users = await UserDAO.find_all(name="Alex")
```

Метод возвращает список объектов, соответствующих переданным критериям. Если совпадений нет, возвращается пустой список.

### Получение всех записей — `get_all_records()`

```python
users = await UserDAO.get_all_records()

for user in users:
    print(user.name)
```

Этот метод возвращает все записи модели без фильтрации.

### Обновление записи — `update()`

```python
updated_count = await UserDAO.update(
    {"id": 1},
    name="Alexander",
    email="alexander@example.com",
)

print(f"Обновлено записей: {updated_count}")
```

Первый аргумент — словарь с критериями поиска записей. Именованные аргументы после него — новые значения полей.

Можно обновить несколько записей:

```python
updated_count = await UserDAO.update(
    {"name": "Alex"},
    name="Alexander",
)
```

**Важно:** передавайте достаточно точные критерии фильтрации, поскольку запрос может обновить несколько строк.

### Удаление записи — `delete()`

```python
deleted_count = await UserDAO.delete(id=1)

print(f"Удалено записей: {deleted_count}")
```

Можно использовать несколько критериев:

```python
deleted_count = await UserDAO.delete(
    name="Alex",
    email="alex@example.com",
)
```

Метод возвращает количество затронутых строк, если драйвер и диалект базы данных предоставляют достоверное значение `rowcount`.

## 5. Полный пример запуска

Создадим файл `main.py`.

```python
import asyncio

from database.database_core import init_db, close_db
from dao.user import UserDAO


async def main():
    await init_db()

    try:
        # Создание пользователя
        user = await UserDAO.add(
            name="Alex",
            email="alex@example.com",
        )
        print("Создан пользователь:", user.id)

        # Поиск пользователя
        found_user = await UserDAO.find_one(
            email="alex@example.com",
        )
        print("Найден:", found_user.name if found_user else None)

        # Получение всех пользователей
        users = await UserDAO.find_all()

        for item in users:
            print(item.id, item.name, item.email)

        # Обновление пользователя
        updated_count = await UserDAO.update(
            {"id": user.id},
            name="Alexander",
        )
        print("Обновлено:", updated_count)

        # Удаление пользователя
        deleted_count = await UserDAO.delete(id=user.id)
        print("Удалено:", deleted_count)

    finally:
        await close_db()


if __name__ == "__main__":
    asyncio.run(main())
```

Запуск приложения:

```bash
python main.py
```

Этот пример создаёт таблицы, выполняет CRUD-операции и закрывает соединения с базой данных после завершения работы.

Если в проекте используется Alembic, создание таблиц обычно следует выполнять через миграции, а не через `create_all()`.

## 6. Поддерживаемые методы

| Метод | Назначение | Возвращаемое значение |
|---|---|---|
| `add(**values)` | Создать запись | Экземпляр модели |
| `find_one(**filter_by)` | Найти одну запись | Модель или `None` |
| `find_all(**filter_by)` | Найти записи по фильтру | Список моделей |
| `get_all_records()` | Получить все записи | Список моделей |
| `update(filter_by, **values)` | Обновить записи | Количество затронутых строк |
| `delete(**filter_by)` | Удалить записи | Количество затронутых строк |

## 7. Обработка ошибок

Ошибки SQLAlchemy могут возникать при нарушении ограничений базы данных, потере соединения или некорректном выполнении запроса.

Например:

```python
from sqlalchemy.exc import SQLAlchemyError


async def create_user():
    try:
        user = await UserDAO.add(
            name="Alex",
            email="alex@example.com",
        )
        return user

    except SQLAlchemyError:
        # Здесь можно записать ошибку в лог
        raise
```

При обработке ошибок учитывайте, что транзакции и откат должны быть корректно реализованы внутри библиотеки. Не следует скрывать исключения, если вызывающий код должен знать об ошибке.

## Лицензия

Проект распространяется под лицензией MIT. Подробности приведены в файле `LICENSE`.

## Вклад в проект

Предложения по улучшению, сообщения об ошибках и pull request приветствуются.

Если библиотека оказалась полезной, поставьте проекту звезду на GitHub.
