From ba0990d05b0eec632f61e6d603f81a36cad1ccf7 Mon Sep 17 00:00:00 2001 From: collerek Date: Tue, 6 Oct 2020 19:09:58 +0200 Subject: [PATCH] update docs part 1 --- README.md | 2 +- docs/index.md | 399 +++++++++++++++++++++++++++++++++---- docs/models.md | 61 +++--- docs_src/models/docs001.py | 11 +- docs_src/models/docs002.py | 17 +- docs_src/models/docs003.py | 26 +-- docs_src/models/docs004.py | 14 +- docs_src/models/docs005.py | 89 +++++---- docs_src/models/docs006.py | 41 ---- docs_src/models/docs007.py | 7 +- ormar/models/metaclass.py | 50 ++--- 11 files changed, 505 insertions(+), 212 deletions(-) delete mode 100644 docs_src/models/docs006.py diff --git a/README.md b/README.md index cb31386..c897f33 100644 --- a/README.md +++ b/README.md @@ -303,7 +303,7 @@ 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 +# 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 await Book.objects.update(each=True, genre='Fiction') all_books = await Book.objects.filter(genre='Fiction').all() diff --git a/docs/index.md b/docs/index.md index bf035bf..cb31386 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,5 +1,4 @@ # ORMar -

Pypi version @@ -22,7 +21,12 @@

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. * [`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 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. -Initial work was inspired by [`encode/orm`][encode/orm]. +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], 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. -**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`. @@ -47,16 +52,18 @@ import sqlalchemy database = databases.Database("sqlite:///db.sqlite") metadata = sqlalchemy.MetaData() - class Note(ormar.Model): - __tablename__ = "notes" - __database__ = database - __metadata__ = metadata + class Meta: + tablename = "notes" + database = database + metadata = metadata # primary keys of type int by dafault are set to autoincrement - id = ormar.Integer(primary_key=True) - text = ormar.String(length=100) - completed = ormar.Boolean(default=False) + id: ormar.Integer(primary_key=True) + text: ormar.String(length=100) + 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 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 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() note = await Note.objects.get(id=1) @@ -102,23 +117,25 @@ metadata = sqlalchemy.MetaData() class Album(ormar.Model): - __tablename__ = "album" - __metadata__ = metadata - __database__ = database + class Meta: + tablename = "album" + metadata = metadata + database = database - id = ormar.Integer(primary_key=True) - name = ormar.String(length=100) + id: ormar.Integer(primary_key=True) + name: ormar.String(length=100) class Track(ormar.Model): - __tablename__ = "track" - __metadata__ = metadata - __database__ = database + class Meta: + tablename = "track" + metadata = metadata + database = database - id = ormar.Integer(primary_key=True) - album = ormar.ForeignKey(Album) - title = ormar.String(length=100) - position = ormar.Integer() + id: ormar.Integer(primary_key=True) + album: ormar.ForeignKey(Album) + title: ormar.String(length=100) + position: ormar.Integer() # Create some records to work with. @@ -167,33 +184,330 @@ tracks = await Track.objects.limit(1).all() 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 The following keyword arguments are supported on all field types. - * `primary_key` - * `nullable` - * `default` - * `server_default` - * `index` - * `unique` - -## Model Fields - -### Common parameters + * `primary_key: bool` + * `nullable: bool` + * `default: Any` + * `server_default: Any` + * `index: bool` + * `unique: bool` + * `choices: typing.Sequence` All fields are required unless one of the following is set: * `nullable` - Creates a nullable column. Sets the default to `None`. * `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()`). - * `primary key` - Set a primary key on a 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. + * `primary key` with `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. -### Fields Types - -* `String(length)` +Available Model Fields (with required args - optional ones in docs): +* `String(max_length)` * `Text()` * `Boolean()` * `Integer()` @@ -203,7 +517,10 @@ All fields are required unless one of the following is set: * `DateTime()` * `JSON()` * `BigInteger()` -* `Decimal(lenght, precision)` +* `Decimal(scale, precision)` +* `UUID()` +* `ForeignKey(to)` +* `Many2Many(to, through)` [sqlalchemy-core]: https://docs.sqlalchemy.org/en/latest/core/ [databases]: https://github.com/encode/databases diff --git a/docs/models.md b/docs/models.md index 5fd3cd6..e1c5f31 100644 --- a/docs/models.md +++ b/docs/models.md @@ -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. -```Python hl_lines="14 15 16" +```Python hl_lines="15 16 17" --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`. ```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. @@ -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. -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" ``` @@ -62,9 +62,9 @@ Created instance needs to be passed to every `Model` with `__database__` paramet 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" ``` @@ -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'. -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" ``` @@ -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. -```Python hl_lines="19 20" +```Python hl_lines="20 21" --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 -```Python hl_lines="22" +```Python hl_lines="23" --8<-- "../docs_src/models/docs007.py" ``` !!!info 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 Apart from special parameters defined in the `Model` during definition (tablename, metadata etc.) the `Model` provides you with useful internals. ### 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: -```Python hl_lines="18" +```Python hl_lines="20" --8<-- "../docs_src/models/docs003.py" ``` @@ -145,11 +128,11 @@ For example to list model fields you can: ### 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: -```Python hl_lines="18" +```Python hl_lines="20" --8<-- "../docs_src/models/docs004.py" ``` @@ -161,14 +144,26 @@ For example to list table columns you can: ### 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: -```Python hl_lines="18" +```Python hl_lines="19" --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': , ` + + `'name': , ` + + `'completed': }` + + [fields]: ./fields.md [relations]: ./relations.md [queries]: ./queries.md diff --git a/docs_src/models/docs001.py b/docs_src/models/docs001.py index 9c8f8f1..47b3581 100644 --- a/docs_src/models/docs001.py +++ b/docs_src/models/docs001.py @@ -8,9 +8,10 @@ metadata = sqlalchemy.MetaData() class Course(ormar.Model): - __database__ = database - __metadata__ = metadata + class Meta: + database = database + metadata = metadata - id = ormar.Integer(primary_key=True) - name = ormar.String(length=100) - completed = ormar.Boolean(default=False) + id: ormar.Integer(primary_key=True) + name: ormar.String(max_length=100) + completed: ormar.Boolean(default=False) diff --git a/docs_src/models/docs002.py b/docs_src/models/docs002.py index 1d63371..96ee368 100644 --- a/docs_src/models/docs002.py +++ b/docs_src/models/docs002.py @@ -8,12 +8,13 @@ metadata = sqlalchemy.MetaData() class Course(ormar.Model): - # if you omit this parameter it will be created automatically - # as class.__name__.lower()+'s' -> "courses" in this example - __tablename__ = "my_courses" - __database__ = database - __metadata__ = metadata + class Meta: + # if you omit this parameter it will be created automatically + # as class.__name__.lower()+'s' -> "courses" in this example + tablename = "my_courses" + database = database + metadata = metadata - id = ormar.Integer(primary_key=True) - name = ormar.String(length=100) - completed = ormar.Boolean(default=False) + id: ormar.Integer(primary_key=True) + name: ormar.String(max_length=100) + completed: ormar.Boolean(default=False) diff --git a/docs_src/models/docs003.py b/docs_src/models/docs003.py index 754f6d4..91a83cf 100644 --- a/docs_src/models/docs003.py +++ b/docs_src/models/docs003.py @@ -8,26 +8,28 @@ metadata = sqlalchemy.MetaData() class Course(ormar.Model): - __database__ = database - __metadata__ = metadata + class Meta: + database = database + metadata = metadata - id = ormar.Integer(primary_key=True) - name = ormar.String(length=100) - completed = ormar.Boolean(default=False) + id: ormar.Integer(primary_key=True) + name: ormar.String(max_length=100) + completed: ormar.Boolean(default=False) -print(Course.__pydantic_model__.__fields__) + +print(Course.__fields__) """ Will produce: -{'completed': ModelField(name='completed', - type=bool, - required=False, - default=False), - 'id': ModelField(name='id', +{'id': ModelField(name='id', type=Optional[int], required=False, default=None), 'name': ModelField(name='name', type=Optional[str], required=False, - default=None)} + default=None), +'completed': ModelField(name='completed', + type=bool, + required=False, + default=False)} """ diff --git a/docs_src/models/docs004.py b/docs_src/models/docs004.py index 36e5da0..a1db655 100644 --- a/docs_src/models/docs004.py +++ b/docs_src/models/docs004.py @@ -8,14 +8,16 @@ metadata = sqlalchemy.MetaData() class Course(ormar.Model): - __database__ = database - __metadata__ = metadata + class Meta: + database = database + metadata = metadata - id = ormar.Integer(primary_key=True) - name = ormar.String(length=100) - completed = ormar.Boolean(default=False) + id: ormar.Integer(primary_key=True) + name: ormar.String(max_length=100) + completed: ormar.Boolean(default=False) -print(Course.__table__.columns) + +print(Course.Meta.table.columns) """ Will produce: ['courses.id', 'courses.name', 'courses.completed'] diff --git a/docs_src/models/docs005.py b/docs_src/models/docs005.py index e1a85e8..ce81887 100644 --- a/docs_src/models/docs005.py +++ b/docs_src/models/docs005.py @@ -8,44 +8,59 @@ metadata = sqlalchemy.MetaData() class Course(ormar.Model): - __database__ = database - __metadata__ = metadata + class Meta: + database = database + metadata = metadata - id = ormar.Integer(primary_key=True) - name = ormar.String(length=100) - completed = ormar.Boolean(default=False) + id: ormar.Integer(primary_key=True) + name: ormar.String(max_length=100) + completed: ormar.Boolean(default=False) -print(Course.__model_fields__) +print({x:v.__dict__ for x,v in Course.Meta.model_fields.items()}) """ Will produce: -{ -'id': {'name': 'id', - 'primary_key': True, - 'autoincrement': True, - 'nullable': False, - 'default': None, - 'server_default': None, - 'index': None, - 'unique': None, - 'pydantic_only': False}, -'name': {'name': 'name', - 'primary_key': False, - 'autoincrement': False, - 'nullable': True, - 'default': None, - 'server_default': None, - 'index': None, - 'unique': None, - 'pydantic_only': False, - 'length': 100}, -'completed': {'name': 'completed', - 'primary_key': False, - 'autoincrement': False, - 'nullable': True, - 'default': False, - 'server_default': None, - 'index': None, - 'unique': None, - 'pydantic_only': False} -} -""" +{'completed': mappingproxy({'autoincrement': False, + 'choices': set(), + 'column_type': Boolean(), + 'default': False, + 'index': False, + 'name': 'completed', + 'nullable': True, + 'primary_key': False, + 'pydantic_only': False, + 'server_default': None, + 'unique': False}), + 'id': mappingproxy({'autoincrement': True, + 'choices': set(), + 'column_type': Integer(), + 'default': None, + 'ge': None, + 'index': False, + 'le': None, + 'maximum': None, + 'minimum': None, + 'multiple_of': None, + 'name': 'id', + 'nullable': False, + 'primary_key': True, + 'pydantic_only': False, + 'server_default': None, + 'unique': False}), + 'name': mappingproxy({'allow_blank': 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})} +""" \ No newline at end of file diff --git a/docs_src/models/docs006.py b/docs_src/models/docs006.py deleted file mode 100644 index d3151db..0000000 --- a/docs_src/models/docs006.py +++ /dev/null @@ -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 \ No newline at end of file diff --git a/docs_src/models/docs007.py b/docs_src/models/docs007.py index f98e62a..751cace 100644 --- a/docs_src/models/docs007.py +++ b/docs_src/models/docs007.py @@ -8,11 +8,12 @@ metadata = sqlalchemy.MetaData() class Course(ormar.Model): - __database__ = database - __metadata__ = metadata + class Meta: + database = database + metadata = metadata id = ormar.Integer(primary_key=True) - name = ormar.String(length=100) + name = ormar.String(max_length=100) completed = ormar.Boolean(default=False) diff --git a/ormar/models/metaclass.py b/ormar/models/metaclass.py index 30d3071..0bd711d 100644 --- a/ormar/models/metaclass.py +++ b/ormar/models/metaclass.py @@ -41,7 +41,7 @@ def register_relation_on_build(table_name: str, field: Type[ForeignKeyField]) -> def register_many_to_many_relation_on_build( - table_name: str, field: Type[ManyToManyField] + table_name: str, field: Type[ManyToManyField] ) -> None: alias_manager.add_relation_type(field.through.Meta.tablename, table_name) alias_manager.add_relation_type( @@ -50,11 +50,11 @@ def register_many_to_many_relation_on_build( 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: return ( - child_model_name not in parent_model.__fields__ - and child.get_name() not in parent_model.__fields__ + child_model_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 child = model if reverse_field_not_already_registered( - child, child_model_name, parent_model + child, child_model_name, parent_model ): register_reverse_model_fields( 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( - model: Type["Model"], - child: Type["Model"], - child_model_name: str, - model_field: Type["ForeignKeyField"], + model: Type["Model"], + child: Type["Model"], + child_model_name: str, + model_field: Type["ForeignKeyField"], ) -> None: if issubclass(model_field, ManyToManyField): model.Meta.model_fields[child_model_name] = ManyToMany( @@ -91,7 +91,7 @@ def register_reverse_model_fields( 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: model_field.through.Meta.model_fields[model.get_name()] = ForeignKey( model, name=model.get_name(), ondelete="CASCADE" @@ -108,7 +108,7 @@ def adjust_through_many_to_many_model( 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: model_field.through.__fields__[field_name] = ModelField( name=field_name, @@ -120,7 +120,7 @@ def create_pydantic_field( def create_and_append_m2m_fk( - model: Type["Model"], model_field: Type[ManyToManyField] + model: Type["Model"], model_field: Type[ManyToManyField] ) -> None: column = sqlalchemy.Column( model.get_name(), @@ -136,7 +136,7 @@ def create_and_append_m2m_fk( def check_pk_column_validity( - field_name: str, field: BaseField, pkname: Optional[str] + field_name: str, field: BaseField, pkname: Optional[str] ) -> Optional[str]: if pkname is not None: raise ModelDefinitionError("Only one primary key column is allowed.") @@ -146,7 +146,7 @@ def check_pk_column_validity( def sqlalchemy_columns_from_model_fields( - model_fields: Dict, table_name: str + model_fields: Dict, table_name: str ) -> Tuple[Optional[str], List[sqlalchemy.Column]]: columns = [] pkname = None @@ -160,9 +160,9 @@ def sqlalchemy_columns_from_model_fields( if field.primary_key: pkname = check_pk_column_validity(field_name, field, pkname) if ( - not field.pydantic_only - and not field.virtual - and not issubclass(field, ManyToManyField) + not field.pydantic_only + and not field.virtual + and not issubclass(field, ManyToManyField) ): columns.append(field.get_column(field_name)) register_relation_in_alias_manager(table_name, field) @@ -170,7 +170,7 @@ def sqlalchemy_columns_from_model_fields( def register_relation_in_alias_manager( - table_name: str, field: Type[ForeignKeyField] + table_name: str, field: Type[ForeignKeyField] ) -> None: if issubclass(field, ManyToManyField): 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( - type_: Type[BaseField], field: str, attrs: dict + type_: Type[BaseField], field: str, attrs: dict ) -> dict: def_value = type_.default_value() 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( - attrs: dict, new_model: Type["Model"] + attrs: dict, new_model: Type["Model"] ) -> Type["Model"]: model_fields = { field_name: field @@ -220,10 +220,10 @@ def populate_meta_orm_model_fields( def populate_meta_tablename_columns_and_pk( - name: str, new_model: Type["Model"] + name: str, new_model: Type["Model"] ) -> Type["Model"]: 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] if hasattr(new_model.Meta, "columns"): @@ -244,7 +244,7 @@ def populate_meta_tablename_columns_and_pk( def populate_meta_sqlalchemy_table_if_required( - new_model: Type["Model"], + new_model: Type["Model"], ) -> Type["Model"]: if not hasattr(new_model.Meta, "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 - model: Type["Model"], attrs: Dict + model: Type["Model"], attrs: Dict ) -> None: if model_initialized_and_has_model_fields(model): for _, field in model.Meta.model_fields.items(): @@ -299,7 +299,7 @@ def populate_choices_validators( # noqa CCR001 class ModelMetaclass(pydantic.main.ModelMetaclass): def __new__( # type: ignore - mcs: "ModelMetaclass", name: str, bases: Any, attrs: dict + mcs: "ModelMetaclass", name: str, bases: Any, attrs: dict ) -> "ModelMetaclass": attrs["Config"] = get_pydantic_base_orm_config() attrs["__name__"] = name