middle¶
Flexible, extensible Python data structures for general usage. Get data in and out, reliably, without boilerplate and with speed!
middle
stands on the shoulders of attrs
and aims to be as simple as possible to get data from complex objects to Python primitives and vice-versa, with validators, converters, a lot of sugar and other utilities! middle
can be used with your preferred web framework, background job application, configuration parser and more!
Quick peak¶
The most simple example of middle
and some of its features (using Python 3.6+ syntax):
>>> import typing
>>> import middle
>>> class Address(middle.Model):
... street_name: str
... number: typing.Optional[int]
... city: str
>>> class Person(middle.Model):
... name: str
... age: int
... address: typing.Dict[str, Address]
>>> data = {
... "name": "John Doe",
... "age": 42,
... "address": {
... "home": {
... "street_name": "Foo St",
... "number": None,
... "city": "Python Park"
... },
... "work": {
... "street_name": "Bar Blvd",
... "number": "1337",
... "city": "Park City"
... }
... }
... }
>>> person = Person(data)
>>> person
Person(name='John Doe', age=42, address={'home': Address(street_name='Foo St', number=None, city='Python Park'), 'work': Address(street_name='Bar Blvd', number=1337, city='Park City')})
>>> middle.asdict(person)
{'name': 'John Doe', 'age': 42, 'address': {'home': {'street_name': 'Foo St', 'number': None, 'city': 'Python Park'}, 'work': {'street_name': 'Bar Blvd', 'number': 1337, 'city': 'Park City'}}}
Wanted a more complex example, with Python 3.5 compatible syntax? For sure!
>>> from typing import Dict, List
>>> import middle
>>> class Game(middle.Model):
... name = middle.field(type=str)
... score = middle.field(type=float, minimum=0, maximum=10)
... resolution_tested = middle.field(type=str, pattern="^\d+x\d+$")
... genre = middle.field(type=List[str], unique_items=True)
... rating = middle.field(type=Dict[str, float], max_properties=5)
>>> data = {
... "name": "Cities: Skylines",
... "score": 9.0,
... "resolution_tested": "1920x1200",
... "genre": ["Simulators", "City Building"],
... "rating": {
... "IGN": 8.5,
... "Gamespot": 8.0,
... "Steam": 4.5
... }
... }
>>> game = Game(**data)
>>> game
Game(name='Cities: Skylines', score=9.0, resolution_tested='1920x1200', genre=['Simulators', 'City Building'], rating={'IGN': 8.5, 'Gamespot': 8.0, 'Steam': 4.5})
>>> middle.asdict(game)
{'name': 'Cities: Skylines', 'score': 9.0, 'resolution_tested': '1920x1200', 'genre': ['Simulators', 'City Building'], 'rating': {'IGN': 8.5, 'Gamespot': 8.0, 'Steam': 4.5}}
middle
is flexible enough to understand Enum
, nested models and a large variety of types declared on the typing
module out of the box. Also, you can extend it to your own classes!
Warning
IMPORTANT: middle
is in very early stages of development. There’s a lot of functionalities that needs to be implemented and some known misbehaviors to be addressed, not to mention it needs a lot of testing before moving to any other status rather than alpha.
About middle
¶
middle
is a library created to provide a pythonic way to describe your data structures in the most simple way by using typing
hints (from PEP 484), not only providing better readability but also all the necessary boilerplate to create objects based on your data structures, validate and customize input and output.
middle
is fast. Lightning fast. It can be used with your favourite web framework, background application, configuration parser or any other usage for highly customized input and/or output of primitive values.
Important
middle
was designed with typing
hints in mind. It’ll not work if no type is provided within your classes!
What to expect¶
middle
stands on the shoulders of attrs
and aims to be as simple as possible to get data from complex objects to Python primitives and vice-versa.
As for “primitives”, you can expect to have simple Python values (like list
, dict
, str
, int
, Enum
and others) to convert from and to complex objects, containing even other nested objects from any type you can imagine (do not forget to read how to extend middle
later on).
Another feature you can expect from middle
is it speed. middle
can convert from and to objects in a fraction of time compared to some of his “nearest” relatives, which makes it perfect if you need to handle lots of data. But you can use middle
with any use case you (my dear friend developer) find fit, because it will do the job just fine.
Benchmarks¶
(Sneaky) author took a copy of pydantic benchmark suite and created a benchmark file for middle
(mostly illustrative):
import typing
from datetime import datetime
import middle
class TestMiddle:
package = "middle"
def __init__(self, allow_extra):
class Location(middle.Model):
latitude: float = middle.field(default=None)
longitude: float = middle.field(default=None)
class Skill(middle.Model):
subject: str = middle.field()
subject_id: int = middle.field()
category: str = middle.field()
qual_level: str = middle.field()
qual_level_id: int = middle.field()
qual_level_ranking: float = middle.field(default=0)
class Model(middle.Model):
id: int = middle.field()
client_name: str = middle.field(max_length=255)
sort_index: float = middle.field()
grecaptcha_response: str = middle.field(
min_length=20, max_length=1000
)
client_phone: str = middle.field(max_length=255, default=None)
location: Location = middle.field(default=None)
contractor: int = middle.field(minimum=1, default=None)
upstream_http_referrer: str = middle.field(
max_length=1023, default=None
)
last_updated: datetime = middle.field(default=None)
skills: typing.List[Skill] = middle.field(default=[])
self.model = Model
def validate(self, data):
try:
return True, self.model(**data)
except Exception as e:
return False, str(e)
I must say that the results were really encouraging considering this is pure Python code (on an old AMD machine), no Cython, no Jit:
Framework | Comparison | avg/iter | stdev/iter |
---|---|---|---|
middle (alpha) | 39.1μs | 0.186μs | |
pydantic | 1.6x slower | 62.4μs | 0.278μs |
toasted-marshmallow | 1.7x slower | 64.9μs | 0.352μs |
marshmallow | 2.0x slower | 79.3μs | 0.137μs |
trafaret | 2.5x slower | 97.7μs | 1.586μs |
django-restful-framework | 17.0x slower | 662.8μs | 1.649μs |
Warning
Keep in mind that middle
is still in alpha stage. This benchmark will must likely change as middle
evolves, since there are some features missing yet (to get to version 1.0.0).
Installing¶
To install middle
, use pip
(or your favorite Python package manager), like:
pip install middle
And you’re ready to the next step. Hooray!
Warning
At this time, middle
is only available for Python 3.5+.
Using middle
¶
After installing middle
, it is easy to use it in your project. Most of its functionalities are acessible within the middle
module already:
import middle
middle
consists basically in three parts:
middle.Model
: the base class that needs to be inherited to declare all your models (except submodels);middle.field
: a function that is used to declare your field models; andmiddle.asdict
: a function required to convert your model instances to Python primitives.
middle.Model
¶
The middle.Model
class is the heart of middle
. To have all middle functionality at your disposal, middle.Model
needs to be subclassed when declaring your models:
class MyModel(middle.Model):
name: str = middle.field() # Python 3.6+ syntax
name = middle.field(type=str) # Python 3.5 syntax
In essence, middle.Model
started as a syntactic sugar for the attr.s
decorator but soon evolved to a more complex design, implementing its own metaclass
to handle some aspects of its models and fields.
Note
Since middle.Model
already implements its own metaclass
, it should be wise not to mix it with other classes that have a metaclass
different than type
.
To create an instance of your model, you can:
Use keyword arguments:
>>> MyModel(name="foo") MyModel(name='foo')
Use a
dict
:>>> MyModel({"name": "foo"}) MyModel(name='foo')
Use a
dict
as**kwargs
:>>> MyModel(**{"name": "foo"}) MyModel(name='foo')
Use any
object
instance that have acessible attributes with the same name as the required ones from your model:>>> MyModel(some_obj_with_name_accessible) MyModel(name='foo')
middle.field
¶
The middle.field
function is used to declare your model’s fields, with support to the type definition and other options that can be used later to define your model behavior regarding converting input values, validating values and format values for Python primitives. middle.field
makes heavy usage of attr.ib
calls, specially to store information into the metadata
dict.
There are three ways to declare your fields inside middle.Model
, you don’t have to necessarily use middle.field
, though it will be called under the hood to have a uniform model.
Declaring models, using middle.field
and typing
hints and annotations (PEP-526, for Python 3.6+):
class MyModel(middle.Model):
id: int = middle.field()
name: str = middle.field(min_length=5)
active: bool = middle.field(default=False)
created_on: datetime = middle.field(default=None)
Declaring models, using middle.field
and type
keyword (Python 3.5 compatible):
class MyModel(middle.Model):
id = middle.field(type=int)
name = middle.field(type=str, min_length=5)
active = middle.field(type=bool, default=False)
created_on = middle.field(type=datetime, default=None)
Declaring models, without middle.field
, using typing
hints, annotations (Python 3.6+ only) and a dict
:
class MyModel(middle.Model):
# id: int # or ...
id: int = {}
name: str = {"min_length": 5}
active: bool = {"default": False}
created_on: datetime = {"default": None}
Declaring models, without middle.field
, using only a dict
(Python 3.5 compatible):
class MyModel(middle.Model):
id = {"type": int}
name = {"type": str, "min_length": 5}
active = {"type": str, "default": False}
created_on = {"type": datetime, "default": None}
Declaring models, without middle.field
, using only typing
hints and annotations (inspired by pydantic, works only with Python 3.6+):
class MyModel(middle.Model):
id: int
name: str
active: str
created_on: datetime
Warning
Declaring models using only typing
hints annotations will not enable support for keyword embed validators.
Declaring models, the chaotic way (won’t work on Python 3.5):
class MyModel(middle.Model):
id: int
name = {"type": str, "min_length": 5}
active: bool = middle.field(default=False)
created_on = middle.field(type=datetime, default=None)
Tip
Developers are free to choose their preferred style (matching the Python version), although sticking to one can help readabilty.
middle.asdict
¶
This method, provided with an instance of a middle.Model
class, will return a dict
of key-values that will reflect the data of the instance against the model typing
hints only.
>>> instance = MyModel(
... id=42,
... name="foo bar",
... created_on=datetime.utcnow()
... )
>>> instance
MyModel(id=42, name='foo bar', active=False, created_on=datetime.datetime(2018, 7, 5, 14, 14, 12, 319270))
>>> middle.asdict(instance)
{'id': 42, 'name': 'foo bar', 'active': False, 'created_on': '2018-07-05T17:14:12.319270+00:00'}
Types in middle
¶
Typing hints (from PEP 484) are a major improvement to the Python language when dealing with documentation, code readability, static (and also runtime) code analysis and, in some cases, to provide extra logic to your code (which is, of course, the goal of middle
).
middle
supports a lot of builtin types, some from regularly used modules. There’s also the possibility of customize middle
to support many other types, including custom ones.
Supported types¶
str
int
float
bool
bytes
datetime.date
datetime.datetime
decimal.Decimal
enum.Enum
typing.Dict
typing.List
typing.Set
typing.Tuple
typing.Union
dict
, list
, set
¶
Since dict
, list
and set
can’t have a distinguished type, they will not be supported by middle
. Instead, use typing.Dict
, typing.List
and typing.Set
, respectively.
datetime.date
and datetime.datetime
¶
For now, middle
depends on one extra requirements to properly handle date
and datetime
objects, which is python-dateutil
(to properly format datetime
string representations into datetime
instances), and it is generally found on most Python projects / libraries already (that handles datetime
string parsing).
Important
All naive datetime
string or timestamp representations will be considered as UTC (and converted accordingly) by middle
, thus any of this objects or representations that are not naive will be automatically converted to UTC for uniformity. Naive datetime
objects will transit with the current machine timezone and converted to UTC for uniformity (this transition can be modified to be considered as UTC, see more about configuring middle).
Warning
Even though the datetime
API provides us two methods for getting the current date and time, now
and utcnow
, both instances will be created without tzinfo
, thus making them naive. Basically, it means that no one can determine if a datetime object is not naive if the timezone is not explicitly provided:
>>> from datetime import datetime
>>> x = datetime.now()
>>> x
datetime.datetime(2018, 7, 9, 14, 21, 44, 624833)
>>> x.tzinfo is None
True
>>> y = datetime.utcnow()
>>> y
datetime.datetime(2018, 7, 9, 17, 22, 12, 673103)
>>> y.tzinfo is None
True
The recomended way to get an aware UTC datetime object would be:
>>> from datetime import datetime, timezone
>>> datetime.now(timezone.utc)
datetime.datetime(2018, 7, 9, 17, 26, 8, 874805, tzinfo=datetime.timezone.utc)
Examples¶
Considering a machine configured to GMT-0300 timezone at 10:30 AM local time:
>>> import datetime
... import pytz
>>> from middle.dtutils import dt_convert_to_utc
... from middle.dtutils import dt_from_iso_string
... from middle.dtutils import dt_from_timestamp
... from middle.dtutils import dt_to_iso_string
>>> dt_to_iso_string(datetime.datetime.now())
'2018-07-10T13:30:00+00:00'
>>> dt_to_iso_string(datetime.datetime.utcnow())
'2018-07-10T16:30:00+00:00'
>>> dt_from_iso_string("2018-07-02T08:30:00+01:00")
datetime.datetime(2018, 7, 2, 7, 30, tzinfo=datetime.timezone.utc)
>>> dt_from_iso_string("2018-07-02T08:30:00")
datetime.datetime(2018, 7, 2, 8, 30, tzinfo=datetime.timezone.utc)
>>> dt_from_timestamp(1530520200)
datetime.datetime(2018, 7, 2, 8, 30, tzinfo=datetime.timezone.utc)
>>> dt_from_timestamp(1530520200.000123)
datetime.datetime(2018, 7, 2, 8, 30, 0, 123, tzinfo=datetime.timezone.utc)
>>> dt_convert_to_utc(datetime.datetime(2018, 7, 2, 8, 30, 0, 0, pytz.timezone("CET")))
datetime.datetime(2018, 7, 2, 7, 30, tzinfo=datetime.timezone.utc)
>>> dt_convert_to_utc(dt_from_iso_string("2018-07-02T08:30:00+01:00"))
datetime.datetime(2018, 7, 2, 7, 30, tzinfo=datetime.timezone.utc)
One plus of using datetime
in middle
is that it accepts a wide range of inputs, having in mind that we’re talking about Python here (see the datetime
constructor to understand why):
>>> from datetime import datetime, timezone
>>> import middle
>>> class TestModel(middle.Model):
... created_on: datetime = middle.field() # for Python 3.6+
... created_on = middle.field(type=datetime) # for Python 3.5
>>> TestModel(created_on=datetime.now())
TestModel(created_on=datetime.datetime(2018, 7, 10, 15, 1, 6, 121325, tzinfo=datetime.timezone.utc))
>>> TestModel(created_on=datetime.now(timezone.utc))
TestModel(created_on=datetime.datetime(2018, 7, 10, 15, 1, 40, 769369, tzinfo=datetime.timezone.utc))
>>> TestModel(created_on="2018-7-7 4:42pm")
TestModel(created_on=datetime.datetime(2018, 7, 7, 16, 42, tzinfo=datetime.timezone.utc))
>>> TestModel(created_on=1530520200)
TestModel(created_on=datetime.datetime(2018, 7, 2, 8, 30, tzinfo=datetime.timezone.utc))
>>> TestModel(created_on=(2018, 7, 9, 10))
TestModel(created_on=datetime.datetime(2018, 7, 9, 13, 0, tzinfo=datetime.timezone.utc))
>>> TestModel(created_on=(2018, 7, 9, 10, 30, 0, 0, 1))
TestModel(created_on=datetime.datetime(2018, 7, 9, 9, 30, tzinfo=datetime.timezone.utc))
Important
In the last input (in the example above), where a tuple of 8 integers were given for the created_on
parameter, the last value corresponds to the UTC offset in hours.
“But I only trust on [arrow|momentum|maya]”¶
Well, I don’t blame you. These operations regarding date
and datetime
were created for middle
to provide an out-of-the-box solution for the most used types in Python, but, don’t worry, you can override these operations with your own. Just head out to extending and catch up some examples.
Enum¶
Most enum types will be directly available from and to primitives by acessing the .value
attribute of each instance. A lot of complex examples can work out of the box:
>>> import enum
... import middle
>>> class AutoName(enum.Enum):
... def _generate_next_value_(name, start, count, last_values):
... return name
>>> class TestAutoEnum(AutoName):
... FOO = enum.auto()
... BAR = enum.auto()
... BAZ = enum.auto()
>>> @enum.unique
... class TestStrEnum(str, enum.Enum):
... CAT = "CAT"
... DOG = "DOG"
... BIRD = "BIRD"
>>> @enum.unique
... class TestIntEnum(enum.IntEnum):
... FIRST = 1
... SECOND = 2
... THIRD = 3
>>> class TestFlagEnum(enum.IntFlag):
... R = 4
... W = 2
... X = 1
>>> instance = TestModel(auto_enum=TestAutoEnum.FOO, str_enum=TestStrEnum.CAT, int_enum=TestIntEnum.FIRST, flg_enum=TestFlagEnum.R | TestFlagEnum.W)
>>> instance
TestModel(auto_enum=<TestAutoEnum.FOO: 'FOO'>, str_enum=<TestStrEnum.CAT: 'CAT'>, int_enum=<TestIntEnum.FIRST: 1>, flg_enum=<TestFlagEnum.R|W: 6>)
>>> data = middle.asdict(instance)
>>> data
{'auto_enum': 'FOO', 'str_enum': 'CAT', 'int_enum': 1, 'flg_enum': 6}
>>> TestModel(**data) # to test if flg_enum=6 would work
TestModel(auto_enum=<TestAutoEnum.FOO: 'FOO'>, str_enum=<TestStrEnum.CAT: 'CAT'>, int_enum=<TestIntEnum.FIRST: 1>, flg_enum=<TestFlagEnum.R|W: 6>)
Future plans on types¶
There are some types in the Python stdlib that are planned to be part of middle
in the near future:
- uuid.uuid[1,3-5]
If there’s a type you would like to see on middle
, feel free to open an issue or submit a PR.
Validating data¶
Some types on middle
can be validated based on single or multiple keywords that can be available to the field
method, but will only trigger validation based on the type of the field
.
Tip
Validation on middle
is based on rules declared in the OpenAPI specification, so most of them should not be strange to those familiar with web development.
Important
All types have at least one validator: asserting if the type of the value is the one defined in the class (or None, if typing.Optional
or have the keyword default
set to None
).
Warning
All validators that corresponds to a certain type will be called. This behavior may change in the future because I simply don’t know if, in the OpenAPI specification, one (certain validator) may exclude another.
Range validators¶
There are some validators that works within a certain range that may contains a max
and a min
value (with keywords specific for a type). Given these values are mostly quantitatives, they cannot be negative (or ValueError
will be raised), except for instances of type int
or float
; nor the upper bound value can be equal or less than the lower bound value. Some other keywords may have some effect on these values as well.
Important
Optionally range validators can also be used only with the lower_bound
or upper_bound
value if no bound is needed in one of the values.
String validators¶
There are some validators for the str
type that can
Range: min_length
and max_length
¶
Setting min_length
keyword to an integer would require that the input value should have at least the given value of length:
>>> import middle
>>> class TestModel(middle.Model):
... name = middle.field(type=str, min_length=3)
>>> TestModel(name="hello")
TestModel(name='hello')
>>> TestModel(name="hi")
Given the input value above, here’s the resulting Traceback
:
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/home/dev/middle/src/middle/model.py", line 80, in __call__
return super().__call__(**kwargs)
File "<attrs generated init 70f3aaa3ccac019c22d47311619cd3804d1a9311>", line 4, in __init__
File "/home/dev/.pyenv/versions/middle-3.6.6/lib/python3.6/site-packages/attr/_make.py", line 1668, in __call__
v(inst, attr, value)
File "/home/dev/middle/src/middle/validators/common.py", line 11, in wrapper
return func(meta_value, instance, attribute, value)
File "/home/dev/middle/src/middle/validators/common.py", line 21, in min_str_len
attribute.name, meta_value
middle.exceptions.ValidationError: 'name' must have a minimum length of 3 chars
Setting the max_length
keyword to an integer would require that the input value should have no more than the given value of length:
>>> import middle
>>> class TestModel(middle.Model):
... name: str = middle.field(max_length=5)
>>> TestModel(name="hello")
TestModel(name='hello')
>>> TestModel(name="hello, world")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/home/dev/middle/src/middle/model.py", line 80, in __call__
return super().__call__(**kwargs)
File "<attrs generated init 9b3b7c0ce74ad8f645d202b99d5df010c034e2b0>", line 4, in __init__
File "/home/dev/.pyenv/versions/middle-3.6.6/lib/python3.6/site-packages/attr/_make.py", line 1668, in __call__
v(inst, attr, value)
File "/home/dev/middle/src/middle/validators/common.py", line 11, in wrapper
return func(meta_value, instance, attribute, value)
File "/home/dev/middle/src/middle/validators/common.py", line 31, in max_str_len
attribute.name, meta_value
middle.exceptions.ValidationError: 'name' must have a maximum length of 5 chars
pattern
¶
Setting the pattern
keyword to a string representing a regular expression (or a regular expression object) would require that the input value should match the value given:
>>> import middle
>>> class TestModel(middle.Model):
... serial = {"type": str, "pattern": "^[0-9]+$"}
>>> TestModel(serial="123456")
TestModel(serial='123456')
>>> TestModel(serial="hello")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/home/dev/middle/src/middle/model.py", line 80, in __call__
return super().__call__(**kwargs)
File "<attrs generated init c36746f22b6ca0b15b44dff2665d92e7478d9031>", line 4, in __init__
File "/home/dev/.pyenv/versions/middle-3.6.6/lib/python3.6/site-packages/attr/_make.py", line 1668, in __call__
v(inst, attr, value)
File "/home/dev/middle/src/middle/validators/common.py", line 11, in wrapper
return func(meta_value, instance, attribute, value)
File "/home/dev/middle/src/middle/validators/common.py", line 41, in str_pattern
attribute.name, meta_value
middle.exceptions.ValidationError: 'serial' did not match the given pattern: '^[0-9]+$'
format
¶
To be developed.
Number validators¶
Range: minimum
and maximum
¶
Setting minimum
keyword to an integer or float would require that the input value should have at least the required minimum value:
>>> import middle
>>> class TestModel(middle.Model):
... value = {"type": int, "minimum": 5}
>>> TestModel(value=5)
TestModel(value=5)
>>> TestModel(value=20)
TestModel(value=20)
>>> TestModel(value=4)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/home/dev/middle/src/middle/model.py", line 105, in __call__
return super().__call__(**kwargs)
File "<attrs generated init 6ed99d543406ed37c7405962f27f473476610ca9>", line 4, in __init__
File "/home/dev/.pyenv/versions/middle-3.6.6/lib/python3.6/site-packages/attr/_make.py", line 1668, in __call__
v(inst, attr, value)
File "/home/dev/middle/src/middle/validators/common.py", line 10, in wrapper
return func(meta_value, instance, attribute, value)
File "/home/dev/middle/src/middle/validators/common.py", line 60, in min_num_value
attribute.name, meta_value
middle.exceptions.ValidationError: 'value' must have a minimum value of 5
Setting the maximum
keyword to an integer or float would require that the input value shoud have no more than the maximum value:
>>> import middle
>>> class TestModel(middle.Model):
... value: float = middle.field(maximum=3.14)
>>> TestModel(value=-5.0)
TestModel(value=-5.0)
>>> TestModel(value=3.141)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/home/dev/middle/src/middle/model.py", line 105, in __call__
return super().__call__(**kwargs)
File "<attrs generated init 9fda1659ea481e0eb60414e362b3bd445d031dd3>", line 4, in __init__
File "/home/dev/.pyenv/versions/middle-3.6.6/lib/python3.6/site-packages/attr/_make.py", line 1668, in __call__
v(inst, attr, value)
File "/home/dev/middle/src/middle/validators/common.py", line 10, in wrapper
return func(meta_value, instance, attribute, value)
File "/home/dev/middle/src/middle/validators/common.py", line 79, in max_num_value
attribute.name, meta_value
middle.exceptions.ValidationError: 'value' must have a maximum value of 3.14
exclusive_minimum
¶
The exclusive_minimum
keyword, being True
, would exclude the minimum
value from the validation:
>>> import middle
>>> class TestModel(middle.Model):
... value = {"type": int, "minimum": 5, "exclusive_minimum": True}
>>> TestModel(value=6)
TestModel(value=6)
>>> TestModel(value=5)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/home/dev/middle/src/middle/model.py", line 105, in __call__
return super().__call__(**kwargs)
File "<attrs generated init 29f97f0418f12486a7312929799ce2293fc24900>", line 4, in __init__
File "/home/dev/.pyenv/versions/middle-3.6.6/lib/python3.6/site-packages/attr/_make.py", line 1668, in __call__
v(inst, attr, value)
File "/home/dev/middle/src/middle/validators/common.py", line 10, in wrapper
return func(meta_value, instance, attribute, value)
File "/home/dev/middle/src/middle/validators/common.py", line 53, in min_num_value
attribute.name, meta_value
middle.exceptions.ValidationError: 'value' must have a (exclusive) minimum value of 5
exclusive_maximum
¶
The exclusive_maximum
keyword, being True
, would exclude the maximum
value from the validation:
>>> import middle
>>> class TestModel(middle.Model):
... value: float = middle.field(maximum=3.14, exclusive_maximum=True)
>>> TestModel(value=3.1)
TestModel(value=3.1)
>>> TestModel(value=3.14)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/home/dev/middle/src/middle/model.py", line 105, in __call__
return super().__call__(**kwargs)
File "<attrs generated init 08e8fcc18b83475440f4c0239321aa61610c38b9>", line 4, in __init__
File "/home/dev/.pyenv/versions/middle-3.6.6/lib/python3.6/site-packages/attr/_make.py", line 1668, in __call__
v(inst, attr, value)
File "/home/dev/middle/src/middle/validators/common.py", line 10, in wrapper
return func(meta_value, instance, attribute, value)
File "/home/dev/middle/src/middle/validators/common.py", line 72, in max_num_value
attribute.name, meta_value
middle.exceptions.ValidationError: 'value' must have a (exclusive) maximum value of 3.14
multiple_of
¶
The multiple_of
keyword specifies the multiple value that a value must have in order to have no remainder in a division operation. It works with int
and float
as well.
>>> import middle
>>> class TestModel(middle.Model):
... value = {"type": int, "multiple_of": 3}
>>> TestModel(value=21)
TestModel(value=21)
>>> TestModel(value=22)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/home/dev/middle/src/middle/model.py", line 105, in __call__
return super().__call__(**kwargs)
File "<attrs generated init c24a18c0830f91f6079d00bbafff7e05f204e5a4>", line 4, in __init__
File "/home/dev/.pyenv/versions/middle-3.6.6/lib/python3.6/site-packages/attr/_make.py", line 1668, in __call__
v(inst, attr, value)
File "/home/dev/middle/src/middle/validators/common.py", line 10, in wrapper
return func(meta_value, instance, attribute, value)
File "/home/dev/middle/src/middle/validators/common.py", line 96, in num_multiple_of
"'{}' must be multiple of {}".format(attribute.name, meta_value)
middle.exceptions.ValidationError: 'value' must be multiple of 3
List and Set validators¶
Range: min_items
and max_items
¶
Setting min_items
keyword to a List or Set would require that the input value should have at least the required number of items:
>>> import middle
>>> class TestModel(middle.Model):
... value = {"type": List[int], "min_items": 3}
>>> TestModel(value=[1, 2, 3, 4])
TestModel(value=[1, 2, 3, 4])
>>> TestModel(value=[1, 2])
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/home/dev/middle/src/middle/model.py", line 105, in __call__
return super().__call__(**kwargs)
File "<attrs generated init e1f1466567f03e1497438574116bf161e9400995>", line 4, in __init__
File "/home/dev/.pyenv/versions/middle-3.6.6/lib/python3.6/site-packages/attr/_make.py", line 1668, in __call__
v(inst, attr, value)
File "/home/dev/middle/src/middle/validators/common.py", line 10, in wrapper
return func(meta_value, instance, attribute, value)
File "/home/dev/middle/src/middle/validators/common.py", line 104, in list_min_items
"'{}' has no enough items of {}".format(attribute.name, meta_value)
middle.exceptions.ValidationError: 'value' has no enough items of 3
Setting the max_items
keyword to a List or Set would require that the input value shoud have no more than the required number of items:
>>> import middle
>>> class TestModel(middle.Model):
... value: List[int] = middle.field(max_items=5)
>>> TestModel(value=[1, 2, 3, 4])
TestModel(value=[1, 2, 3, 4])
>>> TestModel(value=[1, 2, 3, 4, 5, 6])
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/home/dev/middle/src/middle/model.py", line 105, in __call__
return super().__call__(**kwargs)
File "<attrs generated init 5180f70ad2fab18f88a0d4d0079cfcf36c2eb02a>", line 4, in __init__
File "/home/dev/.pyenv/versions/middle-3.6.6/lib/python3.6/site-packages/attr/_make.py", line 1668, in __call__
v(inst, attr, value)
File "/home/dev/middle/src/middle/validators/common.py", line 10, in wrapper
return func(meta_value, instance, attribute, value)
File "/home/dev/middle/src/middle/validators/common.py", line 113, in list_max_items
attribute.name, meta_value
middle.exceptions.ValidationError: 'value' has more items than the limit of 5
unique_items
¶
The unique_items
keyword specifies that all values in the input value should be unique.
>>> import middle
>>> class TestModel(middle.Model):
... value: List[int] = middle.field(unique_items=True)
>>> TestModel(value=[1, 2, 3, 4])
TestModel(value=[1, 2, 3, 4])
>>> TestModel(value=[1, 2, 3, 2])
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/home/dev/middle/src/middle/model.py", line 105, in __call__
return super().__call__(**kwargs)
File "<attrs generated init 1687613eb0214900b54beb0d1c7a7831de866678>", line 4, in __init__
File "/home/dev/.pyenv/versions/middle-3.6.6/lib/python3.6/site-packages/attr/_make.py", line 1668, in __call__
v(inst, attr, value)
File "/home/dev/middle/src/middle/validators/common.py", line 10, in wrapper
return func(meta_value, instance, attribute, value)
File "/home/dev/middle/src/middle/validators/common.py", line 126, in list_unique_items
"'{}' must only have unique items".format(attribute.name)
middle.exceptions.ValidationError: 'value' must only have unique items
Important
Remember that, to be unique, one value should be comparable with another. If you are comparing instances of models created with middle
, please take a look on some tips regarding attrs
.
Dict validators¶
Range: min_properties
and max_properties
¶
Setting min_properties
keyword to a Dict would require that the input value should have at least the required number of keys and values:
>>> import middle
>>> class TestModel(middle.Model):
... value = {"type": Dict[str, int], "min_properties": 2}
>>> TestModel(value={"hello": 1, "world": 2})
TestModel(value={'hello': 1, 'world': 2})
>>> TestModel(value={"foo": 99})
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/home/dev/middle/src/middle/model.py", line 105, in __call__
return super().__call__(**kwargs)
File "<attrs generated init 95407a54d787ed82415f9e4fc5762a3d1642f501>", line 4, in __init__
File "/home/dev/.pyenv/versions/middle-3.6.6/lib/python3.6/site-packages/attr/_make.py", line 1668, in __call__
v(inst, attr, value)
File "/home/dev/middle/src/middle/validators/common.py", line 10, in wrapper
return func(meta_value, instance, attribute, value)
File "/home/dev/middle/src/middle/validators/common.py", line 136, in dict_min_properties
attribute.name, meta_value
middle.exceptions.ValidationError: 'value' has no enough properties of 2
Setting the max_properties
keyword to a Dict would require that the input value shoud have no more than the required number of keys and values:
>>> import middle
>>> class TestModel(middle.Model):
... value: Dict[str, int] = middle.field(max_properties=3)
>>> TestModel(value={"hello": 1, "world": 2})
TestModel(value={'hello': 1, 'world': 2})
>>> TestModel(value={"hello": 1, "world": 2, "foo": 3, "bar": 4})
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/home/dev/middle/src/middle/model.py", line 105, in __call__
return super().__call__(**kwargs)
File "<attrs generated init dc8195b57024445a6dc3a09749cde9f62b46333d>", line 4, in __init__
File "/home/dev/.pyenv/versions/middle-3.6.6/lib/python3.6/site-packages/attr/_make.py", line 1668, in __call__
v(inst, attr, value)
File "/home/dev/middle/src/middle/validators/common.py", line 10, in wrapper
return func(meta_value, instance, attribute, value)
File "/home/dev/middle/src/middle/validators/common.py", line 147, in dict_max_properties
attribute.name, meta_value
middle.exceptions.ValidationError: 'value' has more properties than the limit of 3
Configuring¶
Configuring middle
is straightforward, since it has just a few options, all of them regarding how the input data should be treated or converted when creating your models.
String input¶
For an input string value, there are two options that can modify the way certain value can be turned into a str
instance (or not).
force_str
¶
Default: False
By using force_str
(as a bool
), every input value for str
fields will be forced to use the str(value)
function:
>>> import middle
>>> class TestModel(middle.Model):
... value = middle.field(type=str)
>>> middle.config.force_str = True
>>> TestModel(value=3.14)
TestModel(value='3.14')
>>> TestModel(value=object)
TestModel(value="<class 'object'>")
str_method
¶
Default: True
By using str_method
(as a bool
), every input value for str
fields will be checked for the existence of a __str__
method and, if found, str(value)
will be called:
>>> import middle
>>> class TestModel(middle.Model):
... value = middle.field(type=str)
>>> TestModel(value=3.14)
TestModel(value='3.14')
>>> middle.config.str_method = False
>>> TestModel(value=3.14)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/home/dev/middle/src/middle/model.py", line 105, in __call__
return super().__call__(**kwargs)
File "<attrs generated init b2f9a9c2c12524cd8fd8cd7557d4ba62494b3007>", line 2, in __init__
File "/home/dev/.pyenv/versions/middle-3.6.6/lib/python3.6/site-packages/attr/converters.py", line 22, in optional_converter
return converter(val)
File "/home/dev/middle/src/middle/converters.py", line 46, in _str_converter
'the value "{!s}" given should not be converted to str'.format(value)
TypeError: the value "3.14" given should not be converted to str
Datetime input¶
no_transit_local_dtime
¶
Default: False
By using no_transit_local_dtime
(as a bool
), every datetime input value that doesn’t have a timezone (naive instances) set will be treated as using the current machine timezone and automatically converted to UTC. If set to True
, all naive datetime instances will be already set as UTC. The examples bellow ran in a machine configured with the GMT-0300 timezone:
>>> from datetime import datetime
>>> import middle
>>> class TestModel(middle.Model):
... value = middle.field(type=datetime)
>>> TestModel(value=(2018, 7, 18, 14, 0))
TestModel(value=datetime.datetime(2018, 7, 18, 17, 0, tzinfo=datetime.timezone.utc))
>>> TestModel(value=datetime(2018, 7, 18, 14))
TestModel(value=datetime.datetime(2018, 7, 18, 17, 0, tzinfo=datetime.timezone.utc))
>>> TestModel(value="2018-07-18T14:00:00")
TestModel(value=datetime.datetime(2018, 7, 18, 17, 0, tzinfo=datetime.timezone.utc))
>>> middle.config.no_transit_local_dtime = True
>>> TestModel(value=(2018, 7, 18, 14, 0))
TestModel(value=datetime.datetime(2018, 7, 18, 14, 0, tzinfo=datetime.timezone.utc))
>>> TestModel(value=datetime(2018, 7, 18, 14))
TestModel(value=datetime.datetime(2018, 7, 18, 14, 0, tzinfo=datetime.timezone.utc))
>>> TestModel(value="2018-07-18T14:00:00")
TestModel(value=datetime.datetime(2018, 7, 18, 14, 0, tzinfo=datetime.timezone.utc))
If an input is not naive, it will be transformed to UTC regardless the value of no_transit_local_dtime
:
>>> from datetime import datetime, timezone
>>> import middle
>>> class TestModel(middle.Model):
... value = middle.field(type=datetime)
>>> # quick hack to get the local timezone
... current_tz = datetime.now(timezone.utc).astimezone().tzinfo
>>> current_tz
datetime.timezone(datetime.timedelta(-1, 75600), '-03')
>>> middle.config.no_transit_local_dtime
False
>>> TestModel(value=datetime(2018, 7, 18, 14, 0, tzinfo=current_tz))
TestModel(value=datetime.datetime(2018, 7, 18, 17, 0, tzinfo=datetime.timezone.utc))
>>> TestModel(value=(2018, 7, 18, 14, 0, 0, 0, -3))
TestModel(value=datetime.datetime(2018, 7, 18, 17, 0, tzinfo=datetime.timezone.utc))
>>> TestModel(value="2018-07-18T14:00:00-03:00")
TestModel(value=datetime.datetime(2018, 7, 18, 17, 0, tzinfo=datetime.timezone.utc))
>>> middle.config.no_transit_local_dtime = True
>>> TestModel(value=datetime(2018, 7, 18, 14, 0, tzinfo=current_tz))
TestModel(value=datetime.datetime(2018, 7, 18, 17, 0, tzinfo=datetime.timezone.utc))
>>> TestModel(value=(2018, 7, 18, 14, 0, 0, 0, -3))
TestModel(value=datetime.datetime(2018, 7, 18, 17, 0, tzinfo=datetime.timezone.utc))
>>> TestModel(value="2018-07-18T14:00:00-03:00")
TestModel(value=datetime.datetime(2018, 7, 18, 17, 0, tzinfo=datetime.timezone.utc))
Temporary options¶
middle.config
offers a context manager, called temp
, to provide all options as keywords inside the context for convenience:
>>> from datetime import datetime
>>> import middle
>>> class TestModel(middle.Model):
... value = middle.field(type=datetime)
>>> TestModel(value=(2018, 7, 18, 14, 0))
TestModel(value=datetime.datetime(2018, 7, 18, 17, 0, tzinfo=datetime.timezone.utc))
>>> with middle.config.temp(no_transit_local_dtime=True):
... TestModel(value=(2018, 7, 18, 14, 0))
TestModel(value=datetime.datetime(2018, 7, 18, 14, 0, tzinfo=datetime.timezone.utc))
>>> middle.config.no_transit_local_dtime
False
Extending and customizations¶
Todo.
attrs
tips¶
Todo.
Troubleshooting¶
Todo.
Contributing¶
Contributions are welcome, and they are greatly appreciated! Every little bit helps, and credit will always be given.
Bug reports¶
When reporting a bug please include:
- Your operating system name and version.
- Any details about your local setup that might be helpful in troubleshooting.
- Detailed steps to reproduce the bug.
Documentation improvements¶
middle could always use more documentation, whether as part of the official middle docs, in docstrings, or even on the web in blog posts, articles, and such.
Feature requests and feedback¶
The best way to send feedback is to file an issue at https://github.com/vltr/middle/issues.
If you are proposing a feature:
- Explain in detail how it would work.
- Keep the scope as narrow as possible, to make it easier to implement.
- Remember that this is a volunteer-driven project, and that code contributions are welcome :)
Development¶
To set up middle for local development:
Fork middle (look for the “Fork” button).
Clone your fork locally:
git clone git@github.com:your_name_here/middle.git
Create a branch for local development:
git checkout -b name-of-your-bugfix-or-feature
Now you can make your changes locally.
When you’re done making changes, run all the checks, doc builder and spell checker with tox one command:
tox
Commit your changes and push your branch to GitHub:
git add . git commit -m "Your detailed description of your changes." git push origin name-of-your-bugfix-or-feature
Submit a pull request through the GitHub website.
Pull Request Guidelines¶
If you need some code review or feedback while you’re developing the code just make the pull request.
For merging, you should:
- Include passing tests (run
tox
) [1]. - Update documentation when there’s new API, functionality etc.
- Add a note to
CHANGELOG.rst
about the changes. - Add yourself to
AUTHORS.rst
.
[1] | If you don’t have all the necessary python versions available locally you can rely on Travis - it will run the tests for each change you add in the pull request. It will be slower though … |
Tips¶
To run a subset of tests:
tox -e envname -- pytest -k test_myfeature
To run all the test environments in parallel (you need to pip install detox
):
detox
Changelog¶
v0.2.1 on 2018-07-26¶
- Quick fix related to the change log and MANIFEST.in files
v0.2.0 on 2018-07-26¶
- Released (part) of the documentation
- Got 99% coverage (finally combined)
- Python 3.5 support added
v0.1.1 on 2018-07-02¶
- Add proper unit testing and support for Python 3.6 and 3.7
- Made the API a bit more flexible
- Code format and check done with black
v0.1.0 on 2018-06-21¶
- First release on PyPI.
Authors¶
- Richard Kuesters - https://vltr.github.io/
Useful links¶
Inspirations and thanks¶
Some libs that inspired the creation of middle
:
- attrs: how such a simple library can be such flexible, extendable and fast?
- cattrs: for its speed on creating
attrs
instances fromdict
and to instances again; - pydantic: for such pythonic and beautiful approach on creating classes using
typing
hints; - mashmallow: it is one of the most feature rich modelling APIs I’ve seen;
- apistar: it’s almost magical!
- Sanic: “Gotta go fast!”
- ionelmc/cookiecutter-pylibrary: The most complete (or interesting)
cookiecutter
template I found so far (make sure to read this article too);