Skip to content

Commit 277a9ba

Browse files
authoredAug 6, 2024··
feat: Adds celery integration (PR #8)
Pull requested created by @angeloalvarez - #8
2 parents aaba7a6 + 70effda commit 277a9ba

20 files changed

+668
-22
lines changed
 

‎.pre-commit-config.yaml

+2-1
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,9 @@ repos:
3030
rev: v1.10.1
3131
hooks:
3232
- id: mypy
33+
args: [--config-file=./pyproject.toml, "."]
3334
additional_dependencies: [types-requests]
34-
exclude: ^tests/
35+
pass_filenames: false
3536
- repo: local
3637
hooks:
3738
- id: pytest

‎eolic/base.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import functools
44
from typing import Any, Callable, List, TypeVar, cast
55

6-
from eolic.integrations.base import Integration
6+
from .integrations.base import Integration
77

88
from .listener import EventListenerHandler
99
from .meta.singleton import Singleton

‎eolic/helpers/modules.py

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
"""File containing helpers for modules."""
2+
3+
import importlib.util
4+
5+
from typing import Any
6+
7+
8+
def is_module_installed(module_name: str) -> bool:
9+
"""
10+
Check if a Python module is installed.
11+
12+
Args:
13+
module_name (str): The name of the module to check.
14+
15+
Returns:
16+
bool: True if the module is installed, False otherwise.
17+
"""
18+
return importlib.util.find_spec(module_name) is not None
19+
20+
21+
def get_module(module_name: str) -> Any:
22+
"""
23+
Import and returns a module by its name.
24+
25+
Args:
26+
module_name (str): The name of the module to import, as a string.
27+
Returns:
28+
Any: The imported module.
29+
"""
30+
return importlib.import_module(module_name)

‎eolic/integrations/celery.py

+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
"""Module for Celery integration."""
2+
3+
from typing import TYPE_CHECKING, Optional
4+
5+
from ..base import Integration
6+
from ..model import EventDTO
7+
from ..helpers.modules import is_module_installed
8+
9+
if TYPE_CHECKING:
10+
from celery import Celery
11+
from ..base import Eolic
12+
13+
14+
class CeleryIntegration(Integration):
15+
"""Class for CeleryIntegration integration setup with Eolic."""
16+
17+
def __init__(
18+
self,
19+
app: Optional["Celery"],
20+
event_function: str = "events",
21+
queue_name: str = "eolic",
22+
) -> None:
23+
"""
24+
Initialize the Celery integration.
25+
26+
Args:
27+
app (Celery): The Celery app instance.
28+
event_function (str): The event hook function event receiving.
29+
30+
Raises:
31+
Exception: If Celery extra is not installed.
32+
Exception: If Celery is None.
33+
"""
34+
if not is_module_installed("celery"):
35+
raise Exception(
36+
"Celery Integration is not installed. "
37+
"Please install eolic[celery] (using celery extras) to use this integration."
38+
)
39+
40+
if app is None:
41+
raise Exception("Please declare you app to setup the integration.")
42+
43+
super().__init__()
44+
self.app = app
45+
self.event_function = event_function
46+
self.queue_name = queue_name
47+
48+
def setup(self, eolic: "Eolic") -> None:
49+
"""
50+
Apply the Celery integration setup.
51+
52+
Args:
53+
eolic (Eolic): The Eolic instance to integrate with.
54+
"""
55+
self.eolic = eolic
56+
event_function = self.event_function
57+
58+
if event_function is None:
59+
raise Exception("Event function is required for Celery integration.")
60+
61+
def listener_event(event_name: str, *args, **kwargs):
62+
event = EventDTO(event=event_name, args=args, kwargs=kwargs)
63+
self.forward_event(event)
64+
65+
self.app.task(listener_event, name=self.event_function, queue=self.queue_name)

‎eolic/integrations/fastapi.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
"""Module for FastAPI integration."""
22

3-
import sys
43
from typing import TYPE_CHECKING, Optional
4+
55
from ..base import Integration
66
from ..model import EventDTO
7+
from ..helpers.modules import is_module_installed
78

89
if TYPE_CHECKING:
910
from fastapi import FastAPI
@@ -25,7 +26,7 @@ def __init__(self, app: Optional["FastAPI"], event_route: str = "/events") -> No
2526
Exception: If FastAPI extra is is not installed.
2627
Exception: If FastAPI is None.
2728
"""
28-
if "fastapi" not in sys.modules:
29+
if not is_module_installed("fastapi"):
2930
raise Exception(
3031
"""FastAPI Integration is not installed.
3132
Please install eolic[fastapi] (using fastapi extras) to use this integration."""

‎eolic/meta/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Module containing metaclasses."""

‎eolic/model.py

+13
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,19 @@ class EventRemoteURLTarget(EventRemoteTarget):
6161
headers: Dict[str, Any] = {}
6262

6363

64+
class EventRemoteCeleryTarget(EventRemoteTarget):
65+
"""
66+
Model for Celery event remote targets.
67+
68+
Attributes:
69+
queue_name: String of queue used in celery worker, default: eolic.
70+
function_name: String of function name used in celery worker, default: events.
71+
"""
72+
73+
queue_name: str = "eolic"
74+
function_name: str = "events"
75+
76+
6477
class EventListener(BaseModel):
6578
"""
6679
Model for event listeners.

‎eolic/remote.py

+100-8
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@
77

88
from __future__ import annotations
99

10-
from enum import Enum
1110
import logging
1211
from abc import ABC, abstractmethod
13-
from typing import Any, Dict, List
12+
from enum import Enum
13+
from typing import Any, Dict, List, Optional, Union
1414
import requests
1515

16-
from eolic.task_manager import TaskManager
16+
from .task_manager import TaskManager
1717

1818
from .helpers.coroutines import run_coroutine
1919

@@ -22,7 +22,9 @@
2222
EventRemoteTarget,
2323
EventRemoteTargetType,
2424
EventRemoteURLTarget,
25+
EventRemoteCeleryTarget,
2526
)
27+
from .helpers.modules import is_module_installed, get_module
2628

2729

2830
class EventRemoteTargetHandler(TaskManager):
@@ -41,7 +43,8 @@ def __init__(self) -> None:
4143
"""Initialize the EventRemoteTargetHandler."""
4244
super().__init__()
4345

44-
def _parse_target(self, target: Any) -> EventRemoteTarget:
46+
@staticmethod
47+
def _parse_target(target: Union[str, Dict[str, Any]]) -> EventRemoteTarget:
4548
"""
4649
Parse and convert a target to an EventRemoteTarget instance.
4750
@@ -52,18 +55,60 @@ def _parse_target(self, target: Any) -> EventRemoteTarget:
5255
EventRemoteTarget: Parsed remote target.
5356
"""
5457
if isinstance(target, str):
55-
return EventRemoteURLTarget(
56-
type=EventRemoteTargetType("url"), address=target
58+
target = {"type": "url", "address": target}
59+
60+
if not isinstance(target, dict):
61+
raise TypeError(
62+
"Target needs to be of type str or Dict[str, str] but received {}".format(
63+
type(target)
64+
)
5765
)
5866

59-
if isinstance(target, dict):
67+
target_type = target.get("type")
68+
69+
if not isinstance(target_type, str):
70+
raise TypeError(
71+
"Target type needs to be of type str but received {}".format(
72+
type(target_type)
73+
)
74+
)
75+
76+
event_remote_target_type = EventRemoteTargetType[target_type]
77+
78+
if event_remote_target_type == EventRemoteTargetType.url:
6079
return EventRemoteURLTarget(
61-
type=EventRemoteTargetType("url"),
80+
type=event_remote_target_type,
6281
address=target["address"],
6382
headers=target.get("headers", {}),
6483
events=target.get("events"),
6584
)
6685

86+
elif event_remote_target_type == EventRemoteTargetType.celery:
87+
address = str(target["address"])
88+
events: Optional[List[Any]] = target.get("events")
89+
90+
queue_name = (
91+
str(target.get("queue_name")) if target.get("queue_name") else None
92+
)
93+
function_name = (
94+
str(target.get("function_name"))
95+
if target.get("function_name")
96+
else None
97+
)
98+
99+
optional_kwargs = {}
100+
101+
if queue_name:
102+
optional_kwargs["queue_name"] = queue_name
103+
if function_name:
104+
optional_kwargs["function_name"] = function_name
105+
106+
return EventRemoteCeleryTarget(
107+
type=event_remote_target_type,
108+
address=address,
109+
events=events,
110+
**optional_kwargs,
111+
)
67112
return EventRemoteTarget(**target)
68113

69114
def register(self, target: Any) -> None:
@@ -127,6 +172,9 @@ def create(self, target: EventRemoteTarget) -> EventRemoteDispatcher:
127172
if isinstance(target, EventRemoteURLTarget):
128173
return EventRemoteURLDispatcher(target)
129174

175+
if isinstance(target, EventRemoteCeleryTarget):
176+
return EventRemoteCeleryDispatcher(target)
177+
130178
raise NotImplementedError(
131179
f"EventRemoteDispatcher for {target.type} not implemented"
132180
)
@@ -199,3 +247,47 @@ async def dispatch(self, event: Any, *args, **kwargs) -> None:
199247
timeout=10,
200248
)
201249
logging.debug(f"Response from {self.target.address}: {response.status_code}")
250+
251+
252+
class EventRemoteCeleryDispatcher(EventRemoteDispatcher):
253+
"""Dispatcher for Celery remote targets."""
254+
255+
def __init__(self, target: EventRemoteCeleryTarget) -> None:
256+
"""
257+
Initialize the Celery dispatcher with a target.
258+
259+
Args:
260+
target (EventRemoteCeleryTarget): The Celery remote target.
261+
"""
262+
self.target = target
263+
264+
if not is_module_installed("celery"):
265+
raise Exception(
266+
"Celery Integration is not installed. "
267+
"Please install eolic[celery] (using celery extras) to use this integration."
268+
)
269+
270+
self.celery = get_module("celery").Celery(self.target.address)
271+
272+
async def dispatch(self, event: Any, *args, **kwargs) -> None:
273+
"""
274+
Dispatch the event to the URL remote target.
275+
276+
Args:
277+
event (Any): The event to dispatch.
278+
*args: Variable length argument list for the event.
279+
**kwargs: Arbitrary keyword arguments for the event.
280+
"""
281+
event_value = str(event)
282+
283+
if isinstance(event, Enum):
284+
event_value = event.value
285+
286+
task = self.celery.send_task(
287+
self.target.function_name,
288+
args=[event_value, *args],
289+
kwargs=kwargs,
290+
queue=self.target.queue_name,
291+
)
292+
293+
logging.debug(f"Celery task id {task}")

‎eolic/task_manager.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import atexit
55
import signal
66
from typing import Any, Callable
7-
from eolic.helpers.coroutines import run_coroutine
7+
from .helpers.coroutines import run_coroutine
88

99

1010
class TaskManager:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
"""Notification service example with celery."""
2+
3+
from celery import Celery
4+
5+
from eolic import Eolic
6+
from eolic.integrations.celery import CeleryIntegration
7+
8+
app = Celery(
9+
"tasks",
10+
backend=False,
11+
)
12+
13+
eolic = Eolic()
14+
15+
16+
# Create an instance of CeleryIntegration with a custom event function
17+
celery_integration = CeleryIntegration(app)
18+
# Set up the integration
19+
eolic.setup_integration(celery_integration)
20+
21+
22+
@eolic.on("user_created")
23+
def handle_user_created(id_: int, name: str, email: str):
24+
"""Handle the user created event."""
25+
# Handle the user created event (e.g., send a notification)
26+
print(f"Notification: [{id_}] User {name} with email {email} was created!")
27+
# TODO: Send-email for email confirmation
28+
return {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
"""User service example with celery."""
2+
3+
from typing import List
4+
5+
from pydantic import BaseModel
6+
7+
from eolic import Eolic
8+
9+
eolic = Eolic(remote_targets=[{"type": "celery", "address": "amqp://localhost:5672"}])
10+
11+
12+
class User(BaseModel):
13+
"""User model."""
14+
15+
id: int
16+
name: str
17+
email: str
18+
19+
20+
users: List[User] = []
21+
22+
23+
def create_user(user: User):
24+
"""Endpoint to create a user."""
25+
if any(u.id == user.id for u in users):
26+
raise NotImplementedError("User already exists")
27+
28+
users.append(user)
29+
eolic.emit("user_created", user.id, user.name, user.email)
30+
31+
return user
32+
33+
34+
create_user(User(id=1, name="Name", email="email@example.com"))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# Eolic Celery Integration Example
2+
3+
This example demonstrates how to integrate Eolic Celery to handle events, such as sending notifications between a User Service and a Notification Service.
4+
5+
## Overview
6+
7+
- **User Service**: Create a new user and emit an event.
8+
- **Notification Service**: Listens for user creation events and handles them by sending a notification.
9+
10+
## Prerequisites
11+
12+
- Python 3.8+
13+
- Celery
14+
- Eolic with Celery extras: eolic[**celery**]
15+
16+
## Installation
17+
18+
1. **Clone the repository**:
19+
```bash
20+
git clone git@github.com:thiarthur/eolic.git
21+
```
22+
23+
2. **Create a virtual env (optional)**:
24+
```bash
25+
python -m venv venv
26+
source venv/bin/activate # On Windows use `venv\Scripts\activate`
27+
```
28+
29+
3. **Install the dependencies**:
30+
```bash
31+
pip install requirements.txt
32+
```
33+
34+
## Running
35+
36+
**Note:** To run Celery, you will need a queue service or any other backend to run it. In this example, we are using [RabbitMQ](https://www.rabbitmq.com/tutorials). See more at the [Celery documentation](https://docs.celeryq.dev/en/stable/getting-started/introduction.html).
37+
38+
39+
1. **Start the Notification Service**:
40+
```bash
41+
celery -A 'examples.integrations.celery_plugin.celery_notification_service' worker --loglevel=INFO -Q eolic
42+
```
43+
44+
2. **Create a New User**:
45+
46+
```bash
47+
python examples/integrations/celery_plugin/celery_user_service.py
48+
```
49+
50+
3. **Check Notifications**:
51+
Check the output of the Notification Service to see the notification for the new user creation.
52+
53+
54+
55+
## Conclusion
56+
57+
This example illustrates the process of setting up and using Celery integration with Eolic to efficiently handle events across microservices, ensuring smooth and reliable communication between services.

‎examples/integrations/fastapi_plugin/readme.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,13 @@ This example demonstrates how to use Eolic to integrate event handling between t
3838
1. **Start the User Service**:
3939

4040
```bash
41-
python user_service.py
41+
python fastapi_user_service.py
4242
Start the Notification Service:
4343
```
4444

4545
2. **Start the Notification Service**:
4646
```bash
47-
python notification_service.py
47+
python fastapi_notification_service.py
4848
```
4949

5050
3. **Create a New User**:

‎pyproject.toml

+5-2
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,12 @@ pydantic = "^2.8.2"
1616
requests = "^2.32.3"
1717
fastapi = { version="*", optional= true }
1818
uvicorn = { version="*", optional= true }
19+
celery = { version="*", optional= true }
1920

2021
[tool.poetry.extras]
2122
fastapi = ["fastapi", "uvicorn"]
22-
all = ["fastapi", "uvicorn"]
23+
celery = ["celery"]
24+
all = ["fastapi", "uvicorn", "celery"]
2325

2426
[tool.poetry.group.dev.dependencies]
2527
black = "^24.4.2"
@@ -32,7 +34,6 @@ flake8-pyproject = "^1.2.3"
3234
commitizen = "^3.28.0"
3335
httpx = "^0.27.0"
3436

35-
3637
[tool.poetry.group.test.dependencies]
3738
pytest = "^8.2.2"
3839
pytest-mock = "^3.14.0"
@@ -54,6 +55,8 @@ build-backend = "poetry.core.masonry.api"
5455

5556
[tool.mypy]
5657
exclude = ["tests/"]
58+
no_namespace_packages = true
59+
ignore_missing_imports = true
5760

5861
[tool.flake8]
5962
max-line-length = 105

‎requirements.txt

+43
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,52 @@
1+
amqp==5.2.0 ; python_version >= "3.9" and python_version < "4.0"
12
annotated-types==0.7.0 ; python_version >= "3.9" and python_version < "4.0"
3+
anyio==4.4.0 ; python_version >= "3.9" and python_version < "4.0"
4+
billiard==4.2.0 ; python_version >= "3.9" and python_version < "4.0"
5+
celery==5.4.0 ; python_version >= "3.9" and python_version < "4.0"
26
certifi==2024.7.4 ; python_version >= "3.9" and python_version < "4.0"
37
charset-normalizer==3.3.2 ; python_version >= "3.9" and python_version < "4.0"
8+
click-didyoumean==0.3.1 ; python_version >= "3.9" and python_version < "4.0"
9+
click-plugins==1.1.1 ; python_version >= "3.9" and python_version < "4.0"
10+
click-repl==0.3.0 ; python_version >= "3.9" and python_version < "4.0"
11+
click==8.1.7 ; python_version >= "3.9" and python_version < "4.0"
12+
colorama==0.4.6 ; python_version >= "3.9" and python_version < "4.0" and (sys_platform == "win32" or platform_system == "Windows")
13+
dnspython==2.6.1 ; python_version >= "3.9" and python_version < "4.0"
14+
email-validator==2.2.0 ; python_version >= "3.9" and python_version < "4.0"
15+
exceptiongroup==1.2.2 ; python_version >= "3.9" and python_version < "3.11"
16+
fastapi-cli==0.0.4 ; python_version >= "3.9" and python_version < "4.0"
17+
fastapi==0.111.1 ; python_version >= "3.9" and python_version < "4.0"
18+
h11==0.14.0 ; python_version >= "3.9" and python_version < "4.0"
19+
httpcore==1.0.5 ; python_version >= "3.9" and python_version < "4.0"
20+
httptools==0.6.1 ; python_version >= "3.9" and python_version < "4.0"
21+
httpx==0.27.0 ; python_version >= "3.9" and python_version < "4.0"
422
idna==3.7 ; python_version >= "3.9" and python_version < "4.0"
23+
jinja2==3.1.4 ; python_version >= "3.9" and python_version < "4.0"
24+
kombu==5.3.7 ; python_version >= "3.9" and python_version < "4.0"
25+
markdown-it-py==3.0.0 ; python_version >= "3.9" and python_version < "4.0"
26+
markupsafe==2.1.5 ; python_version >= "3.9" and python_version < "4.0"
27+
mdurl==0.1.2 ; python_version >= "3.9" and python_version < "4.0"
28+
prompt-toolkit==3.0.36 ; python_version >= "3.9" and python_version < "4.0"
529
pydantic-core==2.20.1 ; python_version >= "3.9" and python_version < "4.0"
630
pydantic==2.8.2 ; python_version >= "3.9" and python_version < "4.0"
31+
pygments==2.18.0 ; python_version >= "3.9" and python_version < "4.0"
32+
python-dateutil==2.9.0.post0 ; python_version >= "3.9" and python_version < "4.0"
33+
python-dotenv==1.0.1 ; python_version >= "3.9" and python_version < "4.0"
34+
python-multipart==0.0.9 ; python_version >= "3.9" and python_version < "4.0"
35+
pyyaml==6.0.1 ; python_version >= "3.9" and python_version < "4.0"
736
requests==2.32.3 ; python_version >= "3.9" and python_version < "4.0"
37+
rich==13.7.1 ; python_version >= "3.9" and python_version < "4.0"
38+
shellingham==1.5.4 ; python_version >= "3.9" and python_version < "4.0"
39+
six==1.16.0 ; python_version >= "3.9" and python_version < "4.0"
40+
sniffio==1.3.1 ; python_version >= "3.9" and python_version < "4.0"
41+
starlette==0.37.2 ; python_version >= "3.9" and python_version < "4.0"
42+
typer==0.12.3 ; python_version >= "3.9" and python_version < "4.0"
843
typing-extensions==4.12.2 ; python_version >= "3.9" and python_version < "4.0"
44+
tzdata==2024.1 ; python_version >= "3.9" and python_version < "4.0"
945
urllib3==2.2.2 ; python_version >= "3.9" and python_version < "4.0"
46+
uvicorn==0.30.3 ; python_version >= "3.9" and python_version < "4.0"
47+
uvicorn[standard]==0.30.3 ; python_version >= "3.9" and python_version < "4.0"
48+
uvloop==0.19.0 ; (sys_platform != "win32" and sys_platform != "cygwin") and platform_python_implementation != "PyPy" and python_version >= "3.9" and python_version < "4.0"
49+
vine==5.1.0 ; python_version >= "3.9" and python_version < "4.0"
50+
watchfiles==0.22.0 ; python_version >= "3.9" and python_version < "4.0"
51+
wcwidth==0.2.13 ; python_version >= "3.9" and python_version < "4.0"
52+
websockets==12.0 ; python_version >= "3.9" and python_version < "4.0"

‎tests/integrations/test_celery.py

+252
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
import importlib.util
2+
from enum import Enum
3+
4+
import pytest
5+
from celery import Celery
6+
7+
from eolic.base import Eolic
8+
from eolic.helpers.modules import get_module
9+
from eolic.integrations.celery import CeleryIntegration
10+
from eolic.model import EventDTO, EventRemoteCeleryTarget
11+
from eolic.remote import (
12+
EventRemoteTargetHandler,
13+
EventRemoteCeleryDispatcher,
14+
EventRemoteDispatcherFactory,
15+
)
16+
17+
18+
@pytest.fixture
19+
def app():
20+
"""
21+
Fixture for creating a Celery app instance.
22+
23+
Returns:
24+
Celery: An instance of Celery.
25+
"""
26+
return Celery()
27+
28+
29+
@pytest.fixture
30+
def eolic():
31+
"""
32+
Fixture for creating an Eolic instance.
33+
34+
Returns:
35+
Eolic: An instance of Eolic.
36+
"""
37+
return Eolic()
38+
39+
40+
@pytest.fixture
41+
def target_handler() -> EventRemoteTargetHandler:
42+
"""
43+
Fixture to provide an instance of EventRemoteTargetHandler.
44+
45+
Returns:
46+
EventRemoteTargetHandler: An instance of EventRemoteTargetHandler.
47+
"""
48+
handler = EventRemoteTargetHandler()
49+
return handler
50+
51+
52+
@pytest.fixture
53+
def event_remote_celery_target() -> EventRemoteCeleryTarget:
54+
"""
55+
Fixture to provide an instance of EventRemoteCeleryTarget.
56+
57+
Returns:
58+
EventRemoteCeleryTarget: An instance of EventRemoteCeleryTarget.
59+
"""
60+
return EventRemoteCeleryTarget(type="celery", address="amqp://localhost:5672")
61+
62+
63+
@pytest.fixture
64+
def target_celery_handler(
65+
event_remote_celery_target: EventRemoteCeleryTarget,
66+
) -> EventRemoteCeleryDispatcher:
67+
"""
68+
Fixture to provide an instance of EventRemoteTargetHandler.
69+
70+
Args:
71+
event_remote_celery_target (EventRemoteCeleryTarget): The EventRemoteCeleryTarget instance.
72+
73+
Returns:
74+
EventRemoteTargetHandler: An instance of EventRemoteTargetHandler.
75+
"""
76+
event_remote_celery_dispatcher = EventRemoteCeleryDispatcher(
77+
event_remote_celery_target
78+
)
79+
80+
return event_remote_celery_dispatcher
81+
82+
83+
@pytest.fixture
84+
def event_dto():
85+
"""
86+
Fixture for creating an EventDTO instance.
87+
88+
Returns:
89+
EventDTO: An instance of EventDTO with predefined attributes.
90+
"""
91+
return EventDTO(event="test_event", args=("arg1",), kwargs={"key": "value"})
92+
93+
94+
def test_celery_integration_init(eolic: Eolic, app: Celery):
95+
"""
96+
Test initialization of CeleryIntegration.
97+
98+
Args:
99+
eolic (Eolic): The Eolic instance.
100+
app (Celery): The Celery app instance.
101+
"""
102+
integration = CeleryIntegration(app=app)
103+
eolic.setup_integration(integration)
104+
105+
assert integration.eolic is not None
106+
assert integration.app == app
107+
assert integration.event_function == "events"
108+
assert integration.queue_name == "eolic"
109+
110+
111+
def test_celery_integration_no_app():
112+
"""
113+
Test that an exception is raised when CeleryIntegration is initialized with no app.
114+
"""
115+
with pytest.raises(
116+
Exception, match="Please declare you app to setup the integration."
117+
):
118+
CeleryIntegration(app=None)
119+
120+
121+
def test_celery_not_installed(
122+
monkeypatch: pytest.MonkeyPatch,
123+
app: Celery,
124+
target_celery_handler: EventRemoteCeleryDispatcher,
125+
event_remote_celery_target: EventRemoteCeleryTarget,
126+
):
127+
"""
128+
Test that an exception is raised when Celery is not installed.
129+
130+
Args:
131+
monkeypatch: The pytest monkeypatch fixture.
132+
app (Celery): The Celery app instance.
133+
target_celery_handler (EventRemoteCeleryDispatcher): The EventRemoteCeleryDispatcher instance.
134+
event_remote_celery_target (EventRemoteCeleryTarget): The EventRemoteCeleryTarget instance.
135+
"""
136+
monkeypatch.setattr(importlib.util, "find_spec", lambda _: None)
137+
138+
with pytest.raises(Exception, match="Celery Integration is not installed.*"):
139+
CeleryIntegration(app=app)
140+
141+
with pytest.raises(Exception, match="Celery Integration is not installed.*"):
142+
EventRemoteCeleryDispatcher(event_remote_celery_target)
143+
144+
145+
def test_celery_empty_event_function(eolic: Eolic, app: Celery):
146+
"""
147+
Test that an exception is raised when event_route is None.
148+
149+
Args:
150+
eolic (Eolic): The Eolic instance.
151+
app (Celery): The Celery app instance.
152+
"""
153+
integration = CeleryIntegration(app, event_function=None)
154+
with pytest.raises(
155+
Exception, match="Event function is required for Celery integration."
156+
):
157+
eolic.setup_integration(integration)
158+
159+
160+
def test_celery_listener_event(
161+
monkeypatch: pytest.MonkeyPatch, eolic: Eolic, app: Celery
162+
):
163+
"""
164+
Test that an exception is raised when event_route is None.
165+
166+
Args:
167+
monkeypatch: The pytest monkeypatch fixture.
168+
eolic (Eolic): The Eolic instance.
169+
app (Celery): The Celery app instance.
170+
"""
171+
integration = CeleryIntegration(app, event_function="event")
172+
integration.setup(eolic)
173+
174+
def mock_lambda_handler(event, *args, **kwargs):
175+
return event, args, kwargs
176+
177+
monkeypatch.setattr(integration, "forward_event", mock_lambda_handler)
178+
179+
integration.app.tasks["event"]("test_event", "arg1", key="value")
180+
181+
182+
def test_celery_module(eolic: Eolic, app: Celery):
183+
"""
184+
Test that an exception is raised when event_route is None.
185+
186+
Args:
187+
eolic (Eolic): The Eolic instance.
188+
app (Celery): The Celery app instance.
189+
"""
190+
assert isinstance(get_module("celery").Celery(), Celery)
191+
192+
193+
def test_parse_celery_target(target_handler: EventRemoteTargetHandler) -> None:
194+
"""
195+
Test parsing a dictionary target.
196+
197+
Args:
198+
target_handler (EventRemoteTargetHandler): An instance of EventRemoteTargetHandler.
199+
"""
200+
target = {
201+
"type": "celery",
202+
"address": "amqp://localhost:5672",
203+
"queue_name": "q",
204+
"function_name": "f",
205+
}
206+
207+
parsed_target = target_handler._parse_target(target)
208+
209+
assert isinstance(parsed_target, EventRemoteCeleryTarget)
210+
assert parsed_target.address == "amqp://localhost:5672"
211+
assert parsed_target.function_name == "f"
212+
assert parsed_target.queue_name == "q"
213+
214+
215+
@pytest.mark.asyncio
216+
async def test_target_celery_handler(
217+
monkeypatch: pytest.MonkeyPatch,
218+
target_celery_handler: EventRemoteCeleryDispatcher,
219+
) -> None:
220+
"""
221+
Test parsing a dictionary target.
222+
223+
Args:
224+
monkeypatch: The pytest monkeypatch fixture.
225+
target_celery_handler (EventRemoteCeleryDispatcher): An instance of EventRemoteCeleryDispatcher.
226+
"""
227+
228+
class Events(str, Enum):
229+
230+
event = "event"
231+
232+
def send_task_mock(function_name: str, *args, **kwargs):
233+
print("{} - {} - {}".format(function_name, args, kwargs))
234+
235+
monkeypatch.setattr(target_celery_handler.celery, "send_task", send_task_mock)
236+
237+
await target_celery_handler.dispatch(Events.event, "1", "2", "3", k={"4": "4"})
238+
239+
240+
async def test_event_remote_dispatcher_factory(
241+
target_celery_handler: EventRemoteCeleryDispatcher,
242+
event_remote_celery_target: EventRemoteCeleryTarget,
243+
) -> None:
244+
"""
245+
Test parsing a dictionary target.
246+
247+
Args:
248+
target_celery_handler (EventRemoteCeleryDispatcher): An instance of EventRemoteCeleryDispatcher.
249+
event_remote_celery_target (EventRemoteCeleryTarget): The EventRemoteCeleryTarget instance.
250+
"""
251+
252+
EventRemoteDispatcherFactory().create(event_remote_celery_target)

‎tests/integrations/test_fastapi.py

+7-4
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1-
import sys
2-
from fastapi.testclient import TestClient
1+
import importlib.util
2+
33
import pytest
4+
from fastapi import FastAPI
5+
from fastapi.testclient import TestClient
6+
47
from eolic.base import Eolic
58
from eolic.integrations.fastapi import FastAPIIntegration
6-
from fastapi import FastAPI
79
from eolic.model import EventDTO
810

911

@@ -96,7 +98,8 @@ def test_fastapi_not_installed(monkeypatch: pytest.MonkeyPatch, app: FastAPI):
9698
monkeypatch: The pytest monkeypatch fixture.
9799
app (FastAPI): The FastAPI app instance.
98100
"""
99-
monkeypatch.delitem(sys.modules, "fastapi", raising=False)
101+
monkeypatch.setattr(importlib.util, "find_spec", lambda _: None)
102+
100103
with pytest.raises(Exception, match="FastAPI Integration is not installed..*"):
101104
FastAPIIntegration(app=app)
102105

‎tests/test_remote.py

+24-1
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@
55
to remote targets using the EventRemoteTargetHandler class.
66
"""
77

8+
from unittest.mock import patch
9+
810
import pytest
11+
912
from eolic.remote import EventRemoteTargetHandler, EventRemoteURLTarget
10-
from unittest.mock import patch
1113
from tests.common import GameEvents
1214

1315

@@ -79,6 +81,27 @@ def test_parse_invalid_target(target_handler: EventRemoteTargetHandler) -> None:
7981
target_handler._parse_target(123) # Invalid type
8082

8183

84+
def test_parse_target_type_error(target_handler: EventRemoteTargetHandler) -> None:
85+
"""
86+
Test handling of invalid target formats.
87+
88+
Args:
89+
target_handler (EventRemoteTargetHandler): An instance of EventRemoteTargetHandler.
90+
"""
91+
with pytest.raises(TypeError):
92+
target_handler._parse_target({"type": 123}) # Invalid type
93+
94+
95+
def test_event_remote_type_not_exists(target_handler: EventRemoteTargetHandler) -> None:
96+
"""
97+
Test handling of invalid target formats.
98+
99+
Args:
100+
target_handler (EventRemoteTargetHandler): An instance of EventRemoteTargetHandler.
101+
"""
102+
target_handler._parse_target({"type": "rabbitmq", "address": ""})
103+
104+
82105
# Registering Targets
83106

84107

0 commit comments

Comments
 (0)
Please sign in to comment.