Skip to main content

Multi-Backend Object-Document Mapper (ODM) for ClickHouse, SurrealDB, and more - Quantum-Powered Database Engine

Project description

โš›๏ธ QuantumEngine โšก

A powerful, multi-backend Object-Document Mapper (ODM) for Python

Unified API for both transactional and analytical databases
Supporting SurrealDB (graph/document) and ClickHouse (columnar analytical) with a single, consistent interface

Documentation PyPI version Python versions License Tests Downloads


๐Ÿ“ฆ Installation

QuantumEngine uses a modular installation system - install only the backends you need:

# Core package only (lightweight)
pip install quantumengine

# With ClickHouse support
pip install quantumengine[clickhouse]

# With SurrealDB support  
pip install quantumengine[surrealdb]

# With Redis support
pip install quantumengine[redis]

# With multiple backends
pip install quantumengine[clickhouse,surrealdb,redis]

# Everything (all backends + dev tools)
pip install quantumengine[all]

See INSTALLATION.md for detailed installation options and troubleshooting.

๐Ÿ†• What's New in v0.4.0

  • ๐Ÿ”— Connection Pooling: Advanced connection pooling across all backends (SurrealDB, ClickHouse, Redis) with configurable pool sizes, health checks, and idle timeouts
  • ๐ŸŽฏ Standardized Connection API: Consistent create_connection() interface across all backends
  • ๐Ÿ”„ Sync API Support: Full synchronous API with async_mode=False and sync method variants
  • ๐Ÿ›ก๏ธ Enhanced Parameter Validation: Clear error messages with suggested fixes for connection parameters
  • โšก Redis Backend: Complete Redis/Dragonfly support for caching and real-time data
  • ๐Ÿ“Š ClickHouse Connection Pooling: Full connection pooling implementation for ClickHouse backend with automatic resource management
  • ๐Ÿ“š Comprehensive Documentation: Complete API guide and migration documentation
  • ๐Ÿ”ง Backward Compatibility: Seamless migration from legacy connection APIs

๐Ÿš€ Quick Start

import os
from quantumengine import Document, StringField, IntField, create_connection

# Define a document model
class User(Document):
    username = StringField(required=True)
    email = StringField(required=True)
    age = IntField(min_value=0)
    
    class Meta:
        collection = "users"
        backend = "surrealdb"  # or "clickhouse", "redis"

# Create connection with standardized API
connection = create_connection(
    backend="surrealdb",  # Explicitly specify backend
    url="ws://localhost:8000/rpc",
    namespace="test_ns",
    database="test_db",
    username=os.environ.get("SURREALDB_USER", "root"),
    password=os.environ.get("SURREALDB_PASS", "root"),
    make_default=True
)

# Use async context manager
async with connection:
    # Create table
    await User.create_table()

    # CRUD operations
    user = User(username="alice", email="alice@example.com", age=25)
    await user.save()

    users = await User.objects.filter(age__gt=18).all()
    await User.objects.filter(username="alice").update(age=26)

๐Ÿ—๏ธ Architecture

QuantumEngine provides a unified interface for multiple database backends:

  • SurrealDB Backend: Graph database with native support for relations, transactions, and complex queries
  • ClickHouse Backend: High-performance columnar database optimized for analytics and time-series data
  • Redis Backend: In-memory database for caching, sessions, and real-time data storage

Multi-Backend Support with Connection Pooling

from quantumengine.connection import PoolConfig

# Configure connection pooling
pool_config = PoolConfig(
    max_size=20,
    min_size=5,
    max_idle_time=300  # seconds
)

# SurrealDB connection for transactional data
user_connection = create_connection(
    backend="surrealdb",
    url="ws://localhost:8000/rpc",
    username="root",
    password="root",
    namespace="production",
    database="users",
    pool_config=pool_config,
    make_default=True
)

# ClickHouse connection for analytics
analytics_connection = create_connection(
    backend="clickhouse",
    url="localhost",  # Standardized parameter
    username="default",
    password="",
    port=8123,  # Auto-set if not provided
    secure=False,  # Auto-set if not provided
    pool_config=pool_config
)

# Redis connection for caching
cache_connection = create_connection(
    backend="redis",
    url="localhost",
    port=6379,  # Auto-set if not provided
    password="cache_password"
)

class User(Document):
    class Meta:
        backend = "surrealdb"

class AnalyticsEvent(Document):
    class Meta:
        backend = "clickhouse"

class SessionCache(Document):
    class Meta:
        backend = "redis"

๐Ÿ“‹ Features

โœ… Core Features

  • ๐Ÿ”— Multi-Backend Architecture: SurrealDB + ClickHouse + Redis support
  • โšก Connection Pooling: Production-ready connection pooling for all backends with configurable pool sizes, health checks, and automatic resource management
  • ๐ŸŽฏ Standardized Connection API: Consistent interface across all backends with parameter validation
  • ๐Ÿ”„ Async/Sync APIs: Complete async/await support with sync alternatives (create_connection(..., async_mode=False))
  • ๐Ÿ”ฅ Intelligent Update System: Safe partial updates preventing data loss
  • ๐Ÿ›ก๏ธ Type-Safe Field System: 15+ field types with validation and backend-specific optimization
  • ๐Ÿ” Advanced Query System: Q objects, QueryExpressions, and Pythonic query syntax (User.age > 30)
  • ๐Ÿ“Š Relationship Management: Graph relations and references with relation documents
  • ๐Ÿ—๏ธ Schema Management: Both SCHEMAFULL and SCHEMALESS table support with migrations
  • โš™๏ธ Connection Management: Named connections, default connections, and connection registry
  • ๐Ÿš€ Performance Optimization: Direct record access, bulk operations, and connection reuse
  • ๐Ÿ”ง Migration Tools: Schema migration, table dropping, and hybrid schema support

๐Ÿ”ง Field Types

Field Type Description SurrealDB ClickHouse Redis
StringField Text fields with validation โœ… โœ… โœ…
IntField Integer with min/max constraints โœ… โœ… โœ…
FloatField Floating point numbers โœ… โœ… โœ…
BooleanField Boolean values โœ… โœ… โœ…
DateTimeField Date and time with timezone โœ… โœ… โœ…
DecimalField High-precision decimals โœ… โœ… โœ…
UUIDField UUID generation and validation โœ… โœ… โœ…
ListField Arrays/lists with typed elements โœ… โœ… โœ…
DictField JSON/dictionary storage โœ… โœ…
ReferenceField References to other documents โœ… โŒ
IPAddressField IPv4/IPv6 address validation โœ… โœ…
DurationField Time periods and durations โœ… โŒ
RangeField Range values with bounds โœ… โŒ
OptionField Optional field wrapper โœ… โŒ
RecordIDField SurrealDB record identifiers โœ… โŒ

๐Ÿ” Query Capabilities

Basic Filtering

# Simple filters
users = await User.objects.filter(age__gt=18).all()
users = await User.objects.filter(username__contains="admin").all()
users = await User.objects.filter(active=True).all()

# Count
count = await User.objects.filter(age__gte=21).count()

Q Objects for Complex Queries

from quantumengine import Q

# Combine conditions
complex_query = Q(age__gte=18) & Q(age__lte=65) & Q(active=True)
users = await User.objects.filter(complex_query).all()

# OR conditions
either_query = Q(role="admin") | Q(permissions__contains="admin")
users = await User.objects.filter(either_query).all()

# NOT conditions
not_query = ~Q(status="banned")
users = await User.objects.filter(not_query).all()

# Raw queries
raw_query = Q.raw("age > 25 AND string::contains(username, 'admin')")
users = await User.objects.filter(raw_query).all()

QueryExpressions with FETCH

# Fetch related documents (SurrealDB)
expr = QueryExpression(where=Q(published=True)).fetch("author")
posts = await Post.objects.filter(expr).all()

# Complex expressions
complex_expr = (QueryExpression(where=Q(active=True))
               .order_by("created_at", "DESC")
               .limit(10)
               .fetch("profile"))
users = await User.objects.filter(complex_expr).all()

๐Ÿ”Œ Connection Management

Standardized Connection API (v0.4.0+)

All backends now use a consistent connection interface:

from quantumengine import create_connection
from quantumengine.connection import PoolConfig

# SurrealDB - Graph database
surrealdb_conn = create_connection(
    backend="surrealdb",
    url="ws://localhost:8000/rpc",
    username="root",
    password="root",
    namespace="production",
    database="main",
    make_default=True
)

# ClickHouse - Analytics database  
clickhouse_conn = create_connection(
    backend="clickhouse",
    url="localhost",
    username="default", 
    password="",
    # port=8123, secure=False  # Auto-set defaults
    make_default=True
)

# Redis - Cache/Session store
redis_conn = create_connection(
    backend="redis", 
    url="localhost",
    # port=6379  # Auto-set default
    password="redis_password",
    make_default=True
)

Connection Pooling

Connection pooling is available across all backends for optimal performance:

# Configure connection pool
pool_config = PoolConfig(
    max_size=50,        # Maximum connections
    min_size=10,        # Minimum connections  
    max_idle_time=300,  # Idle timeout (seconds)
    validate_on_borrow=True  # Health check on borrow
)

# SurrealDB with connection pooling
surrealdb_conn = create_connection(
    backend="surrealdb",
    url="ws://localhost:8000/rpc",
    pool_config=pool_config,
    make_default=True
)

# ClickHouse with connection pooling
clickhouse_conn = create_connection(
    backend="clickhouse", 
    url="localhost",
    pool_config=pool_config,
    make_default=True
)

# Redis with connection pooling
redis_conn = create_connection(
    backend="redis",
    url="localhost",
    pool_config=pool_config,
    make_default=True
)

Sync API Support

# Async API (default)
async_conn = create_connection(
    backend="surrealdb",
    url="ws://localhost:8000/rpc",
    auto_connect=True,
    async_mode=True  # Default
)

# Sync API  
sync_conn = create_connection(
    backend="surrealdb", 
    url="ws://localhost:8000/rpc",
    auto_connect=True,
    async_mode=False
)

# Use sync connection
with sync_conn:
    # Sync operations
    User.create_table_sync()
    user = User(name="Alice")
    user.save_sync()
    
    # Sync queries
    users = User.objects.all_sync()
    active_users = User.objects.filter_sync(active=True).all_sync()

๐Ÿ”— Relationships (SurrealDB)

Document References

class Post(Document):
    title = StringField(required=True)
    author = ReferenceField(User, required=True)
    categories = ListField(field_type=ReferenceField(Category))
    
    class Meta:
        collection = "posts"
        backend = "surrealdb"

Graph Relations

# Create relations between documents
await author1.relate_to("collaborated_with", author2, project="Novel")

# Fetch relations
collaborators = await author1.fetch_relation("collaborated_with")

# Resolve relations (get related documents)
related_authors = await author1.resolve_relation("collaborated_with")

Relation Documents

class AuthorCollaboration(RelationDocument):
    project_name = StringField(required=True)
    start_date = DateTimeField()
    contribution_percent = FloatField()
    
    class Meta:
        collection = "collaborated_with"

# Create relation with metadata
relation = await AuthorCollaboration.create_relation(
    author1, author2,
    project_name="Science Fiction Novel",
    start_date=datetime.now(),
    contribution_percent=60.0
)

๐Ÿ“Š Schema Management

Table Creation

# SCHEMAFULL tables (strict schema)
await User.create_table(schemafull=True)

# SCHEMALESS tables (flexible schema)  
await User.create_table(schemafull=False)

# Backend-specific table creation
await analytics_backend.create_table(
    AnalyticsEvent,
    engine="MergeTree",
    order_by="(event_time, user_id)"
)

Drop Tables & Migration Support

from quantumengine import (
    generate_drop_statements, generate_migration_statements,
    drop_tables_from_module
)

# Drop table functionality
await User.drop_table(if_exists=True)
User.drop_table_sync(if_exists=True)

# Generate drop statements for migration scripts
drop_statements = generate_drop_statements(User)
# ['REMOVE INDEX IF EXISTS idx_email ON users;', 
#  'REMOVE FIELD IF EXISTS email ON users;', ...]

# Generate migration between document versions
migration = generate_migration_statements(UserV1, UserV2, schemafull=True)
print(migration['up'])    # Forward migration statements
print(migration['down'])  # Rollback migration statements

# Drop all tables in a module
await drop_tables_from_module('myapp.models')

Hybrid Schema Support

class Product(Document):
    # Always defined in schema
    name = StringField(required=True, define_schema=True)
    price = FloatField(define_schema=True)
    
    # Only defined in SCHEMAFULL tables
    description = StringField()
    metadata = DictField()
    
    class Meta:
        collection = "products"

Index Management

class User(Document):
    username = StringField(required=True)
    email = StringField(required=True)
    
    class Meta:
        collection = "users"
        indexes = [
            {"name": "user_username_idx", "fields": ["username"], "unique": True},
            {"name": "user_email_idx", "fields": ["email"], "unique": True},
            {"name": "user_age_idx", "fields": ["age"]}
        ]

# Create indexes
await User.create_indexes()

โšก Performance Features

Direct Record Access

# Optimized ID-based queries use direct record access
users = await User.objects.filter(id__in=['user:1', 'user:2']).all()

# Convenience methods for ID operations
users = await User.objects.get_many([1, 2, 3]).all()
users = await User.objects.get_range(100, 200, inclusive=True).all()

Query Analysis

# Explain query execution plan
plan = await User.objects.filter(age__gt=25).explain()

# Get index suggestions
suggestions = User.objects.filter(age__lt=30).suggest_indexes()

Bulk Operations

# Bulk updates
updated = await User.objects.filter(active=False).update(status="inactive")

# Bulk deletes
deleted_count = await User.objects.filter(last_login__lt=cutoff_date).delete()

๐Ÿ”„ Sync API Support

# Create sync connection
connection = create_connection(
    url="ws://localhost:8000/rpc",
    namespace="test_ns",
    database="test_db", 
    username=os.environ.get("SURREALDB_USER"),
    password=os.environ.get("SURREALDB_PASS"),
    async_mode=False
)

with connection:
    # Synchronous operations
    User.create_table_sync(schemafull=True)
    
    user = User(username="alice", email="alice@example.com")
    user.save_sync()
    
    users = User.objects.all_sync()
    user = User.objects.get_sync(id=user_id)
    user.delete_sync()

๐Ÿ“ˆ DataGrid Helpers

from quantumengine import get_grid_data, parse_datatables_params

# Efficient grid operations for web interfaces
result = await get_grid_data(
    User,                      # Document class
    request_args,              # Request parameters
    search_fields=['username', 'email'],
    custom_filters={'active': 'active'},
    default_sort='created_at'
)

# DataTables integration
params = parse_datatables_params(request_args)
result = format_datatables_response(total, rows, draw)

๐Ÿ“ฆ Installation

pip install quantumengine

# For SurrealDB support
pip install surrealdb

# For ClickHouse support  
pip install clickhouse-connect

๐Ÿ”ง Configuration

Environment Variables

import os
from quantumengine import create_connection

# Using environment variables
connection = create_connection(
    url=os.environ.get("SURREALDB_URL", "ws://localhost:8000/rpc"),
    namespace=os.environ.get("SURREALDB_NS", "production"),
    database=os.environ.get("SURREALDB_DB", "main"),
    username=os.environ.get("SURREALDB_USER"),
    password=os.environ.get("SURREALDB_PASS"),
    make_default=True
)

Multiple Named Connections

# Main transactional database
main_db = create_connection(
    name="main_db",
    url="ws://localhost:8000/rpc",
    backend="surrealdb",
    make_default=True
)

# Analytics database
analytics_db = create_connection(
    name="analytics_db",
    url="https://analytics.clickhouse.cloud",
    backend="clickhouse"
)

# Use specific connection
await User.create_table(connection=main_db)
await AnalyticsEvent.create_table(connection=analytics_db)

๐Ÿƒโ€โ™‚๏ธ Quick Examples

Basic CRUD

# Create
user = User(username="alice", email="alice@example.com", age=25)
await user.save()

# Read
user = await User.objects.get(username="alice")
users = await User.objects.filter(age__gte=18).all()

# Update
user.age = 26
await user.save()

# Delete
await user.delete()

Advanced Queries

from quantumengine import Q, QueryExpression

# Complex filtering
active_adults = await User.objects.filter(
    Q(age__gte=18) & Q(active=True)
).all()

# Fetch relations
posts_with_authors = await Post.objects.filter(
    QueryExpression(where=Q(published=True)).fetch("author")
).all()

# Pagination and sorting
recent_users = await User.objects.filter(
    active=True
).order_by("-created_at").limit(10).all()

Multi-Backend Usage

# User data in SurrealDB (transactional)
class User(Document):
    username = StringField(required=True)
    email = StringField(required=True)
    
    class Meta:
        collection = "users"
        backend = "surrealdb"

# Analytics events in ClickHouse (analytical)
class PageView(Document):
    user_id = StringField(required=True)
    page_url = StringField(required=True)
    timestamp = DateTimeField(required=True)
    
    class Meta:
        collection = "page_views"
        backend = "clickhouse"

# Use both seamlessly
user = await User.objects.get(username="alice")
page_views = await PageView.objects.filter(user_id=str(user.id)).all()

๐Ÿ”ฅ NEW in v0.3.0: Intelligent Update System

QuantumEngine now features a comprehensive intelligent update system that prevents data loss and provides safe partial document updates:

# Safe partial updates - only modify specified fields
user = await User.objects.get(username="alice")
await user.update(age=26, status="premium")  # Only updates age and status

# Intelligent save() with change tracking
user = await User.objects.get(username="alice")
user.age = 27
user.email = "alice@newdomain.com"
await user.save()  # Only updates changed fields (age and email)

# Relation updates preserve endpoints
class Friendship(RelationDocument):
    status = StringField(choices=["pending", "accepted", "blocked"])
    since = DateTimeField()
    
    class Meta:
        collection = "friendships"

friendship = await Friendship.objects.get(id="friendship123")
await friendship.update_relation_attributes(status="blocked")
# Preserves in_document and out_document, only updates status

# Multi-backend partial updates
await ClickHouseDoc.update(metrics_count=1500)  # ClickHouse ALTER TABLE UPDATE
await RedisDoc.update(session_data={"active": True})  # Redis hash updates

Key Benefits:

  • Data Loss Prevention: Partial updates preserve unchanged fields
  • Change Tracking: Intelligent save() only updates modified fields
  • Multi-Backend Support: Works consistently across SurrealDB, ClickHouse, and Redis
  • Relation Safety: RelationDocument updates preserve relationship endpoints
  • Backend Optimization: Uses optimal update syntax for each database type

Schema Management Examples

# Generate schema statements
from quantumengine import generate_schema_statements, generate_drop_statements

# Create schema
schema_statements = generate_schema_statements(User, schemafull=True)
for stmt in schema_statements:
    print(stmt)

# Drop schema
drop_statements = generate_drop_statements(User)
for stmt in drop_statements:
    print(stmt)

# Generate migration between versions
from quantumengine import generate_migration_statements

class UserV1(Document):
    username = StringField(required=True)
    email = StringField(required=True)

class UserV2(Document):
    username = StringField(required=True)
    email = StringField(required=True)
    active = BooleanField(default=True)  # New field

migration = generate_migration_statements(UserV1, UserV2)
print("UP migration:")
for stmt in migration['up']:
    print(f"  {stmt}")

print("DOWN migration:")
for stmt in migration['down']:
    print(f"  {stmt}")

๐Ÿงช Testing

The codebase includes comprehensive tests demonstrating real database operations:

# Run working tests
python tests/working/test_multi_backend_real_connections.py
python tests/working/test_clickhouse_simple_working.py
python tests/working/test_working_surrealdb_backend.py

# Run working examples
python example_scripts/working/basic_crud_example.py
python example_scripts/working/multi_backend_example.py
python example_scripts/working/advanced_features_example.py

๐Ÿ“š Examples

The example_scripts/working/ directory contains fully functional examples:

  • basic_crud_example.py: Core CRUD operations
  • advanced_features_example.py: Complex field types and validation
  • multi_backend_example.py: Using SurrealDB and ClickHouse together
  • relation_example.py: Graph relations and RelationDocuments
  • query_expressions_example.py: Advanced querying with Q objects
  • sync_api_example.py: Synchronous API usage
  • test_performance_optimizations.py: Performance features and optimization
  • test_drop_and_migration.py: Drop table and migration functionality

๐Ÿ”„ Backend Capabilities

SurrealDB Backend Features

  • โœ… Graph relations and traversal
  • โœ… Transactions
  • โœ… Direct record access
  • โœ… Full-text search
  • โœ… References between documents
  • โœ… Complex data types (Duration, Range, Option)
  • โœ… SCHEMAFULL and SCHEMALESS tables

ClickHouse Backend Features

  • โœ… High-performance analytical queries
  • โœ… Bulk operations optimization
  • โœ… Time-series data handling
  • โœ… Columnar storage benefits
  • โœ… Aggregation functions
  • โŒ Graph relations (not applicable)
  • โŒ Transactions (limited support)

Backend Detection

# Check backend capabilities
if connection.backend.supports_graph_relations():
    await user.relate_to("follows", other_user)

if connection.backend.supports_bulk_operations():
    await Document.objects.filter(...).bulk_update(status="processed")

# Get backend-specific optimizations
optimizations = connection.backend.get_optimized_methods()
print(optimizations)

โšก Performance Features

Automatic Query Optimizations

  • Direct Record Access: ID-based queries use SELECT * FROM user:1, user:2
  • Range Access: Range queries use SELECT * FROM user:1..=100
  • Bulk Operations: Optimized batch processing
  • Index Utilization: Automatic index suggestions

Measured Performance Improvements

  • Direct Record Access: Up to 3.4x faster than traditional WHERE clauses
  • Bulk Operations: Significant improvement for batch processing
  • Memory Efficiency: Reduced data transfer and memory usage

๐Ÿ“š Documentation

Online Documentation

  • GitHub Pages: Full Sphinx Documentation - Complete API reference with examples
  • API Reference: Detailed class and method documentation
  • Quick Start Guide: Step-by-step getting started tutorial
  • Module Documentation: Auto-generated from source code docstrings

Local Documentation

  • API Reference: API_REFERENCE.md - Complete class and method documentation
  • Examples: example_scripts/working/ - Working examples for all features
  • Tests: tests/working/ - Test files demonstrating functionality
  • Sphinx Docs: docs/ - Build locally with cd docs && uv run make html

Building Documentation

# Install dependencies
uv sync

# Build Sphinx documentation
cd docs
uv run make html

# View locally
open docs/_build/html/index.html

๐Ÿค Contributing

QuantumORM is actively developed with a focus on real-world usage and multi-backend support. See the tests/working/ directory for examples of tested functionality.

For detailed contribution guidelines, see:

  • CONTRIBUTING.md: Development setup, Docker instructions, and contribution workflow
  • docs/README.md: Documentation contribution guidelines

๐Ÿ™ Acknowledgments

QuantumEngine draws significant inspiration from MongoEngine, whose elegant document-oriented design patterns and query API have influenced our multi-backend approach. We're grateful to the MongoEngine community for pioneering many of the concepts that make QuantumEngine possible.

๐Ÿ“„ License

MIT License - see LICENSE file for details.


QuantumEngine: Unified database access for modern Python applications with comprehensive multi-backend support, schema management, and performance optimizations.

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

quantumengine-0.4.0.tar.gz (152.2 kB view details)

Uploaded Source

Built Distribution

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

quantumengine-0.4.0-py3-none-any.whl (154.1 kB view details)

Uploaded Python 3

File details

Details for the file quantumengine-0.4.0.tar.gz.

File metadata

  • Download URL: quantumengine-0.4.0.tar.gz
  • Upload date:
  • Size: 152.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.12.7

File hashes

Hashes for quantumengine-0.4.0.tar.gz
Algorithm Hash digest
SHA256 f47ccca7faf66b4f9c56a5609a8f0776480f8e97dbc8388202b8ec23cf835626
MD5 b6e283d8b2c8f086745636b714ad7567
BLAKE2b-256 dedaa4f461fe1634e5e8036873eb728b5336af6dae202adc6adeb10306339051

See more details on using hashes here.

File details

Details for the file quantumengine-0.4.0-py3-none-any.whl.

File metadata

  • Download URL: quantumengine-0.4.0-py3-none-any.whl
  • Upload date:
  • Size: 154.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.12.7

File hashes

Hashes for quantumengine-0.4.0-py3-none-any.whl
Algorithm Hash digest
SHA256 0550a9471d8a88e98052eda1e255eb697570238b7bb3d8096ec95768101029f6
MD5 1ce6ced339de7caaaa54bcfe2fce5bc2
BLAKE2b-256 109e3b39dda9fa9c1ebab95b576c0fad02f3a7509eb3cbb114b8c0465861a3af

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