Defining a document
The Document class in Bunnet is responsible for mapping and handling the data
from the collection. It is inherited from the BaseModel Pydantic class, so it
follows the same data typing and parsing behavior.
from typing import Optional
import pymongo
from pydantic import BaseModel
from bunnet import Document
from bunnet import Indexed
class Category(BaseModel):
    name: str
    description: str
class Product(Document):  # This is the model
    name: str
    description: Optional[str] = None
    price: Indexed(float, pymongo.DESCENDING)
    category: Category
    class Settings:
        name = "products"
        indexes = [
            [
                ("name", pymongo.TEXT),
                ("description", pymongo.TEXT),
            ],
        ]
Fields
As it was mentioned before, the Document class is inherited from the Pydantic BaseModel class. 
It uses all the same patterns of BaseModel. But also it has special types of fields:
- id
 - Indexed
 
id
id field of the Document class reflects the unique _id field of the MongoDB document. 
Each object of the Document type has this field. 
The default type of this is PydanticObjectId.
from bunnet import Document
class Sample(Document):
    num: int
    description: str
foo = Sample.find_one(Sample.num > 5).run()
print(foo.id)  # This will print id
bar = Sample.get(foo.id).run()  # get by id
If you prefer another type, you can set it up too. For example, UUID:
from uuid import UUID, uuid4
from pydantic import Field
from bunnet import Document
class Sample(Document):
    id: UUID = Field(default_factory=uuid4)
    num: int
    description: str
Indexed
To set up an index over a single field, the Indexed function can be used to wrap the type:
from bunnet import Indexed
from bunnet import Document
class Sample(Document):
    num: Indexed(int)
    description: str
The Indexed function takes an optional argument index_type, which may be set to a pymongo index type:
from bunnet import Document
from bunnet import Indexed
import pymongo
class Sample(Document):
    description: Indexed(str, index_type=pymongo.TEXT)
The Indexed function also supports pymongo IndexModel kwargs arguments (PyMongo Documentation). 
For example, to create a unique index:
from bunnet import Document
from bunnet import Indexed
class Sample(Document):
    name: Indexed(str, unique=True)
Settings
The inner class Settings is used to configure:
- MongoDB collection name
 - Indexes
 - Encoders
 - Use of 
revision_id - Use of cache
 - Use of state management
 - Validation on save
 - Configure if nulls should be saved to the database
 
Collection name
To set MongoDB collection name, you can use the name field of the Settings inner class.
from bunnet import Document
class Sample(Document):
    num: int
    description: str
    class Settings:
        name = "samples"
Indexes
The indexes field of the inner Settings class is responsible for the indexes' setup. 
It is a list where items can be:
- Single key. Name of the document's field (this is equivalent to using the Indexed function described above)
 - List of (key, direction) pairs. Key - string, name of the document's field. Direction - pymongo direction (
  example: 
pymongo.ASCENDING) pymongo.IndexModelinstance - the most flexible option. PyMongo Documentation
from bunnet import Document
class DocumentTestModelWithIndex(Document):
    test_int: int
    test_list: List[SubDocument]
    test_str: str
    class Settings:
        indexes = [
            "test_int",
            [
                ("test_int", pymongo.ASCENDING),
                ("test_str", pymongo.DESCENDING),
            ],
            IndexModel(
                [("test_str", pymongo.DESCENDING)],
                name="test_string_index_DESCENDING",
            ),
        ]
Encoders
The bson_encoders field of the inner Settings class defines how the Python types are going to be represented 
when saved in the database. The default conversions can be overridden with this.
The ip field in the following example is converted to String by default:
from ipaddress import IPv4Address
from bunnet import Document
class Sample(Document):
    ip: IPv4Address
Note: Default conversions are defined in
bunnet.odm.utils.bson.ENCODERS_BY_TYPE.
However, if you want the ip field to be represented as Integer in the database, 
you need to override the default encoders like this:
from ipaddress import IPv4Address
from bunnet import Document
class Sample(Document):
    ip: IPv4Address
    class Settings:
        bson_encoders = {
          IPv4Address: int
        }
You can also define your own function for the encoding:
from ipaddress import IPv4Address
from bunnet import Document
def ipv4address_to_int(v: IPv4Address):
    return int(v)
class Sample(Document):
    ip: IPv4Address
    class Settings:
        bson_encoders = {
          IPv4Address: ipv4address_to_int
        }
Keep nulls
By default, Bunnet saves fields with None value as null in the database.
But if you don't want to save null values, you can set keep_nulls to False in the Settings class:
class Sample(Document):
    num: int
    description: Optional[str] = None
    class Settings:
        keep_nulls = False