Why Alaric#

TL;DR#

  • Comes with typing and autocomplete support built in

  • Support for class based structures, no more data["_id"] or find_one({...})

  • Simplistic advanced querying support

  • Convenience methods for everyday operations

  • Built on motor, so if Alaric can’t do it your not left in the lurch


For the sake of all examples, document will refer to an instance of Document while collection will refer to an instance of AsyncIOMotorCollection

Basic Queries#

At a basic level, Document is used fairly similar to AsyncIOMotorCollection in that your basic queries cross over, for example:

1# These are the same
2r_1 = await collection.find_one({"_id": 1234})
3r_2 = await document.find({"_id": 1234})

At its core, all methods accept the dictionaries you are used to. Where this is not true, your type checker will notify you.

Automatic Class Conversion#

Lets imagine our data is structured like this:

{
    "_id": int,
    "prefix:" str,
    "activated_premium": bool,
}

With motor the following would be a fairly standard interaction.

1data = await collection.find_one({"_id": 1234})
2prefix = data["prefix"]
3...
4if data["activated_premium"]:
5    ...

Raw dictionaries, no autocomplete, frankly yuck.

Now, by default Alaric also returns raw dictionaries, however, the following is also valid syntax and how I personally like to use the package.

 1class Guild:
 2    def __init__(self, _id, prefix, activated_premium):
 3        self._id: int = _id
 4        self.prefix: str = prefix
 5        self.activated_premium: bool = activated_premium
 6
 7document = Document(..., converter=Guild)
 8guild: Guild = await document.find({"_id": 1234})
 9prefix = guild.prefix
10...
11if guild.activated_premium:
12    ...

Note

Due to how Alaric is built, your __init__ must accept all arguments from your returned data. If there is extra, say an unwanted _id I recommend just adding **kwargs and not using them in the method itself.

Fully utilizing class support#

In the previous example you still have to convert it to a dictionary whenever you wanted to insert / update / filter. Lets change that.

Alaric exposes two protocol methods, which if implemented will be used.

These are:

  • as_filter

    Treat the dictionary returned from this as a filter for a query.

    I.e. as_filter would return {"_id": 1234}

  • as_dict

    Treat the dictionary returned from this as a full representation of the current object instance.

    I.e. as_dict would return {"_id": 1234, "prefix": "!", "activated_premium": True}

Lets see them in action.

 1from typing import Dict
 2
 3class Guild:
 4    def __init__(self, _id, prefix, activated_premium):
 5        self._id: int = _id
 6        self.prefix: str = prefix
 7        self.activated_premium: bool = activated_premium
 8
 9    def as_filter(self) -> Dict:
10        return {"_id": self._id}
11
12    def as_dict(self) -> Dict:
13        return {
14            "_id": self._id,
15            "prefix": self.prefix,
16            "activated_premium": self.activated_premium,
17        }
18
19document = Document(..., converter=Guild)
20guild: Guild = Guild(5678, "py.", False)
21await document.insert(guild)
22
23# Alternatively
24guild: Guild = await document.find({"_id": 1234})
25guild.prefix = "?"
26await document.upsert(guild, guild)

Note

For the last example you should actually use alaric.Document.change_field_to()

Conditional Class Returns#

In a situation where you don’t want you returned data to be converted to your class?

Simply pass try_convert=False to the method.

Advanced Querying#

This is a hidden gem, but MongoDB actually supports some extremely powerful queries. The issue however is the relevant dictionaries get big, quick.

Using our prior data structure, lets run a query to return all guilds that have the prefix ?.

await document.find({"prefix": "?"})

Simple right?

How about all guilds where the prefix is either ! or ??

Now, the raw query for this would look something like this.

await document.find({'prefix': {'$in': ['!', '?']}})

But with Alaric you can make the same query like this.

from alaric import AQ
from alaric.comparison import IN

await document.find(AQ(IN("prefix", ["!", "?"])))

I know what I’d prefer.


But lets make it even more complex!

Lets query for all the guilds that have activated premium, and have a prefix as either ! or ?.

Now, the raw query for this would look something like this.

await document.find(
    {
        "$and": [
            {"prefix": {"$in": ["!", "?"]}},
            {"activated_premium": {"$eq": True}},
        ]
    }
)

But with Alaric you can make the same query like this.

from alaric import AQ
from alaric.logical import AND
from alaric.comparison import EQ, IN

await document.find(AQ(AND(IN("prefix", ["!", "?"]), EQ("activated_premium", True))))

And this is only the tip of the iceberg, there are so many types of queries you can do.