Skeleton ready
This commit is contained in:
0
kernel/__init__.py
Normal file
0
kernel/__init__.py
Normal file
0
kernel/application/__init__.py
Normal file
0
kernel/application/__init__.py
Normal file
39
kernel/application/application.py
Normal file
39
kernel/application/application.py
Normal file
@@ -0,0 +1,39 @@
|
||||
from typing import List, Callable, Concatenate, Awaitable
|
||||
from abc import ABC, abstractmethod
|
||||
from punq import Container
|
||||
|
||||
from kernel.feature.provider import Provider
|
||||
from kernel.application.kernel import Kernel
|
||||
|
||||
class Application(ABC):
|
||||
__kernel: Kernel
|
||||
__features: List[Provider]
|
||||
|
||||
@abstractmethod
|
||||
async def do(
|
||||
self,
|
||||
callback: Callable[Concatenate[Container, ...], Awaitable[None]]
|
||||
) -> None:
|
||||
pass
|
||||
|
||||
def __init__(self, kernel: Kernel, features: List[type[Provider]]) -> None:
|
||||
self.__kernel = kernel
|
||||
self.__features = []
|
||||
|
||||
for feature in features:
|
||||
feature_instance = feature(kernel.get_container())
|
||||
feature_instance.register()
|
||||
|
||||
self.__features.append(feature_instance)
|
||||
|
||||
async def launch(
|
||||
self,
|
||||
callback: Callable[Concatenate[Container, ...], Awaitable[None]]
|
||||
) -> None:
|
||||
for feature in self.__features:
|
||||
feature.bootstrap()
|
||||
|
||||
await self.do(callback)
|
||||
|
||||
def get_container(self) -> Container:
|
||||
return self.__kernel.get_container()
|
||||
0
kernel/application/enum/__init__.py
Normal file
0
kernel/application/enum/__init__.py
Normal file
4
kernel/application/enum/date_time.py
Normal file
4
kernel/application/enum/date_time.py
Normal file
@@ -0,0 +1,4 @@
|
||||
from enum import Enum
|
||||
|
||||
class DateTime(str, Enum):
|
||||
Ymd = "%Y-%m-%d"
|
||||
4
kernel/application/enum/encoding.py
Normal file
4
kernel/application/enum/encoding.py
Normal file
@@ -0,0 +1,4 @@
|
||||
from enum import Enum
|
||||
|
||||
class Encoding(str, Enum):
|
||||
utf8 = "utf-8"
|
||||
5
kernel/application/kernel.py
Normal file
5
kernel/application/kernel.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from punq import Container
|
||||
|
||||
class Kernel:
|
||||
def get_container(self) -> Container:
|
||||
raise NotImplementedError()
|
||||
0
kernel/application/manager/__init__.py
Normal file
0
kernel/application/manager/__init__.py
Normal file
13
kernel/application/manager/stop_event.py
Normal file
13
kernel/application/manager/stop_event.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from asyncio import Event
|
||||
from signal import signal, SIGINT, SIGTERM
|
||||
|
||||
class StopEvent:
|
||||
__event: Event
|
||||
|
||||
def __init__(self, event: Event) -> None:
|
||||
self.__event = event
|
||||
|
||||
def setup_stop_signals(self) -> None:
|
||||
for signum in (SIGINT, SIGTERM):
|
||||
signal(signum, lambda sig, frame: self.__event.set())
|
||||
|
||||
4
kernel/application/stop_event.py
Normal file
4
kernel/application/stop_event.py
Normal file
@@ -0,0 +1,4 @@
|
||||
from asyncio import Event
|
||||
|
||||
class StopEvent(Event):
|
||||
pass
|
||||
0
kernel/application/view/__init__.py
Normal file
0
kernel/application/view/__init__.py
Normal file
7
kernel/application/view/application.py
Normal file
7
kernel/application/view/application.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Application:
|
||||
name: str
|
||||
env: str
|
||||
28
kernel/application/view/configuration.py
Normal file
28
kernel/application/view/configuration.py
Normal file
@@ -0,0 +1,28 @@
|
||||
from kernel.application.view.application import Application
|
||||
from kernel.application.view.rabbitmq import RabbitMQ
|
||||
from kernel.application.view.redis import Redis
|
||||
|
||||
|
||||
class Configuration():
|
||||
__application: Application
|
||||
__rabbitmq: RabbitMQ
|
||||
__redis: Redis
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
application: Application,
|
||||
rabbitmq: RabbitMQ,
|
||||
redis: Redis,
|
||||
) -> None:
|
||||
self.__application = application
|
||||
self.__rabbitmq = rabbitmq
|
||||
self.__redis = redis
|
||||
|
||||
def application(self) -> Application:
|
||||
return self.__application
|
||||
|
||||
def rabbitmq(self) -> RabbitMQ:
|
||||
return self.__rabbitmq
|
||||
|
||||
def redis(self) -> Redis:
|
||||
return self.__redis
|
||||
11
kernel/application/view/rabbitmq.py
Normal file
11
kernel/application/view/rabbitmq.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RabbitMQ:
|
||||
host: str = "localhost"
|
||||
port: int = 5672
|
||||
user: str = "guest"
|
||||
password: str = "guest"
|
||||
timeout: int = 10
|
||||
vhost: str = "/"
|
||||
8
kernel/application/view/redis.py
Normal file
8
kernel/application/view/redis.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Redis:
|
||||
host: str = "localhost"
|
||||
port: int = 6379
|
||||
db: int = 0
|
||||
0
kernel/event/__init__.py
Normal file
0
kernel/event/__init__.py
Normal file
16
kernel/event/listener.py
Normal file
16
kernel/event/listener.py
Normal file
@@ -0,0 +1,16 @@
|
||||
from typing import Generic, TypeVar
|
||||
from abc import ABC, abstractmethod
|
||||
from punq import Container
|
||||
|
||||
T = TypeVar('T')
|
||||
|
||||
|
||||
class Listener(ABC, Generic[T]):
|
||||
_container: Container
|
||||
|
||||
def __init__(self, container: Container) -> None:
|
||||
self._container = container
|
||||
|
||||
@abstractmethod
|
||||
async def handle(self, event: str, payload: T) -> None:
|
||||
pass
|
||||
25
kernel/event/registrar.py
Normal file
25
kernel/event/registrar.py
Normal file
@@ -0,0 +1,25 @@
|
||||
from punq import Container
|
||||
from typing import TypeVar
|
||||
from pyee import EventEmitter
|
||||
|
||||
from kernel.event.listener import Listener
|
||||
|
||||
T = TypeVar('T')
|
||||
|
||||
|
||||
class Registrar:
|
||||
_container: Container
|
||||
|
||||
def __init__(self, container: Container) -> None:
|
||||
self._container = container
|
||||
|
||||
def add_event_listener(
|
||||
self,
|
||||
event: str,
|
||||
listener: type[Listener[T]]
|
||||
) -> None:
|
||||
async def callback(payload: T) -> None:
|
||||
listener_instance = listener(self._container)
|
||||
await listener_instance.handle(event, payload)
|
||||
|
||||
self._container.resolve(EventEmitter).on(event, callback)
|
||||
0
kernel/feature/__init__.py
Normal file
0
kernel/feature/__init__.py
Normal file
17
kernel/feature/provider.py
Normal file
17
kernel/feature/provider.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from punq import Container
|
||||
|
||||
|
||||
class Provider(ABC):
|
||||
_container: Container
|
||||
|
||||
def __init__(self, container: Container) -> None:
|
||||
self._container = container
|
||||
|
||||
@abstractmethod
|
||||
def register(self) -> None:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def bootstrap(self) -> None:
|
||||
pass
|
||||
0
kernel/rabbitmq/__init__.py
Normal file
0
kernel/rabbitmq/__init__.py
Normal file
144
kernel/rabbitmq/client.py
Normal file
144
kernel/rabbitmq/client.py
Normal file
@@ -0,0 +1,144 @@
|
||||
from asyncio import create_task, Event, sleep
|
||||
from time import time
|
||||
from typing import Any, Callable, Union
|
||||
from logging import Logger, INFO
|
||||
|
||||
from aio_pika import connect_robust, Message, DeliveryMode
|
||||
|
||||
from aio_pika.abc import AbstractIncomingMessage
|
||||
from aio_pika.abc import AbstractChannel
|
||||
from aio_pika.abc import AbstractConnection
|
||||
from aio_pika.abc import AbstractExchange
|
||||
from aio_pika.abc import AbstractQueue
|
||||
from aio_pika.exceptions import AMQPConnectionError
|
||||
|
||||
from kernel.rabbitmq.enum.content_type import ContentType
|
||||
from kernel.rabbitmq.view.authentication_params import AuthenticationParams
|
||||
from kernel.rabbitmq.view.connection_params import ConnectionParams
|
||||
from kernel.rabbitmq.exception.connection_timeout import ConnectionTimeout
|
||||
from kernel.rabbitmq.exception.connection_not_established import ConnectionNotEstablished
|
||||
|
||||
|
||||
class Client:
|
||||
__authentication_params: AuthenticationParams
|
||||
__connection_params: ConnectionParams
|
||||
__logger: Union[None, Logger]
|
||||
__connection: Union[None, AbstractConnection] = None
|
||||
__stop_event: Union[None, Event] = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
authentication_params: AuthenticationParams,
|
||||
connection_params: ConnectionParams,
|
||||
logger: Union[None, Logger] = None
|
||||
) -> None:
|
||||
self.__authentication_params = authentication_params
|
||||
self.__connection_params = connection_params
|
||||
self.__logger = logger
|
||||
|
||||
async def connect(self, stop_event: Event, timeout: int = 15) -> None:
|
||||
self.__stop_event = stop_event
|
||||
|
||||
ran_at: float = time()
|
||||
|
||||
self.__log(f"Waiting for RabbitMQ connection..")
|
||||
|
||||
while True:
|
||||
host = self.__connection_params.host
|
||||
port = self.__connection_params.port
|
||||
vhost = self.__connection_params.vhost
|
||||
|
||||
try:
|
||||
self.__connection = await connect_robust(
|
||||
host=host,
|
||||
port=port,
|
||||
virtualhost=vhost,
|
||||
login=self.__authentication_params.username,
|
||||
password=self.__authentication_params.password,
|
||||
)
|
||||
|
||||
self.__log(
|
||||
f"Successfully connected to {host}:{port}{vhost}"
|
||||
)
|
||||
|
||||
break
|
||||
except AMQPConnectionError:
|
||||
seconds_wasted: float = time() - ran_at
|
||||
|
||||
self.__log("Connection failed! Retry after 3s..")
|
||||
|
||||
if seconds_wasted > timeout:
|
||||
raise ConnectionTimeout(
|
||||
f"Connection timeout to {host}:{port}{vhost}"
|
||||
)
|
||||
|
||||
await sleep(3)
|
||||
|
||||
create_task(self.__wait_for_stop())
|
||||
|
||||
async def consume(
|
||||
self,
|
||||
queue: str,
|
||||
exchange: str,
|
||||
consumer: Callable[[AbstractIncomingMessage], Any],
|
||||
routing_key: Union[None, str] = None,
|
||||
) -> None:
|
||||
self.__raise_if_disconnected()
|
||||
assert self.__connection is not None
|
||||
|
||||
abstract_channel: AbstractChannel = await self.__connection.channel()
|
||||
abstract_exchange: AbstractExchange = await abstract_channel\
|
||||
.get_exchange(exchange)
|
||||
abstract_queue: AbstractQueue = await abstract_channel\
|
||||
.declare_queue(queue, durable=True)
|
||||
|
||||
await abstract_queue.bind(abstract_exchange, routing_key)
|
||||
|
||||
self.__log(
|
||||
f"Listening queue {abstract_queue.name} binded to {abstract_exchange.name}..."
|
||||
)
|
||||
|
||||
await abstract_queue.consume(consumer)
|
||||
|
||||
async def publish(
|
||||
self,
|
||||
exchange: str,
|
||||
message: str,
|
||||
routing_key: str = "",
|
||||
content_type: str = ContentType.APPLICATION_JSON
|
||||
) -> None:
|
||||
self.__raise_if_disconnected()
|
||||
assert self.__connection is not None
|
||||
|
||||
abstract_exchange: AbstractExchange = await self.__connection.channel()\
|
||||
.get_exchange(exchange)
|
||||
|
||||
amqp_message = Message(
|
||||
body=message.encode(),
|
||||
content_type=content_type,
|
||||
delivery_mode=DeliveryMode.PERSISTENT,
|
||||
)
|
||||
|
||||
await abstract_exchange.publish(amqp_message, routing_key=routing_key)
|
||||
|
||||
def __raise_if_disconnected(self) -> None:
|
||||
if self.__connection is None or self.__connection.is_closed:
|
||||
raise ConnectionNotEstablished("Connection not established")
|
||||
|
||||
async def __wait_for_stop(self) -> None:
|
||||
if self.__stop_event is not None:
|
||||
await self.__stop_event.wait()
|
||||
|
||||
self.__log(
|
||||
"Received stop signal. Terminating RabbitMQ client.."
|
||||
)
|
||||
|
||||
connection_established = self.__connection is not None \
|
||||
and not self.__connection.is_closed
|
||||
|
||||
if connection_established:
|
||||
await self.__connection.close() # type: ignore
|
||||
|
||||
def __log(self, message: str, level: int = INFO) -> None:
|
||||
if self.__logger is not None:
|
||||
self.__logger.log(level, message)
|
||||
0
kernel/rabbitmq/enum/__init__.py
Normal file
0
kernel/rabbitmq/enum/__init__.py
Normal file
5
kernel/rabbitmq/enum/content_type.py
Normal file
5
kernel/rabbitmq/enum/content_type.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class ContentType(str, Enum):
|
||||
APPLICATION_JSON = "application/json"
|
||||
0
kernel/rabbitmq/exception/__init__.py
Normal file
0
kernel/rabbitmq/exception/__init__.py
Normal file
7
kernel/rabbitmq/exception/connection_not_established.py
Normal file
7
kernel/rabbitmq/exception/connection_not_established.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from aio_pika.exceptions import AMQPConnectionError
|
||||
|
||||
from kernel.rabbitmq.exception.rabbitmq_exception import RabbitMQException
|
||||
|
||||
|
||||
class ConnectionNotEstablished(RabbitMQException, AMQPConnectionError):
|
||||
pass
|
||||
7
kernel/rabbitmq/exception/connection_timeout.py
Normal file
7
kernel/rabbitmq/exception/connection_timeout.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from aio_pika.exceptions import AMQPConnectionError
|
||||
|
||||
from kernel.rabbitmq.exception.rabbitmq_exception import RabbitMQException
|
||||
|
||||
|
||||
class ConnectionTimeout(RabbitMQException, AMQPConnectionError):
|
||||
pass
|
||||
5
kernel/rabbitmq/exception/rabbitmq_exception.py
Normal file
5
kernel/rabbitmq/exception/rabbitmq_exception.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from aio_pika.exceptions import AMQPException
|
||||
|
||||
|
||||
class RabbitMQException(AMQPException):
|
||||
pass
|
||||
0
kernel/rabbitmq/view/__init__.py
Normal file
0
kernel/rabbitmq/view/__init__.py
Normal file
7
kernel/rabbitmq/view/authentication_params.py
Normal file
7
kernel/rabbitmq/view/authentication_params.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AuthenticationParams:
|
||||
username: str = 'guest'
|
||||
password: str = 'guest'
|
||||
8
kernel/rabbitmq/view/connection_params.py
Normal file
8
kernel/rabbitmq/view/connection_params.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ConnectionParams:
|
||||
host: str = "localhost"
|
||||
port: int = 5672
|
||||
vhost: str = "/"
|
||||
Reference in New Issue
Block a user