update docs, cleaning

This commit is contained in:
collerek
2020-11-01 11:41:18 +01:00
parent 358b5c2e52
commit cce59acd99
25 changed files with 468 additions and 220 deletions

View File

@ -44,9 +44,9 @@ git checkout -b my-new-feature-branch
# 5. Formatting and linting
# ormar uses black for formatting, flake8 for linting and mypy for type hints check
# run all of the following as all those calls will be run on travis after every push
black ormar
black ormar tests
flake8 ormar
mypy --config-file mypy.ini ormar
mypy --config-file mypy.ini ormar tests
# 6. Run tests
# on localhost all tests are run against sglite backend

View File

@ -78,7 +78,7 @@ Used in sql only.
Sample usage:
```Python hl_lines="19-21"
```Python hl_lines="21-23"
--8<-- "../docs_src/fields/docs004.py"
```

View File

@ -50,14 +50,44 @@ Here you have a sample model with changed names
```
Note that you can also change the ForeignKey column name
```Python hl_lines="9"
```Python hl_lines="21"
--8<-- "../docs_src/models/docs009.py"
```
But for now you cannot change the ManyToMany column names as they go through other Model anyway.
```Python hl_lines="18"
```Python hl_lines="28"
--8<-- "../docs_src/models/docs010.py"
```
### Type Hints & Legacy
Before version 0.4.0 `ormar` supported only one way of defining `Fields` on a `Model` using python type hints as pydantic.
```Python hl_lines="15-17"
--8<-- "../docs_src/models/docs011.py"
```
But that didn't play well with static type checkers like `mypy` and `pydantic` PyCharm plugin.
Therefore from version >=0.4.0 `ormar` switched to new notation.
```Python hl_lines="15-17"
--8<-- "../docs_src/models/docs001.py"
```
Note that type hints are **optional** so perfectly valid `ormar` code can look like this:
```Python hl_lines="15-17"
--8<-- "../docs_src/models/docs001.py"
```
!!!warning
Even if you use type hints **`ormar` does not use them to construct `pydantic` fields!**
Type hints are there only to support static checkers and linting,
`ormar` construct annotations used by `pydantic` from own fields.
### Database initialization/ migrations
Note that all examples assume that you already have a database.
@ -133,6 +163,20 @@ Created instance needs to be passed to every `Model` with `Meta` class `metadata
You need to create the `MetaData` instance **only once** and use it for all models.
You can create several ones if you want to use multiple databases.
#### Best practice
Only thing that `ormar` expects is a class with name `Meta` and two class variables: `metadata` and `databases`.
So instead of providing the same parameters over and over again for all models you should creata a class and subclass it in all models.
```Python hl_lines="14 20 33"
--8<-- "../docs_src/models/docs013.py"
```
!!!warning
You need to subclass your `MainMeta` class in each `Model` class as those classes store configuration variables
that otherwise would be overwritten by each `Model`.
### Table Names
By default table name is created from Model class name as lowercase name plus 's'.
@ -278,7 +322,7 @@ To access ormar `Fields` you can use `Model.Meta.model_fields` parameter
For example to list table model fields you can:
```Python hl_lines="19"
```Python hl_lines="20"
--8<-- "../docs_src/models/docs005.py"
```

28
docs/mypy.md Normal file
View File

@ -0,0 +1,28 @@
To provide better errors check you should use mypy with pydantic [plugin][plugin]
Note that legacy model declaration type will raise static type analyzers errors.
So you **cannot use the old notation** like this:
```Python hl_lines="15-17"
--8<-- "../docs_src/models/docs011.py"
```
Instead switch to notation introduced in version 0.4.0.
```Python hl_lines="15-17"
--8<-- "../docs_src/models/docs012.py"
```
Note that above example is not using the type hints, so further operations with mypy might fail, depending on the context.
Preferred notation should look liked this:
```Python hl_lines="15-17"
--8<-- "../docs_src/models/docs001.py"
```
[plugin]: https://pydantic-docs.helpmanual.io/mypy_plugin/

18
docs/plugin.md Normal file
View File

@ -0,0 +1,18 @@
While `ormar` will work with any IDE there is a PyCharm `pydantic` plugin that enhances the user experience for this IDE.
Plugin is available on the JetBrains Plugins Repository for PyCharm: [plugin page][plugin page].
You can install the plugin for free from the plugin marketplace
(PyCharm's Preferences -> Plugin -> Marketplace -> search "pydantic").
!!!note
For plugin to work properly you need to provide valid type hints for model fields.
!!!info
Plugin supports type hints, argument inspection and more but mainly only for __init__ methods
More information can be found on the
[official plugin page](https://plugins.jetbrains.com/plugin/12861-pydantic)
and [github repository](https://github.com/koxudaxi/pydantic-pycharm-plugin).
[plugin page]: https://plugins.jetbrains.com/plugin/12861-pydantic

View File

@ -2,10 +2,13 @@
## QuerySet
Each Model is auto registered with a QuerySet that represents the underlaying query and it's options.
Each Model is auto registered with a `QuerySet` that represents the underlaying query and it's options.
Most of the methods are also available through many to many relation interface.
!!!info
To see which one are supported and how to construct relations visit [relations][relations].
Given the Models like this
```Python
@ -95,74 +98,24 @@ If you do not provide this flag or a filter a `QueryDefinitionError` will be rai
Return number of rows updated.
```python hl_lines="24-28"
import databases
import ormar
import sqlalchemy
database = databases.Database("sqlite:///db.sqlite")
metadata = sqlalchemy.MetaData()
class Book(ormar.Model):
class Meta:
tablename = "books"
metadata = metadata
database = database
id: int = ormar.Integer(primary_key=True)
title: str = ormar.String(max_length=200)
author: str = ormar.String(max_length=100)
genre: str = ormar.String(max_length=100, default='Fiction', choices=['Fiction', 'Adventure', 'Historic', 'Fantasy'])
await Book.objects.create(title='Tom Sawyer', author="Twain, Mark", genre='Adventure')
await Book.objects.create(title='War and Peace', author="Tolstoy, Leo", genre='Fiction')
await Book.objects.create(title='Anna Karenina', author="Tolstoy, Leo", genre='Fiction')
# queryset needs to be filtered before deleting to prevent accidental overwrite
# to update whole database table each=True needs to be provided as a safety switch
await Book.objects.update(each=True, genre='Fiction')
all_books = await Book.objects.filter(genre='Fiction').all()
assert len(all_books) == 3
```Python hl_lines="26-28"
--8<-- "../docs_src/queries/docs002.py"
```
!!!warning
Queryset needs to be filtered before updating to prevent accidental overwrite.
To update whole database table `each=True` needs to be provided as a safety switch
### update_or_create
`update_or_create(**kwargs) -> Model`
Updates the model, or in case there is no match in database creates a new one.
```python hl_lines="24-30"
import databases
import ormar
import sqlalchemy
database = databases.Database("sqlite:///db.sqlite")
metadata = sqlalchemy.MetaData()
class Book(ormar.Model):
class Meta:
tablename = "books"
metadata = metadata
database = database
id: int = ormar.Integer(primary_key=True)
title: str = ormar.String(max_length=200)
author: str = ormar.String(max_length=100)
genre: str = ormar.String(max_length=100, default='Fiction', choices=['Fiction', 'Adventure', 'Historic', 'Fantasy'])
await Book.objects.create(title='Tom Sawyer', author="Twain, Mark", genre='Adventure')
await Book.objects.create(title='War and Peace', author="Tolstoy, Leo", genre='Fiction')
await Book.objects.create(title='Anna Karenina', author="Tolstoy, Leo", genre='Fiction')
# if not exist the instance will be persisted in db
vol2 = await Book.objects.update_or_create(title="Volume II", author='Anonymous', genre='Fiction')
assert await Book.objects.count() == 1
# if pk or pkname passed in kwargs (like id here) the object will be updated
assert await Book.objects.update_or_create(id=vol2.id, genre='Historic')
assert await Book.objects.count() == 1
```Python hl_lines="26-32"
--8<-- "../docs_src/queries/docs003.py"
```
!!!note
@ -177,36 +130,8 @@ Allows you to create multiple objects at once.
A valid list of `Model` objects needs to be passed.
```python hl_lines="20-26"
import databases
import ormar
import sqlalchemy
database = databases.Database("sqlite:///db.sqlite")
metadata = sqlalchemy.MetaData()
class ToDo(ormar.Model):
class Meta:
tablename = "todos"
metadata = metadata
database = database
id: int = ormar.Integer(primary_key=True)
text: str = ormar.String(max_length=500)
completed= ormar.Boolean(default=False)
# create multiple instances at once with bulk_create
await ToDo.objects.bulk_create(
[
ToDo(text="Buy the groceries."),
ToDo(text="Call Mum.", completed=True),
ToDo(text="Send invoices.", completed=True),
]
)
todoes = await ToDo.objects.all()
assert len(todoes) == 3
```python hl_lines="21-27"
--8<-- "../docs_src/queries/docs004.py"
```
### bulk_update
@ -245,34 +170,8 @@ If you do not provide this flag or a filter a `QueryDefinitionError` will be rai
Return number of rows deleted.
```python hl_lines="23-27"
import databases
import ormar
import sqlalchemy
database = databases.Database("sqlite:///db.sqlite")
metadata = sqlalchemy.MetaData()
class Book(ormar.Model):
class Meta:
tablename = "books"
metadata = metadata
database = database
id: int = ormar.Integer(primary_key=True)
title: str = ormar.String(max_length=200)
author: str = ormar.String(max_length=100)
genre: str = ormar.String(max_length=100, default='Fiction', choices=['Fiction', 'Adventure', 'Historic', 'Fantasy'])
await Book.objects.create(title='Tom Sawyer', author="Twain, Mark", genre='Adventure')
await Book.objects.create(title='War and Peace in Space', author="Tolstoy, Leo", genre='Fantasy')
await Book.objects.create(title='Anna Karenina', author="Tolstoy, Leo", genre='Fiction')
# delete accepts kwargs that will be used in filter
# acting in same way as queryset.filter(**kwargs).delete()
await Book.objects.delete(genre='Fantasy') # delete all fantasy books
all_books = await Book.objects.all()
assert len(all_books) == 2
```python hl_lines="26-30"
--8<-- "../docs_src/queries/docs005.py"
```
### all
@ -453,76 +352,8 @@ has_sample = await Book.objects.filter(title='Sample').exists()
With `fields()` you can select subset of model columns to limit the data load.
```python hl_lines="48 60 61 67"
import databases
import sqlalchemy
import ormar
from tests.settings import DATABASE_URL
database = databases.Database(DATABASE_URL, force_rollback=True)
metadata = sqlalchemy.MetaData()
class Company(ormar.Model):
class Meta:
tablename = "companies"
metadata = metadata
database = database
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
founded: int = ormar.Integer(nullable=True)
class Car(ormar.Model):
class Meta:
tablename = "cars"
metadata = metadata
database = database
id: int = ormar.Integer(primary_key=True)
manufacturer= ormar.ForeignKey(Company)
name: str = ormar.String(max_length=100)
year: int = ormar.Integer(nullable=True)
gearbox_type: str = ormar.String(max_length=20, nullable=True)
gears: int = ormar.Integer(nullable=True)
aircon_type: str = ormar.String(max_length=20, nullable=True)
# build some sample data
toyota = await Company.objects.create(name="Toyota", founded=1937)
await Car.objects.create(manufacturer=toyota, name="Corolla", year=2020, gearbox_type='Manual', gears=5,
aircon_type='Manual')
await Car.objects.create(manufacturer=toyota, name="Yaris", year=2019, gearbox_type='Manual', gears=5,
aircon_type='Manual')
await Car.objects.create(manufacturer=toyota, name="Supreme", year=2020, gearbox_type='Auto', gears=6,
aircon_type='Auto')
# select manufacturer but only name - to include related models use notation {model_name}__{column}
all_cars = await Car.objects.select_related('manufacturer').fields(['id', 'name', 'company__name']).all()
for car in all_cars:
# excluded columns will yield None
assert all(getattr(car, x) is None for x in ['year', 'gearbox_type', 'gears', 'aircon_type'])
# included column on related models will be available, pk column is always included
# even if you do not include it in fields list
assert car.manufacturer.name == 'Toyota'
# also in the nested related models - you cannot exclude pk - it's always auto added
assert car.manufacturer.founded is None
# fields() can be called several times, building up the columns to select
# models selected in select_related but with no columns in fields list implies all fields
all_cars = await Car.objects.select_related('manufacturer').fields('id').fields(
['name']).all()
# all fiels from company model are selected
assert all_cars[0].manufacturer.name == 'Toyota'
assert all_cars[0].manufacturer.founded == 1937
# cannot exclude mandatory model columns - company__name in this example
await Car.objects.select_related('manufacturer').fields(['id', 'name', 'company__founded']).all()
# will raise pydantic ValidationError as company.name is required
```python hl_lines="47 59 60 66"
--8<-- "../docs_src/queries/docs006.py"
```
!!!warning
@ -539,4 +370,5 @@ await Car.objects.select_related('manufacturer').fields(['id', 'name', 'company_
Something like `Track.object.select_related("album").filter(album__name="Malibu").offset(1).limit(1).all()`
[models]: ./models.md
[models]: ./models.md
[relations]: ./relations.md

View File

@ -15,7 +15,7 @@ Sqlalchemy column and Type are automatically taken from target `Model`.
To define a relation add `ForeignKey` field that points to related `Model`.
```Python hl_lines="27"
```Python hl_lines="29"
--8<-- "../docs_src/fields/docs003.py"
```
@ -25,7 +25,7 @@ To define a relation add `ForeignKey` field that points to related `Model`.
By default it's child (source) `Model` name + s, like courses in snippet below:
```Python hl_lines="27 33"
```Python hl_lines="29 35"
--8<-- "../docs_src/fields/docs001.py"
```
@ -33,7 +33,7 @@ By default it's child (source) `Model` name + s, like courses in snippet below:
But you can overwrite this name by providing `related_name` parameter like below:
```Python hl_lines="27 33"
```Python hl_lines="29 35"
--8<-- "../docs_src/fields/docs002.py"
```
@ -49,7 +49,7 @@ You have several ways to set-up a relationship connection.
The most obvious one is to pass a related `Model` instance to the constructor.
```Python hl_lines="32-33"
```Python hl_lines="34-35"
--8<-- "../docs_src/relations/docs001.py"
```
@ -57,7 +57,7 @@ The most obvious one is to pass a related `Model` instance to the constructor.
You can setup the relation also with just the pk column value of the related model.
```Python hl_lines="35-36"
```Python hl_lines="37-38"
--8<-- "../docs_src/relations/docs001.py"
```
@ -67,7 +67,7 @@ Next option is with a dictionary of key-values of the related model.
You can build the dictionary yourself or get it from existing model with `dict()` method.
```Python hl_lines="38-39"
```Python hl_lines="40-41"
--8<-- "../docs_src/relations/docs001.py"
```
@ -75,7 +75,7 @@ You can build the dictionary yourself or get it from existing model with `dict()
Finally you can explicitly set it to None (default behavior if no value passed).
```Python hl_lines="41-42"
```Python hl_lines="43-44"
--8<-- "../docs_src/relations/docs001.py"
```
@ -121,7 +121,11 @@ await news.posts.add(post)
Otherwise an IntegrityError will be raised by your database driver library.
#### Creating new related `Model` instances
#### create()
Create related `Model` directly from parent `Model`.
The link table is automatically populated, as well as relation ids in the database.
```python
# Creating columns object from instance:
@ -136,15 +140,27 @@ assert len(await post.categories.all()) == 2
To learn more about available QuerySet methods visit [queries][queries]
#### Removing related models
#### remove()
Removal of the related model one by one.
Removes also the relation in the database.
```python
# Removal of the relationship by one
await news.posts.remove(post)
# or all at once
```
#### clear()
Removal all related models in one call.
Removes also the relation in the database.
```python
await news.posts.clear()
```
#### All other queryset methods
#### Other queryset methods
When access directly the related `ManyToMany` field returns the list of related models.
@ -164,7 +180,18 @@ news_posts = await news.posts.select_related("author").all()
assert news_posts[0].author == guido
```
Currently supported methods are:
!!!tip
To learn more about available QuerySet methods visit [queries][queries]
##### get()
##### all()
##### filter()
##### select_related()
##### limit()
##### offset()
##### count()
##### exists()
[queries]: ./queries.md

View File

@ -1,6 +1,22 @@
# 0.4.0
* Changed notation in Model definition -> now use name = ormar.Field() not name: ormar.Field()
* Note that old notation is still supported but deprecated and will not play nice with static checkers like mypy and pydantic pycharm plugin
* Type hint docs and test
* Use mypy for tests also not, only ormar package
* Fix scale and precision translation with max_digits and decimal_places pydantic Decimal field
* Update docs - add best practices for dependencies
* Refactor metaclass and model_fields to play nice with type hints
* Add mypy and pydantic plugin to docs
* Expand the docs on ManyToMany relation
# 0.3.11
* Fix setting server_default as default field value in python
# 0.3.10
* Fix
* Fix postgresql check to avoid exceptions with drivers not installed if using different backend
# 0.3.9