Skip to main content

IronHammer

A production-ready Python library that provides a drop-in replacement for Anvil Data Tables API.

Features

  • 95%+ API Compatibility with Anvil Data Tables server-side API
  • Multiple Storage Backends: SQLite, PostgreSQL, MySQL, and in-memory
  • Flexible Media Storage: Filesystem, Cloudflare R2, Amazon S3, MinIO
  • Advanced Query Engine: Full support for complex queries with boolean expressions
  • Relationships: One-to-one, one-to-many, many-to-many with cascade delete
  • Transactions: ACID compliance with nested transactions and savepoints
  • Schema Management: Automatic migrations with versioning
  • Indexing: Unique and composite indexes for performance optimization
  • Serialization: JSON, CSV, YAML import/export
  • Permissions: Role-based access control with custom authentication hooks
  • Thread-Safe: Safe for concurrent access in multi-threaded environments
  • Type Hints: Full type annotations for IDE support

Installation

pip install IronHammer

Quick Start

from IronHammer.apptables import app_tables
import IronHammer.apptables.query as q

# Add a row
user = app_tables.users.add_row(
    name="John",
    age=25
)

# Get a row
user = app_tables.users.get(name="John")

# Search with queries
rows = app_tables.users.search(
    age=q.greater_than(18)
)

for row in rows:
    print(row["name"])

# Update a row
user["age"] = 30
user.update(active=True)

# Delete a row
user.delete()

Configuration

SQLite Backend (Default)

from IronHammer.apptables import app_tables

app_tables.configure(
    backend="sqlite",
    database_path="my_data.db"
)

PostgreSQL Backend

app_tables.configure(
    backend="postgresql",
    host="localhost",
    port=5432,
    database="mydb",
    user="postgres",
    password="secret"
)

MySQL Backend

app_tables.configure(
    backend="mysql",
    host="localhost",
    port=3306,
    database="mydb",
    user="root",
    password="secret"
)

In-Memory Backend

app_tables.configure(
    backend="memory"
)

Table Operations

# Create a table
app_tables.create_table("users", columns={
    "name": "string",
    "age": "number",
    "active": "bool"
})

# List tables
tables = app_tables.list_tables()

# Delete a table
app_tables.delete_table("users")

# Rename a table
app_tables.rename_table("users", "people")

Query API

import IronHammer.apptables.query as q

# Basic comparisons
app_tables.users.search(age=q.equal_to(25))
app_tables.users.search(age=q.not_equal_to(25))
app_tables.users.search(age=q.greater_than(18))
app_tables.users.search(age=q.greater_than_or_equal_to(18))
app_tables.users.search(age=q.less_than(65))
app_tables.users.search(age=q.less_than_or_equal_to(65))

# String operations
app_tables.users.search(name=q.contains("John"))
app_tables.users.search(name=q.startswith("J"))
app_tables.users.search(name=q.endswith("n"))
app_tables.users.search(name=q.like("J%"))
app_tables.users.search(name=q.ilike("j%"))
app_tables.users.search(name=q.regexp(r"^J.*n$"))

# Null checks
app_tables.users.search(email=q.is_none())
app_tables.users.search(email=q.not_none())

# List operations
app_tables.users.search(age=q.in_list([18, 25, 30]))
app_tables.users.search(age=q.not_in_list([10, 15, 20]))

# Range queries
app_tables.users.search(age=q.between(18, 65))

# Boolean expressions
app_tables.users.search(
    q.all_of(
        age=q.greater_than(18),
        active=True
    )
)

app_tables.users.search(
    q.any_of(
        name=q.contains("John"),
        name=q.contains("Jane")
    )
)

app_tables.users.search(
    q.not_(
        age=q.less_than(18)
    )
)

# Sorting
app_tables.users.search(
    age=q.greater_than(18),
    order_by="name",
    ascending=True
)

Transactions

from IronHammer.apptables import transaction

with transaction():
    user = app_tables.users.add_row(name="John", age=25)
    app_tables.orders.add_row(user_id=user.get_id(), total=100.0)
    # If an exception occurs, changes are rolled back automatically

Nested Transactions

with transaction() as outer:
    user = app_tables.users.add_row(name="John", age=25)
    
    with transaction(savepoint=True):
        app_tables.orders.add_row(user_id=user.get_id(), total=100.0)
        # Can rollback to this savepoint

Relationships

# Define relationships in schema
app_tables.create_table("users", columns={
    "name": "string",
    "email": "string"
})

app_tables.create_table("posts", columns={
    "title": "string",
    "content": "text",
    "user_id": "reference:users"
})

# Access related data
user = app_tables.users.get(name="John")
posts = user["posts"]  # One-to-many relationship

post = app_tables.posts.get(title="My Post")
author = post["user"]  # Many-to-one relationship

Media Storage

from IronHammer.apptables.media import Media

# Configure media storage
app_tables.configure_media(
    backend="filesystem",
    path="/path/to/media"
)

# Cloudflare R2
app_tables.configure_media(
    backend="r2",
    account_id="your-account-id",
    access_key="your-access-key",
    secret_key="your-secret-key",
    bucket="my-bucket"
)

# Amazon S3
app_tables.configure_media(
    backend="s3",
    access_key="your-access-key",
    secret_key="your-secret-key",
    bucket="my-bucket",
    region="us-east-1"
)

# Use media objects
media = Media.from_file("image.png")
app_tables.users.add_row(name="John", avatar=media)

Serialization

# Export to JSON
app_tables.users.export_to_json("users.json")

# Import from JSON
app_tables.users.import_from_json("users.json")

# Export to CSV
app_tables.users.export_to_csv("users.csv")

# Export to YAML
app_tables.users.export_to_yaml("users.yaml")

Bulk Operations

# Bulk insert
data = [
    {"name": "John", "age": 25},
    {"name": "Jane", "age": 30},
    {"name": "Bob", "age": 35}
]
app_tables.users.bulk_add(data)

# Bulk update
app_tables.users.bulk_update(
    ids=[id1, id2, id3],
    updates={"active": True}
)

# Bulk delete
app_tables.users.bulk_delete([id1, id2, id3])

Permissions

# Configure permissions
app_tables.configure_permissions({
    "users": {
        "read": ["admin", "user"],
        "write": ["admin"],
        "delete": ["admin"]
    }
})

# Custom authentication hook
def authenticate(role, table, operation):
    # Your custom authentication logic
    return True

app_tables.set_auth_hook(authenticate)

Compatibility Mode

For existing Anvil projects, use the compatibility package:

# Instead of:
# from anvil.tables import app_tables
# import anvil.tables.query as q

# Use:
from IronHammer.apptables.compat import app_tables
import IronHammer.apptables.compat.query as q

API Reference

See the docs directory for complete API documentation.

Testing

# Run all tests
pytest

# Run with coverage
pytest --cov=IronHammer --cov-report=html

# Run specific test file
pytest tests/test_table.py

Contributing

Contributions are welcome! Please see CONTRIBUTING.md for guidelines.

License

AGPL-3.0-or-later - see LICENSE for details.

Changelog

See CHANGELOG.md for version history.

Metadata

Release files for IronHammer 1.0.5

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for IronHammer 1.0.5
File Size Uploaded Provenance Actions
ironhammer-1.0.5.tar.gz 67.4 kB Without provenance Details for ironhammer-1.0.5.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for IronHammer 1.0.5
File Interpreter ABI Platform Actions
ironhammer-1.0.5-py3-none-any.whl Python 3 none any Details for ironhammer-1.0.5-py3-none-any.whl

Total release size: 144.4 kB

Release files / ironhammer-1.0.5.tar.gz

Download URL ironhammer-1.0.5.tar.gz
Size 67.4 kB
Tags Source
SHA-256 checksum
How to use checksums
5d2aad8302ad60e4307e98aebc96d482d83982912c8358a304bb65480f87c5d0
BLAKE2b-256 checksum
How to use checksums
9e1ac05b260e060cfe8561798f1e992b0615f62fd67f6afb2347faafda58caee
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release files / ironhammer-1.0.5-py3-none-any.whl

Download URL ironhammer-1.0.5-py3-none-any.whl
Size 77.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
a4e8ccb6f84b36d16d21b3ea2264ef3d7ef74c16270ea5402eca5113cd948c93
BLAKE2b-256 checksum
How to use checksums
a04029f3e1f0459fcdddf1243e0c5a98f8e8b2e6af577a5900d96db85780b8d4
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Security for IronHammer 1.0.5

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page