add base signal class
This commit is contained in:
@ -1,3 +1,10 @@
|
|||||||
|
# 0.7.0
|
||||||
|
|
||||||
|
* **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`
|
||||||
|
*
|
||||||
|
* Performance optimization
|
||||||
|
|
||||||
# 0.6.2
|
# 0.6.2
|
||||||
|
|
||||||
* Performance optimization
|
* Performance optimization
|
||||||
@ -12,7 +19,7 @@
|
|||||||
|
|
||||||
# 0.6.0
|
# 0.6.0
|
||||||
|
|
||||||
* **Breaking:** calling instance.load() when the instance row was deleted from db now raises ormar.NoMatch instead of ValueError
|
* **Breaking:** calling instance.load() when the instance row was deleted from db now raises `NoMatch` instead of `ValueError`
|
||||||
* **Breaking:** calling add and remove on ReverseForeignKey relation now updates the child model in db setting/removing fk column
|
* **Breaking:** calling add and remove on ReverseForeignKey relation now updates the child model in db setting/removing fk column
|
||||||
* **Breaking:** ReverseForeignKey relation now exposes QuerySetProxy API like ManyToMany relation
|
* **Breaking:** ReverseForeignKey relation now exposes QuerySetProxy API like ManyToMany relation
|
||||||
* **Breaking:** querying related models from ManyToMany cleans list of related models loaded on parent model:
|
* **Breaking:** querying related models from ManyToMany cleans list of related models loaded on parent model:
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
from ormar.decorators import property_field
|
from ormar.decorators import property_field
|
||||||
from ormar.exceptions import ModelDefinitionError, ModelNotSet, MultipleMatches, NoMatch
|
from ormar.exceptions import ModelDefinitionError, MultipleMatches, NoMatch
|
||||||
from ormar.protocols import QuerySetProtocol, RelationProtocol # noqa: I100
|
from ormar.protocols import QuerySetProtocol, RelationProtocol # noqa: I100
|
||||||
from ormar.fields import ( # noqa: I100
|
from ormar.fields import ( # noqa: I100
|
||||||
BigInteger,
|
BigInteger,
|
||||||
@ -47,7 +47,6 @@ __all__ = [
|
|||||||
"ManyToMany",
|
"ManyToMany",
|
||||||
"Model",
|
"Model",
|
||||||
"ModelDefinitionError",
|
"ModelDefinitionError",
|
||||||
"ModelNotSet",
|
|
||||||
"MultipleMatches",
|
"MultipleMatches",
|
||||||
"NoMatch",
|
"NoMatch",
|
||||||
"ForeignKey",
|
"ForeignKey",
|
||||||
|
|||||||
@ -13,7 +13,7 @@ def property_field(func: Callable) -> Union[property, Callable]:
|
|||||||
if len(arguments) > 1 or arguments[0] != "self":
|
if len(arguments) > 1 or arguments[0] != "self":
|
||||||
raise ModelDefinitionError(
|
raise ModelDefinitionError(
|
||||||
"property_field decorator can be used "
|
"property_field decorator can be used "
|
||||||
"only on class methods with no arguments"
|
"only on methods with no arguments"
|
||||||
)
|
)
|
||||||
func.__dict__["__property_field__"] = True
|
func.__dict__["__property_field__"] = True
|
||||||
return func
|
return func
|
||||||
|
|||||||
@ -1,28 +1,57 @@
|
|||||||
class AsyncOrmException(Exception):
|
class AsyncOrmException(Exception):
|
||||||
|
"""
|
||||||
|
Base ormar Exception
|
||||||
|
"""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ModelDefinitionError(AsyncOrmException):
|
class ModelDefinitionError(AsyncOrmException):
|
||||||
|
"""
|
||||||
|
Raised for errors related to the model definition itself.
|
||||||
|
* setting @property_field on method with arguments other than func(self)
|
||||||
|
* defining a Field without required parameters
|
||||||
|
* defining a model with more than one primary_key
|
||||||
|
* defining a model without primary_key
|
||||||
|
* setting primary_key column as pydantic_only
|
||||||
|
"""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ModelError(AsyncOrmException):
|
class ModelError(AsyncOrmException):
|
||||||
pass
|
"""
|
||||||
|
Raised for initialization of model with non-existing field keyword.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
class ModelNotSet(AsyncOrmException):
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class NoMatch(AsyncOrmException):
|
class NoMatch(AsyncOrmException):
|
||||||
|
"""
|
||||||
|
Raised for database queries that has no matching result (empty result).
|
||||||
|
"""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class MultipleMatches(AsyncOrmException):
|
class MultipleMatches(AsyncOrmException):
|
||||||
|
"""
|
||||||
|
Raised for database queries that should return one row (i.e. get, first etc.)
|
||||||
|
but has multiple matching results in response.
|
||||||
|
"""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class QueryDefinitionError(AsyncOrmException):
|
class QueryDefinitionError(AsyncOrmException):
|
||||||
|
"""
|
||||||
|
Raised for errors in query definition.
|
||||||
|
* using contains or icontains filter with instance of the Model
|
||||||
|
* using Queryset.update() without filter and setting each flag to True
|
||||||
|
* using Queryset.delete() without filter and setting each flag to True
|
||||||
|
"""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@ -31,4 +60,17 @@ class RelationshipInstanceError(AsyncOrmException):
|
|||||||
|
|
||||||
|
|
||||||
class ModelPersistenceError(AsyncOrmException):
|
class ModelPersistenceError(AsyncOrmException):
|
||||||
|
"""
|
||||||
|
Raised for update of models without primary_key set (cannot retrieve from db)
|
||||||
|
or for saving a model with relation to unsaved model (cannot extract fk value).
|
||||||
|
"""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class SignalDefinitionError(AsyncOrmException):
|
||||||
|
"""
|
||||||
|
Raised when non callable receiver is passed as signal callback.
|
||||||
|
"""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|||||||
@ -6,7 +6,7 @@ from sqlalchemy import bindparam
|
|||||||
|
|
||||||
import ormar # noqa I100
|
import ormar # noqa I100
|
||||||
from ormar import MultipleMatches, NoMatch
|
from ormar import MultipleMatches, NoMatch
|
||||||
from ormar.exceptions import QueryDefinitionError
|
from ormar.exceptions import ModelPersistenceError, QueryDefinitionError
|
||||||
from ormar.queryset import FilterQuery
|
from ormar.queryset import FilterQuery
|
||||||
from ormar.queryset.clause import QueryClause
|
from ormar.queryset.clause import QueryClause
|
||||||
from ormar.queryset.prefetch_query import PrefetchQuery
|
from ormar.queryset.prefetch_query import PrefetchQuery
|
||||||
@ -446,7 +446,7 @@ class QuerySet:
|
|||||||
for objt in objects:
|
for objt in objects:
|
||||||
new_kwargs = objt.dict()
|
new_kwargs = objt.dict()
|
||||||
if pk_name not in new_kwargs or new_kwargs.get(pk_name) is None:
|
if pk_name not in new_kwargs or new_kwargs.get(pk_name) is None:
|
||||||
raise QueryDefinitionError(
|
raise ModelPersistenceError(
|
||||||
"You cannot update unsaved objects. "
|
"You cannot update unsaved objects. "
|
||||||
f"{self.model.__name__} has to have {pk_name} filled."
|
f"{self.model.__name__} has to have {pk_name} filled."
|
||||||
)
|
)
|
||||||
|
|||||||
0
ormar/signals/__init__.py
Normal file
0
ormar/signals/__init__.py
Normal file
53
ormar/signals/signal.py
Normal file
53
ormar/signals/signal.py
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import asyncio
|
||||||
|
import inspect
|
||||||
|
from typing import Any, Callable, List, Tuple, Union
|
||||||
|
|
||||||
|
from ormar.exceptions import SignalDefinitionError
|
||||||
|
|
||||||
|
|
||||||
|
def callable_accepts_kwargs(func: Callable) -> bool:
|
||||||
|
return any(
|
||||||
|
p
|
||||||
|
for p in inspect.signature(func).parameters.values()
|
||||||
|
if p.kind == p.VAR_KEYWORD
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def make_id(target: Any) -> Union[int, Tuple[int, int]]:
|
||||||
|
if hasattr(target, "__func__"):
|
||||||
|
return id(target.__self__), id(target.__func__)
|
||||||
|
return id(target)
|
||||||
|
|
||||||
|
|
||||||
|
class Signal:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self._receivers: List[Tuple[Union[int, Tuple[int, int]], Callable]] = []
|
||||||
|
|
||||||
|
def connect(self, receiver: Callable) -> None:
|
||||||
|
if not callable(receiver):
|
||||||
|
raise SignalDefinitionError("Signal receivers must be callable.")
|
||||||
|
if not callable_accepts_kwargs(receiver):
|
||||||
|
raise SignalDefinitionError(
|
||||||
|
"Signal receivers must accept **kwargs argument."
|
||||||
|
)
|
||||||
|
new_receiver_key = make_id(receiver)
|
||||||
|
if not any(rec_id == new_receiver_key for rec_id, _ in self._receivers):
|
||||||
|
self._receivers.append((new_receiver_key, receiver))
|
||||||
|
|
||||||
|
def disconnect(self, receiver: Callable) -> bool:
|
||||||
|
removed = False
|
||||||
|
new_receiver_key = make_id(receiver)
|
||||||
|
for ind, rec in enumerate(self._receivers):
|
||||||
|
rec_id, _ = rec
|
||||||
|
if rec_id == new_receiver_key:
|
||||||
|
removed = True
|
||||||
|
del self._receivers[ind]
|
||||||
|
break
|
||||||
|
return removed
|
||||||
|
|
||||||
|
async def send(self, sender: Any, **kwargs: Any) -> None:
|
||||||
|
receivers = []
|
||||||
|
for receiver in self._receivers:
|
||||||
|
_, receiver_func = receiver
|
||||||
|
receivers.append(receiver_func(sender, **kwargs))
|
||||||
|
await asyncio.gather(*receivers)
|
||||||
@ -5,7 +5,7 @@ import pytest
|
|||||||
import sqlalchemy
|
import sqlalchemy
|
||||||
|
|
||||||
import ormar
|
import ormar
|
||||||
from ormar.exceptions import QueryDefinitionError
|
from ormar.exceptions import ModelPersistenceError, QueryDefinitionError
|
||||||
from tests.settings import DATABASE_URL
|
from tests.settings import DATABASE_URL
|
||||||
|
|
||||||
database = databases.Database(DATABASE_URL, force_rollback=True)
|
database = databases.Database(DATABASE_URL, force_rollback=True)
|
||||||
@ -302,7 +302,7 @@ async def test_bulk_update_with_relation():
|
|||||||
async def test_bulk_update_not_saved_objts():
|
async def test_bulk_update_not_saved_objts():
|
||||||
async with database:
|
async with database:
|
||||||
category = await Category.objects.create(name="Sample Category")
|
category = await Category.objects.create(name="Sample Category")
|
||||||
with pytest.raises(QueryDefinitionError):
|
with pytest.raises(ModelPersistenceError):
|
||||||
await Note.objects.bulk_update(
|
await Note.objects.bulk_update(
|
||||||
[
|
[
|
||||||
Note(text="Buy the groceries.", category=category),
|
Note(text="Buy the groceries.", category=category),
|
||||||
|
|||||||
Reference in New Issue
Block a user