bump version, more tests, update docs
This commit is contained in:
@ -2,9 +2,11 @@
|
|||||||
|
|
||||||
* **Breaking:** QuerySet `bulk_update` method now raises `ModelPersistenceError` for unsaved models passed instead of `QueryDefinitionError`
|
* **Breaking:** QuerySet `bulk_update` method now raises `ModelPersistenceError` for unsaved models passed instead of `QueryDefinitionError`
|
||||||
* **Breaking:** Model initialization with unknown field name now raises `ModelError` instead of `KeyError`
|
* **Breaking:** Model initialization with unknown field name now raises `ModelError` instead of `KeyError`
|
||||||
*
|
* Added **Signals**, with pre-defined list signals and decorators: `post_delete`, `post_save`, `post_update`, `pre_delete`,
|
||||||
* Add py.typed and modify setup.py for mypy support
|
`pre_save`, `pre_update`
|
||||||
|
* Add `py.typed` and modify `setup.py` for mypy support
|
||||||
* Performance optimization
|
* Performance optimization
|
||||||
|
* Updated docs
|
||||||
|
|
||||||
# 0.6.2
|
# 0.6.2
|
||||||
|
|
||||||
|
|||||||
249
docs/signals.md
Normal file
249
docs/signals.md
Normal file
@ -0,0 +1,249 @@
|
|||||||
|
# Signals
|
||||||
|
|
||||||
|
Signals are a mechanism to fire your piece of code (function / method) whenever given type of event happens in `ormar`.
|
||||||
|
|
||||||
|
To achieve this you need to register your receiver for a given type of signal for selected model(s).
|
||||||
|
|
||||||
|
## Defining receivers
|
||||||
|
|
||||||
|
Given a sample model like following:
|
||||||
|
|
||||||
|
```Python
|
||||||
|
import databases
|
||||||
|
import sqlalchemy
|
||||||
|
|
||||||
|
import ormar
|
||||||
|
|
||||||
|
database = databases.Database("sqlite:///db.sqlite")
|
||||||
|
metadata = sqlalchemy.MetaData()
|
||||||
|
|
||||||
|
|
||||||
|
class Album(ormar.Model):
|
||||||
|
class Meta:
|
||||||
|
tablename = "albums"
|
||||||
|
metadata = metadata
|
||||||
|
database = database
|
||||||
|
|
||||||
|
id: int = ormar.Integer(primary_key=True)
|
||||||
|
name: str = ormar.String(max_length=100)
|
||||||
|
is_best_seller: bool = ormar.Boolean(default=False)
|
||||||
|
play_count: int = ormar.Integer(default=0)
|
||||||
|
```
|
||||||
|
|
||||||
|
You can for example define a trigger that will set `album.is_best_seller` status if it will be played more than 50 times.
|
||||||
|
|
||||||
|
Import `pre_update` decorator, for list of currently available decorators/ signals check below.
|
||||||
|
|
||||||
|
```Python hl_lines="1"
|
||||||
|
--8<-- "../docs_src/signals/docs002.py"
|
||||||
|
```
|
||||||
|
|
||||||
|
Define your function.
|
||||||
|
|
||||||
|
Note that each receiver function:
|
||||||
|
|
||||||
|
* has to be **callable**
|
||||||
|
* has to accept first **`sender`** argument that receives the class of sending object
|
||||||
|
* has to accept **`**kwargs`** argument as the parameters send in each `ormar.Signal` can change at any time so your function has to serve them.
|
||||||
|
* has to be **`async`** cause callbacks are gathered and awaited.
|
||||||
|
|
||||||
|
`pre_update` currently sends only one argument apart from `sender` and it's `instance` one.
|
||||||
|
|
||||||
|
Note how `pre_update` decorator accepts a `senders` argument that can be a single model or a list of models,
|
||||||
|
for which you want to run the signal receiver.
|
||||||
|
|
||||||
|
Currently there is no way to set signal for all models at once without explicitly passing them all into registration of receiver.
|
||||||
|
|
||||||
|
```Python hl_lines="4-7"
|
||||||
|
--8<-- "../docs_src/signals/docs002.py"
|
||||||
|
```
|
||||||
|
|
||||||
|
!!!note
|
||||||
|
Note that receivers are defined on a class level -> so even if you connect/disconnect function through instance
|
||||||
|
it will run/ stop running for all operations on that `ormar.Model` class.
|
||||||
|
|
||||||
|
Note that our newly created function has instance and class of the instance so you can easily run database
|
||||||
|
queries inside your receivers if you want to.
|
||||||
|
|
||||||
|
```Python hl_lines="15-22"
|
||||||
|
--8<-- "../docs_src/signals/docs002.py"
|
||||||
|
```
|
||||||
|
|
||||||
|
You can define same receiver for multiple models at once by passing a list of models to signal decorator.
|
||||||
|
|
||||||
|
```python
|
||||||
|
# define a dummy debug function
|
||||||
|
@pre_update([Album, Track])
|
||||||
|
async def before_update(sender, instance, **kwargs):
|
||||||
|
print(f"{sender.get_name()}: {instance.json()}: {kwargs}")
|
||||||
|
```
|
||||||
|
|
||||||
|
Of course you can also create multiple functions for the same signal and model. Each of them will run at each signal.
|
||||||
|
|
||||||
|
```python
|
||||||
|
@pre_update(Album)
|
||||||
|
async def before_update(sender, instance, **kwargs):
|
||||||
|
print(f"{sender.get_name()}: {instance.json()}: {kwargs}")
|
||||||
|
|
||||||
|
@pre_update(Album)
|
||||||
|
async def before_update2(sender, instance, **kwargs):
|
||||||
|
print(f'About to update {sender.get_name()} with pk: {instance.pk}')
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that `ormar` decorators are the syntactic sugar, you can directly connect your function or method for given signal for
|
||||||
|
given model. Connect accept only one parameter - your `receiver` function / method.
|
||||||
|
|
||||||
|
```python hl_lines="11 13 16"
|
||||||
|
class AlbumAuditor:
|
||||||
|
def __init__(self):
|
||||||
|
self.event_type = "ALBUM_INSTANCE"
|
||||||
|
|
||||||
|
async def before_save(self, sender, instance, **kwargs):
|
||||||
|
await AuditLog(
|
||||||
|
event_type=f"{self.event_type}_SAVE", event_log=instance.json()
|
||||||
|
).save()
|
||||||
|
|
||||||
|
auditor = AlbumAuditor()
|
||||||
|
pre_save(Album)(auditor.before_save)
|
||||||
|
# call above has same result like the one below
|
||||||
|
Album.Meta.signals.pre_save.connect(auditor.before_save)
|
||||||
|
# signals are also exposed on instance
|
||||||
|
album = Album(name='Miami')
|
||||||
|
album.signals.pre_save.connect(auditor.before_save)
|
||||||
|
```
|
||||||
|
|
||||||
|
!!!warning
|
||||||
|
Note that signals keep the reference to your receiver (not a `weakref`) so keep that in mind to avoid circular references.
|
||||||
|
|
||||||
|
## Disconnecting the receivers
|
||||||
|
|
||||||
|
To disconnect the receiver and stop it for running for given model you need to disconnect it.
|
||||||
|
|
||||||
|
```python hl_lines="7 10"
|
||||||
|
|
||||||
|
@pre_update(Album)
|
||||||
|
async def before_update(sender, instance, **kwargs):
|
||||||
|
if instance.play_count > 50 and not instance.is_best_seller:
|
||||||
|
instance.is_best_seller = True
|
||||||
|
|
||||||
|
# disconnect given function from signal for given Model
|
||||||
|
Album.Meta.signals.pre_save.disconnect(before_save)
|
||||||
|
# signals are also exposed on instance
|
||||||
|
album = Album(name='Miami')
|
||||||
|
album.signals.pre_save.disconnect(before_save)
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Available signals
|
||||||
|
|
||||||
|
!!!warning
|
||||||
|
Note that signals are **not** send for:
|
||||||
|
|
||||||
|
* bulk operations (`QuerySet.bulk_create` and `QuerySet.bulk_update`) as they are designed for speed.
|
||||||
|
|
||||||
|
* queyset table level operations (`QuerySet.update` and `QuerySet.delete`) as they run on the underlying tables
|
||||||
|
(more lak raw sql update/delete operations) and do not have specific instance.
|
||||||
|
|
||||||
|
### pre_save
|
||||||
|
|
||||||
|
`pre_save(sender: Type["Model"], instance: "Model")`
|
||||||
|
|
||||||
|
Send for `Model.save()` and `Model.objects.create()` methods.
|
||||||
|
|
||||||
|
`sender` is a `ormar.Model` class and `instance` is the model to be saved.
|
||||||
|
|
||||||
|
### post_save
|
||||||
|
|
||||||
|
`post_save(sender: Type["Model"], instance: "Model")`
|
||||||
|
|
||||||
|
Send for `Model.save()` and `Model.objects.create()` methods.
|
||||||
|
|
||||||
|
`sender` is a `ormar.Model` class and `instance` is the model that was saved.
|
||||||
|
|
||||||
|
### pre_update
|
||||||
|
|
||||||
|
`pre_update(sender: Type["Model"], instance: "Model")`
|
||||||
|
|
||||||
|
Send for `Model.update()` method.
|
||||||
|
|
||||||
|
`sender` is a `ormar.Model` class and `instance` is the model to be updated.
|
||||||
|
|
||||||
|
### post_update
|
||||||
|
|
||||||
|
`post_update(sender: Type["Model"], instance: "Model")`
|
||||||
|
|
||||||
|
Send for `Model.update()` method.
|
||||||
|
|
||||||
|
`sender` is a `ormar.Model` class and `instance` is the model that was updated.
|
||||||
|
|
||||||
|
### pre_delete
|
||||||
|
|
||||||
|
`pre_delete(sender: Type["Model"], instance: "Model")`
|
||||||
|
|
||||||
|
Send for `Model.save()` and `Model.objects.create()` methods.
|
||||||
|
|
||||||
|
`sender` is a `ormar.Model` class and `instance` is the model to be deleted.
|
||||||
|
|
||||||
|
### post_delete
|
||||||
|
|
||||||
|
`post_delete(sender: Type["Model"], instance: "Model")`
|
||||||
|
|
||||||
|
Send for `Model.update()` method.
|
||||||
|
|
||||||
|
`sender` is a `ormar.Model` class and `instance` is the model that was deleted.
|
||||||
|
|
||||||
|
## Defining your own signals
|
||||||
|
|
||||||
|
Note that you can create your own signals although you will have to send them manually in your code or subclass `ormar.Model`
|
||||||
|
and trigger your signals there.
|
||||||
|
|
||||||
|
Creating new signal is super easy. Following example will set a new signal with name your_custom_signal.
|
||||||
|
|
||||||
|
```python hl_lines="21"
|
||||||
|
import databases
|
||||||
|
import sqlalchemy
|
||||||
|
|
||||||
|
import ormar
|
||||||
|
|
||||||
|
database = databases.Database("sqlite:///db.sqlite")
|
||||||
|
metadata = sqlalchemy.MetaData()
|
||||||
|
|
||||||
|
|
||||||
|
class Album(ormar.Model):
|
||||||
|
class Meta:
|
||||||
|
tablename = "albums"
|
||||||
|
metadata = metadata
|
||||||
|
database = database
|
||||||
|
|
||||||
|
id: int = ormar.Integer(primary_key=True)
|
||||||
|
name: str = ormar.String(max_length=100)
|
||||||
|
is_best_seller: bool = ormar.Boolean(default=False)
|
||||||
|
play_count: int = ormar.Integer(default=0)
|
||||||
|
|
||||||
|
Album.Meta.signals.your_custom_signal = ormar.Signal()
|
||||||
|
Album.Meta.signals.your_custom_signal.connect(your_receiver_name)
|
||||||
|
```
|
||||||
|
|
||||||
|
Actually under the hood signal is a `SignalEmitter` instance that keeps a dictionary of know signals, and allows you
|
||||||
|
to access them as attributes. When you try to access a signal that does not exist `SignalEmitter` will create one for you.
|
||||||
|
|
||||||
|
So example above can be simplified to. The `Signal` will be created for you.
|
||||||
|
|
||||||
|
```
|
||||||
|
Album.Meta.signals.your_custom_signal.connect(your_receiver_name)
|
||||||
|
```
|
||||||
|
|
||||||
|
Now to trigger this signal you need to call send method of the Signal.
|
||||||
|
|
||||||
|
```python
|
||||||
|
await Album.Meta.signals.your_custom_signal.send(sender=Album)
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that sender is the only required parameter and it should be ormar Model class.
|
||||||
|
|
||||||
|
Additional parameters have to be passed as keyword arguments.
|
||||||
|
|
||||||
|
```python
|
||||||
|
await Album.Meta.signals.your_custom_signal.send(sender=Album, my_param=True)
|
||||||
|
```
|
||||||
|
|
||||||
0
docs_src/signals/__init__.py
Normal file
0
docs_src/signals/__init__.py
Normal file
22
docs_src/signals/docs002.py
Normal file
22
docs_src/signals/docs002.py
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
from ormar import pre_update
|
||||||
|
|
||||||
|
|
||||||
|
@pre_update(Album)
|
||||||
|
async def before_update(sender, instance, **kwargs):
|
||||||
|
if instance.play_count > 50 and not instance.is_best_seller:
|
||||||
|
instance.is_best_seller = True
|
||||||
|
|
||||||
|
|
||||||
|
# here album.play_count ans is_best_seller get default values
|
||||||
|
album = await Album.objects.create(name="Venice")
|
||||||
|
assert not album.is_best_seller
|
||||||
|
assert album.play_count == 0
|
||||||
|
|
||||||
|
album.play_count = 30
|
||||||
|
# here a trigger is called but play_count is too low
|
||||||
|
await album.update()
|
||||||
|
assert not album.is_best_seller
|
||||||
|
|
||||||
|
album.play_count = 60
|
||||||
|
await album.update()
|
||||||
|
assert album.is_best_seller
|
||||||
@ -7,6 +7,7 @@ nav:
|
|||||||
- Fields: fields.md
|
- Fields: fields.md
|
||||||
- Relations: relations.md
|
- Relations: relations.md
|
||||||
- Queries: queries.md
|
- Queries: queries.md
|
||||||
|
- Signals: signals.md
|
||||||
- Use with Fastapi: fastapi.md
|
- Use with Fastapi: fastapi.md
|
||||||
- Use with mypy: mypy.md
|
- Use with mypy: mypy.md
|
||||||
- PyCharm plugin: plugin.md
|
- PyCharm plugin: plugin.md
|
||||||
|
|||||||
@ -1,6 +1,18 @@
|
|||||||
from ormar.decorators import property_field
|
from ormar.decorators import (
|
||||||
from ormar.exceptions import ModelDefinitionError, MultipleMatches, NoMatch
|
post_delete,
|
||||||
|
post_save,
|
||||||
|
post_update,
|
||||||
|
pre_delete,
|
||||||
|
pre_save,
|
||||||
|
pre_update,
|
||||||
|
property_field,
|
||||||
|
)
|
||||||
from ormar.protocols import QuerySetProtocol, RelationProtocol # noqa: I100
|
from ormar.protocols import QuerySetProtocol, RelationProtocol # noqa: I100
|
||||||
|
from ormar.exceptions import ( # noqa: I100
|
||||||
|
ModelDefinitionError,
|
||||||
|
MultipleMatches,
|
||||||
|
NoMatch,
|
||||||
|
)
|
||||||
from ormar.fields import ( # noqa: I100
|
from ormar.fields import ( # noqa: I100
|
||||||
BigInteger,
|
BigInteger,
|
||||||
Boolean,
|
Boolean,
|
||||||
@ -22,6 +34,7 @@ from ormar.models import Model
|
|||||||
from ormar.models.metaclass import ModelMeta
|
from ormar.models.metaclass import ModelMeta
|
||||||
from ormar.queryset import QuerySet
|
from ormar.queryset import QuerySet
|
||||||
from ormar.relations import RelationType
|
from ormar.relations import RelationType
|
||||||
|
from ormar.signals import Signal
|
||||||
|
|
||||||
|
|
||||||
class UndefinedType: # pragma no cover
|
class UndefinedType: # pragma no cover
|
||||||
@ -31,7 +44,7 @@ class UndefinedType: # pragma no cover
|
|||||||
|
|
||||||
Undefined = UndefinedType()
|
Undefined = UndefinedType()
|
||||||
|
|
||||||
__version__ = "0.6.2"
|
__version__ = "0.7.0"
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"Integer",
|
"Integer",
|
||||||
"BigInteger",
|
"BigInteger",
|
||||||
@ -59,4 +72,11 @@ __all__ = [
|
|||||||
"RelationProtocol",
|
"RelationProtocol",
|
||||||
"ModelMeta",
|
"ModelMeta",
|
||||||
"property_field",
|
"property_field",
|
||||||
|
"post_delete",
|
||||||
|
"post_save",
|
||||||
|
"post_update",
|
||||||
|
"pre_delete",
|
||||||
|
"pre_save",
|
||||||
|
"pre_update",
|
||||||
|
"Signal",
|
||||||
]
|
]
|
||||||
|
|||||||
@ -1,5 +1,19 @@
|
|||||||
from ormar.decorators.property_field import property_field
|
from ormar.decorators.property_field import property_field
|
||||||
|
from ormar.decorators.signals import (
|
||||||
|
post_delete,
|
||||||
|
post_save,
|
||||||
|
post_update,
|
||||||
|
pre_delete,
|
||||||
|
pre_save,
|
||||||
|
pre_update,
|
||||||
|
)
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"property_field",
|
"property_field",
|
||||||
|
"post_delete",
|
||||||
|
"post_save",
|
||||||
|
"post_update",
|
||||||
|
"pre_delete",
|
||||||
|
"pre_save",
|
||||||
|
"pre_update",
|
||||||
]
|
]
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
from typing import Any, Callable, List, TYPE_CHECKING, Type, Union
|
from typing import Callable, List, TYPE_CHECKING, Type, Union
|
||||||
|
|
||||||
if TYPE_CHECKING: # pragma: no cover
|
if TYPE_CHECKING: # pragma: no cover
|
||||||
from ormar import Model
|
from ormar import Model
|
||||||
|
|
||||||
|
|
||||||
def receiver(
|
def receiver(
|
||||||
signal: str, senders: Union[Type["Model"], List[Type["Model"]]], **kwargs: Any
|
signal: str, senders: Union[Type["Model"], List[Type["Model"]]]
|
||||||
) -> Callable:
|
) -> Callable:
|
||||||
def _decorator(func: Callable) -> Callable:
|
def _decorator(func: Callable) -> Callable:
|
||||||
if not isinstance(senders, list):
|
if not isinstance(senders, list):
|
||||||
@ -14,43 +14,31 @@ def receiver(
|
|||||||
_senders = senders
|
_senders = senders
|
||||||
for sender in _senders:
|
for sender in _senders:
|
||||||
signals = getattr(sender.Meta.signals, signal)
|
signals = getattr(sender.Meta.signals, signal)
|
||||||
signals.connect(func, **kwargs)
|
signals.connect(func)
|
||||||
return func
|
return func
|
||||||
|
|
||||||
return _decorator
|
return _decorator
|
||||||
|
|
||||||
|
|
||||||
def post_save(
|
def post_save(senders: Union[Type["Model"], List[Type["Model"]]],) -> Callable:
|
||||||
senders: Union[Type["Model"], List[Type["Model"]]], **kwargs: Any
|
return receiver(signal="post_save", senders=senders)
|
||||||
) -> Callable:
|
|
||||||
return receiver(signal="post_save", senders=senders, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
def post_update(
|
def post_update(senders: Union[Type["Model"], List[Type["Model"]]],) -> Callable:
|
||||||
senders: Union[Type["Model"], List[Type["Model"]]], **kwargs: Any
|
return receiver(signal="post_update", senders=senders)
|
||||||
) -> Callable:
|
|
||||||
return receiver(signal="post_update", senders=senders, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
def post_delete(
|
def post_delete(senders: Union[Type["Model"], List[Type["Model"]]],) -> Callable:
|
||||||
senders: Union[Type["Model"], List[Type["Model"]]], **kwargs: Any
|
return receiver(signal="post_delete", senders=senders)
|
||||||
) -> Callable:
|
|
||||||
return receiver(signal="post_delete", senders=senders, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
def pre_save(
|
def pre_save(senders: Union[Type["Model"], List[Type["Model"]]],) -> Callable:
|
||||||
senders: Union[Type["Model"], List[Type["Model"]]], **kwargs: Any
|
return receiver(signal="pre_save", senders=senders)
|
||||||
) -> Callable:
|
|
||||||
return receiver(signal="pre_save", senders=senders, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
def pre_update(
|
def pre_update(senders: Union[Type["Model"], List[Type["Model"]]]) -> Callable:
|
||||||
senders: Union[Type["Model"], List[Type["Model"]]], **kwargs: Any
|
return receiver(signal="pre_update", senders=senders)
|
||||||
) -> Callable:
|
|
||||||
return receiver(signal="pre_update", senders=senders, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
def pre_delete(
|
def pre_delete(senders: Union[Type["Model"], List[Type["Model"]]]) -> Callable:
|
||||||
senders: Union[Type["Model"], List[Type["Model"]]], **kwargs: Any
|
return receiver(signal="pre_delete", senders=senders)
|
||||||
) -> Callable:
|
|
||||||
return receiver(signal="pre_delete", senders=senders, **kwargs)
|
|
||||||
|
|||||||
@ -10,7 +10,7 @@ from fastapi import FastAPI
|
|||||||
from starlette.testclient import TestClient
|
from starlette.testclient import TestClient
|
||||||
|
|
||||||
import ormar
|
import ormar
|
||||||
from ormar import property_field
|
from ormar import post_save, property_field
|
||||||
from tests.settings import DATABASE_URL
|
from tests.settings import DATABASE_URL
|
||||||
|
|
||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
@ -65,8 +65,6 @@ class RandomModel(ormar.Model):
|
|||||||
metadata = metadata
|
metadata = metadata
|
||||||
database = database
|
database = database
|
||||||
|
|
||||||
include_props_in_dict = True
|
|
||||||
|
|
||||||
id: int = ormar.Integer(primary_key=True)
|
id: int = ormar.Integer(primary_key=True)
|
||||||
password: str = ormar.String(max_length=255, default=gen_pass)
|
password: str = ormar.String(max_length=255, default=gen_pass)
|
||||||
first_name: str = ormar.String(max_length=255, default="John")
|
first_name: str = ormar.String(max_length=255, default="John")
|
||||||
@ -251,7 +249,6 @@ def test_adding_fields_in_endpoints():
|
|||||||
]
|
]
|
||||||
assert response.json().get("full_name") == "John Test"
|
assert response.json().get("full_name") == "John Test"
|
||||||
|
|
||||||
RandomModel.Meta.include_props_in_fields = False
|
|
||||||
user3 = {"last_name": "Test"}
|
user3 = {"last_name": "Test"}
|
||||||
response = client.post("/random/", json=user3)
|
response = client.post("/random/", json=user3)
|
||||||
assert list(response.json().keys()) == [
|
assert list(response.json().keys()) == [
|
||||||
@ -268,7 +265,6 @@ def test_adding_fields_in_endpoints():
|
|||||||
def test_adding_fields_in_endpoints2():
|
def test_adding_fields_in_endpoints2():
|
||||||
client = TestClient(app)
|
client = TestClient(app)
|
||||||
with client as client:
|
with client as client:
|
||||||
RandomModel.Meta.include_props_in_dict = True
|
|
||||||
user3 = {"last_name": "Test"}
|
user3 = {"last_name": "Test"}
|
||||||
response = client.post("/random2/", json=user3)
|
response = client.post("/random2/", json=user3)
|
||||||
assert list(response.json().keys()) == [
|
assert list(response.json().keys()) == [
|
||||||
@ -283,9 +279,15 @@ def test_adding_fields_in_endpoints2():
|
|||||||
|
|
||||||
|
|
||||||
def test_excluding_property_field_in_endpoints2():
|
def test_excluding_property_field_in_endpoints2():
|
||||||
|
|
||||||
|
dummy_registry = {}
|
||||||
|
|
||||||
|
@post_save(RandomModel)
|
||||||
|
async def after_save(sender, instance, **kwargs):
|
||||||
|
dummy_registry[instance.pk] = instance.dict()
|
||||||
|
|
||||||
client = TestClient(app)
|
client = TestClient(app)
|
||||||
with client as client:
|
with client as client:
|
||||||
RandomModel.Meta.include_props_in_dict = True
|
|
||||||
user3 = {"last_name": "Test"}
|
user3 = {"last_name": "Test"}
|
||||||
response = client.post("/random3/", json=user3)
|
response = client.post("/random3/", json=user3)
|
||||||
assert list(response.json().keys()) == [
|
assert list(response.json().keys()) == [
|
||||||
@ -296,3 +298,7 @@ def test_excluding_property_field_in_endpoints2():
|
|||||||
"created_date",
|
"created_date",
|
||||||
]
|
]
|
||||||
assert response.json().get("full_name") is None
|
assert response.json().get("full_name") is None
|
||||||
|
assert len(dummy_registry) == 1
|
||||||
|
check_dict = dummy_registry.get(response.json().get("id"))
|
||||||
|
check_dict.pop("full_name")
|
||||||
|
assert response.json().get("password") == check_dict.get("password")
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import pytest
|
|||||||
import sqlalchemy
|
import sqlalchemy
|
||||||
|
|
||||||
import ormar
|
import ormar
|
||||||
from ormar.decorators.signals import (
|
from ormar import (
|
||||||
post_delete,
|
post_delete,
|
||||||
post_save,
|
post_save,
|
||||||
post_update,
|
post_update,
|
||||||
@ -51,6 +51,7 @@ class Album(ormar.Model):
|
|||||||
id: int = ormar.Integer(primary_key=True)
|
id: int = ormar.Integer(primary_key=True)
|
||||||
name: str = ormar.String(max_length=100)
|
name: str = ormar.String(max_length=100)
|
||||||
is_best_seller: bool = ormar.Boolean(default=False)
|
is_best_seller: bool = ormar.Boolean(default=False)
|
||||||
|
play_count: int = ormar.Integer(default=0)
|
||||||
cover: Optional[Cover] = ormar.ForeignKey(Cover)
|
cover: Optional[Cover] = ormar.ForeignKey(Cover)
|
||||||
|
|
||||||
|
|
||||||
@ -70,6 +71,7 @@ def test_passing_not_callable():
|
|||||||
|
|
||||||
def test_passing_callable_without_kwargs():
|
def test_passing_callable_without_kwargs():
|
||||||
with pytest.raises(SignalDefinitionError):
|
with pytest.raises(SignalDefinitionError):
|
||||||
|
|
||||||
@pre_save(Album)
|
@pre_save(Album)
|
||||||
def trigger(sender, instance): # pragma: no cover
|
def trigger(sender, instance): # pragma: no cover
|
||||||
pass
|
pass
|
||||||
@ -79,6 +81,7 @@ def test_passing_callable_without_kwargs():
|
|||||||
async def test_signal_functions():
|
async def test_signal_functions():
|
||||||
async with database:
|
async with database:
|
||||||
async with database.transaction(force_rollback=True):
|
async with database.transaction(force_rollback=True):
|
||||||
|
|
||||||
@pre_save(Album)
|
@pre_save(Album)
|
||||||
async def before_save(sender, instance, **kwargs):
|
async def before_save(sender, instance, **kwargs):
|
||||||
await AuditLog(
|
await AuditLog(
|
||||||
@ -162,9 +165,9 @@ async def test_signal_functions():
|
|||||||
assert len(audits) == 2
|
assert len(audits) == 2
|
||||||
assert audits[0].event_type == "PRE_DELETE_album"
|
assert audits[0].event_type == "PRE_DELETE_album"
|
||||||
assert (
|
assert (
|
||||||
audits[0].event_log.get("id")
|
audits[0].event_log.get("id")
|
||||||
== audits[1].event_log.get("id")
|
== audits[1].event_log.get("id")
|
||||||
== album.id
|
== album.id
|
||||||
)
|
)
|
||||||
assert audits[1].event_type == "POST_DELETE_album"
|
assert audits[1].event_type == "POST_DELETE_album"
|
||||||
|
|
||||||
@ -178,6 +181,7 @@ async def test_signal_functions():
|
|||||||
async def test_multiple_signals():
|
async def test_multiple_signals():
|
||||||
async with database:
|
async with database:
|
||||||
async with database.transaction(force_rollback=True):
|
async with database.transaction(force_rollback=True):
|
||||||
|
|
||||||
@pre_save(Album)
|
@pre_save(Album)
|
||||||
async def before_save(sender, instance, **kwargs):
|
async def before_save(sender, instance, **kwargs):
|
||||||
await AuditLog(
|
await AuditLog(
|
||||||
@ -208,6 +212,7 @@ async def test_multiple_signals():
|
|||||||
async def test_static_methods_as_signals():
|
async def test_static_methods_as_signals():
|
||||||
async with database:
|
async with database:
|
||||||
async with database.transaction(force_rollback=True):
|
async with database.transaction(force_rollback=True):
|
||||||
|
|
||||||
class AlbumAuditor:
|
class AlbumAuditor:
|
||||||
event_type = "ALBUM_INSTANCE"
|
event_type = "ALBUM_INSTANCE"
|
||||||
|
|
||||||
@ -232,6 +237,7 @@ async def test_static_methods_as_signals():
|
|||||||
async def test_methods_as_signals():
|
async def test_methods_as_signals():
|
||||||
async with database:
|
async with database:
|
||||||
async with database.transaction(force_rollback=True):
|
async with database.transaction(force_rollback=True):
|
||||||
|
|
||||||
class AlbumAuditor:
|
class AlbumAuditor:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.event_type = "ALBUM_INSTANCE"
|
self.event_type = "ALBUM_INSTANCE"
|
||||||
@ -252,10 +258,12 @@ async def test_methods_as_signals():
|
|||||||
|
|
||||||
album.signals.pre_save.disconnect(auditor.before_save)
|
album.signals.pre_save.disconnect(auditor.before_save)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_multiple_senders_signal():
|
async def test_multiple_senders_signal():
|
||||||
async with database:
|
async with database:
|
||||||
async with database.transaction(force_rollback=True):
|
async with database.transaction(force_rollback=True):
|
||||||
|
|
||||||
@pre_save([Album, Cover])
|
@pre_save([Album, Cover])
|
||||||
async def before_save(sender, instance, **kwargs):
|
async def before_save(sender, instance, **kwargs):
|
||||||
await AuditLog(
|
await AuditLog(
|
||||||
@ -263,7 +271,7 @@ async def test_multiple_senders_signal():
|
|||||||
event_log=instance.json(),
|
event_log=instance.json(),
|
||||||
).save()
|
).save()
|
||||||
|
|
||||||
cover = await Cover(title='Blue').save()
|
cover = await Cover(title="Blue").save()
|
||||||
album = await Album.objects.create(name="San Francisco", cover=cover)
|
album = await Album.objects.create(name="San Francisco", cover=cover)
|
||||||
|
|
||||||
audits = await AuditLog.objects.all()
|
audits = await AuditLog.objects.all()
|
||||||
@ -271,7 +279,72 @@ async def test_multiple_senders_signal():
|
|||||||
assert audits[0].event_type == "PRE_SAVE_cover"
|
assert audits[0].event_type == "PRE_SAVE_cover"
|
||||||
assert audits[0].event_log.get("title") == cover.title
|
assert audits[0].event_log.get("title") == cover.title
|
||||||
assert audits[1].event_type == "PRE_SAVE_album"
|
assert audits[1].event_type == "PRE_SAVE_album"
|
||||||
assert audits[1].event_log.get("cover") == album.cover.dict(exclude={'albums'})
|
assert audits[1].event_log.get("cover") == album.cover.dict(
|
||||||
|
exclude={"albums"}
|
||||||
|
)
|
||||||
|
|
||||||
album.signals.pre_save.disconnect(before_save)
|
album.signals.pre_save.disconnect(before_save)
|
||||||
cover.signals.pre_save.disconnect(before_save)
|
cover.signals.pre_save.disconnect(before_save)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_modifing_the_instance():
|
||||||
|
async with database:
|
||||||
|
async with database.transaction(force_rollback=True):
|
||||||
|
|
||||||
|
@pre_update(Album)
|
||||||
|
async def before_update(sender, instance, **kwargs):
|
||||||
|
if instance.play_count > 50 and not instance.is_best_seller:
|
||||||
|
instance.is_best_seller = True
|
||||||
|
|
||||||
|
# here album.play_count ans is_best_seller get default values
|
||||||
|
album = await Album.objects.create(name="Venice")
|
||||||
|
assert not album.is_best_seller
|
||||||
|
assert album.play_count == 0
|
||||||
|
|
||||||
|
album.play_count = 30
|
||||||
|
# here a trigger is called but play_count is too low
|
||||||
|
await album.update()
|
||||||
|
assert not album.is_best_seller
|
||||||
|
|
||||||
|
album.play_count = 60
|
||||||
|
await album.update()
|
||||||
|
assert album.is_best_seller
|
||||||
|
album.signals.pre_update.disconnect(before_update)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_custom_signal():
|
||||||
|
async with database:
|
||||||
|
async with database.transaction(force_rollback=True):
|
||||||
|
|
||||||
|
async def after_update(sender, instance, **kwargs):
|
||||||
|
if instance.play_count > 50 and not instance.is_best_seller:
|
||||||
|
instance.is_best_seller = True
|
||||||
|
elif instance.play_count < 50 and instance.is_best_seller:
|
||||||
|
instance.is_best_seller = False
|
||||||
|
await instance.update()
|
||||||
|
|
||||||
|
Album.Meta.signals.custom.connect(after_update)
|
||||||
|
|
||||||
|
# here album.play_count ans is_best_seller get default values
|
||||||
|
album = await Album.objects.create(name="Venice")
|
||||||
|
assert not album.is_best_seller
|
||||||
|
assert album.play_count == 0
|
||||||
|
|
||||||
|
album.play_count = 30
|
||||||
|
# here a trigger is called but play_count is too low
|
||||||
|
await album.update()
|
||||||
|
assert not album.is_best_seller
|
||||||
|
|
||||||
|
album.play_count = 60
|
||||||
|
await album.update()
|
||||||
|
assert not album.is_best_seller
|
||||||
|
await Album.Meta.signals.custom.send(sender=Album, instance=album)
|
||||||
|
assert album.is_best_seller
|
||||||
|
|
||||||
|
album.play_count = 30
|
||||||
|
await album.update()
|
||||||
|
assert album.is_best_seller
|
||||||
|
await Album.Meta.signals.custom.send(sender=Album, instance=album)
|
||||||
|
assert not album.is_best_seller
|
||||||
|
|||||||
Reference in New Issue
Block a user