Resolver Examples

Practical examples of resolver usage patterns.

Source code: examples/resolvers/


Resolver Parameters

Shows how parameters flow through the resolver system with three priority levels.

Source: examples/resolvers/resolver_parameters/

Parameter Priority

When a resolver is called, parameters come from two sources (highest priority first):

  1. node.attr: Attributes set on the parent BagNode (mutable)

  2. resolver._kw: Default parameters set at resolver construction

Passing kwargs at call time (bag.get_item('path', x=10)) is syntactic sugar for set_attr(x=10) followed by the read — the call updates node.attr so the cached value stays coherent with it. “Changing arguments = updating resolver state” (no transient overrides).

Basic Example with BagCbResolver

>>> from genro_bag import Bag
>>> from genro_bag.resolver import BagCbResolver
>>>
>>> def multiply(base, multiplier):
...     return base * multiplier
>>>
>>> bag = Bag()
>>> bag['calc'] = BagCbResolver(multiply, base=10, multiplier=2)
>>>
>>> # Defaults: base=10, multiplier=2
>>> bag['calc']
20
>>>
>>> # Update via node attributes
>>> bag.set_attr('calc', multiplier=5)
>>> bag['calc']
50
>>>
>>> # Pass kwargs at call time: updates node.attr and loads
>>> bag.get_item('calc', multiplier=10)
100
>>>
>>> # The update persists: node.attr now has multiplier=10
>>> bag['calc']
100

Query String Syntax in Path

Parameters can also be passed directly in the path using query string syntax:

>>> from genro_bag import Bag
>>> from genro_bag.resolver import BagCbResolver
>>>
>>> bag = Bag()
>>> bag['calc'] = BagCbResolver(lambda x: x * 2, x=5)
>>>
>>> # These two are equivalent:
>>> bag.get_item('calc', x=10)
20
>>> bag.get_item('calc?x=10')
20
>>>
>>> # Multiple parameters:
>>> def add(a, b):
...     return a + b
>>>
>>> bag['sum'] = BagCbResolver(add, a=1, b=2)
>>> bag.get_item('sum?a=10&b=20')
30

Cache Invalidation

Cache is automatically invalidated when effective parameters change:

>>> bag = Bag()
>>> bag['data'] = BagCbResolver(counter, x=5, cache_time=False)  # infinite cache
>>>
>>> bag['data']  # First call -> computed
10
>>> bag['data']  # From cache (same params)
10
>>> bag.set_attr('data', x=7)  # Change param -> invalidates cache
>>> bag['data']  # Recomputed
14

Parameter Flow Diagram

bag.get_item('path', **call_kwargs)
    |
    v
node.get_value(**call_kwargs)
    |
    v
resolver(static=False, **call_kwargs)
    |
    +--- if call_kwargs:
    |       node.set_attr(**call_kwargs)   # write into node.attr
    |
    v
+------------------------------------------+
| effective_kw = {}                         |
| effective_kw.update(resolver._kw)     # 2 |
| effective_kw.update(node.attr)        # 1 |  (node.attr now has the call_kwargs)
+------------------------------------------+
    |
    v
resolver.load()  # reads from self.kw (on_loading applied to merged self._kw)
    |
    v
result (cached if cache_time != 0)

UrlResolver Examples

Source: examples/resolvers/url_resolver/

httpbin Test API

>>> from genro_bag import Bag
>>> from genro_bag.resolvers import UrlResolver
>>>
>>> api = Bag()
>>> api['json'] = UrlResolver('https://httpbin.org/json', as_bag=True)
>>> api['ip'] = UrlResolver('https://httpbin.org/ip', as_bag=True)
>>> api['headers'] = UrlResolver('https://httpbin.org/headers', as_bag=True)
>>>
>>> api['json.slideshow.title']
'Sample Slide Show'
>>>
>>> api['ip.origin']
'203.0.113.42'
>>>
>>> print(api.to_string(static=False))
├── json
│   └── slideshow
│       ├── author: 'Yours Truly'
│       ├── date: 'date of publication'
│       ├── title: 'Sample Slide Show'
│       └── slides
│           └── ...
├── ip
│   └── origin: '203.0.113.42'
└── headers
    └── headers
        ├── Accept: '*/*'
        ├── Host: 'httpbin.org'
        └── User-Agent: 'python-httpx/0.27.0'

ECB Exchange Rates (XML)

Fetching XML data from the European Central Bank:

>>> from genro_bag import Bag
>>> from genro_bag.resolvers import UrlResolver
>>>
>>> ecb = Bag()
>>> ecb['rates'] = UrlResolver(
...     'https://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml',
...     as_bag=True
... )
>>>
>>> ecb['rates.Envelope.Cube.Cube']
<Bag with currency rates>
>>>
>>> # Or use Bag.from_url for immediate fetch
>>> rates = Bag.from_url('https://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml')
>>> print(rates.to_string())
└── Envelope
    ├── subject: 'Reference rates'
    ├── Sender
    │   └── name: 'European Central Bank'
    └── Cube
        └── Cube
            └── ...

Petstore API

>>> from genro_bag import Bag
>>> from genro_bag.resolvers import UrlResolver
>>>
>>> petstore = Bag()
>>> petstore['pets'] = UrlResolver(
...     'https://petstore.swagger.io/v2/pet/findByStatus',
...     qs={'status': 'available'},
...     as_bag=True
... )
>>> petstore['store'] = UrlResolver(
...     'https://petstore.swagger.io/v2/store/inventory',
...     as_bag=True
... )
>>>
>>> petstore['pets.0.name']
'doggie'
>>>
>>> petstore['store.available']
234

OpenApiResolver

Loads an OpenAPI spec and organizes endpoints by tags with ready-to-use UrlResolvers.

Source: examples/resolvers/openapi_resolver/

>>> from genro_bag import Bag
>>> from genro_bag.resolvers import OpenApiResolver
>>>
>>> bag = Bag()
>>> bag['petstore'] = OpenApiResolver('https://petstore3.swagger.io/api/v3/openapi.json')
>>>
>>> api = bag['petstore']
>>> list(api.keys())
['info', 'externalDocs', 'servers', 'api', 'components']
>>>
>>> api['info']
'This is a sample Pet Store Server based on the OpenAPI 3.0 specification.'
>>>
>>> list(api['api'].keys())
['pet', 'store', 'user']
>>>
>>> list(api['api.pet'].keys())
['updatePet', 'addPet', 'findPetsByStatus', 'findPetsByTags', 'getPetById', ...]
>>>
>>> op = api['api.pet.findPetsByStatus']
>>> op['summary']
'Finds Pets by status'
>>>
>>> op['method']
'get'
>>>
>>> # Set query parameters and invoke
>>> op['qs.status'] = 'available'
>>> result = op['value']  # triggers the API call
>>> result['0.name']
'doggie'

POST with Body

The resolver also supports POST/PUT/PATCH operations with body parameters:

>>> # Get the addPet operation
>>> add_pet = api['api.pet.addPet']
>>> add_pet['summary']
'Add a new pet to the store'
>>>
>>> add_pet['method']
'post'
>>>
>>> # View expected body structure
>>> print(add_pet['body'].to_string())
├── id: None
├── name: None
├── category
│   ├── id: None
│   └── name: None
├── photoUrls: []
├── tags: []
└── status: None
>>>
>>> # Set body values and invoke
>>> add_pet['body.name'] = 'Fluffy'
>>> add_pet['body.status'] = 'available'
>>> add_pet['body.category.name'] = 'Cats'
>>>
>>> # Or pass body via _body parameter at call time
>>> from genro_bag import Bag
>>> new_pet = Bag()
>>> new_pet['name'] = 'Buddy'
>>> new_pet['status'] = 'available'
>>> new_pet['category.name'] = 'Dogs'
>>>
>>> result = api.get_item('api.pet.addPet.value', _body=new_pet)
>>> result['name']
'Buddy'

API Structure

>>> print(api.to_string())
├── info: 'This is a sample Pet Store Server...'
├── externalDocs
│   ├── description: 'Find out more about Swagger'
│   └── url: 'https://swagger.io'
├── servers
│   └── 0
│       └── url: '/api/v3'
├── api
│   ├── pet [name='pet', description='Everything about your Pets']
│   │   ├── updatePet
│   │   │   ├── summary: 'Update an existing pet'
│   │   │   ├── method: 'put'
│   │   │   └── ...
│   │   ├── findPetsByStatus
│   │   │   ├── summary: 'Finds Pets by status'
│   │   │   ├── method: 'get'
│   │   │   ├── qs
│   │   │   │   └── status: None
│   │   │   └── value: <UrlResolver>
│   │   └── ...
│   ├── store [name='store', description='Access to Petstore orders']
│   └── user [name='user', description='Operations about user']
└── components
    ├── schemas
    └── securitySchemes

DirectoryResolver

Maps filesystem directories to Bag with lazy loading.

Source: examples/resolvers/directory_resolver/

Basic Directory Scanning

from genro_bag import Bag
from genro_bag.resolvers import DirectoryResolver

# Create resolver
resolver = DirectoryResolver('/path/to/directory')
bag = Bag()
bag['files'] = resolver

# Access triggers scan
files = bag['files']

for path, node in files.walk():
    indent = "  " * path.count(".")
    is_dir = node.is_branch
    print(f"{indent}{node.label}{'/' if is_dir else ''}")

Filtered Directory Scanning

# Include only .xml and .json files
resolver = DirectoryResolver(
    '/path/to/directory',
    include="*.xml,*.json",
    ext="xml,json",
)
bag = Bag()
bag['config_files'] = resolver

files = bag['config_files']
for path, node in files.walk():
    print(node.label)

Custom Resolvers

SystemResolver

Collects system information from multiple sources (platform, psutil, etc.).

Source: examples/resolvers/system_resolver/

>>> from genro_bag import Bag
>>> from genro_bag.resolvers import DirectoryResolver
>>> from system_resolver import SystemResolver
>>>
>>> computer = Bag()
>>> computer['sys'] = SystemResolver()
>>> computer['home'] = DirectoryResolver('~')
>>>
>>> list(computer['sys'].keys())
['platform', 'python', 'user', 'cpu', 'disk', 'network', 'memory']
>>>
>>> computer['sys.platform.system']
'Darwin'
>>>
>>> computer['sys.cpu.count']
10
>>>
>>> computer['sys.disk.free_gb']
182.45
>>>
>>> computer['sys.memory.percent']
49.4
>>>
>>> print(computer.to_string(static=False))
├── sys
│   ├── platform
│   │   ├── system: 'Darwin'
│   │   ├── release: '25.1.0'
│   │   ├── machine: 'arm64'
│   │   └── ...
│   ├── python
│   │   ├── version: '3.12.8'
│   │   └── ...
│   ├── cpu
│   │   ├── count: 10
│   │   ├── percent: 12.5
│   │   └── freq_mhz: 3228
│   ├── disk
│   │   ├── total_gb: 460.43
│   │   ├── free_gb: 182.45
│   │   └── percent: 60.4
│   ├── memory
│   │   ├── total_gb: 36.0
│   │   ├── available_gb: 18.23
│   │   └── percent: 49.4
│   └── ...
└── home
    ├── Desktop
    ├── Documents
    └── ...

OpenMeteoResolver

Fetches weather data from Open-Meteo API.

Source: examples/resolvers/openmeteo_resolver/

Single resolver with query string syntax

>>> from genro_bag import Bag
>>> from open_meteo_resolver import OpenMeteoResolver
>>>
>>> bag = Bag()
>>> bag['meteo'] = OpenMeteoResolver()
>>>
>>> # Pass city via query string in path
>>> bag['meteo?city=London']
<Bag with weather data>
>>>
>>> bag['meteo?city=London'].keys()
['temperature_2m', 'wind_speed_10m', 'relative_humidity_2m', 'weather']
>>>
>>> bag['meteo?city=London.temperature_2m']
7.4
>>>
>>> bag['meteo?city=London.weather']
'Overcast'
>>>
>>> # Different city, same resolver
>>> bag['meteo?city=Rome.weather']
'Clear sky'

Multiple cities via node attributes

>>> from genro_bag import Bag
>>> from open_meteo_resolver import OpenMeteoResolver
>>>
>>> meteo = Bag()
>>> cities = ["london", "paris", "rome", "berlin", "madrid"]
>>> for city in cities:
...     # city=city is passed as node attribute, read by resolver at call time
...     meteo.set_item(city, OpenMeteoResolver(), city=city)
>>>
>>> # Change city dynamically via node attribute
>>> meteo.set_attr('london', city='manchester')
>>> meteo['london']  # Now fetches Manchester weather
>>>
>>> print(meteo.to_string(static=False))
├── london [city='london']
│   ├── temperature_2m: 7.4
│   ├── wind_speed_10m: 14.9
│   ├── relative_humidity_2m: 80
│   └── weather: 'Overcast'
├── paris [city='paris']
│   ├── temperature_2m: 5.2
│   ├── wind_speed_10m: 11.2
│   ├── relative_humidity_2m: 92
│   └── weather: 'Overcast'
├── rome [city='rome']
│   ├── temperature_2m: 10.1
│   ├── wind_speed_10m: 5.4
│   ├── relative_humidity_2m: 71
│   └── weather: 'Clear sky'
└── ...

Common Patterns

Configuration with Fallbacks

from genro_bag import Bag
from genro_bag.resolver import BagCbResolver
import os

def load_config():
    # Try environment first
    if os.getenv('APP_CONFIG'):
        return Bag.from_json(os.getenv('APP_CONFIG'))

    # Then config file
    config_path = os.getenv('CONFIG_PATH', '/etc/myapp/config.json')
    if os.path.exists(config_path):
        return Bag.from_json(open(config_path).read())

    # Defaults
    return Bag({'debug': False, 'port': 8080})

app = Bag()
app['config'] = BagCbResolver(load_config, cache_time=False)

Computed Properties

from genro_bag import Bag
from genro_bag.resolver import BagCbResolver

def create_order():
    order = Bag()
    order['items'] = Bag()

    def compute_total():
        total = 0
        for node in order['items']:
            price = node.get_attr('price', 0)
            qty = node.get_attr('qty', 1)
            total += price * qty
        return total

    order['subtotal'] = BagCbResolver(compute_total, cache_time=0)
    return order

order = create_order()
order['items'].set_item('widget', 'Widget', price=10, qty=2)
order['items'].set_item('gadget', 'Gadget', price=25, qty=1)

print(order['subtotal'])  # 45

API Client with Caching

from genro_bag import Bag
from genro_bag.resolvers import UrlResolver

class ApiClient:
    def __init__(self, base_url):
        self.bag = Bag()
        self.base_url = base_url

    def _resolver(self, endpoint, cache_time=300):
        return UrlResolver(
            f'{self.base_url}{endpoint}',
            as_bag=True,
            cache_time=cache_time
        )

    def setup(self):
        self.bag['users'] = self._resolver('/users', cache_time=600)
        self.bag['stats'] = self._resolver('/stats', cache_time=30)
        self.bag['countries'] = self._resolver('/countries', cache_time=False)

api = ApiClient('https://api.example.com')
api.setup()
users = api.bag['users']

Async Usage

Resolvers work seamlessly in async contexts. Use smartawait to await results:

from genro_bag import Bag
from genro_bag.resolvers import UrlResolver
from genro_toolbox import smartawait

async def fetch_weather():
    bag = Bag()
    bag['weather'] = UrlResolver(
        'https://api.open-meteo.com/v1/forecast',
        qs={'latitude': 51.5, 'longitude': -0.1, 'current_weather': True},
        as_bag=True
    )

    # Await the resolver result
    result = await smartawait(bag['weather'])
    return result['current_weather.temperature']

async def fetch_multiple():
    bag = Bag()
    bag['users'] = UrlResolver('https://api.example.com/users', as_bag=True)
    bag['posts'] = UrlResolver('https://api.example.com/posts', as_bag=True)

    # Fetch both in parallel
    import asyncio
    users, posts = await asyncio.gather(
        smartawait(bag['users']),
        smartawait(bag['posts'])
    )
    return users, posts

With query string parameters:

from genro_bag import Bag
from genro_bag.resolver import BagCbResolver
from genro_toolbox import smartawait

async def calculate():
    bag = Bag()
    bag['calc'] = BagCbResolver(lambda x, y: x * y, x=1, y=1)

    # Pass parameters via query string, await result
    result = await smartawait(bag.get_item('calc?x=10&y=5'))
    return result  # 50

With OpenMeteoResolver:

from genro_bag import Bag
from open_meteo_resolver import OpenMeteoResolver
from genro_toolbox import smartawait

async def get_weather_async(city: str):
    bag = Bag()
    bag['meteo'] = OpenMeteoResolver()

    # Query string syntax with await
    weather = await smartawait(bag[f'meteo?city={city}'])
    return weather

async def compare_cities():
    bag = Bag()
    bag['meteo'] = OpenMeteoResolver()

    import asyncio
    london, paris = await asyncio.gather(
        smartawait(bag['meteo?city=London']),
        smartawait(bag['meteo?city=Paris'])
    )

    print(f"London: {london['temperature_2m']}°C")
    print(f"Paris: {paris['temperature_2m']}°C")