https://github.com/saurabh0719/object-tracker
A pure python Object state change tracker. Monitor all changes in the object's lifecycle and trigger callback functions. :pencil:
https://github.com/saurabh0719/object-tracker
change-detection changelog inheritance log object-logging tracker
Last synced: 4 months ago
JSON representation
A pure python Object state change tracker. Monitor all changes in the object's lifecycle and trigger callback functions. :pencil:
- Host: GitHub
- URL: https://github.com/saurabh0719/object-tracker
- Owner: saurabh0719
- License: bsd-3-clause
- Created: 2023-03-14T20:03:09.000Z (over 3 years ago)
- Default Branch: main
- Last Pushed: 2024-04-11T15:31:35.000Z (about 2 years ago)
- Last Synced: 2025-09-19T03:07:06.698Z (10 months ago)
- Topics: change-detection, changelog, inheritance, log, object-logging, tracker
- Language: Python
- Homepage: https://pypi.org/project/object-tracker/
- Size: 149 KB
- Stars: 27
- Watchers: 1
- Forks: 1
- Open Issues: 1
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
A pure python object state tracker. Monitor all changes in your object's lifecycle, query the history changelog, and trigger callback functions to capture them. :pencil:
```sh
$ pip install object-tracker
```
Tested for python `3.7` and above.
## Key Features
- Determine if a python object has changed state during it's lifecycle.
- Investigate change history by querying a structured changelog.
- Trigger callback functions whenever an (or any) attribute has changed.
- Use it as a decorator, a class mixin or on its own.
## Table of Contents :
* [Key Features](#features)
* [Basic Usage](#usage)
* [How does it work?](#how)
* [API](#trackerapi)
* [@track](#track)
* [TrackerMixin](#trackermixin)
* [Tracker](#tracker)
* [ChangeLog](#changelog)
* [Tests](#tests)
* [Release notes](#releases)
* [License](#license)
## Basic Usage
Use the `@track` decorator to track an object's attributes.
```python
from object_tracker import track
def observer(attr, old, new):
print(f"Observer : {attr} -> {old} - {new}")
@track('name', 'age', observers=[observer,])
class User:
def __init__(self, name, age):
self.name = name
self.age = age
user = User(name='Alice', age=30)
user.name = 'Bob'
# Observer : name -> Alice - Bob
print(user.tracker.has_changed())
# True
print(user.tracker.has_attribute_changed('name'))
# True
```
Or use the `Tracker` class
```python
class MyClass:
pass
obj = MyClass()
tracker = Tracker(obj)
obj.attribute = 'new_value'
print(tracker.has_changed(obj))
# True
```
Or use it with the mixin class `TrackerMixin`
```python
from object_tracker import TrackerMixin, Tracker
class User(TrackerMixin):
def __init__(self, name, age):
self.name = name
self.age = age
self.tracker = Tracker()
```
## How does it work?
The decorator `@track` and the mixin `TrackerMixin` implement the `__setattr__` and `__setitem__` dunder methods to intercept and log a change to an attribute.
The tracker is an instance of the `Tracker` class which logs all changes to the `ChangeLog`
The entire module is roughly 310 LOC, don't hesitate to read from source directly!
[Go back to the table of contents](#contents)
## API
## @track
```python
def track(
*attributes: List[str],
observers: List[ObserverType] = None,
attribute_observer_map: Dict[str, List[ObserverType]] = None,
auto_notify: bool = True,
stack_trace: bool = True,
tracker_attribute: str = 'tracker',
changes_only: bool = False,
):
"""
Decorator for tracking attribute changes in a class.
```
from object_tracker import track
@track('name', 'age')
class User:
def __init__(self, name, age):
self.name = name
self.age = age
user = User('Alice', 30)
user.name = 'Bob'
print(user.tracker.has_changed('name')) # True
```
Args:
*attributes:
The attributes to track.
observers (list, optional):
The observers to notify when an attribute changes. Default is None.
attribute_observer_map (dict, optional):
A map of attributes to observers.Default is None.
auto_notify (bool, optional):
Whether to automatically notify observers when an attribute changes.
Default is True.
stack_trace (bool, optional):
Whether to store the call stack when an attribute changes. Default is True.
tracker_attribute (str, optional):
The attribute holding the Tracker object. Default is 'tracker'.
changes_only (bool, optional):
Whether to track only changes to attributes or all assignments.
Default is False.
Returns:
The decorated class with attribute tracking.
"""
```
[Go back to the table of contents](#contents)
## TrackerMixin
```python
class TrackerMixin:
"""
Mixin class for tracking attribute changes.
Overrides the `__setattr__` and `__setitem__` methods to track changes.
It uses the `Tracker` object, stored in `tracker_attr`, to record changes.
Modify the `tracker_attr` attribute to change the attribute name.
```
from object_tracker import TrackerMixin, Tracker
class User(TrackerMixin):
def __init__(self, name, age):
self.name = name
self.age = age
self.tracker = Tracker()
```
Attributes:
tracker_attr (str):
The attribute holding the Tracker object. Default is `tracker`.
"""
```
[Go back to the table of contents](#contents)
## Tracker
```python
class Tracker:
"""
The Tracker class is responsible for tracking changes to an object's attributes.
```
from object_tracker import Tracker
# Track changes to an object's attributes.
class MyClass:
pass
obj = MyClass()
tracker = Tracker(obj)
obj.attribute = 'new_value'
print(tracker.has_changed(obj))
# Manually calling the track method to track changes to an attribute.
tracker = Tracker()
tracker.track('attribute', 'old_value', 'new_value')
print(tracker.has_attribute_changed('attribute'))
```
"""
def __init__(
self,
initial_state: any = None,
attributes: List[str] = None,
observers: List[ObserverType] = None,
attribute_observer_map: Dict[str, List[ObserverType]] = None,
auto_notify: bool = True,
stack_trace: bool = True,
changes_only: bool = False,
) -> None:
"""
Initializes the Tracker instance.
Args:
initial_state (any):
The initial state of the object to be tracked. Default is None.
attributes (List[str]):
The attributes to track. Default is None ie. all attributes are tracked.
observers (List[ObserverType]):
The list of observers to notify on attribute change. Default is None.
attribute_observer_map (Dict[str, List[ObserverType]]):
A map of attributes to observers. Default is None.
auto_notify (bool):
Whether to automatically notify observers on attribute change.
Default is True.
stack_trace (bool):
Whether to store the call stack when an attribute changes.
Default is True.
changes_only (bool):
Whether to track only the attributes that have changed.
Default is False.
Attributes:
log (ChangeLog):
The log to store attribute changes.
"""
```
[Go back to the table of contents](#contents)
## ChangeLog
The `tracker` instance has the `log` inside it containing a list of `Entry` objs
Each `Entry` in a change log has -
- `attr` - The attribute that was changed
- `old` - copy of the old value
- `new` - copy of the new value
- `timestamp` - UTC datetime
- `stack` - a list of `frames` from inspect leading up to the change.
```python
class ChangeLog:
"""
The ChangeLog class is responsible for storing and managing a log of attribute changes.
This class provides methods to add new entries to the log, filter the log based on attribute names,
exclude certain attributes from the log, and clear the log.
Methods:
- push(attr, old, new, stack=None): Pushes a new entry to the log.
- filter(*attrs, changes_only=False): Filters the log based on the given attributes.
- exclude(*attrs, changes_only=False): Excludes the given attributes from the log.
- first(): Returns the first log entry.
- last(): Returns the last log entry.
- all(): Returns all log entries.
- count(): Returns the number of log entries.
- replay(): A generator to print the logs in a human-readable format.
- get_unique_attributes(): Returns all attributes in the log.
- has_changed(attr): Checks if any attribute of the object has been changed by verifying against the log.
- reset_buffer(): Resets the buffer.
Eg.
The `tracker` obj has the `log` attribute which is an instance of the `ChangeLog` class.
tracker.log.filter('name', 'age') -> Returns logs for 'name' and 'age' attributes
tracker.log.exclude('name') -> Excludes logs for 'name' attribute
tracker.log.first() -> Returns the first log entry
tracker.filter('name').count() -> Returns the number of log entries for 'name' attribute
"""
```
[Go back to the table of contents](#contents)
## Examples
The `replay()` method was repurposed from .
```python
from object_tracker import track
@track('name', 'age')
class User:
def __init__(self, name, age):
self.name = name
self.age = age
user = User(name='Alice', age=30)
user.name = 'Bob'
user.name = 'John'
user.age = 31
print("\n Has object changed - ", user.tracker.has_changed()) # True
print("\n Has 'name' changed - ", user.tracker.has_attribute_changed('name')) # True
print("\n Num of changes in 'name' - ", user.tracker.log.filter('name').count()) # 2
print("\n Num of changes in 'age' - ", user.tracker.log.filter('age').count()) # 1
print("\n All logs - ")
for log in user.tracker.log.replay():
print(log)
```
Output -
```
Has object changed - True
Has 'name' changed - True
Num of changes in 'name' - 2
Num of changes in 'age' - 1
All logs -
--------------------------------------------------
name = 'Bob'
trial.py: 10 -
user.name = 'Bob'
--------------------------------------------------
name = 'John'
trial.py: 11 -
user.name = 'John'
--------------------------------------------------
age = 31
trial.py: 12 -
user.age = 31
```
## Tests
Run this command inside the base directory to execute all tests inside the `tests` folder:
```sh
$ python -m unittest -v
```
[Go back to the table of contents](#contents)
## Release notes
* Latest - `v2.0.0`
View object-tracker's detailed [release history](https://github.com/saurabh0719/object-tracker/releases/).
[Go back to the table of contents](#contents)
## License
```
Copyright (c) Saurabh Pujari
All rights reserved.
This source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree.
```
[Go back to the table of contents](#contents)