Skip to main content

Mysql toolkit

Project description

CommonDAO

A powerful, type-safe, and Pydantic-integrated async MySQL toolkit for Python.

Python MySQL Async Type Safe

CommonDAO is a lightweight, type-safe async MySQL toolkit designed to simplify database operations with a clean, intuitive API. It integrates seamlessly with Pydantic for robust data validation while providing a comprehensive set of tools for common database tasks.

✨ Features

  • Async/Await Support: Built on aiomysql for non-blocking database operations
  • Type Safety: Strong typing with Python's type hints and runtime type checking
  • Pydantic Integration: Seamless validation and transformation of database records to Pydantic models
  • SQL Injection Protection: Parameterized queries for secure database access
  • Comprehensive CRUD Operations: Simple methods for common database tasks
  • Raw SQL Support: Full control when you need it with parameterized raw SQL
  • Connection Pooling: Efficient database connection management
  • Minimal Boilerplate: Write less code while maintaining readability and control

🚀 Installation

pip install commondao

🔍 Quick Start

import asyncio
from commondao import connect
from commondao.annotation import TableId
from pydantic import BaseModel
from typing import Annotated

# Define your Pydantic model with TableId annotation
class User(BaseModel):
    id: Annotated[int, TableId('users')]  # First field with TableId is the primary key
    name: str
    email: str

async def main():
    # Connect to database
    config = {
        'host': 'localhost',
        'port': 3306,
        'user': 'root',
        'password': 'password',
        'db': 'testdb',
        'autocommit': True,
    }

    async with connect(**config) as db:
        # Insert a new user using Pydantic model
        user = User(id=1, name='John Doe', email='john@example.com')
        await db.insert(user)

        # Query the user by key with Pydantic model validation
        result = await db.get_by_key(User, key={'email': 'john@example.com'})
        if result:
            print(f"User: {result.name} ({result.email})")  # Output => User: John Doe (john@example.com)

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

📊 Core Operations

Connection

from commondao import connect

async with connect(
    host='localhost', 
    port=3306, 
    user='root', 
    password='password', 
    db='testdb'
) as db:
    # Your database operations here
    pass

Insert Data (with Pydantic Models)

from pydantic import BaseModel
from commondao.annotation import TableId
from typing import Annotated, Optional

class User(BaseModel):
    id: Annotated[Optional[int], TableId('users')] = None  # Auto-increment primary key
    name: str
    email: str

# Insert using Pydantic model (id will be auto-generated)
user = User(name='John', email='john@example.com')
await db.insert(user)
print(f"New user id: {db.lastrowid()}")  # Get the auto-generated id

# Insert with ignore option (skips duplicate key errors)
user2 = User(name='Jane', email='jane@example.com')
await db.insert(user2, ignore=True)

Update Data (with Pydantic Models)

# Update by primary key (id must be provided)
user = User(id=1, name='John Smith', email='john.smith@example.com')
await db.update_by_id(user)

# Update by custom key (partial update - only non-None fields)
user_update = User(name='Jane Doe', email='jane.doe@example.com')  # id can be None
await db.update_by_key(user_update, key={'email': 'john.smith@example.com'})

Delete Data

# Delete by key using entity class
await db.delete_by_key(User, key={'id': 1})

Query Data

# Get a single row by id
user = await db.get_by_id(User, key={'id': 1})

# Get a row by id or raise NotFoundError if not found
user = await db.get_by_id_or_fail(User, key={'id': 1})

# Get by custom key
user = await db.get_by_key(User, key={'email': 'john@example.com'})

# Get by key or raise NotFoundError if not found
user = await db.get_by_key_or_fail(User, key={'email': 'john@example.com'})

# Use with Pydantic models
from pydantic import BaseModel
from commondao.annotation import RawSql
from typing import Annotated

class UserModel(BaseModel):
    id: int
    name: str
    email: str
    full_name: Annotated[str, RawSql("CONCAT(first_name, ' ', last_name)")]

# Query with model validation
user = await db.select_one(
    "select * from users where id = :id",
    UserModel,
    {"id": 1}
)

# Query multiple rows
users = await db.select_all(
    "select * from users where status = :status",
    UserModel,
    {"status": "active"}
)

# Paginated queries
from commondao import Paged

result: Paged[UserModel] = await db.select_paged(
    "select * from users where status = :status",
    UserModel,
    {"status": "active"},
    size=10,
    offset=0
)

print(f"Total users: {result.total}")
print(f"Current page: {len(result.items)} users")

Raw SQL Execution

CommonDAO supports parameterized SQL queries using named parameters with the :parameter_name format for secure and readable queries.

execute_query - For SELECT operations

# Simple query without parameters
rows = await db.execute_query("SELECT * FROM users")

# Query with single parameter
user_rows = await db.execute_query(
    "SELECT * FROM users WHERE id = :user_id",
    {"user_id": 123}
)

# Query with multiple parameters
filtered_rows = await db.execute_query(
    "SELECT * FROM users WHERE name = :name AND age > :min_age",
    {"name": "John", "min_age": 18}
)

# Query with IN clause (using list parameter)
users_in_group = await db.execute_query(
    "SELECT * FROM users WHERE id IN :user_ids",
    {"user_ids": [1, 2, 3, 4]}
)

# Complex query with date filtering
recent_users = await db.execute_query(
    "SELECT * FROM users WHERE created_at > :date AND status = :status",
    {"date": "2023-01-01", "status": "active"}
)

execute_mutation - For INSERT, UPDATE, DELETE operations

# INSERT statement
affected = await db.execute_mutation(
    "INSERT INTO users (name, email, age) VALUES (:name, :email, :age)",
    {"name": "John", "email": "john@example.com", "age": 25}
)
print(f"Inserted {affected} rows")

# UPDATE statement
affected = await db.execute_mutation(
    "UPDATE users SET email = :new_email WHERE id = :user_id",
    {"new_email": "newemail@example.com", "user_id": 123}
)
print(f"Updated {affected} rows")

# DELETE statement
affected = await db.execute_mutation(
    "DELETE FROM users WHERE age < :min_age",
    {"min_age": 18}
)
print(f"Deleted {affected} rows")

# Multiple parameter UPDATE
affected = await db.execute_mutation(
    "UPDATE users SET name = :name, age = :age WHERE id = :id",
    {"name": "Jane", "age": 30, "id": 456}
)

# Bulk operations with loop
user_list = [
    {"name": "Alice", "email": "alice@example.com"},
    {"name": "Bob", "email": "bob@example.com"},
]

for user_data in user_list:
    affected = await db.execute_mutation(
        "INSERT INTO users (name, email) VALUES (:name, :email)",
        user_data
    )

Parameter Format Rules

  • Named Parameters: Use :parameter_name format in SQL
  • Dictionary Keys: Match parameter names without the colon prefix
  • Supported Types: str, int, float, bytes, datetime, date, time, timedelta, Decimal
  • Lists/Tuples: Supported for IN clauses in queries
  • None Values: Properly handled as SQL NULL
# Example with various data types
from datetime import datetime, date
from decimal import Decimal

result = await db.execute_query(
    """
    SELECT * FROM orders
    WHERE customer_id = :customer_id
    AND total >= :min_total
    AND created_date = :order_date
    AND status IN :valid_statuses
    """,
    {
        "customer_id": 123,
        "min_total": Decimal("99.99"),
        "order_date": date(2023, 12, 25),
        "valid_statuses": ["pending", "confirmed", "shipped"]
    }
)

Transactions

from commondao.annotation import TableId
from typing import Annotated, Optional

class Order(BaseModel):
    id: Annotated[Optional[int], TableId('orders')] = None
    customer_id: int
    total: float

class OrderItem(BaseModel):
    id: Annotated[Optional[int], TableId('order_items')] = None
    order_id: int
    product_id: int

async with connect(host='localhost', user='root', db='testdb') as db:
    # Start transaction (autocommit=False by default)
    order = Order(customer_id=1, total=99.99)
    await db.insert(order)
    order_id = db.lastrowid()  # Get the auto-generated order id

    item = OrderItem(order_id=order_id, product_id=42)
    await db.insert(item)

    # Commit the transaction
    await db.commit()

🔐 Type Safety

CommonDAO provides robust type checking to help prevent errors:

from commondao import is_row_dict, is_query_dict
from typing import Dict, Any
from datetime import datetime

# Valid row dict (for updates/inserts)
valid_data: Dict[str, Any] = {
    "id": 1,
    "name": "John",
    "created_at": datetime.now(),
}

# Check type safety
assert is_row_dict(valid_data)  # Type check passes

# Valid query dict (can contain lists/tuples for IN clauses)
valid_query: Dict[str, Any] = {
    "id": 1,
    "status": "active",
    "tags": ["admin", "user"],  # Lists are valid for query dicts
    "codes": (200, 201)  # Tuples are also valid
}

assert is_query_dict(valid_query)  # Type check passes

# Invalid row dict (contains a list)
invalid_data: Dict[str, Any] = {
    "id": 1,
    "tags": ["admin", "user"]  # Lists are not valid row values
}

assert not is_row_dict(invalid_data)  # Type check fails

📖 API Documentation

For complete API documentation, please see the docstrings in the code or visit our documentation website.

🧪 Testing

CommonDAO comes with comprehensive tests to ensure reliability:

# Install test dependencies
pip install -e ".[test]"

# Run tests
pytest tests

🤝 Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

📄 License

This project is licensed under the Apache License 2.0.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

commondao-1.1.0.tar.gz (38.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

commondao-1.1.0-py3-none-any.whl (18.1 kB view details)

Uploaded Python 3

File details

Details for the file commondao-1.1.0.tar.gz.

File metadata

  • Download URL: commondao-1.1.0.tar.gz
  • Upload date:
  • Size: 38.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.11.8

File hashes

Hashes for commondao-1.1.0.tar.gz
Algorithm Hash digest
SHA256 9303d0e7c8116f39861319246006f153f838bc444c1ef66bc7a919baeb9170fb
MD5 79e5f57aca1f863720dda5a9e8376a52
BLAKE2b-256 5a13e524cc8fbbb276b708c5617c660e559e2921949b148633a5b357a1a76a46

See more details on using hashes here.

File details

Details for the file commondao-1.1.0-py3-none-any.whl.

File metadata

  • Download URL: commondao-1.1.0-py3-none-any.whl
  • Upload date:
  • Size: 18.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.11.8

File hashes

Hashes for commondao-1.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 fc9f53e0bfcd6889e2726847fbf32a08e5951c09604816474c45949fd63ddd2e
MD5 e7587c3b917fd579b10bfa237544c714
BLAKE2b-256 91277b9c14f05a70595dcb69b3cb7e03dddfb25add36272c2e5a810ee8995c50

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page