{"id":45526014,"url":"https://github.com/saurabh0719/object-tracker","last_synced_at":"2026-02-22T23:49:17.612Z","repository":{"id":142866847,"uuid":"614057867","full_name":"saurabh0719/object-tracker","owner":"saurabh0719","description":"A pure python Object state change tracker. Monitor all changes in the object's lifecycle and trigger callback functions. :pencil:","archived":false,"fork":false,"pushed_at":"2024-04-11T15:31:35.000Z","size":153,"stargazers_count":27,"open_issues_count":1,"forks_count":1,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-09-19T03:07:06.698Z","etag":null,"topics":["change-detection","changelog","inheritance","log","object-logging","tracker"],"latest_commit_sha":null,"homepage":"https://pypi.org/project/object-tracker/","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"bsd-3-clause","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/saurabh0719.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null}},"created_at":"2023-03-14T20:03:09.000Z","updated_at":"2025-01-17T14:35:23.000Z","dependencies_parsed_at":"2024-04-11T15:55:20.665Z","dependency_job_id":null,"html_url":"https://github.com/saurabh0719/object-tracker","commit_stats":{"total_commits":54,"total_committers":1,"mean_commits":54.0,"dds":0.0,"last_synced_commit":"30e238a9ce31debf4e6cc07d42ca57bb9a815756"},"previous_names":[],"tags_count":5,"template":false,"template_full_name":null,"purl":"pkg:github/saurabh0719/object-tracker","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/saurabh0719%2Fobject-tracker","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/saurabh0719%2Fobject-tracker/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/saurabh0719%2Fobject-tracker/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/saurabh0719%2Fobject-tracker/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/saurabh0719","download_url":"https://codeload.github.com/saurabh0719/object-tracker/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/saurabh0719%2Fobject-tracker/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29731263,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-22T20:09:16.275Z","status":"ssl_error","status_checked_at":"2026-02-22T20:09:13.750Z","response_time":110,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["change-detection","changelog","inheritance","log","object-logging","tracker"],"created_at":"2026-02-22T23:49:16.941Z","updated_at":"2026-02-22T23:49:17.599Z","avatar_url":"https://github.com/saurabh0719.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cp align=\"left\"\u003e\n    \u003cimg src=\"https://github.com/saurabh0719/object-tracker/assets/127945292/d7e9dbc2-9b55-4fae-b40a-bd47dcd8979b\" width =\"600\"\u003e\n\u003c/p\u003e\n\n\u003cbr\u003e\n\nA 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:\n\n```sh\n$ pip install object-tracker\n```\n\n\u003cdiv align=\"center\"\u003e\n    \u003cstrong\u003e\u003ca href=\"https://github.com/saurabh0719/object-tracker\"\u003eGithub\u003c/a\u003e | \u003ca href=\"https://saurabh0719.github.io\"\u003eWebsite\u003c/a\u003e | \u003ca href=\"https://github.com/saurabh0719/object-tracker/releases\"\u003eRelease notes\u003c/a\u003e \u003c/strong\u003e\n\u003c/div\u003e\n\nTested for python `3.7` and above.\n\n\u003cspan id=\"features\"\u003e\u003c/span\u003e\n## Key Features\n\n-  Determine if a python object has changed state during it's lifecycle.\n-  Investigate change history by querying a structured changelog.\n-  Trigger callback functions whenever an (or any) attribute has changed.\n-  Use it as a decorator, a class mixin or on its own.\n\n\u003chr\u003e\n\n\u003cspan id=\"contents\"\u003e\u003c/span\u003e\n## Table of Contents :\n* [Key Features](#features)\n* [Basic Usage](#usage)   \n* [How does it work?](#how)\n* [API](#trackerapi)\n  * [@track](#track)\n  * [TrackerMixin](#trackermixin)\n  * [Tracker](#tracker)\n  * [ChangeLog](#changelog)\n* [Tests](#tests)\n* [Release notes](#releases)\n* [License](#license) \n\n\n\u003cspan id=\"usage\"\u003e\u003c/span\u003e\n## Basic Usage \n\nUse the `@track` decorator to track an object's attributes.\n\n```python\n\nfrom object_tracker import track\n\ndef observer(attr, old, new):\n    print(f\"Observer : {attr} -\u003e {old} - {new}\")\n\n@track('name', 'age', observers=[observer,])\nclass User:\n    def __init__(self, name, age):\n        self.name = name\n        self.age = age\n\nuser = User(name='Alice', age=30)\nuser.name = 'Bob'\n# Observer : name -\u003e Alice - Bob\nprint(user.tracker.has_changed()) \n# True\nprint(user.tracker.has_attribute_changed('name'))\n# True\n\n```\n\nOr use the `Tracker` class \n\n```python\n\nclass MyClass:\n        pass\n    \nobj = MyClass()\ntracker = Tracker(obj)\nobj.attribute = 'new_value'\nprint(tracker.has_changed(obj))\n# True\n\n```\n\nOr use it with the mixin class `TrackerMixin`\n\n```python\n\nfrom object_tracker import TrackerMixin, Tracker\n    \nclass User(TrackerMixin):\n    def __init__(self, name, age):\n        self.name = name\n        self.age = age\n        self.tracker = Tracker()\n\n```\n\n\u003cspan id=\"help\"\u003e\u003c/span\u003e\n## How does it work?\n\nThe decorator `@track` and the mixin `TrackerMixin` implement the `__setattr__` and `__setitem__` dunder methods to intercept and log a change to an attribute. \n\nThe tracker is an instance of the `Tracker` class which logs all changes to the `ChangeLog`\n\nThe entire module is roughly 310 LOC, don't hesitate to read from source directly!\n\n[Go back to the table of contents](#contents)\n\n\n\u003cspan id=\"trackerapi\"\u003e\u003c/span\u003e\n## API\n\n\n\u003cspan id=\"track\"\u003e\u003c/span\u003e\n## @track\n\n```python\ndef track(\n    *attributes: List[str],\n    observers: List[ObserverType] = None,\n    attribute_observer_map: Dict[str, List[ObserverType]] = None,\n    auto_notify: bool = True,\n    stack_trace: bool = True,\n    tracker_attribute: str = 'tracker',\n    changes_only: bool = False,\n):\n    \"\"\"\n    Decorator for tracking attribute changes in a class.\n\n    ```\n    from object_tracker import track\n\n    @track('name', 'age')\n    class User:\n        def __init__(self, name, age):\n            self.name = name\n            self.age = age\n\n    user = User('Alice', 30)\n    user.name = 'Bob'\n    print(user.tracker.has_changed('name')) # True\n    ```\n\n    Args:\n        *attributes: \n            The attributes to track.\n\n        observers (list, optional): \n            The observers to notify when an attribute changes. Default is None.\n\n        attribute_observer_map (dict, optional): \n            A map of attributes to observers.Default is None.\n\n        auto_notify (bool, optional):\n            Whether to automatically notify observers when an attribute changes.\n            Default is True.\n\n        stack_trace (bool, optional):\n            Whether to store the call stack when an attribute changes. Default is True.\n\n        tracker_attribute (str, optional):\n            The attribute holding the Tracker object. Default is 'tracker'.\n\n        changes_only (bool, optional):\n            Whether to track only changes to attributes or all assignments.\n            Default is False.\n\n    Returns:\n        The decorated class with attribute tracking.\n    \"\"\"\n```\n\n[Go back to the table of contents](#contents)\n\n\n\u003cspan id=\"trackermixin\"\u003e\u003c/span\u003e\n## TrackerMixin\n\n```python\nclass TrackerMixin:\n    \"\"\"\n    Mixin class for tracking attribute changes.\n    Overrides the `__setattr__` and `__setitem__` methods to track changes.\n\n    It uses the `Tracker` object, stored in `tracker_attr`, to record changes.\n    Modify the `tracker_attr` attribute to change the attribute name.\n\n    ```\n    from object_tracker import TrackerMixin, Tracker\n    \n    class User(TrackerMixin):\n        def __init__(self, name, age):\n            self.name = name\n            self.age = age\n            self.tracker = Tracker()\n    ```\n\n    Attributes:\n        tracker_attr (str):\n            The attribute holding the Tracker object. Default is `tracker`.\n    \"\"\"\n```\n\n[Go back to the table of contents](#contents)\n\n\n\u003cspan id=\"tracker\"\u003e\u003c/span\u003e\n## Tracker\n\n```python\nclass Tracker:\n    \"\"\"\n    The Tracker class is responsible for tracking changes to an object's attributes.\n    ```\n    from object_tracker import Tracker\n\n    # Track changes to an object's attributes.\n    class MyClass:\n        pass\n    \n    obj = MyClass()\n    tracker = Tracker(obj)\n    obj.attribute = 'new_value'\n    print(tracker.has_changed(obj))\n\n\n    # Manually calling the track method to track changes to an attribute.\n    tracker = Tracker()\n    tracker.track('attribute', 'old_value', 'new_value')\n    print(tracker.has_attribute_changed('attribute'))\n    ```\n    \"\"\"\n\n    def __init__(\n        self,\n        initial_state: any = None,\n        attributes: List[str] = None,\n        observers: List[ObserverType] = None,\n        attribute_observer_map: Dict[str, List[ObserverType]] = None,\n        auto_notify: bool = True,\n        stack_trace: bool = True,\n        changes_only: bool = False,\n    ) -\u003e None:\n        \"\"\"\n        Initializes the Tracker instance.\n\n        Args:\n            initial_state (any):\n                The initial state of the object to be tracked. Default is None.\n\n            attributes (List[str]): \n                The attributes to track. Default is None ie. all attributes are tracked.\n\n            observers (List[ObserverType]):\n                The list of observers to notify on attribute change. Default is None.\n\n            attribute_observer_map (Dict[str, List[ObserverType]]):\n                A map of attributes to observers. Default is None.\n\n            auto_notify (bool):\n                Whether to automatically notify observers on attribute change.\n                Default is True.\n\n            stack_trace (bool):\n                Whether to store the call stack when an attribute changes.\n                Default is True.\n\n            changes_only (bool):\n                Whether to track only the attributes that have changed.\n                Default is False.\n\n        Attributes:\n            log (ChangeLog):\n                The log to store attribute changes.\n        \"\"\"\n```\n\n[Go back to the table of contents](#contents)\n\n\n\u003cspan id=\"changelog\"\u003e\u003c/span\u003e\n## ChangeLog\n\nThe `tracker` instance has the `log` inside it containing a list of `Entry` objs\n\nEach `Entry` in a change log has -\n-   `attr` - The attribute that was changed\n-   `old` - copy of the old value\n-   `new` - copy of the new value\n-   `timestamp` - UTC datetime\n-   `stack` - a list of `frames` from inspect leading up to the change.\n\n\n```python\nclass ChangeLog:\n    \"\"\"\n    The ChangeLog class is responsible for storing and managing a log of attribute changes.\n\n    This class provides methods to add new entries to the log, filter the log based on attribute names, \n    exclude certain attributes from the log, and clear the log.\n\n    Methods:\n\n    - push(attr, old, new, stack=None): Pushes a new entry to the log.\n\n    - filter(*attrs, changes_only=False): Filters the log based on the given attributes.\n\n    - exclude(*attrs, changes_only=False): Excludes the given attributes from the log.\n\n    - first(): Returns the first log entry.\n\n    - last(): Returns the last log entry.\n\n    - all(): Returns all log entries.\n\n    - count(): Returns the number of log entries.\n\n    - replay(): A generator to print the logs in a human-readable format.\n\n    - get_unique_attributes(): Returns all attributes in the log.\n\n    - has_changed(attr): Checks if any attribute of the object has been changed by verifying against the log.\n\n    - reset_buffer(): Resets the buffer.\n\n    Eg.\n\n        The `tracker` obj has the `log` attribute which is an instance of the `ChangeLog` class.\n\n        tracker.log.filter('name', 'age') -\u003e Returns logs for 'name' and 'age' attributes\n\n        tracker.log.exclude('name') -\u003e Excludes logs for 'name' attribute\n\n        tracker.log.first() -\u003e Returns the first log entry\n\n        tracker.filter('name').count() -\u003e Returns the number of log entries for 'name' attribute\n    \"\"\"\n```\n\n\n[Go back to the table of contents](#contents)\n\n\n\u003cspan id=\"examples\"\u003e\u003c/span\u003e\n## Examples\n\nThe `replay()` method was repurposed from ![Madison May's tracker](https://github.com/madisonmay/tracker).\n\n```python\nfrom object_tracker import track\n\n@track('name', 'age')\nclass User:\n    def __init__(self, name, age):\n        self.name = name\n        self.age = age\n\nuser = User(name='Alice', age=30)\nuser.name = 'Bob'\nuser.name = 'John'\nuser.age = 31\n\nprint(\"\\n Has object changed - \", user.tracker.has_changed()) # True\nprint(\"\\n Has 'name' changed - \", user.tracker.has_attribute_changed('name')) # True\nprint(\"\\n Num of changes in 'name' - \", user.tracker.log.filter('name').count()) # 2\nprint(\"\\n Num of changes in 'age' - \", user.tracker.log.filter('age').count()) # 1\n\nprint(\"\\n All logs - \")\nfor log in user.tracker.log.replay():\n    print(log)\n\n```\n\nOutput - \n\n```\n\n Has object changed -  True\n\n Has 'name' changed -  True\n\n Num of changes in 'name' -  2\n\n Num of changes in 'age' -  1\n\n All logs - \n--------------------------------------------------\nname = 'Bob'\n\n    trial.py: 10 - \u003cmodule\u003e\n    user.name = 'Bob'\n\n--------------------------------------------------\nname = 'John'\n\n    trial.py: 11 - \u003cmodule\u003e\n    user.name = 'John'\n\n--------------------------------------------------\nage = 31\n\n    trial.py: 12 - \u003cmodule\u003e\n    user.age = 31\n\n```\n\n\u003chr\u003e\n\n\u003cspan id=\"tests\"\u003e\u003c/span\u003e\n## Tests \n\nRun this command inside the base directory to execute all tests inside the `tests` folder:\n\n```sh\n$ python -m unittest -v\n```\n\n[Go back to the table of contents](#contents)\n\n\u003chr\u003e\n\n\u003cspan id=\"releases\"\u003e\u003c/span\u003e\n## Release notes \n\n* Latest - `v2.0.0` \n\nView object-tracker's detailed [release history](https://github.com/saurabh0719/object-tracker/releases/).\n\n[Go back to the table of contents](#contents)\n\n\u003chr\u003e\n\n\u003cspan id=\"license\"\u003e\u003c/span\u003e\n## License\n\n```\nCopyright (c) Saurabh Pujari\nAll rights reserved.\n\nThis source code is licensed under the BSD-style license found in the LICENSE file in the root directory of this source tree.\n```\n\n[Go back to the table of contents](#contents)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsaurabh0719%2Fobject-tracker","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsaurabh0719%2Fobject-tracker","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsaurabh0719%2Fobject-tracker/lists"}