Skip to content

Commit

Permalink
Active Record pattern (#112)
Browse files Browse the repository at this point in the history
  • Loading branch information
roman-right authored Sep 8, 2021
1 parent b907909 commit 9dce295
Show file tree
Hide file tree
Showing 12 changed files with 271 additions and 9 deletions.
4 changes: 2 additions & 2 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ repos:
rev: 20.8b1
hooks:
- id: black
language_version: python3.8
language_version: python3.9
- repo: https://gitlab.com/pycqa/flake8
rev: 3.9.2
hooks:
- id: flake8
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v0.902
rev: v0.910
hooks:
- id: mypy
additional_dependencies:
Expand Down
8 changes: 7 additions & 1 deletion beanie/__init__.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
from beanie.migrations.controllers.free_fall import free_fall_migration
from beanie.migrations.controllers.iterative import iterative_migration
from beanie.odm.actions import before_event, after_event, Insert, Replace
from beanie.odm.fields import PydanticObjectId, Indexed
from beanie.odm.utils.general import init_beanie
from beanie.odm.documents import Document

__version__ = "1.2.8"
__version__ = "1.3.0"
__all__ = [
# ODM
"Document",
"init_beanie",
"PydanticObjectId",
"Indexed",
# Actions
"before_event",
"after_event",
"Insert",
"Replace",
# Migrations
"iterative_migration",
"free_fall_migration",
Expand Down
188 changes: 188 additions & 0 deletions beanie/odm/actions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
import asyncio
import inspect
from enum import Enum
from functools import wraps
from typing import Callable, List, Union, Dict, TYPE_CHECKING, Any

from beanie.odm.utils.class_path import (
get_class_path_for_method,
get_class_path_for_object,
)

if TYPE_CHECKING:
from beanie.odm.documents import Document


class EventTypes(str, Enum):
INSERT = "INSERT"
REPLACE = "REPLACE"


Insert = EventTypes.INSERT
Replace = EventTypes.REPLACE


class ActionDirections(str, Enum): # TODO think about this name
BEFORE = "BEFORE"
AFTER = "AFTER"


class ActionRegistry:
_actions: Dict[str, Any] = {}

# TODO the real type is
# Dict[str, Dict[EventTypes,Dict[ActionDirections: List[Callable]]]]
# But mypy says it has syntax error inside. Fix it.

@classmethod
def add_action(
cls,
event_types: List[EventTypes],
action_direction: ActionDirections,
funct: Callable,
):
"""
Add action to the action registry
:param event_types: List[EventTypes]
:param action_direction: ActionDirections - before or after
:param funct: Callable - function
"""
class_path = get_class_path_for_method(funct)
if cls._actions.get(class_path) is None:
cls._actions[class_path] = {
action_type: {
action_direction: []
for action_direction in ActionDirections
}
for action_type in EventTypes
}
for event_type in event_types:
cls._actions[class_path][event_type][action_direction].append(
funct
)

@classmethod
def get_action_list(
cls,
class_path: str,
event_types: EventTypes,
action_direction: ActionDirections,
) -> List[Callable]:
"""
Get stored action list
:param class_path: str - path to the class
:param event_types: EventTypes - type of needed event
:param action_direction: ActionDirections - before or after
:return: List[Callable] - list of stored methods
"""
if class_path not in cls._actions:
return []
return cls._actions[class_path][event_types][action_direction]

@classmethod
async def run_actions(
cls,
instance: "Document",
event_type: EventTypes,
action_direction: ActionDirections,
):
"""
Run actions
:param instance: Document - object of the Document subclass
:param event_type: EventTypes - event types
:param action_direction: ActionDirections - before or after
"""
class_path = get_class_path_for_object(instance)
actions_list = cls.get_action_list(
class_path, event_type, action_direction
)
coros = []
for action in actions_list:
if inspect.iscoroutinefunction(action):
coros.append(action(instance))
elif inspect.isfunction(action):
action(instance)
await asyncio.gather(*coros)


def register_action(
event_types: Union[List[EventTypes], EventTypes],
action_direction: ActionDirections,
):
"""
Decorator. Base registration method.
Used inside `before_event` and `after_event`
:param event_types: Union[List[EventTypes], EventTypes] - event types
:param action_direction: ActionDirections - before or after
:return:
"""
if isinstance(event_types, EventTypes):
event_types = [event_types]

def decorator(f):
ActionRegistry.add_action(
event_types=event_types, # type: ignore
action_direction=action_direction,
funct=f,
)
return f

return decorator


def before_event(event_types: Union[List[EventTypes], EventTypes]):
"""
Decorator. It adds action, which should run before mentioned one
or many events happen
:param event_types: Union[List[EventTypes], EventTypes] - event types
:return: None
"""
return register_action(
action_direction=ActionDirections.BEFORE, event_types=event_types
)


def after_event(event_types: Union[List[EventTypes], EventTypes]):
"""
Decorator. It adds action, which should run after mentioned one
or many events happen
:param event_types: Union[List[EventTypes], EventTypes] - event types
:return: None
"""
return register_action(
action_direction=ActionDirections.AFTER, event_types=event_types
)


def wrap_with_actions(event_type: EventTypes):
"""
Helper function to wrap Document methods with
before and after event listeners
:param event_type: EventTypes - event types
:return: None
"""

def decorator(f: Callable):
@wraps(f)
async def wrapper(self, *args, **kwargs):
await ActionRegistry.run_actions(
self,
event_type=event_type,
action_direction=ActionDirections.BEFORE,
)

result = await f(self, *args, **kwargs)

await ActionRegistry.run_actions(
self,
event_type=event_type,
action_direction=ActionDirections.AFTER,
)

return result

return wrapper

return decorator
Empty file.
3 changes: 3 additions & 0 deletions beanie/odm/documents.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
ReplaceError,
DocumentNotFound,
)
from beanie.odm.actions import EventTypes, wrap_with_actions
from beanie.odm.enums import SortDirection
from beanie.odm.fields import PydanticObjectId, ExpressionField
from beanie.odm.interfaces.update import (
Expand Down Expand Up @@ -84,6 +85,7 @@ async def _sync(self) -> None:
for key, value in dict(new_instance).items():
setattr(self, key, value)

@wrap_with_actions(EventTypes.INSERT)
async def insert(
self: DocType, session: Optional[ClientSession] = None
) -> DocType:
Expand Down Expand Up @@ -414,6 +416,7 @@ def all(
session=session,
)

@wrap_with_actions(EventTypes.REPLACE)
async def replace(
self: DocType, session: Optional[ClientSession] = None
) -> DocType:
Expand Down
9 changes: 9 additions & 0 deletions beanie/odm/utils/class_path.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from typing import Callable, Any


def get_class_path_for_method(f: Callable) -> str:
return f"{f.__module__}.{f.__qualname__.split('.')[0]}"


def get_class_path_for_object(o: Any) -> str:
return f"{o.__module__}.{o.__class__.__name__}"
18 changes: 15 additions & 3 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@

Beanie project changes

## [1.3.0] - 2021-09-08

### Added

- Active record pattern

### Implementation

- Issue - <https://github.com/roman-right/beanie/issues/110>

## [1.2.8] - 2021-09-01

### Fix
Expand All @@ -10,7 +20,7 @@ Beanie project changes

### Implementation

- Issue - <https://github.com/roman-right/beanie/pull/109>
- PR - <https://github.com/roman-right/beanie/pull/109>

## [1.2.7] - 2021-09-01

Expand All @@ -21,7 +31,7 @@ Beanie project changes
### Implementation

- Author - [Anthony Shaw](https://github.com/tonybaloney)
- Issue - <https://github.com/roman-right/beanie/pull/106>
- PR - <https://github.com/roman-right/beanie/pull/106>

## [1.2.6] - 2021-08-25

Expand Down Expand Up @@ -427,4 +437,6 @@ Beanie project changes

[1.2.7]: https://pypi.org/project/beanie/1.2.7

[1.2.8]: https://pypi.org/project/beanie/1.2.8
[1.2.8]: https://pypi.org/project/beanie/1.2.8

[1.3.0]: https://pypi.org/project/beanie/1.3.0
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "beanie"
version = "1.2.8"
version = "1.3.0"
description = "Asynchronous Python ODM for MongoDB"
authors = ["Roman <[email protected]>"]
license = "Apache-2.0"
Expand Down
2 changes: 2 additions & 0 deletions tests/odm/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
DocumentTestModelFailInspection,
DocumentWithCustomIdUUID,
DocumentWithCustomIdInt,
DocumentWithActions,
)
from tests.odm.models import (
Sample,
Expand Down Expand Up @@ -122,6 +123,7 @@ async def init(loop, db):
DocumentWithCustomIdUUID,
DocumentWithCustomIdInt,
Sample,
DocumentWithActions,
]
await init_beanie(
database=db,
Expand Down
26 changes: 25 additions & 1 deletion tests/odm/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
from pydantic import BaseModel, Field
from pymongo import IndexModel

from beanie import Document, Indexed
from beanie import Document, Indexed, Insert, Replace
from beanie.odm.actions import before_event, after_event


class Option2(BaseModel):
Expand Down Expand Up @@ -131,3 +132,26 @@ class DocumentWithCustomIdUUID(Document):
class DocumentWithCustomIdInt(Document):
id: int
name: str


class DocumentWithActions(Document):
name: str
num_1: int = 0
num_2: int = 10
num_3: int = 100

@before_event(Insert)
def capitalize_name(self):
self.name = self.name.capitalize()

@before_event([Insert, Replace])
async def add_one(self):
self.num_1 += 1

@after_event(Insert)
def num_2_change(self):
self.num_2 -= 1

@after_event(Replace)
def num_3_change(self):
self.num_3 -= 1
18 changes: 18 additions & 0 deletions tests/odm/test_actions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from tests.odm.models import DocumentWithActions


async def test_actions_insert_replace():
test_name = "test_name"
sample = DocumentWithActions(name=test_name)

# TEST INSERT
await sample.insert()
assert sample.name != test_name
assert sample.name == test_name.capitalize()
assert sample.num_1 == 1
assert sample.num_2 == 9

# TEST REPLACE
await sample.replace()
assert sample.num_1 == 2
assert sample.num_3 == 99
2 changes: 1 addition & 1 deletion tests/test_beanie.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@


def test_version():
assert __version__ == "1.2.8"
assert __version__ == "1.3.0"

0 comments on commit 9dce295

Please sign in to comment.