fix bug in bulk_update, update documentation, update readme, bump version

This commit is contained in:
collerek
2020-10-22 12:48:40 +02:00
parent dbca4367e8
commit 394de2d11c
16 changed files with 347 additions and 104 deletions

BIN
.coverage

Binary file not shown.

View File

@ -97,7 +97,9 @@ await Track.objects.create(album=malibu, title="The Bird", position=1)
await Track.objects.create(album=malibu, title="Heart don't stand a chance", position=2)
await Track.objects.create(album=malibu, title="The Waters", position=3)
fantasies = await Album.objects.create(name="Fantasies")
# alternative creation of object divided into 2 steps
fantasies = Album.objects.create(name="Fantasies")
await fantasies.save()
await Track.objects.create(album=fantasies, title="Help I'm Alive", position=1)
await Track.objects.create(album=fantasies, title="Sick Muse", position=2)
@ -137,12 +139,33 @@ tracks = await Track.objects.limit(1).all()
assert len(tracks) == 1
```
## Data types
## Ormar Specification
### QuerySet methods
* `create(**kwargs): -> Model`
* `get(**kwargs): -> Model`
* `get_or_create(**kwargs) -> Model`
* `update(each: bool = False, **kwargs) -> int`
* `update_or_create(**kwargs) -> Model`
* `bulk_create(objects: List[Model]) -> None`
* `bulk_update(objects: List[Model], columns: List[str] = None) -> None`
* `delete(each: bool = False, **kwargs) -> int`
* `all(self, **kwargs) -> List[Optional[Model]]`
* `filter(**kwargs) -> QuerySet`
* `exclude(**kwargs) -> QuerySet`
* `select_related(related: Union[List, str]) -> QuerySet`
* `limit(limit_count: int) -> QuerySet`
* `offset(offset: int) -> QuerySet`
* `count() -> int`
* `exists() -> bool`
* `fields(columns: Union[List, str]) -> QuerySet`
#### Relation types
* One to many - with `ForeignKey`
* Many to many - with `Many2Many`
* One to many - with `ForeignKey(to: Model)`
* Many to many - with `ManyToMany(to: Model, through: Model)`
#### Model fields types
@ -161,7 +184,7 @@ Available Model Fields (with required args - optional ones in docs):
* `Decimal(scale, precision)`
* `UUID()`
* `ForeignKey(to)`
* `Many2Many(to, through)`
* `ManyToMany(to, through)`
### Available fields options
The following keyword arguments are supported on all field types.

View File

@ -6,97 +6,66 @@ you need to do is substitute pydantic models with ormar models.
Here you can find a very simple sample application code.
## Imports and initialization
First take care of the imports and initialization
```python hl_lines="1-12"
--8<-- "../docs_src/fastapi/docs001.py"
```
## Database connection
Next define startup and shutdown events (or use middleware)
- note that this is `databases` specific setting not the ormar one
```python hl_lines="15-26"
--8<-- "../docs_src/fastapi/docs001.py"
```
!!!info
You can read more on connecting to databases in [fastapi][fastapi] documentation
## Models definition
Define ormar models with appropriate fields.
Those models will be used insted of pydantic ones.
```python hl_lines="29-47"
--8<-- "../docs_src/fastapi/docs001.py"
```
!!!tip
You can read more on defining `Models` in [models][models] section.
## Fastapi endpoints definition
Define your desired endpoints, note how `ormar` models are used both
as `response_model` and as a requests parameters.
```python hl_lines="50-77"
--8<-- "../docs_src/fastapi/docs001.py"
```
!!!note
Note how ormar `Model` methods like save() are available straight out of the box after fastapi initializes it for you.
!!!note
Note that you can return a `Model` (or list of `Models`) directly - fastapi will jsonize it for you
## Test the application
Here you have a sample test that will prove that everything works as intended.
```python
from typing import List
import databases
import pytest
import sqlalchemy
from fastapi import FastAPI
from starlette.testclient import TestClient
import ormar
from tests.settings import DATABASE_URL
app = FastAPI()
metadata = sqlalchemy.MetaData()
database = databases.Database(DATABASE_URL, force_rollback=True)
app.state.database = database
# define startup and shutdown events
@app.on_event("startup")
async def startup() -> None:
database_ = app.state.database
if not database_.is_connected:
await database_.connect()
@app.on_event("shutdown")
async def shutdown() -> None:
database_ = app.state.database
if database_.is_connected:
await database_.disconnect()
# define ormar models
class Category(ormar.Model):
class Meta:
tablename = "categories"
metadata = metadata
database = database
id: ormar.Integer(primary_key=True)
name: ormar.String(max_length=100)
class Item(ormar.Model):
class Meta:
tablename = "items"
metadata = metadata
database = database
id: ormar.Integer(primary_key=True)
name: ormar.String(max_length=100)
category: ormar.ForeignKey(Category, nullable=True)
# define endpoints in fastapi
@app.get("/items/", response_model=List[Item])
async def get_items():
items = await Item.objects.select_related("category").all()
# not that you can return a model directly - fastapi will json-ize it
return items
@app.post("/items/", response_model=Item)
async def create_item(item: Item):
# note how ormar methods like save() are available streight out of the box
await item.save()
return item
@app.post("/categories/", response_model=Category)
async def create_category(category: Category):
await category.save()
return category
@app.put("/items/{item_id}")
async def get_item(item_id: int, item: Item):
# you can work both with item_id or item
item_db = await Item.objects.get(pk=item_id)
return await item_db.update(**item.dict())
@app.delete("/items/{item_id}")
async def delete_item(item_id: int, item: Item):
item_db = await Item.objects.get(pk=item_id)
return {"deleted_rows": await item_db.delete()}
# here is a sample test to check the working of the ormar with fastapi
from starlette.testclient import TestClient
def test_all_endpoints():
# note that TestClient is only sync, don't use asyns here
client = TestClient(app)
# note that you need to connect to database manually
# or use client as contextmanager
# or use client as contextmanager during tests
with client as client:
response = client.post("/categories/", json={"name": "test cat"})
category = response.json()
@ -124,3 +93,9 @@ def test_all_endpoints():
items = response.json()
assert len(items) == 0
```
!!!info
You can read more on testing fastapi in [fastapi][fastapi] docs.
[fastapi]: https://fastapi.tiangolo.com/
[models]: ./models.md

View File

@ -97,7 +97,9 @@ await Track.objects.create(album=malibu, title="The Bird", position=1)
await Track.objects.create(album=malibu, title="Heart don't stand a chance", position=2)
await Track.objects.create(album=malibu, title="The Waters", position=3)
fantasies = await Album.objects.create(name="Fantasies")
# alternative creation of object divided into 2 steps
fantasies = Album.objects.create(name="Fantasies")
await fantasies.save()
await Track.objects.create(album=fantasies, title="Help I'm Alive", position=1)
await Track.objects.create(album=fantasies, title="Sick Muse", position=2)
@ -137,12 +139,33 @@ tracks = await Track.objects.limit(1).all()
assert len(tracks) == 1
```
## Data types
## Ormar Specification
### QuerySet methods
* `create(**kwargs): -> Model`
* `get(**kwargs): -> Model`
* `get_or_create(**kwargs) -> Model`
* `update(each: bool = False, **kwargs) -> int`
* `update_or_create(**kwargs) -> Model`
* `bulk_create(objects: List[Model]) -> None`
* `bulk_update(objects: List[Model], columns: List[str] = None) -> None`
* `delete(each: bool = False, **kwargs) -> int`
* `all(self, **kwargs) -> List[Optional[Model]]`
* `filter(**kwargs) -> QuerySet`
* `exclude(**kwargs) -> QuerySet`
* `select_related(related: Union[List, str]) -> QuerySet`
* `limit(limit_count: int) -> QuerySet`
* `offset(offset: int) -> QuerySet`
* `count() -> int`
* `exists() -> bool`
* `fields(columns: Union[List, str]) -> QuerySet`
#### Relation types
* One to many - with `ForeignKey`
* Many to many - with `Many2Many`
* One to many - with `ForeignKey(to: Model)`
* Many to many - with `ManyToMany(to: Model, through: Model)`
#### Model fields types
@ -161,7 +184,7 @@ Available Model Fields (with required args - optional ones in docs):
* `Decimal(scale, precision)`
* `UUID()`
* `ForeignKey(to)`
* `Many2Many(to, through)`
* `ManyToMany(to, through)`
### Available fields options
The following keyword arguments are supported on all field types.
@ -173,6 +196,7 @@ The following keyword arguments are supported on all field types.
* `index: bool`
* `unique: bool`
* `choices: typing.Sequence`
* `name: str`
All fields are required unless one of the following is set:

View File

@ -37,7 +37,27 @@ You can disable by passing `autoincremant=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.
### Fields names vs Column names
By default names of the fields will be used for both the underlying `pydantic` model and `sqlalchemy` table.
If for whatever reason you prefer to change the name in the database but keep the name in the model you can do this
with specifying `name` parameter during Field declaration
Here you have a sample model with changed names
```Python hl_lines="16-19"
--8<-- "../docs_src/models/docs008.py"
```
Note that you can also change the ForeignKey column name
```Python hl_lines="9"
--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"
--8<-- "../docs_src/models/docs010.py"
```
### Dependencies
@ -128,7 +148,9 @@ Each model has a `QuerySet` initialised as `objects` parameter
### load
By default when you query a table without prefetching related models, the ormar will still construct
your related models, but populate them only with the pk value.
your related models, but populate them only with the pk value. You can load the related model by calling `load()` method.
`load()` can also be used to refresh the model from the database (if it was changed by some other process).
```python
track = await Track.objects.get(name='The Bird')
@ -142,10 +164,36 @@ track.album.name # will return 'Malibu'
### save
You can create new models by using `QuerySet.create()` method or by initializing your model as a normal pydantic model
and later calling `save()` method.
`save()` can also be used to persist changes that you made to the model.
```python
track = Track(name='The Bird')
await track.save() # will persist the model in database
```
### delete
You can delete models by using `QuerySet.delete()` method or by using your model and calling `delete()` method.
```python
track = await Track.objects.get(name='The Bird')
await track.delete() # will delete the model from database
```
!!!tip
Note that that `track` object stays the same, only record in the database is removed.
### update
You can delete models by using `QuerySet.update()` method or by using your model and calling `update()` method.
```python
track = await Track.objects.get(name='The Bird')
await track.update(name='The Bird Strikes Again')
```
## Internals

View File

@ -85,9 +85,9 @@ Finally you can explicitly set it to None (default behavior if no value passed).
Otherwise an IntegrityError will be raised by your database driver library.
### Many2Many
### ManyToMany
`Many2Many(to, through)` has required parameters `to` and `through` that takes target and relation `Model` classes.
`ManyToMany(to, through)` has required parameters `to` and `through` that takes target and relation `Model` classes.
Sqlalchemy column and Type are automatically taken from target `Model`.
@ -131,7 +131,7 @@ assert len(await post.categories.all()) == 2
```
!!!note
Note that when accessing QuerySet API methods through Many2Many relation you don't
Note that when accessing QuerySet API methods through ManyToMany relation you don't
need to use objects attribute like in normal queries.
To learn more about available QuerySet methods visit [queries][queries]
@ -146,7 +146,7 @@ await news.posts.clear()
#### All other queryset methods
When access directly the related `Many2Many` field returns the list of related models.
When access directly the related `ManyToMany` field returns the list of related models.
But at the same time it exposes full QuerySet API, so you can filter, create, select related etc.

45
docs/releases.md Normal file
View File

@ -0,0 +1,45 @@
# 0.3.8
* Added possibility to provide alternative database column names with name parameter to all fields.
* Fix bug with selecting related ManyToMany fields with `fields()` if they are empty.
* Updated documentation
# 0.3.7
* Publish documentation and update readme
# 0.3.6
* Add fields() method to limit the selected columns from database - only nullable columns can be excluded.
* Added UniqueColumns and constraints list in model Meta to build unique constraints on list of columns.
* Added UUID field type based on Char(32) column type.
# 0.3.5
* Added bulk_create and bulk_update for operations on multiple objects.
# 0.3.4
Add queryset level methods
* delete
* update
* get_or_create
* update_or_create
# 0.3.3
* Add additional filters - startswith and endswith
# 0.3.2
* Add choices parameter to all fields - limiting the accepted values to ones provided
# 0.3.1
* Added exclude to filter where not conditions.
* Added tests for mysql and postgres with fixes for postgres.
* Rafactors and cleanup.
# 0.3.0
* Added ManyToMany field and support for many to many relations

View File

View File

@ -0,0 +1,77 @@
from typing import List
import databases
import sqlalchemy
from fastapi import FastAPI
import ormar
app = FastAPI()
metadata = sqlalchemy.MetaData()
database = databases.Database("sqlite:///test.db", force_rollback=True)
app.state.database = database
@app.on_event("startup")
async def startup() -> None:
database_ = app.state.database
if not database_.is_connected:
await database_.connect()
@app.on_event("shutdown")
async def shutdown() -> None:
database_ = app.state.database
if database_.is_connected:
await database_.disconnect()
class Category(ormar.Model):
class Meta:
tablename = "categories"
metadata = metadata
database = database
id: ormar.Integer(primary_key=True)
name: ormar.String(max_length=100)
class Item(ormar.Model):
class Meta:
tablename = "items"
metadata = metadata
database = database
id: ormar.Integer(primary_key=True)
name: ormar.String(max_length=100)
category: ormar.ForeignKey(Category, nullable=True)
@app.get("/items/", response_model=List[Item])
async def get_items():
items = await Item.objects.select_related("category").all()
return items
@app.post("/items/", response_model=Item)
async def create_item(item: Item):
await item.save()
return item
@app.post("/categories/", response_model=Category)
async def create_category(category: Category):
await category.save()
return category
@app.put("/items/{item_id}")
async def get_item(item_id: int, item: Item):
item_db = await Item.objects.get(pk=item_id)
return await item_db.update(**item.dict())
@app.delete("/items/{item_id}")
async def delete_item(item_id: int, item: Item):
item_db = await Item.objects.get(pk=item_id)
return {"deleted_rows": await item_db.delete()}

View File

@ -0,0 +1,19 @@
import databases
import sqlalchemy
import ormar
database = databases.Database("sqlite:///test.db", force_rollback=True)
metadata = sqlalchemy.MetaData()
class Child(ormar.Model):
class Meta:
tablename = "children"
metadata = metadata
database = database
id: ormar.Integer(name='child_id', primary_key=True)
first_name: ormar.String(name='fname', max_length=100)
last_name: ormar.String(name='lname', max_length=100)
born_year: ormar.Integer(name='year_born', nullable=True)

View File

@ -0,0 +1,9 @@
class Album(ormar.Model):
class Meta:
tablename = "music_albums"
metadata = metadata
database = database
id: ormar.Integer(name='album_id', primary_key=True)
name: ormar.String(name='album_name', max_length=100)
artist: ormar.ForeignKey(Artist, name='artist_id')

View File

@ -0,0 +1,18 @@
class ArtistChildren(ormar.Model):
class Meta:
tablename = "children_x_artists"
metadata = metadata
database = database
class Artist(ormar.Model):
class Meta:
tablename = "artists"
metadata = metadata
database = database
id: ormar.Integer(name='artist_id', primary_key=True)
first_name: ormar.String(name='fname', max_length=100)
last_name: ormar.String(name='lname', max_length=100)
born_year: ormar.Integer(name='year')
children: ormar.ManyToMany(Child, through=ArtistChildren)

View File

@ -9,6 +9,7 @@ nav:
- Queries: queries.md
- Use with Fastapi: fastapi.md
- Contributing: contributing.md
- Release Notes: releases.md
repo_name: collerek/ormar
repo_url: https://github.com/collerek/ormar
google_analytics:

View File

@ -28,7 +28,7 @@ class UndefinedType: # pragma no cover
Undefined = UndefinedType()
__version__ = "0.3.7"
__version__ = "0.3.8"
__all__ = [
"Integer",
"BigInteger",

View File

@ -357,7 +357,7 @@ class QuerySet:
new_kwargs = {"new_" + k: v for k, v in new_kwargs.items() if k in columns}
ready_objects.append(new_kwargs)
pk_column = self.model_meta.table.c.get(pk_name)
pk_column = self.model_meta.table.c.get(self.model.get_column_alias(pk_name))
pk_column_name = self.model.get_column_alias(pk_name)
table_columns = [c.name for c in self.model_meta.table.c]
expr = self.table.update().where(
@ -371,6 +371,6 @@ class QuerySet:
}
)
# databases bind params only where query is passed as string
# otherwise it just pases all data to values and results in unconsumed columns
# otherwise it just passes all data to values and results in unconsumed columns
expr = str(expr)
await self.database.execute_many(expr, ready_objects)

View File

@ -121,6 +121,10 @@ async def test_bulk_operations_and_fields():
await Child.objects.bulk_update(children)
children = await Child.objects.filter(first_name='Daughter').all()
assert len(children) == 2
assert children[0].born_year == 1890
children = await Child.objects.fields(['first_name', 'last_name']).all()
assert len(children) == 2
for child in children: