update docs part 1

This commit is contained in:
collerek
2020-10-06 19:09:58 +02:00
parent dd46dbcfd4
commit ba0990d05b
11 changed files with 505 additions and 212 deletions

View File

@ -303,7 +303,7 @@ await Book.objects.delete(genre='Fantasy') # delete all fantasy books
all_books = await Book.objects.all() all_books = await Book.objects.all()
assert len(all_books) == 3 assert len(all_books) == 3
# queryset needs to be filtered before deleting to prevent accidental overwrite # queryset needs to be filtered before deleting/ updating to prevent accidental overwrite
# to update whole database table each=True needs to be provided as a safety switch # to update whole database table each=True needs to be provided as a safety switch
await Book.objects.update(each=True, genre='Fiction') await Book.objects.update(each=True, genre='Fiction')
all_books = await Book.objects.filter(genre='Fiction').all() all_books = await Book.objects.filter(genre='Fiction').all()

View File

@ -1,5 +1,4 @@
# ORMar # ORMar
<p> <p>
<a href="https://pypi.org/project/ormar"> <a href="https://pypi.org/project/ormar">
<img src="https://img.shields.io/pypi/v/ormar.svg" alt="Pypi version"> <img src="https://img.shields.io/pypi/v/ormar.svg" alt="Pypi version">
@ -22,7 +21,12 @@
</p> </p>
The `ormar` package is an async ORM for Python, with support for Postgres, The `ormar` package is an async ORM for Python, with support for Postgres,
MySQL, and SQLite. Ormar is built with: MySQL, and SQLite.
Ormar - apart form obvious ORM in name - get it's name from ormar in swedish which means snakes, and ormar(e) in italian which means cabinet.
And what's a better name for python ORM than snakes cabinet :)
Ormar is built with:
* [`SQLAlchemy core`][sqlalchemy-core] for query building. * [`SQLAlchemy core`][sqlalchemy-core] for query building.
* [`databases`][databases] for cross-database async support. * [`databases`][databases] for cross-database async support.
@ -31,11 +35,12 @@ MySQL, and SQLite. Ormar is built with:
Because ormar is built on SQLAlchemy core, you can use [`alembic`][alembic] to provide Because ormar is built on SQLAlchemy core, you can use [`alembic`][alembic] to provide
database migrations. database migrations.
The goal was to create a simple ORM that can be used directly with [`fastapi`][fastapi] that bases it's data validation on pydantic. The goal was to create a simple ORM that can be used directly (as request and response models) with [`fastapi`][fastapi] that bases it's data validation on pydantic.
Initial work was inspired by [`encode/orm`][encode/orm]. Initial work was inspired by [`encode/orm`][encode/orm], later I found `ormantic` and used it as a further inspiration.
The encode package was too simple (i.e. no ability to join two times to the same table) and used typesystem for data checks. The encode package was too simple (i.e. no ability to join two times to the same table) and used typesystem for data checks.
**ormar is still under development:** We recommend pinning any dependencies with `ormar~=0.0.1`
**ormar is still under development:** We recommend pinning any dependencies with `ormar~=0.2.0`
**Note**: Use `ipython` to try this from the console, since it supports `await`. **Note**: Use `ipython` to try this from the console, since it supports `await`.
@ -47,16 +52,18 @@ import sqlalchemy
database = databases.Database("sqlite:///db.sqlite") database = databases.Database("sqlite:///db.sqlite")
metadata = sqlalchemy.MetaData() metadata = sqlalchemy.MetaData()
class Note(ormar.Model): class Note(ormar.Model):
__tablename__ = "notes" class Meta:
__database__ = database tablename = "notes"
__metadata__ = metadata database = database
metadata = metadata
# primary keys of type int by dafault are set to autoincrement # primary keys of type int by dafault are set to autoincrement
id = ormar.Integer(primary_key=True) id: ormar.Integer(primary_key=True)
text = ormar.String(length=100) text: ormar.String(length=100)
completed = ormar.Boolean(default=False) completed: ormar.Boolean(default=False)
# as of ormar >=0.3.2 you can provide a list of choices that will be validated
flag: ormar.String(default='To do', choices=['To do', 'Pending', 'Done'])
# Create the database # Create the database
engine = sqlalchemy.create_engine(str(database.url)) engine = sqlalchemy.create_engine(str(database.url))
@ -76,6 +83,14 @@ notes = await Note.objects.filter(completed=True).all()
# exact, iexact, contains, icontains, lt, lte, gt, gte, in # exact, iexact, contains, icontains, lt, lte, gt, gte, in
notes = await Note.objects.filter(text__icontains="mum").all() notes = await Note.objects.filter(text__icontains="mum").all()
# exclude - from ormar >= 0.3.1
notes = await Note.objects.exclude(text__icontains="mum").all()
# startswith, istartswith, endswith, iendswith - from ormar >= 0.3.3
notes = await Note.objects.filter(text__iendswith="mum.").all()
notes = await Note.objects.filter(text__istartswith="call").all()
notes = await Note.objects.filter(text__startswith="Buy").all()
# .get() # .get()
note = await Note.objects.get(id=1) note = await Note.objects.get(id=1)
@ -102,23 +117,25 @@ metadata = sqlalchemy.MetaData()
class Album(ormar.Model): class Album(ormar.Model):
__tablename__ = "album" class Meta:
__metadata__ = metadata tablename = "album"
__database__ = database metadata = metadata
database = database
id = ormar.Integer(primary_key=True) id: ormar.Integer(primary_key=True)
name = ormar.String(length=100) name: ormar.String(length=100)
class Track(ormar.Model): class Track(ormar.Model):
__tablename__ = "track" class Meta:
__metadata__ = metadata tablename = "track"
__database__ = database metadata = metadata
database = database
id = ormar.Integer(primary_key=True) id: ormar.Integer(primary_key=True)
album = ormar.ForeignKey(Album) album: ormar.ForeignKey(Album)
title = ormar.String(length=100) title: ormar.String(length=100)
position = ormar.Integer() position: ormar.Integer()
# Create some records to work with. # Create some records to work with.
@ -167,33 +184,330 @@ tracks = await Track.objects.limit(1).all()
assert len(tracks) == 1 assert len(tracks) == 1
``` ```
Since version >=0.3 Ormar supports also many to many relationships
```python
import databases
import ormar
import sqlalchemy
database = databases.Database("sqlite:///db.sqlite")
metadata = sqlalchemy.MetaData()
class Author(ormar.Model):
class Meta:
tablename = "authors"
database = database
metadata = metadata
id: ormar.Integer(primary_key=True)
first_name: ormar.String(max_length=80)
last_name: ormar.String(max_length=80)
class Category(ormar.Model):
class Meta:
tablename = "categories"
database = database
metadata = metadata
id: ormar.Integer(primary_key=True)
name: ormar.String(max_length=40)
class PostCategory(ormar.Model):
class Meta:
tablename = "posts_categories"
database = database
metadata = metadata
class Post(ormar.Model):
class Meta:
tablename = "posts"
database = database
metadata = metadata
id: ormar.Integer(primary_key=True)
title: ormar.String(max_length=200)
categories: ormar.ManyToMany(Category, through=PostCategory)
author: ormar.ForeignKey(Author)
guido = await Author.objects.create(first_name="Guido", last_name="Van Rossum")
post = await Post.objects.create(title="Hello, M2M", author=guido)
news = await Category.objects.create(name="News")
# Add a category to a post.
await post.categories.add(news)
# or from the other end:
await news.posts.add(post)
# Creating columns object from instance:
await post.categories.create(name="Tips")
assert len(await post.categories.all()) == 2
# Many to many relation exposes a list of columns models
# and an API of the Queryset:
assert news == await post.categories.get(name="News")
# with all Queryset methods - filtering, selecting columns, counting etc.
await news.posts.filter(title__contains="M2M").all()
await Category.objects.filter(posts__author=guido).get()
# columns models of many to many relation can be prefetched
news_posts = await news.posts.select_related("author").all()
assert news_posts[0].author == guido
# Removal of the relationship by one
await news.posts.remove(post)
# or all at once
await news.posts.clear()
```
Since version >=0.3.4 Ormar supports also queryset level delete and update statements,
as well as get_or_create and update_or_create
```python
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: ormar.Integer(primary_key=True)
title: ormar.String(max_length=200)
author: ormar.String(max_length=100)
genre: 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.create(title='Harry Potter', author="Rowling, J.K.", genre='Fantasy')
await Book.objects.create(title='Lord of the Rings', author="Tolkien, J.R.", genre='Fantasy')
# update accepts kwargs that are used to update queryset model
# all other arguments are ignored (argument names not in own model table)
await Book.objects.filter(author="Tolstoy, Leo").update(author="Lenin, Vladimir") # update all Tolstoy's books
all_books = await Book.objects.filter(author="Lenin, Vladimir").all()
assert len(all_books) == 2
# 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) == 3
# 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
# helper get/update or create methods of queryset
# if not exists it will be created
vol1 = await Book.objects.get_or_create(title="Volume I", author='Anonymous', genre='Fiction')
assert await Book.objects.count() == 1
# if exists it will be returned
assert await Book.objects.get_or_create(title="Volume I", author='Anonymous', genre='Fiction') == vol1
assert await Book.objects.count() == 1
# 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
```
Since version >=0.3.5 Ormar supports also bulk operations -> bulk_create and bulk_update
```python
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: ormar.Integer(primary_key=True)
text: 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
# update objects
for todo in todoes:
todo.completed = False
# perform update of all objects at once
# objects need to have pk column set, otherwise exception is raised
await ToDo.objects.bulk_update(todoes)
completed = await ToDo.objects.filter(completed=False).all()
assert len(completed) == 3
```
Since version >=0.3.6 Ormar supports unique constraints on multiple columns
```python
import databases
import ormar
import sqlalchemy
database = databases.Database("sqlite:///db.sqlite")
metadata = sqlalchemy.MetaData()
class Product(ormar.Model):
class Meta:
tablename = "products"
metadata = metadata
database = database
# define your constraints in Meta class of the model
# it's a list that can contain multiple constraints
constraints = [ormar.UniqueColumns("name", "company")]
id: ormar.Integer(primary_key=True)
name: ormar.String(max_length=100)
company: ormar.String(max_length=200)
await Product.objects.create(name="Cookies", company="Nestle")
await Product.objects.create(name="Mars", company="Mars")
await Product.objects.create(name="Mars", company="Nestle")
# will raise error based on backend
# (sqlite3.IntegrityError, pymysql.IntegrityError, asyncpg.exceptions.UniqueViolationError)
await Product.objects.create(name="Mars", company="Mars")
```
Since version >=0.3.6 Ormar supports selecting subset of model columns to limit the data load.
Warning - mandatory fields cannot be excluded as it will raise validation error, to exclude a field it has to be nullable.
Pk column cannot be excluded - it's always auto added even if not explicitly included.
```python
import databases
import pydantic
import pytest
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: ormar.Integer(primary_key=True)
name: ormar.String(max_length=100)
founded: ormar.Integer(nullable=True)
class Car(ormar.Model):
class Meta:
tablename = "cars"
metadata = metadata
database = database
id: ormar.Integer(primary_key=True)
manufacturer: ormar.ForeignKey(Company)
name: ormar.String(max_length=100)
year: ormar.Integer(nullable=True)
gearbox_type: ormar.String(max_length=20, nullable=True)
gears: ormar.Integer(nullable=True)
aircon_type: 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
```
## Data types ## Data types
The following keyword arguments are supported on all field types. The following keyword arguments are supported on all field types.
* `primary_key` * `primary_key: bool`
* `nullable` * `nullable: bool`
* `default` * `default: Any`
* `server_default` * `server_default: Any`
* `index` * `index: bool`
* `unique` * `unique: bool`
* `choices: typing.Sequence`
## Model Fields
### Common parameters
All fields are required unless one of the following is set: All fields are required unless one of the following is set:
* `nullable` - Creates a nullable column. Sets the default to `None`. * `nullable` - Creates a nullable column. Sets the default to `None`.
* `default` - Set a default value for the field. * `default` - Set a default value for the field.
* `server_default` - Set a default value for the field on server side (like sqlalchemy's `func.now()`). * `server_default` - Set a default value for the field on server side (like sqlalchemy's `func.now()`).
* `primary key` - Set a primary key on a column. * `primary key` with `autoincrement` - When a column is set to primary key and autoincrement is set on this column.
* `autoincrement` - When a column is set to primary key and autoincrement is set on this column. Autoincrement is set by default on int primary keys.
Autoincrement is set by default on int primary keys.
### Fields Types Available Model Fields (with required args - optional ones in docs):
* `String(max_length)`
* `String(length)`
* `Text()` * `Text()`
* `Boolean()` * `Boolean()`
* `Integer()` * `Integer()`
@ -203,7 +517,10 @@ All fields are required unless one of the following is set:
* `DateTime()` * `DateTime()`
* `JSON()` * `JSON()`
* `BigInteger()` * `BigInteger()`
* `Decimal(lenght, precision)` * `Decimal(scale, precision)`
* `UUID()`
* `ForeignKey(to)`
* `Many2Many(to, through)`
[sqlalchemy-core]: https://docs.sqlalchemy.org/en/latest/core/ [sqlalchemy-core]: https://docs.sqlalchemy.org/en/latest/core/
[databases]: https://github.com/encode/databases [databases]: https://github.com/encode/databases

View File

@ -21,7 +21,7 @@ Each table **has to** have a primary key column, which you specify by setting `p
Only one primary key column is allowed. Only one primary key column is allowed.
```Python hl_lines="14 15 16" ```Python hl_lines="15 16 17"
--8<-- "../docs_src/models/docs001.py" --8<-- "../docs_src/models/docs001.py"
``` ```
@ -34,7 +34,7 @@ By default if you assign primary key to `Integer` field, the `autoincrement` opt
You can disable by passing `autoincremant=False`. You can disable by passing `autoincremant=False`.
```Python ```Python
id = ormar.Integer(primary_key=True, autoincrement=False) id: ormar.Integer(primary_key=True, autoincrement=False)
``` ```
Names of the fields will be used for both the underlying `pydantic` model and `sqlalchemy` table. Names of the fields will be used for both the underlying `pydantic` model and `sqlalchemy` table.
@ -48,9 +48,9 @@ and table creation you need to assign each `Model` with two special parameters.
One is `Database` instance created with your database url in [sqlalchemy connection string][sqlalchemy connection string] format. One is `Database` instance created with your database url in [sqlalchemy connection string][sqlalchemy connection string] format.
Created instance needs to be passed to every `Model` with `__database__` parameter. Created instance needs to be passed to every `Model` with `Meta` class `database` parameter.
```Python hl_lines="1 6 11" ```Python hl_lines="1 6 12"
--8<-- "../docs_src/models/docs001.py" --8<-- "../docs_src/models/docs001.py"
``` ```
@ -62,9 +62,9 @@ Created instance needs to be passed to every `Model` with `__database__` paramet
Second dependency is sqlalchemy `MetaData` instance. Second dependency is sqlalchemy `MetaData` instance.
Created instance needs to be passed to every `Model` with `__metadata__` parameter. Created instance needs to be passed to every `Model` with `Meta` class `metadata` parameter.
```Python hl_lines="2 7 12" ```Python hl_lines="2 7 13"
--8<-- "../docs_src/models/docs001.py" --8<-- "../docs_src/models/docs001.py"
``` ```
@ -76,9 +76,9 @@ Created instance needs to be passed to every `Model` with `__metadata__` paramet
By default table name is created from Model class name as lowercase name plus 's'. By default table name is created from Model class name as lowercase name plus 's'.
You can overwrite this parameter by providing `__tablename__` argument. You can overwrite this parameter by providing `Meta` class `tablename` argument.
```Python hl_lines="11 12 13" ```Python hl_lines="12 13 14"
--8<-- "../docs_src/models/docs002.py" --8<-- "../docs_src/models/docs002.py"
``` ```
@ -91,7 +91,7 @@ There are two ways to create and persist the `Model` instance in the database.
If you plan to modify the instance in the later execution of your program you can initiate your `Model` as a normal class and later await a `save()` call. If you plan to modify the instance in the later execution of your program you can initiate your `Model` as a normal class and later await a `save()` call.
```Python hl_lines="19 20" ```Python hl_lines="20 21"
--8<-- "../docs_src/models/docs007.py" --8<-- "../docs_src/models/docs007.py"
``` ```
@ -99,41 +99,24 @@ If you want to initiate your `Model` and at the same time save in in the databas
Each model has a `QuerySet` initialised as `objects` parameter Each model has a `QuerySet` initialised as `objects` parameter
```Python hl_lines="22" ```Python hl_lines="23"
--8<-- "../docs_src/models/docs007.py" --8<-- "../docs_src/models/docs007.py"
``` ```
!!!info !!!info
To read more about `QuerySets` and available methods visit [queries][queries] To read more about `QuerySets` and available methods visit [queries][queries]
## Attributes Delegation
Each call to `Model` fields parameter under the hood is delegated to either the `pydantic` model
or other related `Model` in case of relations.
The fields and relations are not stored on the `Model` itself
```Python hl_lines="31 32 33 34 35 36 37 38 39 40 41"
--8<-- "../docs_src/models/docs006.py"
```
!!! warning
In example above model instances are created but not persisted that's why `id` of `department` is None!
!!!info
To read more about `ForeignKeys` and `Model` relations visit [relations][relations]
## Internals ## Internals
Apart from special parameters defined in the `Model` during definition (tablename, metadata etc.) the `Model` provides you with useful internals. Apart from special parameters defined in the `Model` during definition (tablename, metadata etc.) the `Model` provides you with useful internals.
### Pydantic Model ### Pydantic Model
To access auto created pydantic model you can use `Model.__pydantic_model__` parameter All `Model` classes inherit from `pydantic.BaseModel` so you can access all normal attributes of pydantic models.
For example to list model fields you can: For example to list model fields you can:
```Python hl_lines="18" ```Python hl_lines="20"
--8<-- "../docs_src/models/docs003.py" --8<-- "../docs_src/models/docs003.py"
``` ```
@ -145,11 +128,11 @@ For example to list model fields you can:
### Sqlalchemy Table ### Sqlalchemy Table
To access auto created sqlalchemy table you can use `Model.__table__` parameter To access auto created sqlalchemy table you can use `Model.Meta.table` parameter
For example to list table columns you can: For example to list table columns you can:
```Python hl_lines="18" ```Python hl_lines="20"
--8<-- "../docs_src/models/docs004.py" --8<-- "../docs_src/models/docs004.py"
``` ```
@ -161,14 +144,26 @@ For example to list table columns you can:
### Fields Definition ### Fields Definition
To access ormar `Fields` you can use `Model.__model_fields__` parameter To access ormar `Fields` you can use `Model.Meta.model_fields` parameter
For example to list table model fields you can: For example to list table model fields you can:
```Python hl_lines="18" ```Python hl_lines="19"
--8<-- "../docs_src/models/docs005.py" --8<-- "../docs_src/models/docs005.py"
``` ```
!!!info
Note that fields stored on a model are `classes` not `instances`.
So if you print just model fields you will get:
`{'id': <class 'ormar.fields.model_fields.Integer'>, `
`'name': <class 'ormar.fields.model_fields.String'>, `
`'completed': <class 'ormar.fields.model_fields.Boolean'>}`
[fields]: ./fields.md [fields]: ./fields.md
[relations]: ./relations.md [relations]: ./relations.md
[queries]: ./queries.md [queries]: ./queries.md

View File

@ -8,9 +8,10 @@ metadata = sqlalchemy.MetaData()
class Course(ormar.Model): class Course(ormar.Model):
__database__ = database class Meta:
__metadata__ = metadata database = database
metadata = metadata
id = ormar.Integer(primary_key=True) id: ormar.Integer(primary_key=True)
name = ormar.String(length=100) name: ormar.String(max_length=100)
completed = ormar.Boolean(default=False) completed: ormar.Boolean(default=False)

View File

@ -8,12 +8,13 @@ metadata = sqlalchemy.MetaData()
class Course(ormar.Model): class Course(ormar.Model):
# if you omit this parameter it will be created automatically class Meta:
# as class.__name__.lower()+'s' -> "courses" in this example # if you omit this parameter it will be created automatically
__tablename__ = "my_courses" # as class.__name__.lower()+'s' -> "courses" in this example
__database__ = database tablename = "my_courses"
__metadata__ = metadata database = database
metadata = metadata
id = ormar.Integer(primary_key=True) id: ormar.Integer(primary_key=True)
name = ormar.String(length=100) name: ormar.String(max_length=100)
completed = ormar.Boolean(default=False) completed: ormar.Boolean(default=False)

View File

@ -8,26 +8,28 @@ metadata = sqlalchemy.MetaData()
class Course(ormar.Model): class Course(ormar.Model):
__database__ = database class Meta:
__metadata__ = metadata database = database
metadata = metadata
id = ormar.Integer(primary_key=True) id: ormar.Integer(primary_key=True)
name = ormar.String(length=100) name: ormar.String(max_length=100)
completed = ormar.Boolean(default=False) completed: ormar.Boolean(default=False)
print(Course.__pydantic_model__.__fields__)
print(Course.__fields__)
""" """
Will produce: Will produce:
{'completed': ModelField(name='completed', {'id': ModelField(name='id',
type=bool,
required=False,
default=False),
'id': ModelField(name='id',
type=Optional[int], type=Optional[int],
required=False, required=False,
default=None), default=None),
'name': ModelField(name='name', 'name': ModelField(name='name',
type=Optional[str], type=Optional[str],
required=False, required=False,
default=None)} default=None),
'completed': ModelField(name='completed',
type=bool,
required=False,
default=False)}
""" """

View File

@ -8,14 +8,16 @@ metadata = sqlalchemy.MetaData()
class Course(ormar.Model): class Course(ormar.Model):
__database__ = database class Meta:
__metadata__ = metadata database = database
metadata = metadata
id = ormar.Integer(primary_key=True) id: ormar.Integer(primary_key=True)
name = ormar.String(length=100) name: ormar.String(max_length=100)
completed = ormar.Boolean(default=False) completed: ormar.Boolean(default=False)
print(Course.__table__.columns)
print(Course.Meta.table.columns)
""" """
Will produce: Will produce:
['courses.id', 'courses.name', 'courses.completed'] ['courses.id', 'courses.name', 'courses.completed']

View File

@ -8,44 +8,59 @@ metadata = sqlalchemy.MetaData()
class Course(ormar.Model): class Course(ormar.Model):
__database__ = database class Meta:
__metadata__ = metadata database = database
metadata = metadata
id = ormar.Integer(primary_key=True) id: ormar.Integer(primary_key=True)
name = ormar.String(length=100) name: ormar.String(max_length=100)
completed = ormar.Boolean(default=False) completed: ormar.Boolean(default=False)
print(Course.__model_fields__) print({x:v.__dict__ for x,v in Course.Meta.model_fields.items()})
""" """
Will produce: Will produce:
{ {'completed': mappingproxy({'autoincrement': False,
'id': {'name': 'id', 'choices': set(),
'primary_key': True, 'column_type': Boolean(),
'autoincrement': True, 'default': False,
'nullable': False, 'index': False,
'default': None, 'name': 'completed',
'server_default': None, 'nullable': True,
'index': None, 'primary_key': False,
'unique': None, 'pydantic_only': False,
'pydantic_only': False}, 'server_default': None,
'name': {'name': 'name', 'unique': False}),
'primary_key': False, 'id': mappingproxy({'autoincrement': True,
'autoincrement': False, 'choices': set(),
'nullable': True, 'column_type': Integer(),
'default': None, 'default': None,
'server_default': None, 'ge': None,
'index': None, 'index': False,
'unique': None, 'le': None,
'pydantic_only': False, 'maximum': None,
'length': 100}, 'minimum': None,
'completed': {'name': 'completed', 'multiple_of': None,
'primary_key': False, 'name': 'id',
'autoincrement': False, 'nullable': False,
'nullable': True, 'primary_key': True,
'default': False, 'pydantic_only': False,
'server_default': None, 'server_default': None,
'index': None, 'unique': False}),
'unique': None, 'name': mappingproxy({'allow_blank': False,
'pydantic_only': False} 'autoincrement': False,
} 'choices': set(),
'column_type': String(length=100),
'curtail_length': None,
'default': None,
'index': False,
'max_length': 100,
'min_length': None,
'name': 'name',
'nullable': False,
'primary_key': False,
'pydantic_only': False,
'regex': None,
'server_default': None,
'strip_whitespace': False,
'unique': False})}
""" """

View File

@ -1,41 +0,0 @@
import databases
import sqlalchemy
import ormar
database = databases.Database("sqlite:///db.sqlite")
metadata = sqlalchemy.MetaData()
class Department(ormar.Model):
__database__ = database
__metadata__ = metadata
id = ormar.Integer(primary_key=True)
name = ormar.String(length=100)
class Course(ormar.Model):
__database__ = database
__metadata__ = metadata
id = ormar.Integer(primary_key=True)
name = ormar.String(length=100)
completed = ormar.Boolean(default=False)
department = ormar.ForeignKey(Department)
department = Department(name="Science")
course = Course(name="Math", completed=False, department=department)
print('name' in course.__dict__)
# False <- property name is not stored on Course instance
print(course.name)
# Math <- value returned from underlying pydantic model
print('department' in course.__dict__)
# False <- columns model is not stored on Course instance
print(course.department)
# Department(id=None, name='Science') <- Department model
# returned from AliasManager
print(course.department.name)
# Science

View File

@ -8,11 +8,12 @@ metadata = sqlalchemy.MetaData()
class Course(ormar.Model): class Course(ormar.Model):
__database__ = database class Meta:
__metadata__ = metadata database = database
metadata = metadata
id = ormar.Integer(primary_key=True) id = ormar.Integer(primary_key=True)
name = ormar.String(length=100) name = ormar.String(max_length=100)
completed = ormar.Boolean(default=False) completed = ormar.Boolean(default=False)

View File

@ -41,7 +41,7 @@ def register_relation_on_build(table_name: str, field: Type[ForeignKeyField]) ->
def register_many_to_many_relation_on_build( def register_many_to_many_relation_on_build(
table_name: str, field: Type[ManyToManyField] table_name: str, field: Type[ManyToManyField]
) -> None: ) -> None:
alias_manager.add_relation_type(field.through.Meta.tablename, table_name) alias_manager.add_relation_type(field.through.Meta.tablename, table_name)
alias_manager.add_relation_type( alias_manager.add_relation_type(
@ -50,11 +50,11 @@ def register_many_to_many_relation_on_build(
def reverse_field_not_already_registered( def reverse_field_not_already_registered(
child: Type["Model"], child_model_name: str, parent_model: Type["Model"] child: Type["Model"], child_model_name: str, parent_model: Type["Model"]
) -> bool: ) -> bool:
return ( return (
child_model_name not in parent_model.__fields__ child_model_name not in parent_model.__fields__
and child.get_name() not in parent_model.__fields__ and child.get_name() not in parent_model.__fields__
) )
@ -65,7 +65,7 @@ def expand_reverse_relationships(model: Type["Model"]) -> None:
parent_model = model_field.to parent_model = model_field.to
child = model child = model
if reverse_field_not_already_registered( if reverse_field_not_already_registered(
child, child_model_name, parent_model child, child_model_name, parent_model
): ):
register_reverse_model_fields( register_reverse_model_fields(
parent_model, child, child_model_name, model_field parent_model, child, child_model_name, model_field
@ -73,10 +73,10 @@ def expand_reverse_relationships(model: Type["Model"]) -> None:
def register_reverse_model_fields( def register_reverse_model_fields(
model: Type["Model"], model: Type["Model"],
child: Type["Model"], child: Type["Model"],
child_model_name: str, child_model_name: str,
model_field: Type["ForeignKeyField"], model_field: Type["ForeignKeyField"],
) -> None: ) -> None:
if issubclass(model_field, ManyToManyField): if issubclass(model_field, ManyToManyField):
model.Meta.model_fields[child_model_name] = ManyToMany( model.Meta.model_fields[child_model_name] = ManyToMany(
@ -91,7 +91,7 @@ def register_reverse_model_fields(
def adjust_through_many_to_many_model( def adjust_through_many_to_many_model(
model: Type["Model"], child: Type["Model"], model_field: Type[ManyToManyField] model: Type["Model"], child: Type["Model"], model_field: Type[ManyToManyField]
) -> None: ) -> None:
model_field.through.Meta.model_fields[model.get_name()] = ForeignKey( model_field.through.Meta.model_fields[model.get_name()] = ForeignKey(
model, name=model.get_name(), ondelete="CASCADE" model, name=model.get_name(), ondelete="CASCADE"
@ -108,7 +108,7 @@ def adjust_through_many_to_many_model(
def create_pydantic_field( def create_pydantic_field(
field_name: str, model: Type["Model"], model_field: Type[ManyToManyField] field_name: str, model: Type["Model"], model_field: Type[ManyToManyField]
) -> None: ) -> None:
model_field.through.__fields__[field_name] = ModelField( model_field.through.__fields__[field_name] = ModelField(
name=field_name, name=field_name,
@ -120,7 +120,7 @@ def create_pydantic_field(
def create_and_append_m2m_fk( def create_and_append_m2m_fk(
model: Type["Model"], model_field: Type[ManyToManyField] model: Type["Model"], model_field: Type[ManyToManyField]
) -> None: ) -> None:
column = sqlalchemy.Column( column = sqlalchemy.Column(
model.get_name(), model.get_name(),
@ -136,7 +136,7 @@ def create_and_append_m2m_fk(
def check_pk_column_validity( def check_pk_column_validity(
field_name: str, field: BaseField, pkname: Optional[str] field_name: str, field: BaseField, pkname: Optional[str]
) -> Optional[str]: ) -> Optional[str]:
if pkname is not None: if pkname is not None:
raise ModelDefinitionError("Only one primary key column is allowed.") raise ModelDefinitionError("Only one primary key column is allowed.")
@ -146,7 +146,7 @@ def check_pk_column_validity(
def sqlalchemy_columns_from_model_fields( def sqlalchemy_columns_from_model_fields(
model_fields: Dict, table_name: str model_fields: Dict, table_name: str
) -> Tuple[Optional[str], List[sqlalchemy.Column]]: ) -> Tuple[Optional[str], List[sqlalchemy.Column]]:
columns = [] columns = []
pkname = None pkname = None
@ -160,9 +160,9 @@ def sqlalchemy_columns_from_model_fields(
if field.primary_key: if field.primary_key:
pkname = check_pk_column_validity(field_name, field, pkname) pkname = check_pk_column_validity(field_name, field, pkname)
if ( if (
not field.pydantic_only not field.pydantic_only
and not field.virtual and not field.virtual
and not issubclass(field, ManyToManyField) and not issubclass(field, ManyToManyField)
): ):
columns.append(field.get_column(field_name)) columns.append(field.get_column(field_name))
register_relation_in_alias_manager(table_name, field) register_relation_in_alias_manager(table_name, field)
@ -170,7 +170,7 @@ def sqlalchemy_columns_from_model_fields(
def register_relation_in_alias_manager( def register_relation_in_alias_manager(
table_name: str, field: Type[ForeignKeyField] table_name: str, field: Type[ForeignKeyField]
) -> None: ) -> None:
if issubclass(field, ManyToManyField): if issubclass(field, ManyToManyField):
register_many_to_many_relation_on_build(table_name, field) register_many_to_many_relation_on_build(table_name, field)
@ -179,7 +179,7 @@ def register_relation_in_alias_manager(
def populate_default_pydantic_field_value( def populate_default_pydantic_field_value(
type_: Type[BaseField], field: str, attrs: dict type_: Type[BaseField], field: str, attrs: dict
) -> dict: ) -> dict:
def_value = type_.default_value() def_value = type_.default_value()
curr_def_value = attrs.get(field, "NONE") curr_def_value = attrs.get(field, "NONE")
@ -208,7 +208,7 @@ def extract_annotations_and_default_vals(attrs: dict, bases: Tuple) -> dict:
def populate_meta_orm_model_fields( def populate_meta_orm_model_fields(
attrs: dict, new_model: Type["Model"] attrs: dict, new_model: Type["Model"]
) -> Type["Model"]: ) -> Type["Model"]:
model_fields = { model_fields = {
field_name: field field_name: field
@ -220,10 +220,10 @@ def populate_meta_orm_model_fields(
def populate_meta_tablename_columns_and_pk( def populate_meta_tablename_columns_and_pk(
name: str, new_model: Type["Model"] name: str, new_model: Type["Model"]
) -> Type["Model"]: ) -> Type["Model"]:
tablename = name.lower() + "s" tablename = name.lower() + "s"
new_model.Meta.tablename = new_model.Meta.tablename or tablename new_model.Meta.tablename = new_model.Meta.tablename if hasattr(new_model.Meta, 'tablename') else tablename
pkname: Optional[str] pkname: Optional[str]
if hasattr(new_model.Meta, "columns"): if hasattr(new_model.Meta, "columns"):
@ -244,7 +244,7 @@ def populate_meta_tablename_columns_and_pk(
def populate_meta_sqlalchemy_table_if_required( def populate_meta_sqlalchemy_table_if_required(
new_model: Type["Model"], new_model: Type["Model"],
) -> Type["Model"]: ) -> Type["Model"]:
if not hasattr(new_model.Meta, "table"): if not hasattr(new_model.Meta, "table"):
new_model.Meta.table = sqlalchemy.Table( new_model.Meta.table = sqlalchemy.Table(
@ -286,7 +286,7 @@ def choices_validator(cls: Type["Model"], values: Dict[str, Any]) -> Dict[str, A
def populate_choices_validators( # noqa CCR001 def populate_choices_validators( # noqa CCR001
model: Type["Model"], attrs: Dict model: Type["Model"], attrs: Dict
) -> None: ) -> None:
if model_initialized_and_has_model_fields(model): if model_initialized_and_has_model_fields(model):
for _, field in model.Meta.model_fields.items(): for _, field in model.Meta.model_fields.items():
@ -299,7 +299,7 @@ def populate_choices_validators( # noqa CCR001
class ModelMetaclass(pydantic.main.ModelMetaclass): class ModelMetaclass(pydantic.main.ModelMetaclass):
def __new__( # type: ignore def __new__( # type: ignore
mcs: "ModelMetaclass", name: str, bases: Any, attrs: dict mcs: "ModelMetaclass", name: str, bases: Any, attrs: dict
) -> "ModelMetaclass": ) -> "ModelMetaclass":
attrs["Config"] = get_pydantic_base_orm_config() attrs["Config"] = get_pydantic_base_orm_config()
attrs["__name__"] = name attrs["__name__"] = name