Skip to main content

Toolkit for dynamic data modeling

Project description

contextualproperties

Toolkit for creating dynamic data layers for python classes

Installation

contextualproperties can be added to any existing python project via pip:

pip install contextualproperties

How-To Guide

The Basics

You can do a lot with contextualproperties, but the most useful feature is being able to change the output value of an attribute by changing the object's context.

from contextualproperties import contextualobject, contextualproperty

@contextualobject
class ExampleClass:
    
    @contextualproperty
    def property0(self):
        return 'output of property 0, default context'

    @property0.getter(context="context1")
    def property0(self):
        return 'output of property 0, context 1'


if __name__ == '__main__':
    obj = ExampleClass()
    print(obj.property0)
    obj.push_context('context1')
    print(obj.property0)
>>> output of property 0, default context
>>> output of property 0, context 1

Intermediate Example

Contextual properties are very useful if what you are working with involves a complex data structure where sometimes you need to interact with an object, and other times you need to get the raw data.

In this example, let's assume we have an object for keeping track of what accounts belong to the same user.

from typing import List
from dataclasses import dataclass
from contextualproperties import contextualobject, contextualproperty

@dataclass
class Account:
  """
  Takes an account type, username, and link to profile. Produces markdown
  formatted information as a string
  """
  type: str
  user: str
  url: str

@contextualobject
class Accounts:
    """
    Returns aliases from an underlying data structure
    """ 
    @contextualproperty
    def accounts(self) -> List[Account]:
        return [
          Account(**account) for account in self._data.get("accounts", [])
        ]
    
    @accounts.getter(context="raw")
    def accounts(self) -> List[dict]:
        return self._data.get("accounts", [])
    
    def __init__(self, **data):
        self._data = data

if __name__ == '__main__':
    example_data = {
        "uuid": "9a3d7e79-4e24-467d-8444-eaff377bb56d",
        "accessed": "2026-03-31",
        "accounts": [
            {
                "type": "mastodon",
                "user": "malogan@mastodon.social",
                "url": "https://mastodon.social/@malogan"
            },
            {
                "type": "codeberg",
                "user": "malogan",
                "url": "https://codeberg.org/malogan"
            }
        ]
    }
    
    obj = Accounts(**example_data)
    
    print("Object form:")
    for account in obj.accounts:
        print(account)
    print("Raw form:")
    obj.push_context('raw')
    for account in obj.accounts:
        print(account)
>>> Object form:
>>> Alias(type='mastodon', user='malogan@mastodon.social', url='https://mastodon.social/@malogan')
>>> Alias(type='codeberg', user='malogan', url='https://codeberg.org/malogan')
>>> Raw form:
>>> {'type': 'mastodon', 'user': 'malogan@mastodon.social', 'url': 'https://mastodon.social/@malogan'}
>>> {'type': 'codeberg', 'user': 'malogan', 'url': 'https://codeberg.org/malogan'}

Layering

Contexts work as layers and properties will use the most recent layer they have a recognized context for. For example, if your object has five active context layers and you try to access a property that has two relevant matches, it will return the value of the most recent match for that property.

In this example, property0 has contexts 1 and 3, and property1 has contexts 1 and 2. When the properties are called, the most recent relevant context will be selected for each.

from typing import List
from dataclasses import dataclass
from contextualproperties import contextualobject, contextualproperty


@contextualobject
class ExampleClass:
    """
    Property 0 has contexts 1 and 3, Property 1 has contexts 1 and 2
    """
    
    @contextualproperty
    def property0(self):
        return "property 0 default context"
    
    @property0.getter(context="context1")
    def property0(self):
        return "property 0 context 1"
    
    @property0.getter(context="context3")
    def property0(self):
        return "property 0 context 3"
    
    @contextualproperty
    def property1(self):
        return "property 1 default context"
    
    @property1.getter(context="context1")
    def property1(self):
        return "property 1 context 1"
    
    @property1.getter(context="context2")
    def property1(self):
        return "property 1 context 2"


if __name__ == '__main__':
    obj = ExampleClass()
    print(f"{obj.property0} | {obj.property1}")
    
    obj.push_context("context1")
    print(f"{obj.property0} | {obj.property1}")

    obj.push_context("context2")
    print(f"{obj.property0} | {obj.property1}")

    obj.push_context("context3")
    print(f"{obj.property0} | {obj.property1}")
>>> property 0 default context | property 1 default context
>>> property 0 context 1 | property 1 context 1
>>> property 0 context 1 | property 1 context 2
>>> property 0 context 3 | property 1 context 2

Caching

Contextual properties have an optional cache on a per-context basis. You can set a context to be cached by providing cache=True in the decorator and the value will be stored.

In this example, we have a getter function that reconstructs a Person object whenever it is called. To prevent this unnecessary work, rather than reconstructing the dataclass every time the property is used, the cache will give us the same object that was previously created.

Notice that the UUID in contact and contact2 is the same. Had the object been regenerated, the UUID would have changed in the second object.

from dataclasses import dataclass, field
from contextualproperties import contextualobject, contextualproperty
from uuid import uuid4

@dataclass
class Person:
    name: str
    uuid: uuid4 = field(default_factory = uuid4)
    address: str = None
    phone: str = None


@contextualobject
class User:
    
    @contextualproperty(cache=True)
    def contact_card(self) -> Person:
        # in an actual setup you would probably pull this from a db.
        # the uuid has been left off intentionally for demo purposes
        return Person(name = self._data.get('name'), **self._data.get('contact'))
    
    def __init__(self, **data):
      self._data = data

if __name__ == '__main__':
    obj = User(
      **{
        'username': 'malogan',
        'name': 'Mason Logan',
        'contact': {
          'address': '123 Road St, Townplace, NC 12345',
          'phone': '+1 123-456-7890'
        }
      }
    )

    contact = obj.contact_card
    print(contact)

    contact2 = obj.contact_card
    print(contact)
    print(f"Same object: {contact is contact2}")
>>> Person(name='Mason Logan', uuid=UUID('15a1d938-8fff-484c-8291-9966eacfeef9'), address='123 Road St, Townplace, NC 12345', phone='+1 123-456-7890')
>>> Person(name='Mason Logan', uuid=UUID('15a1d938-8fff-484c-8291-9966eacfeef9'), address='123 Road St, Townplace, NC 12345', phone='+1 123-456-7890')
>>> Same object: True

Cache Invalidation

Property setters can be used to invalidate a cache by providing them with one or more context values in the invalidate parameter

from dataclasses import dataclass, field
from contextualproperties import contextualobject, contextualproperty
from uuid import uuid4

@dataclass
class Person:
    name: str
    uuid: uuid4 = field(default_factory = uuid4)
    address: str = None
    phone: str = None


@contextualobject
class User:
  
    @contextualproperty
    def name(self) -> str:
        return getattr(self, '__User_name', None)
    
    @name.setter
    def name(self, name):
        setattr(self, '__User_name', name)
    
    @contextualproperty(cache=True)
    def contact_card(self) -> Person:
        # in an actual setup you would probably pull this from a db.
        # the uuid has been left off intentionally for demo purposes
        return Person(name=self.name, **self._data.get('contact'))
    
    def __init__(self, name, **data):
      self.name = name
      self._data = data

if __name__ == '__main__':
    obj = User(
      **{
        'username': 'malogan',
        'name': 'Mason Logan',
        'contact': {
          'address': '123 Road St, Townplace, NC 12345',
          'phone': '+1 123-456-7890'
        }
      }
    )

    contact = obj.contact_card
    print(contact)

    contact2 = obj.contact_card
    print(contact)
    print(f"Same object: {contact is contact2}")

Cache Access

Sometimes your setter may need to interact with objects in your cache, like if you are modifying an underlying data layer and want to update the objects representing those parts (i.e avoid full reevaluation of complex objects every time a single value changes). By passing cache_aware=True into the setter decorator, you can add a new parameter to the setter function to access the cache.

NOTE: directly modifying the cache is discouraged for most purposes, but it is a very powerful tool when done right.

More Examples

Check the "examples" folder for a recipe book on how to use contextual properties to put together dynamic classes quickly

Full Docs

As soon as I get MKDocs or a similar static doc generator running, I will make the pages available

TODO: set up a static docs page

Licensing

This project is licensed under the LGPL 3.0. If you need a different license for some specific purpose, please reach out.

Roadmap

Check ROADMAP.md for full details, but the following are features planned in the near term:

  • timeouts and maximum uses for cached values
    • setting up for passing in user-defined functions for condition-based decache
  • ability to pass a dict into invalidate to clear cached values for other properties
  • PropertyTemplate class for passing into @contextualobject class to avoid messy inheritance trees and jungles of boilerplate copy-paste
    • definition of base properties that can be expanded on
    • proper MRO modification for class to put ContextualObject and anything pulled from templates at bottom of heirarchy
  • context grouping
    • pass multiple context values to duplicate functionality
  • setter return value caching
    • allow setter functions to return a value that replaces cached values under the same property

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

contextualproperties-0.1.4.tar.gz (31.8 kB view details)

Uploaded Source

Built Distribution

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

contextualproperties-0.1.4-py3-none-any.whl (36.5 kB view details)

Uploaded Python 3

File details

Details for the file contextualproperties-0.1.4.tar.gz.

File metadata

  • Download URL: contextualproperties-0.1.4.tar.gz
  • Upload date:
  • Size: 31.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.3

File hashes

Hashes for contextualproperties-0.1.4.tar.gz
Algorithm Hash digest
SHA256 80ad9072dfa267f024ed76dfe696848c254777085ccc317419ba6d41f149e130
MD5 25a0700ec7971401794cf8ca8820257a
BLAKE2b-256 3939d09f7457c5777af7aabcfee2c908ee9e7f826f3734cda19ceea593eb5826

See more details on using hashes here.

File details

Details for the file contextualproperties-0.1.4-py3-none-any.whl.

File metadata

File hashes

Hashes for contextualproperties-0.1.4-py3-none-any.whl
Algorithm Hash digest
SHA256 0653db4edab0688e49f8965bd5199548e73ef65c445f9a6753b05e6f1d43236c
MD5 d1eb723f14dc07269fe559c4faf465b4
BLAKE2b-256 68d95f749c61fc1630ca6b7d4f02af021caf4eb990ab57753c474dae75586a78

See more details on using hashes here.

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