Metadata-Version: 2.4
Name: djanorm
Version: 3.0.0
Summary: A Django-like ORM with synchronous and asynchronous support
Project-URL: Homepage, https://rroblf01.github.io/d-orm/
Project-URL: Repository, https://github.com/rroblf01/d-orm/
Author-email: Ricardo Robles <ricardo.r.f@hotmail.com>
License: MIT
License-File: LICENSE
Keywords: asgi,async,auth,database,django,embeddings,fastapi,libsql,litestar,mariadb,migrations,mysql,orm,pgvector,postgresql,sql,sqlite,tenants,turso,vector
Classifier: Development Status :: 5 - Production/Stable
Classifier: Framework :: AsyncIO
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Database
Classifier: Typing :: Typed
Requires-Python: >=3.11
Provides-Extra: all
Requires-Dist: aiosqlite<0.23,>=0.22.1; extra == 'all'
Requires-Dist: boto3>=1.43.1; extra == 'all'
Requires-Dist: cryptography>=42.0; extra == 'all'
Requires-Dist: fastapi>=0.115; extra == 'all'
Requires-Dist: httpx>=0.27; extra == 'all'
Requires-Dist: mkdocs-material>=9.7.6; extra == 'all'
Requires-Dist: mkdocs-static-i18n>=1.3.1; extra == 'all'
Requires-Dist: mkdocs>=1.6.1; extra == 'all'
Requires-Dist: mkdocstrings[python]>=1.0.4; extra == 'all'
Requires-Dist: opentelemetry-api>=1.41.1; extra == 'all'
Requires-Dist: opentelemetry-sdk>=1.41.1; extra == 'all'
Requires-Dist: pgvector>=0.4.2; extra == 'all'
Requires-Dist: pillow>=10.0; extra == 'all'
Requires-Dist: pillow>=12.2.0; extra == 'all'
Requires-Dist: psycopg[binary,pool]>=3.3.3; extra == 'all'
Requires-Dist: pydantic>=2.0; extra == 'all'
Requires-Dist: pytest-asyncio>=1.3.0; extra == 'all'
Requires-Dist: pytest-cov>=7.1.0; extra == 'all'
Requires-Dist: pytest-timeout>=2.4.0; extra == 'all'
Requires-Dist: pytest-xdist>=3.8.0; extra == 'all'
Requires-Dist: pytest>=9.0.3; extra == 'all'
Requires-Dist: python-multipart>=0.0.9; extra == 'all'
Requires-Dist: pyturso>=0.5.1; extra == 'all'
Requires-Dist: redis>=7.4.0; extra == 'all'
Requires-Dist: ruff>=0.15.12; extra == 'all'
Requires-Dist: sqlite-vec>=0.1.9; extra == 'all'
Requires-Dist: testcontainers[minio,postgres]>=4.14.2; extra == 'all'
Requires-Dist: ty>=0.0.33; extra == 'all'
Provides-Extra: dev
Requires-Dist: boto3>=1.43.1; extra == 'dev'
Requires-Dist: fastapi>=0.115; extra == 'dev'
Requires-Dist: httpx>=0.27; extra == 'dev'
Requires-Dist: opentelemetry-api>=1.41.1; extra == 'dev'
Requires-Dist: opentelemetry-sdk>=1.41.1; extra == 'dev'
Requires-Dist: pillow>=10.0; extra == 'dev'
Requires-Dist: pytest-asyncio>=1.3.0; extra == 'dev'
Requires-Dist: pytest-cov>=7.1.0; extra == 'dev'
Requires-Dist: pytest-timeout>=2.4.0; extra == 'dev'
Requires-Dist: pytest-xdist>=3.8.0; extra == 'dev'
Requires-Dist: pytest>=9.0.3; extra == 'dev'
Requires-Dist: python-multipart>=0.0.9; extra == 'dev'
Requires-Dist: ruff>=0.15.12; extra == 'dev'
Requires-Dist: testcontainers[minio,postgres]>=4.14.2; extra == 'dev'
Requires-Dist: ty>=0.0.33; extra == 'dev'
Provides-Extra: docs
Requires-Dist: mkdocs-material>=9.7.6; extra == 'docs'
Requires-Dist: mkdocs-static-i18n>=1.3.1; extra == 'docs'
Requires-Dist: mkdocs>=1.6.1; extra == 'docs'
Requires-Dist: mkdocstrings[python]>=1.0.4; extra == 'docs'
Provides-Extra: encrypted
Requires-Dist: cryptography>=42.0; extra == 'encrypted'
Provides-Extra: image
Requires-Dist: pillow>=12.2.0; extra == 'image'
Provides-Extra: libsql
Requires-Dist: pyturso>=0.5.1; extra == 'libsql'
Provides-Extra: mysql
Provides-Extra: otel
Requires-Dist: opentelemetry-api>=1.41.1; extra == 'otel'
Requires-Dist: opentelemetry-sdk>=1.41.1; extra == 'otel'
Provides-Extra: pgvector
Requires-Dist: pgvector>=0.4.2; extra == 'pgvector'
Provides-Extra: postgresql
Requires-Dist: psycopg[binary,pool]>=3.3.3; extra == 'postgresql'
Provides-Extra: pydantic
Requires-Dist: pydantic>=2.0; extra == 'pydantic'
Provides-Extra: redis
Requires-Dist: redis>=7.4.0; extra == 'redis'
Provides-Extra: s3
Requires-Dist: boto3>=1.43.1; extra == 's3'
Provides-Extra: sqlite
Requires-Dist: aiosqlite<0.23,>=0.22.1; extra == 'sqlite'
Provides-Extra: sqlite-vec
Requires-Dist: sqlite-vec>=0.1.9; extra == 'sqlite-vec'
Provides-Extra: vector
Requires-Dist: pgvector>=0.4.2; extra == 'vector'
Requires-Dist: sqlite-vec>=0.1.9; extra == 'vector'
Description-Content-Type: text/markdown

# djanorm

A Django-inspired ORM for Python with full **synchronous and asynchronous** support. The same API you know from Django, without depending on the full framework.

Works with **SQLite**, **PostgreSQL** and **libsql / Turso**. Ships with migrations + linter, atomic transactions, signals, validation, relationship loading (`select_related` / `prefetch_related`), aggregations, DB functions, async-native ORM path, queryset & row caching, and Pydantic interop — all with real static typing (`Field[T]`).

## What's new in 3.1

- **`settings.USE_TZ = True`** — Django ≥4-compatible timezone-aware datetimes (UTC normalisation on insert, `TIMESTAMP WITH TIME ZONE` on PG).
- **`Meta.proxy = True`** — proxy models share the parent's table; autodetector skips them so `makemigrations` doesn't emit a phantom `CreateModel`.
- **`QuerySet.dates(field, kind)` / `datetimes(...)`** — distinct truncated values for archive listings.
- **`dorm migrate --fake` / `--fake-initial`** — record migrations as applied without running operations. Ideal for adopting dorm against a legacy schema.
- **JSONField PG operators** — `__contained_by`, `__has_key`, `__has_keys`, `__has_any_keys`, `__overlap`, `__len`. Same spelling as Django's `contrib.postgres`.
- **`Field.deconstruct()`** — base-class implementation for migration serialisation. Custom field subclasses get it for free.
- **`Model.from_db(db, field_names, values)`** — Django-parity hydration hook; stamps `_state.db` with the alias.
- **`dorm.transaction.savepoint()` / `savepoint_commit()` / `savepoint_rollback()`** — manual savepoints inside `atomic()`.
- **`dorm.contrib.auth.tokens`** — stateless HMAC-signed reset tokens for password-reset / email-verification flows.
- **`Meta.permissions = [...]`** + **`sync_permissions()`** — declare custom permissions, materialise into `auth_permission`.
- **`dorm.contrib.tenants`** — `TenantContext` / `aTenantContext` for PostgreSQL `search_path` switching.
- **MySQL / MariaDB scaffold** — `ENGINE = "mysql"` parses through `parse_database_url`; the connection wrapper raises `ImproperlyConfigured` pointing at v3.1 for the full implementation.
- **MySQL / MariaDB vector support** — `VectorField` returns `VECTOR(N)` and distance expressions compile to `VEC_DISTANCE_EUCLIDEAN` / `VEC_DISTANCE_COSINE`.

## What's new in 3.0

- **`dorm.contrib.auth`** — `User` / `Group` / `Permission` with stdlib PBKDF2 hashing. Same shape as Django, no `passlib` dependency.
- **`dorm.contrib.encrypted`** — `EncryptedCharField` / `EncryptedTextField` (AES-GCM with key rotation; `pip install 'djanorm[encrypted]'`).
- **`dorm.contrib.asyncguard`** — surfaces sync ORM calls inside an event loop as warnings or exceptions.
- **`dorm.contrib.querylog`** + **`dorm.contrib.querycount`** — request-scoped collectors for SQL traffic and N+1 guards.
- **`dorm.contrib.prometheus`** — stdlib-only metrics exposer for the `/metrics` endpoint.
- **`dorm lint-migrations`** — pre-merge gate that flags online-deploy footguns (full-table backfills, missing `concurrently=True`, irreversible `RunPython`).
- **`LocMemCache`** + **`Manager.cache_get(pk=…)`** / `cache_get_many(pks=[…])` — in-process LRU + single-row caching that piggy-backs on the same invalidation signal as queryset cache.
- **Sticky read-after-write window** for the DB router — no stale replica reads after a write on the same request.
- **`settings.SLOW_QUERY_MS`** — slow-query WARNING; `settings.RETRY_ATTEMPTS` / `RETRY_BACKOFF` — transient-error retry knobs (resolution: explicit setting > env var > default).
- **`dorm.test.assertNumQueries`** + `assertMaxQueries` — context-manager and decorator forms (sync + async).
- **Async-aware `dorm shell`** — top-level `await` works in the stdlib REPL fallback.
- **Math / string DB functions** — `Power`, `Sqrt`, `Mod`, `Sign`, `Ceil`, `Floor`, `Log`, `Ln`, `Exp`, `Random`, `Trim`, `LTrim`, `RTrim`, `NullIf`.

Full notes in [CHANGELOG.md](CHANGELOG.md). **Zero breaking changes vs 2.5** — every addition is opt-in or zero-cost when unused.

## Installation

```bash
# SQLite
pip install "djanorm[sqlite]"

# PostgreSQL
pip install "djanorm[postgresql]"

# libsql / Turso (local, embedded replica or remote)
pip install "djanorm[libsql]"

# Optional extras
pip install "djanorm[redis]"      # queryset + row cache backend
pip install "djanorm[encrypted]"  # AES-GCM EncryptedCharField/TextField
pip install "djanorm[pydantic]"   # FastAPI-friendly DormSchema
```

## Quick start

### 1. Scaffold a project

```bash
dorm init blog
```

That creates:

- `settings.py` — uncomment the `DATABASES` block matching your backend.
- `blog/` — an app package with an empty `models.py`.

A minimal `settings.py` looks like:

```python
DATABASES = {
    "default": {
        "ENGINE": "sqlite",
        "NAME": "db.sqlite3",
    },
}
INSTALLED_APPS = ["blog"]
```

### 2. Define a model

```python
# blog/models.py
import dorm


class Author(dorm.Model):
    name = dorm.CharField(max_length=100)
    email = dorm.EmailField(unique=True)
    is_active = dorm.BooleanField(default=True)


class Post(dorm.Model):
    title = dorm.CharField(max_length=200)
    body = dorm.TextField()
    author = dorm.ForeignKey(Author, on_delete=dorm.CASCADE)
    published_at = dorm.DateTimeField(null=True, blank=True)

    class Meta:
        ordering = ["-published_at"]
```

### 3. Generate and apply migrations

```bash
dorm makemigrations blog
dorm migrate
```

### 4. Use it

Open a shell with `dorm shell` (IPython auto-detected) or import
the models from your own script.

```python
from blog.models import Author, Post

# Create
alice = Author.objects.create(name="Alice", email="alice@example.com")
post = Post.objects.create(
    title="Hello world",
    body="First post body.",
    author=alice,
)

# Bulk create
Post.objects.bulk_create([
    Post(title=f"Draft {i}", body="...", author=alice)
    for i in range(5)
])

# Filter / exclude / Q / F
from dorm import Q, F

active_authors = Author.objects.filter(is_active=True)
some_posts = Post.objects.filter(
    Q(title__icontains="hello") | Q(title__startswith="Draft")
).exclude(published_at__isnull=True)

# Lookups across relations
alices_posts = Post.objects.filter(author__name="Alice")

# select_related / prefetch_related to dodge N+1
for post in Post.objects.select_related("author"):
    print(post.author.name, post.title)   # 1 query, JOIN

# Get one
post = Post.objects.get(pk=1)

# Update — single instance
post.title = "Renamed"
post.save()

# Update — bulk via queryset
Post.objects.filter(author=alice).update(title=F("title") + " (by Alice)")

# Delete — single instance
post.delete()

# Delete — bulk
Post.objects.filter(published_at__isnull=True).delete()
```

### Async API (same names with `a` prefix)

```python
from blog.models import Author, Post

async def main():
    alice = await Author.objects.acreate(name="Alice", email="a@x.com")
    post = await Post.objects.acreate(title="Hi", body="...", author=alice)

    async for p in Post.objects.filter(author=alice):
        print(p.title)

    await Post.objects.filter(pk=post.pk).aupdate(title="Hi!")
    await post.adelete()
```

### Atomic transactions

```python
from dorm import transaction

with transaction.atomic():
    alice = Author.objects.create(name="Alice", email="a@x.com")
    Post.objects.create(title="t", body="b", author=alice)
    # any exception here rolls back both inserts
```

## Documentation

The full documentation, tutorials and API reference are published at:

**https://rroblf01.github.io/d-orm/**

You will find the getting-started guide, complete examples, the API reference and production deployment notes there.

## Contributing

Everyone is welcome to get involved! If you want to suggest changes, propose improvements or discuss the direction of the project, open an issue or a pull request on this repository. Discussions, ideas and critiques are very welcome.

## License

See [LICENSE](LICENSE).
