Package: odmanticti
ODManticDocumentation:https://art049.github.io/odmantic/This version has been updated to support motor 2.5+ (and therefore python 3.9 and mongo v5.)Asynchronous ODM (Object Document Mapper) forMongoDBbased on standard python type hints. It's built on top ofpydanticfor model
definition and validation.Core features:Simple: define your model by typing your fields using python types, build queries
using python comparison operatorsDeveloper experience: field/method autocompletion, type hints, data validation,
perform database operations with a functional APIFully typed: leverage static analysis to reduce runtime issuesAsyncIO: works well with ASGI frameworks (FastAPI,quart,sanic,Starlette, ...)Serialization: built in JSON serialization and JSON schema generationRequirementsPython: 3.6 and later (tested against 3.6, 3.7, 3.8 and 3.9)MongoDB: 4.0 and laterTwo direct dependencies:pydantic: makes
data validation and schema definition both handy and elegant.motor: an
asyncio MongoDB driver officially developed by the MongoDB team.InstallationpipinstallodmanticExampleTo enjoy an async context without any code boilerplate, you can reproduce the
following steps using the AsyncIO REPL (only for Python 3.8+).python3.8 -m asyncioIf you are using an earlier version of Python you can useIPythonwhich provide an Autoawait feature (starting from Python
3.6).Define your first modelfromtypingimportOptionalfromodmanticimportField,ModelclassPublisher(Model):name:strfounded:int=Field(ge=1440)location:Optional[str]=NoneBy defining thePublisherclass, we've just created an ODMantic model 🎉. In this
example, the model will represent book publishers.This model contains three fields:name: This is the name of the Publisher. This is a simple string field without any
specific validation but it will be required to build a new Publisher.founded: This is the year of foundation of the Publisher. Since the printing press
has been invented in 1440, it would be handy to allow only values above 1440. Thegekeyword argument passed to the Field is exactly doing this. The model will
require a founded value greater or equal than 1440.location: This field will contain the country code of the Publisher. Defining this
field asOptionalwith aNonedefault value makes it a non required field that
will be set automatically when not specified.The collection name has been defined by ODMantic as well. In this case it will bepublisher.Create some instancesinstances=[Publisher(name="HarperCollins",founded=1989,location="US"),Publisher(name="Hachette Livre",founded=1826,location="FR"),Publisher(name="Lulu",founded=2002)]We defined three instances of the Publisher model. They all have anameproperty as it
was required. All the foundations years are later than 1440. The last publisher has no
location specified so by default this field is set toNone(it will be stored asnullin the database).For now, those instances only exists locally. We will persist them in a database in the
next step.Populate the database with your instancesFor the next steps, you'll need to start a local MongoDB server.The easiest way is
to use docker. Simply run the next command in a terminal (closing the terminal will
terminate the MongoDB instance and remove the container).dockerrun--rm-p27017:27017mongoFirst, let's connect to the database using the engine. In ODMantic, every database
operation is performed using the engine object.fromodmanticimportAIOEngineengine=AIOEngine()By default, theAIOEngine(stands for AsyncIOEngine) automatically tries to connect to a
MongoDB instance running locally (on port 27017). Since we didn't provide any database name, it will use
the database namedtestby default.The next step is to persist the instances we created before. We can perform this
operation using theAIOEngine.save_allmethod.awaitengine.save_all(instances)Most of the engine I/O methods are asynchronous, hence theawaitkeyword used here.
Once the operation is complete, we should be able to see our created documents in the
database. You can useCompassorRoboMongoif you'd like to have a graphical interface.Another possibility is to usemongoCLI directly:mongo--eval"db.publisher.find({})"Output:connectingto:mongodb://127.0.0.1:27017{"_id":ObjectId("5f67b331514d6855bc5c54c9"),"founded":1989,"location":"US","name":"HarperCollins"},{"_id":ObjectId("5f67b331514d6855bc5c54ca"),"founded":1826,"location":"FR","name":"Hachette Livre"},{"_id":ObjectId("5f67b331514d6855bc5c54cb"),"founded":2002,"location":null,"name":"Lulu"}The created instances are stored in thetestdatabase under thepublishercollection.We can see that an_idfield has been added to each document. MongoDB need this field
to act as a primary key. Actually, this field is added by ODMantic and you can access it
under the nameid.print(instances[0].id)#> ObjectId("5f67b331514d6855bc5c54c9")Find instances matching a criteriaSince we now have some documents in the database, we can start building some queries.First, let's find publishers created before the 2000s:early_publishers=awaitengine.find(Publisher,Publisher.founded<=2000)print(early_publishers)#> [Publisher(name="HarperCollins", founded=1989, location="US),#>  Publisher(name="Hachette Livre", founded=1826, location="FR")]Here, we called theengine.findmethod. The first argument we need to specify is the
Model class we want to query on (in our casePublisher). The second argument is the
actual query. Similarly toSQLAlchemy, you can build ODMantic queries using the regular python
operators.When awaited, theengine.findmethod will return the list of matching instances stored
in the database.Another possibility is to query for at most one instance. For example, if we want to
retrieve a publisher from Canada (CA):ca_publisher=awaitengine.find_one(Publisher,Publisher.location=="CA")print(ca_publisher)#> NoneHere the result isNonebecause no matching instances have been found in the database.
Theengine.find_onemethod returns an instance if one exists in the database
otherwise, it will returnNone.Modify an instanceFinally, let's edit some instances. For example, we can set thelocationfor the
publisher namedLulu.
First, we need to gather the instance from the database:lulu=awaitengine.find_one(Publisher,Publisher.name=="Lulu")print(lulu)#> Publisher(name="Lulu", founded=2002, location=None)We still have the same instance, with no location set. We can change this field:lulu.location="US"print(lulu)#> Publisher(name="Lulu", founded=2002, location="US)The location has been changed locally but the last step to persist this change is to
save the document:awaitengine.save(lulu)We can now check the database state:mongo--eval"db.publisher.find({name: 'Lulu'})"Output:connectingto:mongodb://127.0.0.1:27017{"_id":ObjectId("5f67b331514d6855bc5c54cb"),"founded":2002,"location":"US","name":"Lulu"}The document have been successfully updated !Now, what if we would like to change the foundation date with an invalid one (before 1440) ?lulu.founded=1000#> ValidationError: 1 validation error for Publisher#> founded#>   ensure this value is greater than 1440#>   (type=value_error.number.not_gt; limit_value=1440)This will raise an exception as it's not matching the model definition. The raised
exception is actually aValidationErrorcreated by frompydantic.Next stepsIf you already have experience with Pydantic and FastAPI, theUsage with FastAPIexample might be interesting for you.Otherwise, to get started on more advanced practices like relations and building more
advanced queries, you can directly check the other sections of thedocumentation.If you wish to contribute to the project (Thank you! :smiley:), you can have a look to theContributingsection of the
documentation.LicenseThis project is licensed under the terms of theISC license.