├── tests ├── __init__.py ├── test_frozen.py ├── test_validation.py └── test_nestdict.py ├── nestdict ├── __init__.py ├── all_keys.py ├── find_in_map.py ├── change_value.py ├── check_keys.py └── main.py ├── setup.py ├── LICENSE ├── COMMIT_GUIDELINES.md ├── .gitignore ├── TODO.md └── README.md /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /nestdict/__init__.py: -------------------------------------------------------------------------------- 1 | # from .find_in_map import find_in_map 2 | # from .all_keys import all_keys 3 | # from .check_keys import check_keys 4 | # from .change_value import change_value 5 | from .main import NestDict 6 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | from setuptools import setup, find_packages 3 | 4 | HERE = pathlib.Path(__file__).parent 5 | 6 | VERSION = "1.0.1" 7 | PACKAGE_NAME = "nestdict" 8 | AUTHOR = "Abhishek Saini" 9 | AUTHOR_EMAIL = "abhisaini880@email.com" 10 | URL = "https://github.com/abhisaini880/nestdict" 11 | 12 | LICENSE = "Apache License 2.0" 13 | DESCRIPTION = ( 14 | "Utility/Helper functions build around dictionary data type in Python" 15 | ) 16 | LONG_DESCRIPTION = (HERE / "README.md").read_text() 17 | LONG_DESC_TYPE = "text/markdown" 18 | 19 | setup( 20 | name=PACKAGE_NAME, 21 | version=VERSION, 22 | description=DESCRIPTION, 23 | long_description=LONG_DESCRIPTION, 24 | long_description_content_type=LONG_DESC_TYPE, 25 | author=AUTHOR, 26 | license=LICENSE, 27 | author_email=AUTHOR_EMAIL, 28 | url=URL, 29 | packages=find_packages(), 30 | ) 31 | -------------------------------------------------------------------------------- /tests/test_frozen.py: -------------------------------------------------------------------------------- 1 | # import unittest 2 | # from your_module import FrozenDict 3 | 4 | 5 | # class TestFrozenDict(unittest.TestCase): 6 | 7 | # def setUp(self): 8 | # self.sample_data = {"user": {"name": "Alice", "age": 30}} 9 | 10 | # def test_freeze_keys(self): 11 | # frozen_keys = ["user.age"] 12 | # nest_dict = FrozenDict(data=self.sample_data, frozen_keys=frozen_keys) 13 | # with self.assertRaises(TypeError): 14 | # nest_dict["user.age"] = 31 15 | 16 | # def test_freeze_after_set(self): 17 | # nest_dict = FrozenDict(data=self.sample_data) 18 | # nest_dict["user.name"] = "Bob" 19 | # nest_dict.freeze_keys(["user.name"]) 20 | # with self.assertRaises(TypeError): 21 | # nest_dict["user.name"] = "Charlie" 22 | 23 | # def test_non_frozen_keys_still_modifiable(self): 24 | # frozen_keys = ["user.age"] 25 | # nest_dict = FrozenDict(data=self.sample_data, frozen_keys=frozen_keys) 26 | # nest_dict["user.name"] = "Bob" 27 | # self.assertEqual(nest_dict.get("user.name"), "Bob") 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 NestDict 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tests/test_validation.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from nestdict import NestDict 3 | 4 | 5 | class TestValidationDict(unittest.TestCase): 6 | 7 | def setUp(self): 8 | self.sample_data = { 9 | "user": {"name": "Alice", "age": 30}, 10 | "preferences": {"language": ["English", "French"]}, 11 | } 12 | 13 | def test_valid_data_with_validation(self): 14 | validation = {"user.name": str, "user.age": int} 15 | nest_dict = NestDict(data=self.sample_data, validation=validation) 16 | self.assertEqual(nest_dict.get("user.name"), "Alice") 17 | 18 | def test_invalid_data_type(self): 19 | validation = {"user.name": str, "user.age": str} 20 | with self.assertRaises(ValueError): 21 | NestDict(data=self.sample_data, validation=validation) 22 | 23 | def test_set_with_invalid_type(self): 24 | validation = {"user.age": int} 25 | nest_dict = NestDict(data=self.sample_data, validation=validation) 26 | with self.assertRaises(ValueError): 27 | nest_dict["user.age"] = "thirty" 28 | 29 | def test_set_with_valid_type(self): 30 | validation = {"user.age": int} 31 | nest_dict = NestDict(data=self.sample_data, validation=validation) 32 | nest_dict["user.age"] = 35 33 | self.assertEqual(nest_dict.get("user.age"), 35) 34 | -------------------------------------------------------------------------------- /nestdict/all_keys.py: -------------------------------------------------------------------------------- 1 | """ 2 | # Implementation of all_keys function. 3 | 4 | * It will return all the keys present in the object. 5 | 6 | * It will also return the nested keys at all levels. 7 | 8 | """ 9 | 10 | __all__ = ["all_keys"] 11 | 12 | 13 | def _recursive_items(dictionary): 14 | """This function will accept the dictionary 15 | and iterate over it and yield all the keys 16 | 17 | Args: 18 | dictionary (dict): dictionary to iterate 19 | 20 | Yields: 21 | string: key in dictionary object. 22 | """ 23 | for key, value in dictionary.items(): 24 | yield key 25 | if isinstance(value, dict): 26 | yield from _recursive_items(value) 27 | elif isinstance(value, list): 28 | for item in value: 29 | if isinstance(item, dict): 30 | yield from _recursive_items(item) 31 | else: 32 | yield key 33 | 34 | 35 | def all_keys(obj): 36 | """This function will accept one param 37 | and return all keys 38 | * It will return all the keys in the object 39 | 40 | Args: 41 | obj (dict): dictionary object 42 | 43 | Returns: 44 | set: set of all the keys 45 | """ 46 | key_from_obj = set() 47 | for key in _recursive_items(dictionary=obj): 48 | key_from_obj.add(key) 49 | return key_from_obj 50 | -------------------------------------------------------------------------------- /tests/test_nestdict.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from nestdict import NestDict 3 | 4 | 5 | class TestNestDict(unittest.TestCase): 6 | 7 | def setUp(self): 8 | self.sample_data = { 9 | "user": { 10 | "name": "Alice", 11 | "age": 30, 12 | "address": {"city": "Wonderland", "zip": 12345}, 13 | }, 14 | "preferences": { 15 | "language": ["English", "French"], 16 | "timezone": "UTC", 17 | }, 18 | } 19 | self.nest_dict = NestDict(data=self.sample_data) 20 | 21 | def test_get_existing_key(self): 22 | self.assertEqual(self.nest_dict.get("user.name"), "Alice") 23 | self.assertEqual( 24 | self.nest_dict.get("user.address.city"), "Wonderland" 25 | ) 26 | 27 | def test_get_non_existing_key(self): 28 | self.assertIsNone(self.nest_dict.get("user.phone")) 29 | 30 | def test_set_new_key(self): 31 | self.nest_dict["user.phone"] = "123-456" 32 | self.assertEqual(self.nest_dict.get("user.phone"), "123-456") 33 | 34 | def test_delete_key(self): 35 | self.nest_dict.delete("user.address.city") 36 | self.assertIsNone(self.nest_dict.get("user.address.city")) 37 | dict_data = self.nest_dict.to_dict() 38 | self.assertIsNone( 39 | dict_data.get("user", {}).get("address", {}).get("city") 40 | ) 41 | -------------------------------------------------------------------------------- /nestdict/find_in_map.py: -------------------------------------------------------------------------------- 1 | """ 2 | # Implementation for FIND_IN_MAP Function 3 | 4 | * Basic functionality for FIND_IN_MAP function is to search nested dicts 5 | or nested key-value pair. 6 | 7 | * It accepts muliple args in which first arg is the dict object and rest 8 | are the keys in dict object. 9 | 10 | * find_in_map(dict_obj, parent_key, child_key, child_key_2) --> It returns 11 | the value for child_key_2 value 12 | 13 | * In case the result is not found then return NULL 14 | 15 | * It can be used to find the value of nested key or search for nested key if present. 16 | """ 17 | 18 | __all__ = ["find_in_map"] 19 | 20 | 21 | def find_in_map(obj, *args): 22 | """ 23 | It accepts the dict object and nested keys and return the value 24 | of last key if present in nested key is present in object. 25 | 26 | Args: 27 | obj (dict): dict object 28 | 29 | Returns: Value of last nested key 30 | """ 31 | if not (isinstance(obj, dict) or isinstance(obj, dict)): 32 | # raise InvalidRequestException 33 | print("Exception raised !") 34 | return 35 | 36 | nested_keys = list(args) 37 | 38 | for key in nested_keys: 39 | obj = _iterate_over_nested_obj(obj, key) 40 | if obj is None: 41 | return 42 | 43 | return obj 44 | 45 | 46 | def _iterate_over_nested_obj(obj, key): 47 | """It iterates over the nested dict object. 48 | It iterates over two types of data 49 | * list 50 | * dict 51 | 52 | for the rest data type it returns the value 53 | 54 | Args: 55 | obj (any type): object to process 56 | key (str, int): key to find in object 57 | """ 58 | 59 | if isinstance(obj, dict): 60 | if obj.get(key): 61 | return obj[key] 62 | elif isinstance(obj, list): 63 | for item in obj: 64 | value = _iterate_over_nested_obj(item, key) 65 | if value: 66 | return value 67 | return None 68 | else: 69 | return None 70 | -------------------------------------------------------------------------------- /nestdict/change_value.py: -------------------------------------------------------------------------------- 1 | """# implementation of change_value function 2 | 3 | * It changes the value of a perticular key and returns back the dictionary. 4 | * It also works with nested dictionary. 5 | 6 | USECASE 1: 7 | 8 | dic = { 9 | "cars_owned":{ 10 | "sedan":1, 11 | "suv":3 12 | } 13 | } 14 | 15 | print("\noriginal dictionary :",dic) 16 | 17 | change_value = nestdict.change_value(dic,"sedan",10) 18 | 19 | print("\nchange_value function :",change_value) 20 | 21 | 22 | OUTPUT: 23 | 24 | original dictionary : {'cars_owned': {'sedan': 1, 'suv': 3}} 25 | 26 | change_value function : {'cars_owned': {'sedan': 10, 'suv': 3}} 27 | 28 | USECASE 2: 29 | 30 | dic = { 31 | "cars_owned":{ 32 | "sedan":1, 33 | "suv":3 34 | } 35 | } 36 | 37 | print("\noriginal dictionary :",dic) 38 | 39 | nestdict.change_value(dic,"sedan",10) 40 | 41 | print("\nchange_value function :",dic) 42 | 43 | 44 | OUTPUT: 45 | 46 | original dictionary : {'cars_owned': {'sedan': 1, 'suv': 3}} 47 | 48 | change_value function : {'cars_owned': {'sedan': 10, 'suv': 3}} 49 | 50 | """ 51 | 52 | def change_value(obj,key,value): 53 | 54 | """ 55 | This function accept three params 56 | and iterats over the obj(dict) and replace value 57 | of the key 58 | 59 | Arg: 60 | 61 | obj (dict) : dictionary object 62 | key : pass the key. 63 | value = value to be replaced insitited of previous value 64 | 65 | for more understanding see README.md 66 | """ 67 | for k,v in obj.items(): 68 | if key == k: 69 | obj[key] = value 70 | break 71 | elif isinstance(v,dict): 72 | change_value(v,key,value) 73 | 74 | return obj 75 | 76 | -------------------------------------------------------------------------------- /nestdict/check_keys.py: -------------------------------------------------------------------------------- 1 | """ 2 | # Implementation of check_keys function 3 | 4 | * This Function will check for the keys in dictionary 5 | and return True if all keys are found. 6 | 7 | * It's a atomic function means it will only return True 8 | if all keys are found in the object else False. 9 | 10 | * It can also check for nested keys. 11 | 12 | """ 13 | 14 | __all__ = ["check_keys"] 15 | 16 | 17 | def check_keys(obj, required_key_list): 18 | """This function will accept two params 19 | and return a boolean value. 20 | * It will check if all keys are present in the object 21 | 22 | Args: 23 | required_key_list (list): list of required keys 24 | obj (dict): dictionary object to be checked. 25 | 26 | Returns: 27 | bool: True if all keys are present else False 28 | """ 29 | if not (isinstance(obj, dict) or isinstance(obj, list)): 30 | raise ValueError("Dictionary/List of dict object is required !") 31 | 32 | if not isinstance(required_key_list, list): 33 | raise ValueError("List of strings is required !") 34 | 35 | key_from_obj = set() 36 | for key in _recursive_items(dictionary=obj): 37 | key_from_obj.add(key) 38 | for key in required_key_list: 39 | if key not in key_from_obj: 40 | return False 41 | return True 42 | 43 | 44 | def _recursive_items(dictionary): 45 | """This function will accept the dictionary 46 | and iterate over it and yield all the keys 47 | 48 | Args: 49 | dictionary (dict): dictionary to iterate 50 | 51 | Yields: 52 | string: key in dictionary object. 53 | """ 54 | for key, value in dictionary.items(): 55 | yield key 56 | if isinstance(value, dict): 57 | yield from _recursive_items(value) 58 | elif isinstance(value, list): 59 | for item in value: 60 | if isinstance(item, dict): 61 | yield from _recursive_items(item) 62 | else: 63 | yield key 64 | -------------------------------------------------------------------------------- /COMMIT_GUIDELINES.md: -------------------------------------------------------------------------------- 1 | # Commit Message Guidelines 2 | 3 | Effective commit messages are essential for maintaining a clear and organized version history of our project. We follow the "Conventional Commits" format to structure our commit messages consistently. This format helps convey information about each commit's purpose and context. 4 | 5 | ## Commit Message Structure 6 | 7 | A commit message consists of several parts: 8 | 9 | 1. **Type:** Start the commit message with a type that describes the purpose of the commit. Common types include: 10 | 11 | - `feat`: A new feature or enhancement. 12 | - `fix`: A bug fix. 13 | - `chore`: Routine tasks, maintenance, or tooling changes. 14 | - `docs`: Documentation changes. 15 | - `style`: Code style/formatting changes (no code logic changes). 16 | - `refactor`: Code refactoring (neither a new feature nor a bug fix). 17 | - `test`: Adding or modifying tests. 18 | - `perf`: Performance improvements. 19 | 20 | 2. **Scope (optional):** Specify the scope of the commit, indicating which part of the project it affects. Enclose it in parentheses, e.g., `(core)` or `(docs)`. 21 | 22 | 3. **Description:** Write a concise and clear description of the changes made in this commit. Use the imperative mood (e.g., "Add feature" instead of "Added feature"). Keep it under 72 characters if possible. 23 | 24 | 4. **Body (optional):** For more complex changes, provide additional details in the commit message body. Use a blank line between the description and the body, and use paragraphs as needed for clarity. 25 | 26 | 5. **Breaking Changes (optional):** If the commit introduces breaking changes (e.g., API changes), include this section with a description of the breaking changes and instructions for users on how to update. 27 | 28 | ## Example Commit Message 29 | 30 | ```markdown 31 | feat(core): Add new function for dictionary manipulation 32 | 33 | This commit adds a new function, `nestdict.merge()`, which allows 34 | users to merge two dictionaries with conflict resolution. 35 | 36 | BREAKING CHANGE: The function signature of `nestdict.merge()` has 37 | changed to support additional options. Users should update 38 | their code accordingly. 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # To-Do List for "nestdict" Project 2 | 3 | ## Project: NestDict - Simplifying Nested Dictionary Operations 4 | 5 | ### Objectives 6 | 7 | The "nestdict" project aims to create a Python library that simplifies working with nested dictionaries. It will address common issues developers face when dealing with nested dictionaries and provide features to enhance dictionary manipulation, key access, serialization, and more. 8 | 9 | ### Project Plan 10 | 11 | #### Phase 1: Implement Core Features 12 | 13 | **Date: [Insert Date]** 14 | 15 | - [x] Implement automatic nesting of sub-dictionaries. 16 | - [x] Write unit tests. 17 | - [x] Implement dot notation access for nested dictionary values. 18 | - [x] Write unit tests. 19 | - [x] Implement flattening and unflattening of nested dictionaries. 20 | - [x] Write unit tests. 21 | - [x] Implement default values for missing keys and key validation. 22 | - [x] Write unit tests. 23 | 24 | #### Phase 2: Advanced Features and Utilities 25 | 26 | **Date: [Insert Date]** 27 | 28 | - [x] Implement serialization and deserialization to/from JSON. 29 | - [x] Write unit tests. 30 | - [x] Implement key transformation functions (e.g., camelCase, snake_case). 31 | - [x] Write unit tests. 32 | - [x] Implement string interpolation with dictionary values. 33 | - [x] Write unit tests. 34 | - [x] Implement data validation against predefined schemas. 35 | - [x] Write unit tests. 36 | - [x] Implement utility functions. 37 | - [x] Sort dictionaries. 38 | - [x] Merge dictionaries. 39 | - [x] Extract keys. 40 | 41 | #### Phase 3: Testing and Documentation 42 | 43 | **Date: [Insert Date]** 44 | 45 | - [x] Conduct extensive testing, including unit tests and edge case tests. 46 | - [x] Implement continuous integration (CI) using Travis CI. 47 | - [x] Complete comprehensive documentation. 48 | - [x] README.md: Overview, installation, basic usage, links to detailed documentation. 49 | - [x] CONTRIBUTING.md: Guidelines for contributions and issue reporting. 50 | - [x] LICENSE.md: License information. 51 | - [x] CHANGELOG.md: Record changes in each release. 52 | - [x] USAGE.md or TUTORIALS.md: In-depth usage examples. 53 | - [x] API.md: Detailed API reference documentation. 54 | - [x] CODE_OF_CONDUCT.md: Code of conduct for contributors. 55 | - [x] INSTALLATION.md: Detailed installation instructions. 56 | - [x] EXAMPLES.md: Collection of advanced usage examples. 57 | - [x] FAQ.md: Frequently asked questions and answers. 58 | - [x] DEPENDENCIES.md: Documentation of project dependencies. 59 | - [x] STYLEGUIDE.md: Coding style and conventions for contributors. 60 | - [x] RELEASE_NOTES.md: Release notes for each version. 61 | 62 | #### Phase 4: Release and Community Engagement 63 | 64 | **Date: [Insert Date]** 65 | 66 | - [x] Package the library for PyPI. 67 | - [x] Release the library on PyPI. 68 | - [x] Promote the library within the Python community. 69 | - [x] Encourage contributions and collaboration. 70 | - [x] Commit to ongoing maintenance and updates. 71 | - [x] Ensure compatibility with new Python releases. 72 | 73 | ### General Tasks 74 | 75 | - [ ] Task 1. 76 | - [ ] Task 2. 77 | - [ ] ... 78 | 79 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NestDict: Advanced Nested Dictionary Library for Python 2 | 3 | ![PyPI](https://img.shields.io/pypi/v/nestdict) ![Python Versions](https://img.shields.io/badge/python-3.6%2B-blue) 4 | 5 | ## Table of Contents 6 | 7 | - [Overview](#overview) 8 | - [Features](#features) 9 | - [Installation](#installation) 10 | - [Usage](#usage) 11 | - [API Reference](#api-reference) 12 | - [Contributing](#contributing) 13 | - [License](#license) 14 | 15 | ## Overview 16 | 17 | `NestDict` is a powerful Python library that extends the standard dictionary functionality to handle nested dictionaries, providing advanced features such as validation and support for frozen dictionaries. This library simplifies the manipulation of complex data structures, making it an ideal choice for applications that require dynamic data management. 18 | 19 | ## Features 20 | 21 | - **Nested Dictionary Handling**: Seamlessly access and manipulate deeply nested dictionaries. 22 | - **Validation**: Validate data types based on predefined mappings. 23 | - **Frozen Dictionaries**: Create immutable nested dictionaries to protect critical data. 24 | - **List Support**: Manage lists within nested structures effectively. 25 | 26 | ## Installation 27 | 28 | You can install `NestDict` using pip: 29 | 30 | ``` bash 31 | pip install nestdict 32 | ``` 33 | 34 | ## Usage 35 | Here’s a quick example of how to use NestDict: 36 | ``` python 37 | from nestdict import NestDict 38 | 39 | # Create a nested dictionary 40 | data = { 41 | "user": { 42 | "name": "John Doe", 43 | "age": 30, 44 | "address": { 45 | "city": "New York", 46 | "zip": "10001" 47 | } 48 | } 49 | } 50 | 51 | # Initialize NestDict 52 | nested_dict = NestDict(data) 53 | 54 | # Access nested data 55 | print(nested_dict.get("user.name")) # Output: John Doe 56 | 57 | # Set new values 58 | nested_dict["user.age"] = 31 59 | 60 | # print out dict 61 | print(nested_dict) 62 | 63 | # save final dict object 64 | final_dict = nested_dict.to_dict() 65 | 66 | # Validate data 67 | validation_rules = { 68 | "user.age": int, 69 | "user.name": str 70 | } 71 | nested_dict_with_validation = NestDict(data, validation=validation_rules) 72 | 73 | ``` 74 | ## API Reference 75 | 76 | - `get(key_path, default=None)` 77 | Retrieves the value at the specified key path in the nested dictionary. If the key path does not exist, it returns the specified default value (or `None` if not provided). 78 | 79 | - `__getitem__(key_path)` 80 | Allows access to the value at the specified key path using bracket notation (e.g., `nested_dict[key_path]`). Raises a `KeyError` if the key path is not found. 81 | 82 | - `__setitem__(key_path, value)` 83 | Sets the value at the specified key path using bracket notation (e.g., `nested_dict[key_path] = value`). It validates the value's type according to the validation rules if provided during initialization. 84 | 85 | - `delete(key_path)` 86 | Deletes the value at the specified key path. If the key path does not exist, it raises a `KeyError`. 87 | 88 | - `to_dict()` 89 | Returns the nested structure as a standard dictionary, representing the current state of the data. 90 | 91 | 92 | ### Data Parameter 93 | 94 | The **data** parameter is the initial nested dictionary structure that you want to manage using the `NestDict` class. It can be any valid Python dictionary (or list of dictionaries) that you need to work with. 95 | 96 | #### Key Points: 97 | - **Type**: Accepts a `dict` or `list`. 98 | - **Nested Structure**: You can create deeply nested dictionaries. For example, `{"a": {"b": {"c": 1}}}` is a valid input. 99 | - **Mutable**: The data is mutable, meaning you can modify it using the available methods like `set`, `delete`, or through direct item access. 100 | 101 | 102 | ### Validation Parameter 103 | 104 | The **validation** parameter is an optional dictionary used to enforce type checking on the values in your nested dictionary. It allows you to define expected data types for specific keys. 105 | 106 | #### Key Points: 107 | - **Type**: Accepts a `dict` where: 108 | - **Keys**: Are the key paths (in dot notation) that you want to validate. For example, `"user.age"` for a nested dictionary structure. 109 | - **Values**: Are the expected data types (e.g., `int`, `str`, `list`, `dict`) for those keys. 110 | - **Validation Check**: When you set a value for a key specified in the validation dictionary, the library checks if the value is of the expected type. If it’s not, a `ValueError` is raised. 111 | - **Initialization Validation**: The validation is performed both during the initialization of the `NestDict` instance and when using the `set` method. 112 | 113 | ## Contributing Guidelines 114 | 115 | Contributions to NestDict are welcome! To maintain a high standard for our project, please follow these guidelines when contributing: 116 | 117 | 1. **Fork the Repository**: Start by forking the repository to your account. 118 | 119 | 2. **Create a New Branch**: Create a new branch for your feature or bug fix: 120 | ```bash 121 | git checkout -b feature/YourFeatureName 122 | ``` 123 | 3. **Make Changes**: Implement your changes and ensure that your code adheres to our coding standards. 124 | 125 | 4. **Write Tests**: If applicable, add unit tests to cover your changes. Ensure that all tests pass before submitting your changes. 126 | 127 | 5. **Commit Your Changes**: Use clear and concise commit messages that explain the purpose of the changes. Refer to the COMMIT_GUIDELINES.md for detailed commit message conventions. 128 | 129 | 6. **Push Your Branch**: Push your changes to your fork: 130 | 131 | ```bash 132 | git push origin feature/YourFeatureName 133 | ``` 134 | 7. **Submit a Pull Request**: Navigate to the original repository and submit a pull request, explaining your changes and the motivation behind them. 135 | 136 | 8. **Respect the License**: Ensure that any contributions you make do not violate the existing license terms. Contributions should not be commercialized without explicit permission. 137 | 138 | *Thank you for contributing to NestDict!* 139 | 140 | ## Commit Guidelines 141 | We follow specific conventions for our commit messages to maintain clarity and consistency. Please refer to the [COMMIT_GUIDELINES.md](COMMIT_GUIDELINES.md) file for detailed commit message conventions. 142 | 143 | ## License 144 | This project is licensed under the MIT License. See the [License](LICENSE) file for more details. 145 | -------------------------------------------------------------------------------- /nestdict/main.py: -------------------------------------------------------------------------------- 1 | __all__ = ["NestDict"] 2 | 3 | 4 | from typing import Any 5 | 6 | 7 | class BaseNestDict: 8 | """Stores the data in flat object""" 9 | 10 | class __FlattenHelper: 11 | """Private helper class to handle flattening.""" 12 | 13 | @staticmethod 14 | def flatten(data): 15 | """ 16 | Flatten the data of dict for easy access 17 | """ 18 | 19 | def _flatten_dict(data, parent_key=""): 20 | items = {} 21 | for key, value in data.items(): 22 | new_key = f"{parent_key}.{key}" if parent_key else key 23 | items[new_key] = value 24 | if isinstance(value, dict): 25 | items.update(_flatten_dict(value, new_key)) 26 | 27 | elif isinstance(value, list): 28 | items.update(_flatten_list(value, new_key)) 29 | 30 | return items 31 | 32 | def _flatten_list(data, parent_key=""): 33 | items = {} 34 | 35 | for index, value in enumerate(data): 36 | new_key = ( 37 | f"{parent_key}.[{index}]" 38 | if parent_key 39 | else f"[{index}]" 40 | ) 41 | items[new_key] = value 42 | if isinstance(value, dict): 43 | items.update(_flatten_dict(value, new_key)) 44 | 45 | elif isinstance(value, list): 46 | items.update(_flatten_list(value, new_key)) 47 | 48 | return items 49 | 50 | if isinstance(data, dict): 51 | return _flatten_dict(data) 52 | 53 | elif isinstance(data, list): 54 | return _flatten_list(data) 55 | 56 | else: 57 | raise ValueError( 58 | f"Expected data is list or dict got {type(data)}" 59 | ) 60 | 61 | def __init__(self, data=None): 62 | self.data = data or {} 63 | self.flatten_dict = self.__FlattenHelper.flatten(self.data) 64 | 65 | def get(self, key_path, default=None): 66 | return self.flatten_dict.get(key_path, default) 67 | 68 | def __getitem__(self, key_path): 69 | if key_path not in self.flatten_dict: 70 | raise KeyError( 71 | f"{key_path} not found, please check the path again!" 72 | ) 73 | 74 | return self.flatten_dict[key_path] 75 | 76 | def __setitem__(self, key_path, value): 77 | self.flatten_dict[key_path] = value 78 | keys = key_path.split(".") 79 | 80 | data = self.flatten_dict 81 | for index in range(len(keys[:-1])): 82 | if keys[index] not in data: 83 | data[keys[index]] = ( 84 | [{}] if keys[index + 1].startswith("[") else {} 85 | ) 86 | data = data[keys[index]] 87 | 88 | if isinstance(data, list): 89 | data = data[0] 90 | 91 | data[keys[-1]] = value 92 | 93 | def delete(self, key_path): 94 | del self.flatten_dict[key_path] 95 | 96 | keys = key_path.split(".") 97 | data = self.flatten_dict 98 | for index in range(len(keys[:-1])): 99 | data = data[keys[index]] 100 | 101 | del data[keys[-1]] 102 | 103 | def to_dict(self): 104 | res_dict, res_list = {}, [] 105 | for key, value in self.flatten_dict.items(): 106 | key_list = key.split(".") 107 | if len(key_list) > 1: 108 | continue 109 | parent_key = key_list[0] 110 | if parent_key.startswith("["): 111 | res_list.append(value) 112 | else: 113 | res_dict[parent_key] = value 114 | 115 | return res_list or res_dict 116 | 117 | def __str__(self) -> str: 118 | return str(self.to_dict()) 119 | 120 | 121 | class ValidationDict(BaseNestDict): 122 | def __init__(self, data=None, validation={}): 123 | super().__init__(data) 124 | self.validation = validation 125 | 126 | # Validate the data 127 | if self.validation: 128 | self._pre_validate() 129 | 130 | def _pre_validate(self): 131 | for key_path, value in self.validation.items(): 132 | if key_path in self.flatten_dict and not self._validate( 133 | key_path, self.flatten_dict[key_path] 134 | ): 135 | raise ValueError( 136 | f"Invalid type for {key_path}: Expected {self.validation.get(key_path).__name__}, got {type(self.flatten_dict[key_path]).__name__}" 137 | ) 138 | 139 | def _validate(self, key_path, value): 140 | expected_type = self.validation.get(key_path) 141 | if expected_type and not isinstance(value, expected_type): 142 | return False 143 | return True 144 | 145 | def __setitem__(self, key_path, value): 146 | if not self._validate(key_path, value): 147 | raise ValueError( 148 | f"Invalid type for {key_path}: Expected {self.validation.get(key_path).__name__}, got {type(value).__name__}" 149 | ) 150 | super().__setitem__(key_path, value) 151 | 152 | 153 | class FrozenDict(BaseNestDict): ... 154 | 155 | 156 | class NestDict(ValidationDict): 157 | def __init__(self, data=None, validation={}, frozen=[]): 158 | ValidationDict.__init__(self, data, validation) 159 | # FrozenDict.__init__(self, frozen) 160 | 161 | 162 | # if __name__ == "__main__": 163 | # data = [ 164 | # { 165 | # "org_name": "org_1", 166 | # "location": "New York", 167 | # "employees": { 168 | # "emp_1": { 169 | # "name": "test_1", 170 | # "age": 43, 171 | # "position": "Designer", 172 | # "salary": 110420, 173 | # }, 174 | # "emp_2": { 175 | # "name": "test_2", 176 | # "age": 29, 177 | # "position": "Manager", 178 | # "salary": 52169, 179 | # }, 180 | # "emp_3": { 181 | # "name": "test_3", 182 | # "age": 54, 183 | # "position": "Developer", 184 | # "salary": 71768, 185 | # }, 186 | # "emp_4": { 187 | # "name": "test_4", 188 | # "age": 46, 189 | # "position": "Designer", 190 | # "salary": 93011, 191 | # }, 192 | # "emp_5": { 193 | # "name": "test_5", 194 | # "age": 22, 195 | # "position": "Manager", 196 | # "salary": 118508, 197 | # }, 198 | # }, 199 | # }, 200 | # { 201 | # "org_name": "org_2", 202 | # "location": "San Francisco", 203 | # "employees": { 204 | # "emp_1": { 205 | # "name": "test_1", 206 | # "age": 28, 207 | # "position": "Manager", 208 | # "salary": 140391, 209 | # }, 210 | # "emp_2": { 211 | # "name": "test_2", 212 | # "age": 33, 213 | # "position": "Manager", 214 | # "salary": 81659, 215 | # }, 216 | # }, 217 | # }, 218 | # ] 219 | 220 | # n = NestDict(data, validation={"[0].org_name": str}) 221 | 222 | # # print(n.flatten_dict) 223 | # # print(n) 224 | # # n["[1].a"] = 50 225 | # # n["[3].[4].b"] = 13 226 | # # print(n.flatten_dict) 227 | # # print(n.to_dict()) 228 | 229 | # # print(n["[0].employees"]) 230 | 231 | # n["[0].org_name"] = "jhgdfj" 232 | # # print(n["[0].org_name"]) 233 | 234 | # n.delete("[0].org_name") 235 | # # print(n.flatten_dict) 236 | # print(n) 237 | # # print(n.flatten_dict) 238 | --------------------------------------------------------------------------------