EBlock (Easy Block Builder)
A flexible, schema-driven library for building dynamic, context-aware content blocks with variable substitution, nested composition, validation, and asynchronous rendering.
Designed for use in page builders, CMS backends, or dynamic UI generation systems where content is defined as structured JSON, rendered with context variables, and validated via Pydantic models.
Installation
pip install eblock
or with uv:
uv add eblock
Core Concepts
Context
A container for variables and metadata used during block rendering.
- Variables are case-normalized to uppercase.
- Supports deep copying for safety.
- Allows dynamic assignment via
ctx["key"] = value. - Stores:
vars: dictionary of scalar or structured values (strings, numbers, lists, dicts, etc.)extra: arbitrary metadata (not used in substitution)
BaseBlock
Abstract base class for all content blocks.
Each block: - Accepts properties (dict) on initialization. -
Optionally validates input/output using Pydantic models. - Recursively
resolves {{ variable }} placeholders from Context. - Supports
nesting inside dicts, lists, tuples, and sets. - Exposes an async
build() method returning (result_dict, output_schema). - Allows
custom output transformation via prepare_output().
Block Registration Mechanism
EBlock uses an automatic class-based registration system powered by the
BlockMeta metaclass.
How It Works
Every subclass of BaseBlock that defines a _type attribute is
automatically registered on import.
class TextBlock(BaseBlock):
_type = "text"
When Python loads this class:
BlockMeta.__new__is triggered.- If
_typeis not"base"and not private (_SomethingBlock), the block is added to the global registry. - Duplicate
_typevalues produce a warning and are ignored.
Accessing Registered Blocks
You can list all registered block types:
from eblock import get_registered_block_types
print(get_registered_block_types())
# ["text", "score_card", ...]
Creating Blocks Dynamically
Use the factory function:
from eblock import create_block
block = create_block("text", {"content": "Hello"})
Building Blocks from JSON-like Config
The library supports nested block trees:
from eblock import create_blocks_from_config
blocks = create_blocks_from_config([
{"type": "text", "properties": {"content": "Hi"}},
{"type": "score_card", "properties": {"title": "Test", "score": 95}},
])
Nested blocks inside dicts/lists are resolved recursively.
Why Use Automatic Registration?
- No need for manual registries.
- Blocks register themselves as soon as they are imported.
- Decoupled plugins/modules can define their own block types.
- Ensures consistency when building from JSON configs.
Features
✅ Variable substitution
Placeholders like {{ user_name }} are replaced with values from
context.
✅ Recursive structure support
Handles dicts, lists, tuples, sets, and nested BaseBlock instances.
✅ Input/Output validation
Define _input_schema and _output_schema using Pydantic models.
✅ Computed fields
Override prepare_output() to modify or add fields before output
validation.
✅ Static dependency analysis
Use .get_vars() to extract all required variables, including in nested
blocks.
✅ Async-first design
Full async/await support throughout the rendering pipeline.
✅ Structured logging
Blocks and context instances log operations through hierarchical
loggers.
Usage Examples
Basic Block
from eblock import BaseBlock, Context
class TextBlock(BaseBlock):
_type = "text"
ctx = Context(vars={"name": "Alice"})
block = TextBlock({"content": "Hello, {{ name }}!"})
result, _ = await block.build(ctx)
# result == {"content": "Hello, Alice!"}
Validated Block with Computed Output
from pydantic import BaseModel
from eblock import BaseBlock
class Input(BaseModel):
title: str
score: int
class Output(BaseModel):
title: str
score: int
badge: str
class ScoreCard(BaseBlock):
_type = "score_card"
_input_schema = Input
_output_schema = Output
async def prepare_output(self, result, ctx):
result["badge"] = "🏆" if result["score"] >= 90 else "📝"
return result
Nested Blocks
header = TextBlock({"text": "Welcome, {{ user }}!"})
page = PageBlock({"header": header, "theme": "{{ theme }}"})
# Both variables will be resolved from context
Variable Inspection
block = TextBlock({"msg": "Hi {{ first }}, {{ last }}!"})
print(block.get_vars()) # {"FIRST", "LAST"}
API Reference
class Context
__init__(self, vars: dict | None = None, **extra)__getitem__(key)__setitem__(key, value)- Properties:
vars--- deep copy of stored variables\extra--- shallow copy of extra data
class BaseBlock
__init__(self, properties: dict)async build(self, ctx: Context) -> tuple[dict, Type[BaseModel] | None]async prepare_output(self, result: dict, ctx: Context) -> dictget_vars(self) -> set[str]- Property:
type--- block identifier
Define in subclasses
_type: str--- identifier (required)_input_schema: Type[BaseModel] | None_output_schema: Type[BaseModel] | None
Registry Helpers
create_block(type, properties)create_blocks_from_config(config)get_registered_block_types()
Requirements
- Python ≥ 3.10
- Pydantic ≥ 2.0
Notes
- Placeholders must use double curly braces:
{{ var_name }}. - Whitespace is trimmed:
{{ user }}→user. - Missing variables resolve to empty string and produce a warning.
- Blocks should be treated as stateless and recreated when reused.
Metadata
Release files for eblock 0.3.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| eblock-0.3.0.tar.gz | 20.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| eblock-0.3.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 33.5 kB
Release files / eblock-0.3.0.tar.gz
| Download URL | eblock-0.3.0.tar.gz |
|---|---|
| Size | 20.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
206f83b6b02cc0938527ef693fedb69fb44fdc6d031d2117b49a872f9f33d889
|
|
BLAKE2b-256 checksum How to use checksums |
1f62c7b4ca5a7217ae70e809d493a5f4262f1ee4b610b6b25f26fc9b77210288
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.7
|
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.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Dec 7, 2025.
Transparency logRelease files / eblock-0.3.0-py3-none-any.whl
| Download URL | eblock-0.3.0-py3-none-any.whl |
|---|---|
| Size | 13.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
acd48586b56dc6ba79b79a218867b46bdd9a37aad8b4e78f58a22582972d788a
|
|
BLAKE2b-256 checksum How to use checksums |
ffed80ace570de9d5928e7a0992a97ceb137b6d679a722486b233c492df79973
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.7
|
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.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Dec 7, 2025.
Transparency log