# MongoGetterSetter - A Clean Realtime MongoDB Getter Setter API for Python


[![PyPI version](https://badge.fury.io/py/mongogettersetter.svg)](https://badge.fury.io/py/mongogettersetter)
[![Downloads](https://pepy.tech/badge/mongogettersetter)](https://pepy.tech/project/mongogettersetter)
[![Downloads](https://pepy.tech/badge/mongogettersetter/month)](https://pepy.tech/project/mongogettersetter/month)
[![Downloads](https://pepy.tech/badge/mongogettersetter/week)](https://pepy.tech/project/mongogettersetter/week)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

`MongoGetterSetter` is a metaclass that provides a convenient getter and setter API for instances of the classes that use it, allowing natural operations in Python objects to easily reflect in MongoDB documents.

The idea is to convert MongoDB Document into Python Object of Type `MongoGetterSetter` in High Level, and all other document's sub documents are treated as `dict` wrapped with `MongoDictWrapper` and other datatypes like `int`, `bool` `float` and `list` are all handled by `MongoDataWrapper`.

Here, we initialize `Employee` and `EmployeeCollection` class with `_collection` and `_filter_query` as mandatory attributes for `MongoGetterSetter` to function properly. These 2 attributes are used internally to do further manipulations to MongoDB documents. You can change the `_filter_query` attribute to customize the filter query as per your needs. `_collection` should point to the MongoDB collection.


### Usage of `MongoGetterSetter` metaclass
```
from pymongo import MongoClient
from mongogettersetter import MongoGetterSetter

# Connect to the MongoDB database and collection

client = MongoClient("mongodb://localhost:27017/")
db = client["example_db"]
collection = db["employee"]

# Wrapper for MongoDB Collection with metaclass, use this inside your actual class.
class EmployeeClass(metaclass=MongoGetterSetter):
    def __init__(self, _id):
        self._filter_query = {"id": _id} # or the ObjectID, at your convinence
        self._collection = collection # Should be a pymongo.MongoClient[database].collection

class Employee:
    def __init__(self, _id):
        self._filter_query = {"id": _id}
        self._collection = collection
        self.collection = EmployeeCollection(_id)

        # Create a new document if it doesn't exist
        if self.collection.get() is None:
            self._collection.insert_one(self._filter_query)
    
    def someOtherOperation(self):
        self.collection.hello = "Hello World"  

```

Now, save the above code in a file named `employee.py` and run the following command in the same directory:

```
$ python3 -i employee.py
```
This will run the contents of employee.py in interactive mode. Now, you can create an instance of `Employee` class and do operations on it. 

Before that, assume you have a MongoDB Collection called `employee` with an object like this:
```json
{'_id': ObjectId('640311ab0469a9c4eaf3d2bd'), 'id': 4051, 'email': 'manoj123@gmail.com', 'password': 'SomeNew SecurePassword', 'about': None, 'token': '7f471974-ae46-4ac0-a882-1980c300c4d6', 'country': 'India', 'location': None, 'lng': 0, 'lat': 0, 'dob': None, 'gender': 0, 'userType': 1, 'userStatus': 1, 'profilePicture': 'Images/9b291404-bc2e-4806-88c5-08d29e65a5ad.png', 'coverPicture': 'Images/44af97d9-b8c9-4ec1-a099-010671db25b7.png', 'enablefollowme': False, 'sendmenotifications': False, 'sendTextmessages': False, 'enabletagging': False, 'createdAt': '2020-01-01T11:13:27.1107739', 'updatedAt': '2020-01-02T09:16:49.284864', 'livelng': 77.389849, 'livelat': 28.6282231, 'liveLocation': 'Unnamed Road, Chhijarsi, Sector 63, Noida, Uttar Pradesh 201307, India', 'creditBalance': 130, 'myCash': 0, 'data': {'name': 'array_test', 'arr': [1, 2, 3, 4, 5, 6, 7, 8], 'hobies': {'composer': ['anirudh', {'co_singer': ['rakshitha', 'divagar', 'sibi']}, 'yuvan'], 'music': 'helo'}}, 'scores': [{'subject': 'math', 'score': 100}, {'subject': 'physics', 'score': 85}, {'subject': 'chemistry', 'score': 95}], 'fix': 1, 'hello': 1, 'recent_views': [200], 'exam': '', 'subject': '', 'arr': {'name': 'sibidharan', 'pass': 'hello', 'score': {'subject': {'minor': 'zoology', 'major': 'biology', 'others': ['evs', {'name': 'shiro', 'inarr': [200, 2, 3, {'sub': 'testsub', 'newsu': 'aksjdad', 'secret': 'skdjfnsdkfjnsdfsdf'}, 4, 12]}]}, 'score': 40, 'new': 'not7', 'hello': {'arr': [5, 2]}}}, 'name': 'ManojKumar', 'd': [1, 3, 4, 5], 'score': {}, 'hgf': 5}
```

This can be accessed by creating an instance of `EmployeeClass` class with the proper `id` as given in the `self._filter_query`. If such ID doesn't exist, it will create a new document with the given `id`.

For example:

```
>>> e = EmployeeCollection(4051)
```

Now this `e` object is an instance of `EmployeeCollection` class, which is a subclass of `MongoGetterSetter` metaclass. This object is a wrapper around the MongoDB document, which is fetched from the MongoDB collection using the `self._filter_query` from the `self._collection` attribute. You can access the MongoDB document and do CURD essential operations just by accessing this object's attributes/indexes. For the available methods, see the `MongoDataWrapper` and `MongoDictWrapper` methods. For example:

```
>>> e = EmployeeCollection(4051)
>>> e
{'_id': ObjectId('640311ab0469a9c4eaf3d2bd'), 'id': 4051, 'email': 'manoj123@gmail.com', 'password': 'SomeNew SecurePassword', 'about': None, 'token': '7f471974-ae46-4ac0-a882-1980c300c4d6', 'country': 'India', 'location': None, 'lng': 0, 'lat': 0, 'dob': None, 'gender': 0, 'userType': 1, 'userStatus': 1, 'profilePicture': 'Images/9b291404-bc2e-4806-88c5-08d29e65a5ad.png', 'coverPicture': 'Images/44af97d9-b8c9-4ec1-a099-010671db25b7.png', 'enablefollowme': False, 'sendmenotifications': False, 'sendTextmessages': False, 'enabletagging': False, 'createdAt': '2020-01-01T11:13:27.1107739', 'updatedAt': '2020-01-02T09:16:49.284864', 'livelng': 77.389849, 'livelat': 28.6282231, 'liveLocation': 'Unnamed Road, Chhijarsi, Sector 63, Noida, Uttar Pradesh 201307, India', 'creditBalance': 130, 'myCash': 0, 'data': {'name': 'array_test', 'arr': [1, 2, 3, 4, 5, 6, 7, 8], 'hobies': {'composer': ['anirudh', {'co_singer': ['rakshitha', 'divagar', 'sibi']}, 'yuvan'], 'music': 'helo'}}, 'scores': [{'subject': 'math', 'score': 100}, {'subject': 'physics', 'score': 85}, {'subject': 'chemistry', 'score': 95}], 'fix': 1, 'hello': 1, 'recent_views': [200], 'exam': '', 'subject': '', 'arr': {'name': 'sibidharan', 'pass': 'hello', 'score': {'subject': {'minor': 'zoology', 'major': 'biology', 'others': ['evs', {'name': 'shiro', 'inarr': [200, 2, 3, {'sub': 'testsub', 'newsu': 'aksjdad', 'secret': 'skdjfnsdkfjnsdfsdf'}, 4, 12]}]}, 'score': 40, 'new': 'not7', 'hello': {'arr': [5, 2]}}}, 'name': 'ManojKumar', 'd': [1, 3, 4, 5], 'score': {}, 'hgf': 5}
>>> e.id
4051
>>> e.id.get()
4051
>>> e.name
ManojKumar
>>> e.name.get()
ManojKumar
```

The MongoDB Document's root level attributes are directly accessible as the attributes of the `MongoGetterSetter` object. For example, `e.id` can also be accessible as `e['id']` and `e.name` can also be accessible as `e['name']`. The `e.id.get()` and `e.name.get()` methods are used to get the original datatype of the MongoDB document's root level attributes. For example, `e.id` is an `int` datatype, so `e.id.get()` will return the original `int` datatype. Similarly, `e.data` is a `dict` datatype wrapped in `MongoDictWrapper`, `e.data.get()` will return the original `dict` datatype.

For Example:

```
>>> e.name = "S. Manoj Kumar"
>>> e.name
S. Manoj Kumar
>>> e.data
{'name': 'array_test', 'arr': [1, 2, 3, 4, 5, 6, 7, 8], 'hobies': {'composer': ['anirudh', {'co_singer': ['rakshitha', 'divagar', 'sibi']}, 'yuvan'], 'music': 'helo'}}
>>> type(e.data)
<class 'mongogettersetter.MongoDataWrapper'>
>>> e.data.name = "ThisIsAwesmoe"
>>> e.data
{'name': 'ThisIsAwesmoe', 'arr': [1, 2, 3, 4, 5, 6, 7, 8], 'hobies': {'composer': ['anirudh', {'co_singer': ['rakshitha', 'divagar', 'sibi']}, 'yuvan'], 'music': 'helo'}}
>>> 
```

The `MongoDataWrapper` class is used to wrap the MongoDB document datatypes to provide MongoDB Array/List Operations over a simple, straightforward API to perform various operations on the MongoDB collection.

You can perform almost all basic array operations MongoDB supports. For example, you can use `e.data.arr.push(9)` to append a new element to the `arr` array. Similarly, you can use `e.data.arr.pop()` to pop the last element from the `arr` array. You can also use `e.data.arr.remove(2)` to remove the element `2` from the `arr` array. You can also use `e.data.arr.insert(0, [1,2,3])` to insert the element `[1,2,3]` at the beginning of the `arr` array. You can also use `e.data.arr[0] = 0` to set the first element of the `arr` array to `0`. You can also use `e.data.arr[0]` to get the first element of the `arr` array. You can also use `e.data.arr[0].get()` to get the original datatype of the first element of the `arr` array.

```
>>> e.data
{'name': 'ThisIsAwesmoe', 'arr': [1, 2, 3, 4, 5, 6, 7, 8], 'hobies': {'composer': ['anirudh', {'co_singer': ['rakshitha', 'divagar', 'sibi']}, 'yuvan'], 'music': 'helo'}}
>>> type(e.data)
<class 'mongogettersetter.MongoDataWrapper'>
>>> type(e.data.get())
<class 'dict'>
>>> e.data.arr.push(9)
True
>>> e.data
{'name': 'ThisIsAwesmoe', 'arr': [1, 2, 3, 4, 5, 6, 7, 8, 9], 'hobies': {'composer': ['anirudh', {'co_singer': ['rakshitha', 'divagar', 'sibi']}, 'yuvan'], 'music': 'helo'}}
>>> e.data.arr[1] = 100
>>> e.data.arr
[1, 100, 3, 4, 5, 6, 7, 8, 9]
>>> e.data.arr[1]
100
```
Access documents in any depth, either as attributes or as keys, to access nested data. For example, `e.data.hobies` is a nested dictionary, so you can access the `hobies` dictionary as `e.data.hobies` or `e.data['hobies']`. Similarly, `e.data.hobies.composer` is a nested list, so you can access the `composer` list as `e.data.hobies.composer` or `e.data.hobies['composer']`. Similarly, `e.data.hobies.composer[1]` is a nested dictionary, so you can access the `co_singer` list as `e.data.hobies.composer[1].co_singer` or `e.data.hobies.composer[1]['co_singer']`. Perform all possible operations on all the nested data, limited to the MongoDB supported operations.

```
>>> e.data.hobies
{'composer': ['anirudh', {'co_singer': ['rakshitha', 'divagar', 'sibi']}, 'yuvan'], 'music': 'helo'}
>>> e.data.hobies.composer
['anirudh', {'co_singer': ['rakshitha', 'divagar', 'sibi']}, 'yuvan']
>>> e.data.hobies.composer.push('rahman')
True
>>> e.data.hobies.composer
['anirudh', {'co_singer': ['rakshitha', 'divagar', 'sibi']}, 'yuvan', 'rahman']
>>> e.data.hobies.composer[1]
{'co_singer': ['rakshitha', 'divagar', 'sibi']}
>>> e.data.hobies.composer[1].co_singer
['rakshitha', 'divagar', 'sibi']
>>> e.data.hobies.composer[1].co_singer.pop()
True
>>> e.data.hobies.composer[1].co_singer
['rakshitha', 'divagar']
>>> e.data.hobies.composer[1].co_singer.insert(0, 'sushila')
True
>>> e.data.hobies.composer[1].co_singer
['sushila', 'rakshitha', 'divagar']
>>> e.data.hobies
{'composer': ['anirudh', {'co_singer': ['sushila', 'rakshitha', 'divagar']}, 'yuvan', 'rahman'], 'music': 'helo'}
>>> e.data.hobies.music = 'melody'
>>> e.data.hobies
{'composer': ['anirudh', {'co_singer': ['sushila', 'rakshitha', 'divagar']}, 'yuvan', 'rahman'], 'music': 'melody'}
>>> e.data.hobies.composer[1].main_singer = 'SPB'
>>> e.data.hobies
{'composer': ['anirudh', {'co_singer': ['sushila', 'rakshitha', 'divagar'], 'main_singer': 'SPB'}, 'yuvan', 'rahman'], 'music': 'melody'}
>>> e.data.hobies.composer
['anirudh', {'co_singer': ['sushila', 'rakshitha', 'divagar'], 'main_singer': 'SPB'}, 'yuvan', 'rahman']
>>> e.data.hobies.composer[1].main_singer
'SPB'
>>> type(e.data.hobies.composer[1].main_singer)
<class 'str'>
>>> type(e.data.hobies.composer)
<class 'mongogettersetter.MongoDataWrapper'>
>>> type(e.data.hobies.composer[1])
<class 'mongogettersetter.MongoDictWrapper'>
>>> e.data.hobies.composer[1].get('co_singer')
['sushila', 'rakshitha', 'divagar']
>>> e.data.hobies.composer[1].get()
{'co_singer': ['sushila', 'rakshitha', 'divagar'], 'main_singer': 'SPB'}
>>> type(e.data.hobies.composer[1].get())
<class 'dict'>
>>> 
```
## MongoGetterSetter

`MongoGetterSetter` is a metaclass that provides a convenient getter and setter API for instances of the classes that use it, allowing natural operations in Python objects to easily reflect in MongoDB documents.
### Methods

- `__getattr__(self, key)`: Returns a `MongoDataWrapper` instance for the given `key`. See below for the capabalities of `MongoDataWrapper`

    Example:
    ```
    obj = EmployeeClass(_id)
    result = obj.some_key
    ```
- `__getitem__(self, key, value)`: Gets the value of the specified `key` from the MongoDB document.

    Example:

    ```
    print(obj['some_key'])
    ```
- `__setattr__(self, key, value)`: Sets the value of the specified `key` in the MongoDB document.

    Example:

    ```
    obj.some_key = "new_value"
    ```

- `__setitem__(self, key, value)`: Sets the value of the specified `key` in the MongoDB document.

    Example:

    ```
    obj['some_key'] = "new_value"
    ```

- `__contains__(self, key)`: Checks if the MongoDB document contains the specified `key`.

    Example:

    ```
    if "some_key" in obj:
        print("Key exists")
    ```

- `__str__(self)`: Returns a string representation of the MongoDB document.

    Example:

    ```
    print(obj)
    ```
- `__delitem__(self, key)`: Removes the specified key from the MongoDB document.

    Example:
    ```
    del obj['some_key']
    ```
- `__delattr__(self, key)`: Removes the specified key from the MongoDB document.

    Example:
    ```
    del obj.some_key
    ```
- `delete(self)`: Removes document itself from the MongoDB

    Example:
    ```
    obj.delete()
    ```
- `get(self)`: Returns the MongoDB document.

    Example:
    ```
    print(obj.get())
    ```
- `set(self, data)`: Sets the given updated document in the MongoDB collection

    Example:
    ```
    obj.set({
        "password": "$2$somenewpassword",
        "country": "Malaysia"
    })
    ```

## MongoDataWrapper

`MongoDataWrapper` is a subscriptable class, which wraps MongoDB document datatypes to provide MongoDB Array/List Operations over a simple, straightforward API to perform various operations on the MongoDB collection. Check the list of methods for the allowed operations.

### Methods

- `__init__(self, _id, key, collection)`: Initialize the instance with the given `_id`, `key`, and `collection`.

- `get(self)`: Returns the value of the key in the MongoDB document.

- `inArray(self, value)`: Checks if the given `value` is present in the array of the document's key.

- `push(self, *values, maximum=-1)`: Pushes one or more `values` into the array of the document's key. If `maximum` is specified, it will limit the array size to the `maximum` value.

- `addToSet(self, value)`: Adds a `value` to the array of the document's key only if it doesn't exist in the array.

- `pop(self, direction=1)`: Removes the first (`direction=-1`) or the last (`direction=1`) element from the array of the document's key.

- `pull(self, value)`: Removes the specified `value` from the array of the document's key.

- `pullAll(self, *values)`: Removes all occurrences of the specified `values` from the array of the document's key.

- `matchSize(self, value)`: Checks if the size of the array of the document's key is equal to the given `value`.

- `elemMatch(self, **kvalues)`: Checks if the array of the document's key contains at least one element that matches the specified key-value pairs in `kvalues`.

- `matchAll(self, *values)`: Checks if the array of the document's key contains all the specified `values`.

- `update(self, field, match, **kvalues)`: Updates the nested field `field` of the document's key where the `field` value matches `match`, with the key-value pairs provided in `kvalues`.

- `insert(self, index, value)`: Inserts the given `value` at the specified `index` in the array of the document's key.

- `index(self, value)`: Find the index of the given value in array. It will return -1 if the value is not present in the list.

- `delete(self)`: Delets the key from MongoDB Document

- `__getitem__(self, index)`: Returns the value of the array of the document's key at the specified `index`.

- `__setitem__(self, index, value)`: Sets the value of the array of the document's key at the specified `index` to the given `value`.

- `__delitem__(self, index)`: Removes the value of the array of the document's key at the specified `index`.

- `__len__(self)`: Returns the length of the array of the document's key.

- `__str__(self)`: Returns a string representation of the value of the document's key.

- `__repr__(self)`: Returns a string representation of the value of the document's key.


## MongoDictWrapper

`MongoDictWrapper` is a class that inherits from the `dict` class and extends its functionalities to access dictionary keys as attributes. It allows you to access, modify, and manipulate MongoDB documents using Python dictionaries. When a MongoDataWrapper returns a `dict`, it automatically is wrapped with `MongoDictWrapper`, when it returns a `list`, it automatically is wrapped with `MongoDataWrapper` to allow manipulation of MongoDB object inside a MongoDB object, like a `dict` inside a `dict`. If you wish to access the value as default datatype, consider get() method.

### Methods

- `__init__(self, *args, **kwargs)`: Constructor method that initializes the base `dict` class.

- `prepare(self, _id, key, collection, filter_query)`: This method initializes the internal data structure that stores information about the document's location in the MongoDB collection.

- `__getitem__(self, key)`: Overrides the base `dict` method to return a wrapped MongoDictWrapper when accessing a nested dictionary.

- `__setitem__(self, key, value)`: Overrides the base `dict` method to update the MongoDB document when setting a key-value pair.

- `__delattr__(self, key)`: Overrides the base `dict` method to delete a key-value pair from the MongoDB document when deleting an attribute.

- `__getattr__(self, key)`: Overrides the base `dict` method to return a wrapped MongoDictWrapper when accessing a nested dictionary.

- `__setattr__(self, key, value)`: Overrides the base `dict` method to update the MongoDB document when setting a key-value pair.

- `__delitem__(self, key)`: Overrides the base `dict` method to delete a key-value pair from the MongoDB document when deleting an item.

- `get(self, key, default=None)`: Overrides the base `dict` method to return the value of the key in the MongoDB document, or the `default` value if the key is not present.

- `pop(self, key, default=None)`: Overrides the base `dict` method to remove and return the value of the key in the MongoDB document, or the `default` value if the key is not present.

- `update(self, other)`: Overrides the base `dict` method to update the MongoDB document with the key-value pairs from the `other` dictionary or iterable.

- `delete(self)`: Delets the key from MongoDB Document

- `clear(self)`: Overrides the base `dict` method to remove all key-value pairs from the MongoDB document.

- `__len__(self)`: Overrides the base `dict` method to return the number of key-value pairs in the MongoDB document.

- `__str__(self)`: Overrides the base `dict` method to return a string representation of the MongoDB document.

- `__repr__(self)`: Overrides the base `dict` method to return a string representation of the MongoDB document.

## Examples

To provide a more detailed example, let's assume you have a MongoDB collection named `people` with the following documents:

```
[
    {
        "id": 1,
        "name": "Alice",
        "age": 30,
        "skills": ["Python", "Django", "JavaScript"],
        "contact": {
            "email": "alice@example.com",
            "phone": "555-1234"
        },
        "projects": [
            {
                "title": "Project A",
                "status": "completed"
            },
            {
                "title": "Project B",
                "status": "in progress"
            }
        ]
    },
    {
        "id": 2,
        "name": "Bob",
        "age": 25,
        "skills": ["Java", "Spring", "JavaScript"],
        "contact": {
            "email": "bob@example.com",
            "phone": "555-5678"
        },
        "projects": [
            {
                "title": "Project X",
                "status": "completed"
            },
            {
                "title": "Project Y",
                "status": "in progress"
            }
        ]
    }
]
```

Now, let's create a class called `People` and `PeopleCollection` with `MongoGetterSetter` as its metaclass.

```
from pymongo import MongoClient
from mongogettersetter import MongoGetterSetter

# Connect to the MongoDB database and collection
client = MongoClient("mongodb://localhost:27017/")
db = client["example_db"]
people_collection = db["people"]

# Wrapper for MongoDB Collection with metaclass, use this inside your actual class.
class PeopleCollection(metaclass=MongoGetterSetter):
    def __init__(self, _id):
        self._filter_query = {"id": _id}  # or the ObjectID, at your convenience
        self._collection = people_collection  # Should be a pymongo.MongoClient[database].collection

class People():
    def __init__(self, _id):
        self.collection = PeopleCollection(_id)
        self._filter_query = {"id": _id}
        self._collection = people_collection
        if self.collection.get() is None:
            self._collection.insert_one(self._filter_query)
    
    def someOtherOperation(self):
        self.collection.hello = "Hello World"       
```



Create a PeopleCollection object for Alice with `id = 1`
```
alice = PeopleCollection(1)
```

Access and modify Alice's name
```
print(alice.name)  # Output: 'Alice'
alice.name = "Alice Johnson"
print(alice.name)  # Output: 'Alice Johnson'
```

Check if Alice's document has a 'contact' field
```
if 'contact' in alice:
    print("Contact field exists")
```

Access and modify Alice's email

```
print(alice.contact)  # Output: {'email': 'alice@example.com', 'phone': '555-1234'}
alice.contact.email = "alice.johnson@example.com"
print(alice.contact.email)  # Output: 'alice.johnson@example.com'
```

Access and modify Alice's skills
```
print(alice.skills)# Output: ['Python', 'Django', 'JavaScript']

print(alice.skills.get())  # Output: ['Python', 'Django', 'JavaScript']
alice.skills.push("React", maximum=4)
print(alice.skills.get())  # Output: ['Python', 'Django', 'JavaScript', 'React']
alice.skills.pop(direction=-1)
print(alice.skills.get())  # Output: ['Python', 'Django', 'JavaScript']
```

Access and modify Alice's projects

```
print(alice.projects.get())  # Output: [{'title': 'Project A', 'status': 'completed'}, {'title': 'Project B', 'status': 'in progress'}]
alice.projects.update("title", "Project A", status="archived")
print(alice.projects.get())  # Output: [{'title': 'Project A', 'status': 'archived'}, {'title': 'Project B', 'status': 'in progress'}]
```

## More `MongoDataWrapper` examples


Create a People object for Alice with id = 1
```
alice = People(1)
```

Create MongoDataWrapper instances for Alice's skills and projects
```
alice_skills = alice.skills
alice_projects = alice.projects
```

### Examples for each method of the MongoDataWrapper class

1. `get()`
```
print(alice_skills.get())  # Output: ['Python', 'Django', 'JavaScript']
```

2. `inArray()`
```
print(alice_skills.inArray("Python"))  # Output: True
```

3. `push()`
```
alice_skills.push("React", "Java", maximum=5)
print(alice_skills.get())  # Output: ['Python', 'Django', 'JavaScript', 'React', 'Java']
```

4. `addToSet()`
```
alice_skills.addToSet("C++")
print(alice_skills.get())  # Output: ['Python', 'Django', 'JavaScript', 'React', 'Java', 'C++']
```

5. `pop()`
```
alice_skills.pop(direction=-1)
print(alice_skills.get())  # Output: ['Python', 'Django', 'JavaScript', 'React', 'Java']
```

6. `pull()`
```
alice_skills.pull("Java")
print(alice_skills.get())  # Output: ['Python', 'Django', 'JavaScript', 'React']
```

7. `pullAll()`
```
alice_skills.pullAll("Python", "React")
print(alice_skills.get())  # Output: ['Django', 'JavaScript']
```

8. `matchSize()`
```
print(alice_skills.size(2))  # Output: True
```

9. `elemMatch()`
```
print(alice_projects.elemMatch(title="Project A", status="completed"))  # Output: True
```

10. `matchAll()`
```
print(alice_skills.all("Django", "JavaScript"))  # Output: True
```

11. `update()`
```
alice_projects.update("title", "Project A", status="archived")
print(alice_projects.get())  # Output: [{'title': 'Project A', 'status': 'archived'}, {'title': 'Project B', 'status': 'in progress'}]
```

12. `__len__()`
```
print(len(alice_skills))  # Output: 2
```

13. `__str__() and __repr__()`
```
print(alice_skills)  # Output: ['Django', 'JavaScript']
print(repr(alice_skills))  # Output: ['Django', 'JavaScript']
```

## More `MongoDictWrapper` examples

```
>>> e = Employee(4051)
>>> e
{'_id': ObjectId('640311ab0469a9c4eaf3d2bd'), 'id': 4051, 'name': 'Manoj', 'email': 'manoj123@gmail.com', 'password': 'different password', 'about': None, 'token': '7f471974-ae46-4ac0-a882-1980c300c4d6', 'country': None, 'location': None, 'lng': 0, 'lat': 0, 'dob': None, 'gender': 0, 'userType': 1, 'userStatus': 1, 'profilePicture': 'Images/9b291404-bc2e-4806-88c5-08d29e65a5ad.png', 'coverPicture': 'Images/44af97d9-b8c9-4ec1-a099-010671db25b7.png', 'enablefollowme': False, 'sendmenotifications': False, 'sendTextmessages': False, 'enabletagging': False, 'createdAt': '2020-01-01T11:13:27.1107739', 'updatedAt': '2020-01-02T09:16:49.284864', 'livelng': 77.389849, 'livelat': 28.6282231, 'liveLocation': 'Unnamed Road, Chhijarsi, Sector 63, Noida, Uttar Pradesh 201307, India', 'creditBalance': 127, 'myCash': 0, 'data': [4, 3, 4, 5, 7], 'arr': {'name': 'shiro', 'pass': 'hello', 'score': {'subject': {'minor': 'physics', 'major': 'science'}, 'score': 95}}, 'scores': [{'subject': 'math', 'score': 95}, {'subject': 'physics', 'score': 85}, {'subject': 'chemistry', 'score': 95}], 'recent_views': [4, 4, 4, 4, 4, 4, 4, 4, 4], 'fix': 1, 'hello': 1}
>>> e.arr
{'name': 'shiro', 'pass': 'hello', 'score': {'subject': {'minor': 'physics', 'major': 'science'}, 'score': 95}}
>>> e.arr['name'] = 'sibidharan' # MongoDataWrapper is also Subscriptable
>>> e.arr
{'name': 'sibidharan', 'pass': 'hello', 'score': {'subject': {'minor': 'physics', 'major': 'science'}, 'score': 95}}
>>> e.arr.score # Queried from the MongoDB directly
{'subject': {'minor': 'physics', 'major': 'science'}, 'score': 95}
>>> e.arr.score['subject']
{'minor': 'physics', 'major': 'science'}
>>> e.arr.score.subject
{'minor': 'physics', 'major': 'science'}
>>> e.arr.score.subject.minor = 'chemistry'
{'minor': 'physics', 'major': 'science'}
# is same as the following
>>> e.arr.score['subject']['minor'] = 'chemistry' # All change are reflected in MongoDB Document
>>> e.arr
{'name': 'sibidharan', 'pass': 'hello', 'score': {'subject': {'minor': 'chemistry', 'major': 'science'}, 'score': 95}}
>>> del e.arr.score['subject'] # Can delete any key in dictionary
>>> del e.arr # Can delete a key itself from the MongoDB Document
>>> e.delete() # Delete the document itself
```

# High-level Overview of the code for contributors to better understand the implementation

Any and all contributions are welcome ❤️

1. `MongoDictWrapper`: A wrapper for dictionaries that provides additional methods for interaction with MongoDB documents.

   Methods:
   - `prepare`
   - `__getitem__`
   - `__setitem__`
   - `__delitem__`
   - `get`
   - `pop`
   - `update`
   - `clear`
   - `delete`

2. `MongoDataWrapper`: A wrapper class for the data stored in MongoDB documents.

   Methods:
   - `get`
   - `inArray`
   - `push`
   - `addToSet`
   - `pop`
   - `pull`
   - `pullAll`
   - `size`
   - `elemMatch`
   - `all`
   - `update`
   - `delete`
   - `__len__`
   - `__str__`
   - `__repr__`
   - `__getattr__`
   - `__getitem__`
   - `__setattr__`
   - `__setitem__`
   - `__delitem__`
   - `__delattr__`

3. `MongoGetterSetter`: A metaclass that provides a way to override the default behavior of `__getattr__`, `__setattr__`, `__contains__`, `__str__`, `__repr__`, and `__delattr__` to work with MongoDB documents.

   Nested class: `PyMongoGetterSetter`

   Methods:
   - `__getattr__`
   - `__getitem__`
   - `__setattr__`
   - `__setitem__`
   - `__contains__`
   - `__str__`
   - `__repr__`
   - `__delattr__`
   - `__delitem__`
   - `delete`


## Credits

Thanks to GPT-4 for helping me write this documentation. If you find any errors or something doesn't work as the documentation says, raise an issue here https://git.selfmade.ninja/sibidharan/pymongogettersetter