From cce59acd993f6a439a8a78351649dc8298c13c01 Mon Sep 17 00:00:00 2001 From: collerek Date: Sun, 1 Nov 2020 11:41:18 +0100 Subject: [PATCH] update docs, cleaning --- .travis.yml | 2 +- docs/contributing.md | 4 +- docs/fields.md | 2 +- docs/models.md | 50 ++++++- docs/mypy.md | 28 ++++ docs/plugin.md | 18 +++ docs/queries.md | 212 ++++-------------------------- docs/relations.md | 51 +++++-- docs/releases.md | 18 ++- docs_src/fastapi/mypy/__init__.py | 0 docs_src/fastapi/mypy/docs001.py | 17 +++ docs_src/models/docs007.py | 5 +- docs_src/models/docs010.py | 6 +- docs_src/models/docs011.py | 19 +++ docs_src/models/docs012.py | 17 +++ docs_src/models/docs013.py | 38 ++++++ docs_src/queries/docs002.py | 28 ++++ docs_src/queries/docs003.py | 32 +++++ docs_src/queries/docs004.py | 30 +++++ docs_src/queries/docs005.py | 30 +++++ docs_src/queries/docs006.py | 67 ++++++++++ mkdocs.yml | 2 + tests/test_aliases.py | 4 +- tests/test_many_to_many.py | 4 +- tests/test_models.py | 4 +- 25 files changed, 468 insertions(+), 220 deletions(-) create mode 100644 docs/mypy.md create mode 100644 docs/plugin.md create mode 100644 docs_src/fastapi/mypy/__init__.py create mode 100644 docs_src/fastapi/mypy/docs001.py create mode 100644 docs_src/models/docs011.py create mode 100644 docs_src/models/docs012.py create mode 100644 docs_src/models/docs013.py create mode 100644 docs_src/queries/docs002.py create mode 100644 docs_src/queries/docs003.py create mode 100644 docs_src/queries/docs004.py create mode 100644 docs_src/queries/docs005.py create mode 100644 docs_src/queries/docs006.py diff --git a/.travis.yml b/.travis.yml index 6996e4f..f3c6d41 100644 --- a/.travis.yml +++ b/.travis.yml @@ -27,7 +27,7 @@ script: - DATABASE_URL=postgresql://localhost/test_database scripts/test.sh - DATABASE_URL=mysql://localhost/test_database scripts/test.sh - DATABASE_URL=sqlite:///test.db scripts/test.sh - - mypy --config-file mypy.ini ormar + - mypy --config-file mypy.ini ormar tests after_script: - codecov \ No newline at end of file diff --git a/docs/contributing.md b/docs/contributing.md index 690a2cf..90c957a 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -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 diff --git a/docs/fields.md b/docs/fields.md index 6dc0a17..67bdc74 100644 --- a/docs/fields.md +++ b/docs/fields.md @@ -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" ``` diff --git a/docs/models.md b/docs/models.md index 495b839..5fdc950 100644 --- a/docs/models.md +++ b/docs/models.md @@ -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" ``` diff --git a/docs/mypy.md b/docs/mypy.md new file mode 100644 index 0000000..4066e3d --- /dev/null +++ b/docs/mypy.md @@ -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/ \ No newline at end of file diff --git a/docs/plugin.md b/docs/plugin.md new file mode 100644 index 0000000..fff0590 --- /dev/null +++ b/docs/plugin.md @@ -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 \ No newline at end of file diff --git a/docs/queries.md b/docs/queries.md index 18c6479..77c6229 100644 --- a/docs/queries.md +++ b/docs/queries.md @@ -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 \ No newline at end of file +[models]: ./models.md +[relations]: ./relations.md \ No newline at end of file diff --git a/docs/relations.md b/docs/relations.md index fd6a3e1..5bda020 100644 --- a/docs/relations.md +++ b/docs/relations.md @@ -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 \ No newline at end of file diff --git a/docs/releases.md b/docs/releases.md index ebc41e0..c24ec54 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -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 diff --git a/docs_src/fastapi/mypy/__init__.py b/docs_src/fastapi/mypy/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/docs_src/fastapi/mypy/docs001.py b/docs_src/fastapi/mypy/docs001.py new file mode 100644 index 0000000..2c74a77 --- /dev/null +++ b/docs_src/fastapi/mypy/docs001.py @@ -0,0 +1,17 @@ +import databases +import sqlalchemy + +import ormar + +database = databases.Database("sqlite:///db.sqlite") +metadata = sqlalchemy.MetaData() + + +class Course(ormar.Model): + class Meta: + database = database + metadata = metadata + + id = ormar.Integer(primary_key=True) + name = ormar.String(max_length=100) + completed = ormar.Boolean(default=False) diff --git a/docs_src/models/docs007.py b/docs_src/models/docs007.py index 2f4b7b5..9dfe28a 100644 --- a/docs_src/models/docs007.py +++ b/docs_src/models/docs007.py @@ -18,5 +18,6 @@ class Course(ormar.Model): course = Course(name="Painting for dummies", completed=False) -await course.save() # type: ignore -await Course.objects.create(name="Painting for dummies", completed=False) # type: ignore +await course.save() + +await Course.objects.create(name="Painting for dummies", completed=False) diff --git a/docs_src/models/docs010.py b/docs_src/models/docs010.py index af2c9dc..d63101e 100644 --- a/docs_src/models/docs010.py +++ b/docs_src/models/docs010.py @@ -1,5 +1,3 @@ -from typing import Optional, Union, List - import databases import sqlalchemy @@ -27,6 +25,4 @@ class Artist(ormar.Model): first_name: str = ormar.String(name="fname", max_length=100) last_name: str = ormar.String(name="lname", max_length=100) born_year: int = ormar.Integer(name="year") - children: Optional[Union[Child, List[Child]]] = ormar.ManyToMany( - Child, through=ArtistChildren - ) + children = ormar.ManyToMany(Child, through=ArtistChildren) diff --git a/docs_src/models/docs011.py b/docs_src/models/docs011.py new file mode 100644 index 0000000..962de1d --- /dev/null +++ b/docs_src/models/docs011.py @@ -0,0 +1,19 @@ +import databases +import sqlalchemy + +import ormar + +database = databases.Database("sqlite:///db.sqlite") +metadata = sqlalchemy.MetaData() + + +class Course(ormar.Model): + class Meta: + database = database + metadata = metadata + + id: ormar.Integer(primary_key=True) + name: ormar.String(max_length=100) + completed: ormar.Boolean(default=False) + +c1 = Course() \ No newline at end of file diff --git a/docs_src/models/docs012.py b/docs_src/models/docs012.py new file mode 100644 index 0000000..68e75b9 --- /dev/null +++ b/docs_src/models/docs012.py @@ -0,0 +1,17 @@ +import databases +import sqlalchemy + +import ormar + +database = databases.Database("sqlite:///db.sqlite") +metadata = sqlalchemy.MetaData() + + +class Course(ormar.Model): + class Meta: + database = database + metadata = metadata + + id = ormar.Integer(primary_key=True) + name = ormar.String(max_length=100) + completed = ormar.Boolean(default=False) \ No newline at end of file diff --git a/docs_src/models/docs013.py b/docs_src/models/docs013.py new file mode 100644 index 0000000..8d584f3 --- /dev/null +++ b/docs_src/models/docs013.py @@ -0,0 +1,38 @@ +from typing import Optional + +import databases +import sqlalchemy + +import ormar + +database = databases.Database("sqlite:///test.db", force_rollback=True) +metadata = sqlalchemy.MetaData() + + +# note that you do not have to subclass ModelMeta, +# it's useful for type hints and code completion +class MainMeta(ormar.ModelMeta): + metadata = metadata + database = database + + +class Artist(ormar.Model): + class Meta(MainMeta): + # note that tablename is optional + # if not provided ormar will user class.__name__.lower()+'s' + # -> artists in this example + pass + + id: int = ormar.Integer(primary_key=True) + first_name: str = ormar.String(max_length=100) + last_name: str = ormar.String(max_length=100) + born_year: int = ormar.Integer(name="year") + + +class Album(ormar.Model): + class Meta(MainMeta): + pass + + id: int = ormar.Integer(primary_key=True) + name: str = ormar.String(max_length=100) + artist: Optional[Artist] = ormar.ForeignKey(Artist) diff --git a/docs_src/queries/docs002.py b/docs_src/queries/docs002.py new file mode 100644 index 0000000..d9cdeff --- /dev/null +++ b/docs_src/queries/docs002.py @@ -0,0 +1,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') + +await Book.objects.update(each=True, genre='Fiction') +all_books = await Book.objects.filter(genre='Fiction').all() +assert len(all_books) == 3 diff --git a/docs_src/queries/docs003.py b/docs_src/queries/docs003.py new file mode 100644 index 0000000..58e4f1b --- /dev/null +++ b/docs_src/queries/docs003.py @@ -0,0 +1,32 @@ +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 diff --git a/docs_src/queries/docs004.py b/docs_src/queries/docs004.py new file mode 100644 index 0000000..6aaf01f --- /dev/null +++ b/docs_src/queries/docs004.py @@ -0,0 +1,30 @@ +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 diff --git a/docs_src/queries/docs005.py b/docs_src/queries/docs005.py new file mode 100644 index 0000000..dca0757 --- /dev/null +++ b/docs_src/queries/docs005.py @@ -0,0 +1,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 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 diff --git a/docs_src/queries/docs006.py b/docs_src/queries/docs006.py new file mode 100644 index 0000000..936c79f --- /dev/null +++ b/docs_src/queries/docs006.py @@ -0,0 +1,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 diff --git a/mkdocs.yml b/mkdocs.yml index a7b99e3..9c3dec6 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -8,6 +8,8 @@ nav: - Relations: relations.md - Queries: queries.md - Use with Fastapi: fastapi.md + - Use with mypy: mypy.md + - PyCharm plugin: plugin.md - Contributing: contributing.md - Release Notes: releases.md repo_name: collerek/ormar diff --git a/tests/test_aliases.py b/tests/test_aliases.py index 581b59c..81c4a8c 100644 --- a/tests/test_aliases.py +++ b/tests/test_aliases.py @@ -40,7 +40,9 @@ class Artist(ormar.Model): first_name: str = ormar.String(name="fname", max_length=100) last_name: str = ormar.String(name="lname", max_length=100) born_year: int = ormar.Integer(name="year") - children: Optional[Union[Child, List[Child]]] = ormar.ManyToMany(Child, through=ArtistChildren) + children: Optional[Union[Child, List[Child]]] = ormar.ManyToMany( + Child, through=ArtistChildren + ) class Album(ormar.Model): diff --git a/tests/test_many_to_many.py b/tests/test_many_to_many.py index a414cae..3ddeb61 100644 --- a/tests/test_many_to_many.py +++ b/tests/test_many_to_many.py @@ -49,7 +49,9 @@ class Post(ormar.Model): id: int = ormar.Integer(primary_key=True) title: str = ormar.String(max_length=200) - categories: Optional[Union[Category, List[Category]]] = ormar.ManyToMany(Category, through=PostCategory) + categories: Optional[Union[Category, List[Category]]] = ormar.ManyToMany( + Category, through=PostCategory + ) author: Optional[Author] = ormar.ForeignKey(Author) diff --git a/tests/test_models.py b/tests/test_models.py index 2bac6d7..490dedd 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -71,7 +71,9 @@ class Country(ormar.Model): database = database id: int = ormar.Integer(primary_key=True) - name: str = ormar.String(max_length=9, choices=country_name_choices, default="Canada",) + name: str = ormar.String( + max_length=9, choices=country_name_choices, default="Canada", + ) taxed: bool = ormar.Boolean(choices=country_taxed_choices, default=True) country_code: int = ormar.Integer( minimum=0, maximum=1000, choices=country_country_code_choices, default=1