Skip to main content

FastAPI sizzles, Django dazzles. The best of both worlds in one framework.

Project description

PyPI Version Python Versions License Downloads

Cotlette 🚀

Cotlette is a modern, Django-inspired web framework built on top of FastAPI. It combines the best of both worlds: the speed and async power of FastAPI with the convenience of Django-like project structure, ORM, templates, and management commands.


Key Features

  • FastAPI Under the Hood: High-performance async web framework
  • Django-like Project Structure: Familiar and easy to organize
  • SQLAlchemy-powered ORM: Simple, Pythonic, and extensible with support for multiple databases
  • Alembic Migrations: Powerful database migration system
  • Jinja2 Templates: Powerful and flexible HTML rendering
  • Admin Panel: Built-in, customizable (inspired by Django admin)
  • Management Commands: CLI for project/app creation, server, shell, migrations, and more
  • Asynchronous Support: Full async views and endpoints with automatic context detection
  • Multi-Database Support: SQLite, PostgreSQL, MySQL, Oracle, and more
  • Extensible: Add your own apps, middleware, commands, and more

🎯 URL-Based Async/Sync Mode Detection

Cotlette uses URL-based mode detection to determine whether to use synchronous or asynchronous database operations. This approach provides explicit control and predictable behavior across all frameworks.

How It Works

The mode is determined by the presence of async drivers in your database URL:

# Synchronous mode (default)
DATABASES = {
    'default': {
        'ENGINE': 'cotlette.core.database.sqlalchemy',
        'URL': 'sqlite:///' + str(BASE_DIR / 'db.sqlite3'),  # Sync mode
    }
}

# Asynchronous mode  
DATABASES = {
    'default': {
        'ENGINE': 'cotlette.core.database.sqlalchemy',
        'URL': 'sqlite+aiosqlite:///' + str(BASE_DIR / 'db.sqlite3'),  # Async mode
    }
}

Supported Database Drivers

Synchronous Drivers:

  • SQLite: sqlite:///db.sqlite3
  • PostgreSQL: postgresql://user:pass@localhost/dbname
  • MySQL: mysql://user:pass@localhost/dbname
  • Oracle: oracle://user:pass@localhost/dbname

Asynchronous Drivers:

  • SQLite: sqlite+aiosqlite:///db.sqlite3
  • PostgreSQL: postgresql+asyncpg://user:pass@localhost/dbname
  • MySQL: mysql+aiomysql://user:pass@localhost/dbname

Benefits

  • Explicit Control: You choose the mode explicitly in settings
  • Predictable Behavior: No dependency on execution context
  • Framework Agnostic: Works with FastAPI, Django, Flask, or any framework
  • Easy Switching: Simply change the URL to switch modes
  • Clear Intent: URL clearly shows sync or async database drivers

Quick Start

1. Install Cotlette

pip install cotlette

2. Create a New Project

cotlette startproject myproject
cd myproject

3. Run the Development Server

cotlette runserver

Open your browser at http://127.0.0.1:8000


Example Projects

Cotlette comes with two complete example projects demonstrating both synchronous and asynchronous modes:

Synchronous Example (example/)

cd example
cotlette runserver
  • Uses sqlite:///db.sqlite3 (synchronous mode)
  • Direct iteration: for user in users:
  • Direct template passing: "users": users

Asynchronous Example (example_async/)

cd example_async
cotlette runserver
  • Uses sqlite+aiosqlite:///db.sqlite3 (asynchronous mode)
  • Async iteration: async for user in users:
  • Template execution: "users": await users.execute()

Project Configuration

Settings Structure

# config/settings.py
import pathlib

BASE_DIR = pathlib.Path(__file__).resolve().parent.parent

DATABASES = {
    'default': {
        'ENGINE': 'cotlette.core.database.sqlalchemy',
        'URL': 'sqlite:///' + str(BASE_DIR / 'db.sqlite3'),  # Sync mode
        # 'URL': 'sqlite+aiosqlite:///' + str(BASE_DIR / 'db.sqlite3'),  # Async mode
    }
}

INSTALLED_APPS = [
    'apps.home',
    'apps.admin',
    'apps.users',
    'apps.accounts',
    'apps.groups',
]

TEMPLATES = [
    {
        "BACKEND": "cotlette.template.backends.jinja2.Jinja2",
        "DIRS": ["templates"],
        "APP_DIRS": True
    },
]

SECRET_KEY = b'your-secret-key'
ALGORITHM = "HS256"

Screenshots

Home Page: Home Page

Login Page: Login Page

Admin Panel: Admin Page


Example: Defining a Model

from cotlette.core.database import Model, CharField, IntegerField, AutoField
from cotlette.core.database.fields.related import ForeignKeyField

class UserModel(Model):
    table = "users_usermodel"
    
    id = AutoField()  # Primary key
    name = CharField(max_length=50)
    age = IntegerField()
    email = CharField(max_length=100)
    password_hash = CharField(max_length=255)
    group = ForeignKeyField(to="GroupModel", related_name="users")
    organization = CharField(max_length=100)

Universal ORM Usage

Cotlette ORM automatically detects the mode based on your database URL configuration and works accordingly. No need for separate sync/async methods!

Basic CRUD Operations

# Create
article = Article.objects.create(title="Hello", content="World", author_id=1)

# Get single object
user = UserModel.objects.get(id=1)
user = UserModel.objects.get(email="john@example.com")

# Filter
users = UserModel.objects.filter(age__gte=25).execute()
users = UserModel.objects.filter(group_id=1).execute()

# Update
user.name = "Jane Doe"
user.save()

# Delete
user.delete()

# Count
count = UserModel.objects.count()
active_users = UserModel.objects.filter(age__gte=18).count()

# Exists
exists = UserModel.objects.filter(email="john@example.com").exists()

In Async Mode

When using async database URLs, the same methods automatically work asynchronously:

async def async_view():
    # Create
    article = await Article.objects.create(title="Hello", content="World", author_id=1)
    
    # Get
    user = await UserModel.objects.get(id=1)
    
    # Filter
    users = await UserModel.objects.filter(age__gte=25).execute()
    
    # Update
    user.name = "Jane Doe"
    await user.save()
    
    # Delete
    await user.delete()
    
    # Count
    count = await UserModel.objects.count()
    
    # Exists
    exists = await UserModel.objects.filter(email="john@example.com").exists()

Example: Creating Views

Synchronous View (with sync database URL)

from fastapi import APIRouter, Request
from cotlette.shortcuts import render_template
from starlette.authentication import requires
from .models import UserModel

router = APIRouter()

@router.get("/users", response_model=None)
@requires("user_auth")
async def users_view(request: Request):
    users = UserModel.objects.all()  # Direct iteration works in sync mode
    
    return render_template(request=request, template_name="admin/users.html", context={
        "users": users,  # Can be passed directly to template
        "config": request.app.settings,
    })

Asynchronous View (with async database URL)

from fastapi import APIRouter, Request
from cotlette.shortcuts import render_template
from starlette.authentication import requires
from .models import UserModel

router = APIRouter()

@router.get("/users", response_model=None)
@requires("user_auth")
async def users_view(request: Request):
    users = UserModel.objects.all()
    
    # Option 1: Async iteration for lazy loading
    async for user in users:
        print("user", user)
    
    # Option 2: Execute for template context
    return render_template(request=request, template_name="admin/users.html", context={
        "users": await users.execute(),  # Must execute for template
        "config": request.app.settings,
    })

Note: The same ORM methods work in both contexts! Cotlette automatically detects the mode based on database URL configuration.


Lazy Loading and Iteration

Cotlette supports lazy loading and iteration over QuerySet objects in both synchronous and asynchronous modes.

Synchronous Mode

# Method 1: Execute to get list
users = UserModel.objects.all().execute()
for user in users:
    print(user.name)

# Method 2: Direct iteration (lazy loading)
users = UserModel.objects.all()
for user in users:
    print(user.name)

# Method 3: Indexing
first_user = UserModel.objects.all().get_item(0)
first_two = UserModel.objects.all().get_item(slice(0, 2))

Asynchronous Mode

# Method 1: Execute to get list
users = await UserModel.objects.all().execute()
for user in users:
    print(user.name)

# Method 2: Async iteration (lazy loading)
users = UserModel.objects.all()
async for user in users:
    print(user.name)

# Method 3: Indexing
first_user = await UserModel.objects.all().get_item(0)
first_two = await UserModel.objects.all().get_item(slice(0, 2))

Template Usage

Synchronous Mode:

@router.get("/users")
async def users_view(request: Request):
    users = UserModel.objects.all()
    return render_template(request=request, template_name="users.html", context={
        "users": users,  # Can be passed directly
    })

Asynchronous Mode:

@router.get("/users")
async def users_view(request: Request):
    users = UserModel.objects.all()
    return render_template(request=request, template_name="users.html", context={
        "users": await users.execute(),  # Must execute for template
    })

Important Notes

  • In synchronous mode: Use regular for loops for iteration
  • In asynchronous mode: Use async for loops for iteration
  • The execute() method always returns a list that can be iterated normally
  • Direct iteration provides lazy loading - data is fetched only when needed
  • Indexing and slicing work in both modes with appropriate await calls
  • For Jinja2 templates in async mode, use await queryset.execute() to get a regular list

Advanced ORM Features

Query Chaining

# Complex queries with chaining (sync mode)
articles = Article.objects.filter(author_id=1).order_by('-id').execute()

# In async mode
articles = await Article.objects.filter(author_id=1).order_by('-id').execute()

Iteration and Lazy Loading

# Iterate over QuerySet results (sync mode)
for article in Article.objects.all().iter():
    print(article.title)

# Get specific items by index or slice (sync mode)
first_article = Article.objects.all().get_item(0)
recent_articles = Article.objects.all().get_item(slice(0, 10))

# In async mode
async for article in Article.objects.all().iter():
    print(article.title)

first_article = await Article.objects.all().get_item(0)
recent_articles = await Article.objects.all().get_item(slice(0, 10))

Bulk Operations

# Create multiple objects (sync mode)
articles = [
    Article(title="Article 1", content="Content 1", author_id=1),
    Article(title="Article 2", content="Content 2", author_id=1),
]

for article in articles:
    article.save()

# In async mode
for article in articles:
    await article.save()

Database Support

Cotlette supports multiple databases through SQLAlchemy with both sync and async drivers:

# SQLite (sync and async)
DATABASES = {
    'default': {
        'ENGINE': 'cotlette.core.database.sqlalchemy',
        'URL': 'sqlite:///db.sqlite3',  # Sync mode
        # 'URL': 'sqlite+aiosqlite:///db.sqlite3',  # Async mode
    }
}

# PostgreSQL (sync and async)
DATABASES = {
    'default': {
        'ENGINE': 'cotlette.core.database.sqlalchemy',
        'URL': 'postgresql://user:pass@localhost/dbname',  # Sync mode
        # 'URL': 'postgresql+asyncpg://user:pass@localhost/dbname',  # Async mode
    }
}

# MySQL (sync and async)
DATABASES = {
    'default': {
        'ENGINE': 'cotlette.core.database.sqlalchemy',
        'URL': 'mysql://user:pass@localhost/dbname',  # Sync mode
        # 'URL': 'mysql+aiomysql://user:pass@localhost/dbname',  # Async mode
    }
}

Async/Sync Mode Configuration

Cotlette determines the database mode (sync/async) based on the URL configuration in settings:

# Synchronous mode (default)
DATABASES = {
    'default': {
        'ENGINE': 'cotlette.core.database.sqlalchemy',
        'URL': 'sqlite:///db.sqlite3',  # Sync mode
    }
}

# Asynchronous mode
DATABASES = {
    'default': {
        'ENGINE': 'cotlette.core.database.sqlalchemy',
        'URL': 'sqlite+aiosqlite:///db.sqlite3',  # Async mode
    }
}

# PostgreSQL examples
DATABASES = {
    'default': {
        'ENGINE': 'cotlette.core.database.sqlalchemy',
        'URL': 'postgresql://user:pass@localhost/dbname',  # Sync mode
    }
}

DATABASES = {
    'default': {
        'ENGINE': 'cotlette.core.database.sqlalchemy',
        'URL': 'postgresql+asyncpg://user:pass@localhost/dbname',  # Async mode
    }
}

# MySQL examples
DATABASES = {
    'default': {
        'ENGINE': 'cotlette.core.database.sqlalchemy',
        'URL': 'mysql://user:pass@localhost/dbname',  # Sync mode
    }
}

DATABASES = {
    'default': {
        'ENGINE': 'cotlette.core.database.sqlalchemy',
        'URL': 'mysql+aiomysql://user:pass@localhost/dbname',  # Async mode
    }
}

Note: The mode is determined by the presence of async drivers in the URL. No automatic conversion - you explicitly choose sync or async mode in your settings!

Benefits of URL-based Mode Detection

  • Explicit Control: You choose the mode explicitly in settings, not based on execution context
  • Predictable Behavior: No dependency on whether you're in an async function or not
  • Framework Agnostic: Works consistently with FastAPI, Django, Flask, or any other framework
  • Easy Switching: Simply change the URL to switch between sync and async modes
  • Clear Intent: The URL clearly shows whether you're using sync or async database drivers

Supported Async Drivers

  • SQLite: sqlite+aiosqlite:// (requires aiosqlite package)
  • PostgreSQL: postgresql+asyncpg:// (requires asyncpg package)
  • MySQL: mysql+aiomysql:// (requires aiomysql package)
  • MySQL: mysql+asyncmy:// (requires asyncmy package)

Management Commands

Project Management

  • cotlette startproject <project_name> — Create a new Cotlette project directory structure
  • cotlette startapp <app_name> — Create a new Cotlette app directory structure

Development Server

  • cotlette runserver [addrport] — Start the development server
    • Optional arguments: --ipv6, --reload
    • Example: cotlette runserver 0.0.0.0:8000

Interactive Shell

  • cotlette shell — Interactive Python shell with auto-imports
    • Options: --no-startup, --no-imports, --interface, --command
    • Supports IPython, bpython, and standard Python

Database Management

  • cotlette makemigrations [--message] [--empty] — Create database migrations
    • Options: --message, --empty
    • Example: cotlette makemigrations --message "Add user model"
  • cotlette migrate [--revision] [--fake] — Apply database migrations
    • Options: --revision, --fake
    • Example: cotlette migrate --revision head

User Management

  • cotlette createsuperuser — Create a superuser account
    • Options: --username, --email, --noinput
    • Interactive mode for secure password input

Documentation


License

MIT License. See LICENSE for details.


Contributing

Pull requests and issues are welcome! See GitHub.

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

cotlette-0.0.29.tar.gz (36.3 MB view details)

Uploaded Source

Built Distribution

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

cotlette-0.0.29-py3-none-any.whl (36.7 MB view details)

Uploaded Python 3

File details

Details for the file cotlette-0.0.29.tar.gz.

File metadata

  • Download URL: cotlette-0.0.29.tar.gz
  • Upload date:
  • Size: 36.3 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.13.5

File hashes

Hashes for cotlette-0.0.29.tar.gz
Algorithm Hash digest
SHA256 a02bf1e9c3934ae7be7de53ba1c5e53540532d8de2da7e71ecb1f321746190ad
MD5 aaade7bf8fe142fe6397d35196888e42
BLAKE2b-256 297b10ba5d5c01dfb3188c2f7dd276434504c8fdb2aa8e0a590827bd672751b2

See more details on using hashes here.

File details

Details for the file cotlette-0.0.29-py3-none-any.whl.

File metadata

  • Download URL: cotlette-0.0.29-py3-none-any.whl
  • Upload date:
  • Size: 36.7 MB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.13.5

File hashes

Hashes for cotlette-0.0.29-py3-none-any.whl
Algorithm Hash digest
SHA256 2892c91971b47cfc5b661ac27f705777dab03d9ab247fa1ca3f96207f2b53947
MD5 239226ce5bc679598d88ae36a9d09423
BLAKE2b-256 54ae0c544dfb3d74256e74d7b197fd9a25c65f10a788f05bbd74dda09ca385cd

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