Modernized bag system for the Genropy framework - hierarchical data container with XML serialization
Project description
genro-bag
Hierarchical data container for Python.
Install
pip install genro-bag
What is a Bag?
A Bag is a dict-like container that organizes data hierarchically. Unlike a standard dict, a Bag is designed to represent tree structures where each element can contain other elements.
Dict-Like Container
At the simplest level, a Bag behaves like a dictionary:
from genro_bag import Bag
bag = Bag()
bag['name'] = 'Alice'
bag['age'] = 30
print(bag['name']) # 'Alice'
print(len(bag)) # 2
for key in bag.keys():
print(key) # 'name', 'age'
Hierarchical Structure
But unlike a dict, a Bag supports hierarchical paths with dot notation. Intermediate levels are created automatically:
bag = Bag()
bag['config.database.host'] = 'localhost'
bag['config.database.port'] = 5432
bag['config.cache.enabled'] = True
# Direct access to any level
print(bag['config.database.host']) # 'localhost'
# Navigate the subtree
db_config = bag['config.database']
print(db_config['host']) # 'localhost'
This eliminates the defensive pattern typical of nested dicts:
# With standard dict - fragile and verbose
email = data.get('user', {}).get('profile', {}).get('settings', {}).get('email')
# With Bag - one path, clear intent
email = bag['user.profile.settings.email']
Nodes
Each element in a Bag is not a simple value, but a BagNode. The node is the fundamental unit: it contains the value, plus additional metadata.
bag = Bag()
bag['user'] = 'Alice'
# Value access (common way)
print(bag['user']) # 'Alice'
# Access to the underlying node
node = bag.get_node('user')
print(node.label) # 'user'
print(node.value) # 'Alice'
Attributes and Values
Each node has two distinct things:
- value: the node's value (string, number, another Bag, any object)
- attr: a dictionary of attributes (metadata)
Attributes allow associating additional information without modifying the value:
bag = Bag()
bag.set_item('api_key', 'sk-xxx', env='production', expires=2025)
# Value and attributes are separate
print(bag['api_key']) # 'sk-xxx' (the value)
print(bag['api_key?env']) # 'production' (an attribute)
print(bag['api_key?expires']) # 2025 (another attribute)
# Access via node
node = bag.get_node('api_key')
print(node.value) # 'sk-xxx'
print(node.attr) # {'env': 'production', 'expires': 2025}
This is particularly useful for XML where attributes are native:
bag = Bag()
bag.set_item('user', 'Alice', role='admin', active=True)
print(bag.to_xml())
# <user role="admin" active="True">Alice</user>
Lazy Values (Resolvers)
Not everything can be stored statically. Some values must be obtained: from APIs, databases, files. Resolvers let you define how to get a value - resolution happens transparently on access:
from genro_bag import Bag
from genro_bag.resolvers import BagCbResolver, UrlResolver
bag = Bag()
# Callback resolver - computes value on-demand
def get_timestamp():
from datetime import datetime
return datetime.now().isoformat()
bag['now'] = BagCbResolver(get_timestamp)
print(bag['now']) # '2025-01-07T10:30:45.123456' - computed now
# URL resolver - HTTP fetch on-demand
bag['weather'] = UrlResolver('https://api.weather.com/today')
print(bag['weather']) # GET request executed here
# With caching - value is stored for N seconds
bag['data'] = BagCbResolver(expensive_function, cache_time=60)
Access is always the same (bag['key']), but the value can come from any source.
Reactivity (Subscriptions)
A Bag can notify when its contents change. You can subscribe to insert, update, and delete events:
bag = Bag()
def on_change(node, evt, **kw):
print(f'{evt}: {node.label} = {node.value}')
bag.subscribe('logger', any=on_change)
bag['name'] = 'Alice' # Prints: ins: name = Alice
bag['name'] = 'Bob' # Prints: upd_value: name = Bob
del bag['name'] # Prints: del: name = Bob
This allows building reactive systems where components automatically react to data changes.
Validated Fluent Construction (Builders)
Builders provide a fluent API to construct domain-specific structures. Instead of building the Bag manually, you use methods that guide and validate the construction:
from genro_bag import Bag
from genro_bag.builders import HtmlBuilder
bag = Bag(builder=HtmlBuilder())
# Fluent API - each method returns the created node
div = bag.div(id='main', class_='container')
div.h1(value='Welcome')
div.p(value='Hello, World!')
print(bag.to_xml())
# <div id="main" class="container">
# <h1>Welcome</h1>
# <p>Hello, World!</p>
# </div>
Builders can also validate the structure:
from genro_bag.builders import BagBuilderBase, element
class MenuBuilder(BagBuilderBase):
@element(children='item') # menu can only contain item
def menu(self, target, tag, **attr):
return self.child(target, tag, **attr)
@element()
def item(self, target, tag, value=None, **attr):
return self.child(target, tag, value=value, **attr)
bag = Bag(builder=MenuBuilder())
menu = bag.menu(id='nav')
menu.item(value='Home')
menu.item(value='About')
# menu.div() # Error! 'div' not allowed inside 'menu'
Query and Aggregation
Bag provides methods to extract and aggregate data:
bag = Bag()
bag.set_item('alice', 100, role='admin')
bag.set_item('bob', 50, role='user')
bag.set_item('carol', 75, role='admin')
# Extract labels and values
bag.digest('#k,#v')
# [('alice', 100), ('bob', 50), ('carol', 75)]
# Filter by condition
bag.digest('#k', condition=lambda n: n.value > 60)
# ['alice', 'carol']
# Sum values
bag.sum() # 225
Serialization
Bag supports multiple serialization formats:
bag = Bag()
bag['count'] = 42
bag.set_item('user', 'Alice', role='admin')
# XML - human readable, native attributes
xml = bag.to_xml()
restored = Bag.from_xml(f'<root>{xml}</root>')
# JSON
json_str = bag.to_json()
restored = Bag.from_json(json_str)
# TYTX - preserves Python types exactly
tytx = bag.to_tytx()
restored = Bag.from_tytx(tytx)
TYTX (Typed Text eXchange) is the recommended format when you need round-trip without losing types (int, Decimal, datetime, etc.).
Use Cases
The same hierarchical model applies to different domains:
- Configurations - hierarchical paths, attributes for metadata
- HTML/XML documents - builders for fluent construction
- API responses - resolvers for lazy loading
- UI state - subscriptions for reactivity
- Structured data - queries for extraction
The structure stays the same. Only the vocabulary changes.
Documentation
Full documentation: genro-bag.readthedocs.io
Development
# Install with dev dependencies
pip install -e ".[dev]"
# Run tests
pytest
# Code quality
ruff check src/
mypy src/
License
Apache License 2.0 - see LICENSE for details.
Copyright 2025 Softwell S.r.l. - Genropy Team
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file genro_bag-0.4.0.tar.gz.
File metadata
- Download URL: genro_bag-0.4.0.tar.gz
- Upload date:
- Size: 135.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e462d42311cc4fe9f0202aa85dd183b54c0739a7534e5090748a59f4c6b283da
|
|
| MD5 |
cb5e6b3f7640330d6439d6c687ebedb2
|
|
| BLAKE2b-256 |
2b58313e49af2663dbb5814e2f4f97478cb992b3e84795eafce9d421b55caa5b
|
Provenance
The following attestation bundles were made for genro_bag-0.4.0.tar.gz:
Publisher:
publish.yml on genropy/genro-bag
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
genro_bag-0.4.0.tar.gz -
Subject digest:
e462d42311cc4fe9f0202aa85dd183b54c0739a7534e5090748a59f4c6b283da - Sigstore transparency entry: 798944385
- Sigstore integration time:
-
Permalink:
genropy/genro-bag@97df8186e98ef18b8866cecfaebf00773792a249 -
Branch / Tag:
refs/tags/v0.4.0 - Owner: https://github.com/genropy
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@97df8186e98ef18b8866cecfaebf00773792a249 -
Trigger Event:
push
-
Statement type:
File details
Details for the file genro_bag-0.4.0-py3-none-any.whl.
File metadata
- Download URL: genro_bag-0.4.0-py3-none-any.whl
- Upload date:
- Size: 70.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2989532a7daab310f4ad046135be1b70cadebadd157517e883b5255f21016256
|
|
| MD5 |
22ce9fca9d8fcdb061eadfccfc1da72e
|
|
| BLAKE2b-256 |
2eeb62214168cca4eb70e426f7b881ca066355875b57cbf8a4408ef46328db6b
|
Provenance
The following attestation bundles were made for genro_bag-0.4.0-py3-none-any.whl:
Publisher:
publish.yml on genropy/genro-bag
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
genro_bag-0.4.0-py3-none-any.whl -
Subject digest:
2989532a7daab310f4ad046135be1b70cadebadd157517e883b5255f21016256 - Sigstore transparency entry: 798944386
- Sigstore integration time:
-
Permalink:
genropy/genro-bag@97df8186e98ef18b8866cecfaebf00773792a249 -
Branch / Tag:
refs/tags/v0.4.0 - Owner: https://github.com/genropy
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@97df8186e98ef18b8866cecfaebf00773792a249 -
Trigger Event:
push
-
Statement type: