Models and Fields

Declare a Model by inheriting from Model. Then simply define your choice of fields. Models are flexible to allow any number of fields and types of fields. A primary key field (or at least the Redis equivalent) will be automatically added if necessary.

from popoto import Model

class MyObject(Model):
    # add fields

KeyField

A KeyField makes objects fast and easy to query for. In the background, Popoto uses all KeyFields to compile the primary key on Redis. There almost no performance downside to using many KeyFields.

from popoto import Model, AutoKeyField, UniqueKeyField, KeyField

class User(Model):
    uuid = AutoKeyField()
    username = UniqueKeyField(max_length=20)
    name = KeyField(max_length=100, unique=False)

It's recommended that at least one KeyField has unique=True enforced.

These KeyFields will each enforce uniqueness across all saved instances: AutoKeyfield, UniqueKeyfield, and KeyField(unique=True)

from popoto import Model, AutoKeyField, UniqueKeyField, KeyField

class User(Model):
    uuid = AutoKeyField()
    username = UniqueKeyField()
    email = KeyField(unique=True)

However, it is enough for all KeyFields to be considered "unique together" In this example, a unique Box is the combination of dimensions. The combined dimensions together will be unique to every instance. And, 2 boxes with the same dimensions will be considered identical and save to the same object.

class Box(Model):
    length = KeyField(type=float)
    width = KeyField(type=float)
    height = KeyField(type=float)

Finally, it is also possible to declare a Model without a KeyField and Popoto will create and maintain a hidden unique key.

from popoto import Model, DecimalField, SortedField
from datetime import datetime

class BitcoinPrice(Model):
    usd_value = DecimalField()
    timestamp = SortedField(type=datetime)

The above example may be useful in situations where all queries are made via special purpose fields, such as SortedField, GeoField, or GraphField

Field

All fields inherit from base Field. A basic Field on any model will provide type validation on create and update events.

The following types are supported for use: int, float, Decimal, str, bool, list, dict, bytes, datetime.date, datetime.datetime, datetime.time

The default type for value = Field() is type str - string

from popoto import *
from decimal import Decimal
from datetime import datetime, date, time

class EveryTypeModel(Model):
    string_val = Field(type=str)
    int_val = Field(type=int)
    float_val = Field(type=float)
    decimal_val = Field(type=Decimal)
    boolean_val = Field(type=bool)
    list_val = Field(type=list)
    set_val = Field(type=set)
    tuple_val = Field(type=tuple)
    dict_val = Field(type=dict)
    bytes_val = Field(type=bytes)
    date_val = Field(type=date)
    datetime_val = Field(type=datetime)
    time_val = Field(type=time)

Named fields shown below are equivalent to the fields above. You may declare fields to your preference.

class EveryTypeModel(Model):
    int_val = IntField()
    float_val = FloatField()
    decimal_val = DecimalField()
    string_val = StringField()
    boolean_val = BooleanField()
    list_val = ListField()
    set_val = SetField()
    tuple_val = TupleField()
    dict_val = DictField()
    bytes_val = BytesField()
    date_val = DateField()
    datetime_val = DatetimeField()
    time_val = TimeField()

Null Values

KeyField and SortedField values are considered required null=False by default. All other fields are optional null=True by default. You may explicitly declare whether to allow null values using the null keywword

class MyModel(Model):
    optional_value = Field(null=True)
    required_value = Field(null=False)

Default Values

All fields will accept a default value for new objects.

class MyModel(Model):
    status = Field(type=str, default="unknown")
    is_true = Field(type=bool, default=False)
    access_count = Field(type=int, default=0)

String Max Length

Set a limit to string length. On SQL-like databases, this is often required. However, on Redis (and Popoto), there is no performance requirement or advantage to setting the max_length. Use it if you want Popoto to raise exceptions on model validation.

class Tweet(Model):
    text = Field(type=str, max_length=280)

SortedField

Use a SortedField for numerical attributes. A SortedField provides fast and efficient access to ordered instances (via Redis ZADD, ZRANGE). Querying for instances by order of a timestamp or attribute counter is one of the most powerful and common reasons for employing a Redis database.

A SortedField is necessary in order to use these query filters: __lt=, __gt=, etc. See details on filters at Query Filters

import datetime

class SortedDateModel(Model):
    name = KeyField()
    birthday = SortedField(type=datetime.date)

lisa = SortedDateModel.create(name="Lisa", birthday=datetime.date(1997, 3, 27))
rose = SortedDateModel.create(name="Rose", birthday=datetime.date(1997, 2, 11))
jisoo = SortedDateModel.create(name="Jisoo", birthday=datetime.date(1995, 1, 3))
jennie = SortedDateModel.create(name="Jennie", birthday=datetime.date(1996, 1, 16))

oldest = SortedDateModel.query.filter(birthday__lt=datetime.date(1996, 1, 1))[0]
assert jisoo == oldest
younger_than_rose = SortedDateModel.query.filter(birthday__gt=rose.birthday)
assert lisa in younger_than_rose

To use a SortedField also as a KeyField, use SortedKeyField

class BitcoinPrice(Model):
    timestamp = SortedKeyField(type=datetime)
    usd_value = DecimalField()

In some cases, you may always sort against a required KeyField. You will see significant performance improvements if you define sort_by (must be a tuple). Going forward, whenever a query filter is called on the SortedField, then all fields in 'sort_by' will also need to be defined in the filter.

In the example below, we've generalized the above BitcoinPrice model to be a generic asset. Because we will query by timestamp ranges for only one asset at a time, we can declare sort_by="asset".

class AssetPrice(Model):
    asset = KeyField()
    timestamp = SortedKeyField(type=datetime, sort_by=('asset',))
    usd_value = DecimalField()

AssetPrice.query.filter(
    asset="Bitcoin", 
    timestamp__gte=datetime(2021,1,1), 
    timestamp__lt=datetime(2021,1,2)
)  ## return Bitcoin prices over 1 day period

Note: because asset was specified as a sort_by in the timestamp field, the query requires the asset to be defined. This limitation, if you choose to use it, enables maximum performance with Redis.

GeoField

The GeoField employs another popular Redis feature - geospatial search. A common use case is searching for objects within a radius of another object. Use the GeoField to set coordinates on a model to enable these powerful query filters.

Popoto provides a namedtuple Coordinates. Although, any tuple of (float, float) for (latitude, longitude) is allowed.

from GeoField import Coordinates

class GeoModel(Model):
    name = KeyField()
    coordinates = GeoField()


rome = GeoModel.create(
    name="Rome", 
    coordinates=Coordinates(latitude=41.902782, longitude=12.496366)
)

vatican = GeoModel.create(
    name="Vatican",
    coordinates=Coordinates(latitude=41.904755, longitude=12.454628)
)

assert vatican in GeoModel.query.filter(coordinates=rome.coordinates, coordinates_radius=5, coordinates_radius_unit='km')

DataFrameField

The DataFrameField allows for storage of Pandas DataFrame objects for generic blocks of tabular data. A common use case is storing machine learning training data and results. No more pesky csv files. Use a database!

import pandas as pd

class DataModel(Model):
    name = KeyField()
    dataframe = DataFrameField()

chicago_home_prices = DataModel.create(
    name="Chicago Home Price",
    df=pandas.read_csv('home_price.csv')
)

chicago_home_prices.df.describe()
>>>
              Price         Year
count      5.000000     5.000000
mean   27600.000000  2016.000000
std     4878.524367     1.581139
min    22000.000000  2014.000000
25%    25000.000000  2015.000000
50%    27000.000000  2016.000000
75%    29000.000000  2017.000000
max    35000.000000  2018.000000

Reserved Field Names

The following names are reserved and cannot be used as field names:

  • limit: is used in query.filter() to limit the size of the returned objects list
  • values: is used in query.filter() to restrict which values are returned for objects
  • order_by: is used in query.filter() to order the results