Node Attributes
Every node in a Bag can carry attributes (metadata) separate from its value.
Why Attributes?
Values represent what the data is. Attributes represent how to interpret or handle it.
>>> from genro_bag import Bag
>>> bag = Bag()
>>> bag.set_item('api_key', 'sk-xxx', env='production', expires=2025, encrypted=True)
>>> bag['api_key'] # The value
'sk-xxx'
>>> bag['api_key?env'] # Metadata about the value
'production'
Setting Attributes
With set_item()
>>> from genro_bag import Bag
>>> bag = Bag()
>>> bag.set_item('user', 'Alice', role='admin', active=True, level=5)
>>> bag['user']
'Alice'
>>> bag['user?role']
'admin'
With _attributes Parameter
>>> from genro_bag import Bag
>>> bag = Bag()
>>> attrs = {'region': 'eu', 'tier': 1, 'tags': ['web', 'api']}
>>> bag.set_item('server', 'prod-01', _attributes=attrs)
>>> bag['server?region']
'eu'
>>> bag['server?tags']
['web', 'api']
After Creation
>>> from genro_bag import Bag
>>> bag = Bag()
>>> bag['user'] = 'Alice'
>>> # Set attribute using ? syntax
>>> bag['user?role'] = 'admin'
>>> bag['user?role']
'admin'
Reading Attributes
Single Attribute
>>> from genro_bag import Bag
>>> bag = Bag()
>>> bag.set_item('item', 100, category='A', priority=1)
>>> bag['item?category']
'A'
>>> bag['item?priority']
1
>>> bag['item?missing'] # Missing attribute returns None
Multiple Attributes
Use & to get multiple attributes as a tuple:
>>> from genro_bag import Bag
>>> bag = Bag()
>>> bag.set_item('item', 100, x=1, y=2, z=3)
>>> bag['item?x&y&z']
(1, 2, 3)
>>> bag['item?x&y']
(1, 2)
All Attributes
>>> from genro_bag import Bag
>>> bag = Bag()
>>> bag.set_item('item', 100, category='A', priority=1)
>>> node = bag.get_node('item')
>>> node.attr
{'category': 'A', 'priority': 1}
Using get_attr()
>>> from genro_bag import Bag
>>> bag = Bag()
>>> bag.set_item('item', 100, category='A')
>>> node = bag.get_node('item')
>>> node.get_attr('category')
'A'
>>> node.get_attr('missing', default='N/A')
'N/A'
Attribute Types
Attributes can be any Python value:
>>> from genro_bag import Bag
>>> bag = Bag()
>>> bag.set_item('config', None,
... enabled=True, # bool
... count=42, # int
... ratio=3.14, # float
... name='test', # str
... tags=['a', 'b'], # list
... meta={'x': 1, 'y': 2} # dict
... )
>>> bag['config?enabled']
True
>>> bag['config?tags']
['a', 'b']
>>> bag['config?meta']
{'x': 1, 'y': 2}
Querying by Attribute
Filter by Attribute Value
>>> from genro_bag import Bag
>>> bag = Bag()
>>> bag.set_item('alice', 'Alice', role='admin')
>>> bag.set_item('bob', 'Bob', role='user')
>>> bag.set_item('carol', 'Carol', role='admin')
>>> bag.query('#k', condition=lambda n: n.get_attr('role') == 'admin')
['alice', 'carol']
Extract Attributes
>>> from genro_bag import Bag
>>> bag = Bag()
>>> bag.set_item('p1', 'Product 1', price=100)
>>> bag.set_item('p2', 'Product 2', price=200)
>>> bag.query('#k,#a.price')
[('p1', 100), ('p2', 200)]
>>> bag.sum('#a.price')
300
Find by Attribute
>>> from genro_bag import Bag
>>> bag = Bag()
>>> bag.set_item('user1', 'Alice', id='u001')
>>> bag.set_item('user2', 'Bob', id='u002')
>>> node = bag.get_node_by_attr('id', 'u002')
>>> node.value
'Bob'
Serialization
Attributes are preserved in all serialization formats:
>>> from genro_bag import Bag
>>> bag = Bag()
>>> bag.set_item('item', 'value', meta='data')
>>> # XML preserves attributes
>>> xml = bag.to_xml()
>>> 'meta="data"' in xml
True
>>> # Round-trip preserves attributes
>>> bag2 = Bag.from_xml(xml)
>>> bag2['item?meta']
'data'
Common Patterns
Configuration with Defaults
>>> from genro_bag import Bag
>>> bag = Bag()
>>> bag.set_item('timeout', 30, unit='seconds', default=True)
>>> bag.set_item('retries', 3, default=True)
>>> bag.set_item('host', 'custom.example.com', default=False)
>>> # Find custom (non-default) settings
>>> bag.query('#k', condition=lambda n: not n.get_attr('default', True))
['host']
Type Hints
>>> from genro_bag import Bag
>>> bag = Bag()
>>> bag.set_item('age', '25', type='int')
>>> bag.set_item('active', 'true', type='bool')
>>> bag.set_item('score', '3.14', type='float')
>>> # Use type hints to convert
>>> def convert(node):
... t = node.get_attr('type')
... v = node.value
... if t == 'int': return int(v)
... if t == 'bool': return v.lower() == 'true'
... if t == 'float': return float(v)
... return v
>>> node = bag.get_node('age')
>>> convert(node)
25
Validation State
>>> from genro_bag import Bag
>>> bag = Bag()
>>> bag.set_item('email', 'test@example.com', validated=True)
>>> bag.set_item('phone', 'invalid', validated=False, error='Invalid format')
>>> # Find invalid fields
>>> bag.query('#k,#a.error', condition=lambda n: not n.get_attr('validated', True))
[('phone', 'Invalid format')]