Skip to main content

Modernized bag system for the Genropy framework - hierarchical data container with XML serialization

Project description

genro-bag

PyPI version Tests codecov Documentation Python 3.10+ License Code style: black Ruff

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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

genro_bag-0.4.0.tar.gz (135.2 kB view details)

Uploaded Source

Built Distribution

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

genro_bag-0.4.0-py3-none-any.whl (70.9 kB view details)

Uploaded Python 3

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

Hashes for genro_bag-0.4.0.tar.gz
Algorithm Hash digest
SHA256 e462d42311cc4fe9f0202aa85dd183b54c0739a7534e5090748a59f4c6b283da
MD5 cb5e6b3f7640330d6439d6c687ebedb2
BLAKE2b-256 2b58313e49af2663dbb5814e2f4f97478cb992b3e84795eafce9d421b55caa5b

See more details on using hashes here.

Provenance

The following attestation bundles were made for genro_bag-0.4.0.tar.gz:

Publisher: publish.yml on genropy/genro-bag

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for genro_bag-0.4.0-py3-none-any.whl
Algorithm Hash digest
SHA256 2989532a7daab310f4ad046135be1b70cadebadd157517e883b5255f21016256
MD5 22ce9fca9d8fcdb061eadfccfc1da72e
BLAKE2b-256 2eeb62214168cca4eb70e426f7b881ca066355875b57cbf8a4408ef46328db6b

See more details on using hashes here.

Provenance

The following attestation bundles were made for genro_bag-0.4.0-py3-none-any.whl:

Publisher: publish.yml on genropy/genro-bag

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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