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.
pip install eblockor with uv:
uv add eblockA 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)
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().
EBlock uses an automatic class-based registration system powered by the
BlockMeta metaclass.
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.
You can list all registered block types:
from eblock import get_registered_block_types
print(get_registered_block_types())
# ["text", "score_card", ...]Use the factory function:
from eblock import create_block
block = create_block("text", {"content": "Hello"})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.
- 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.
✅ 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.
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!"}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 resultheader = TextBlock({"text": "Welcome, {{ user }}!"})
page = PageBlock({"header": header, "theme": "{{ theme }}"})
# Both variables will be resolved from contextblock = TextBlock({"msg": "Hi {{ first }}, {{ last }}!"})
print(block.get_vars()) # {"FIRST", "LAST"}__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
__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
_type: str--- identifier (required)_input_schema: Type[BaseModel] | None_output_schema: Type[BaseModel] | None
create_block(type, properties)create_blocks_from_config(config)get_registered_block_types()
- Python ≥ 3.10
- Pydantic ≥ 2.0
- 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.