Skeleton ready

This commit is contained in:
2026-07-08 23:01:55 +04:00
commit d9e5effdce
75 changed files with 1989 additions and 0 deletions

0
kernel/__init__.py Normal file
View File

View File

View 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()

View File

View File

@@ -0,0 +1,4 @@
from enum import Enum
class DateTime(str, Enum):
Ymd = "%Y-%m-%d"

View File

@@ -0,0 +1,4 @@
from enum import Enum
class Encoding(str, Enum):
utf8 = "utf-8"

View File

@@ -0,0 +1,5 @@
from punq import Container
class Kernel:
def get_container(self) -> Container:
raise NotImplementedError()

View File

View 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())

View File

@@ -0,0 +1,4 @@
from asyncio import Event
class StopEvent(Event):
pass

View File

View File

@@ -0,0 +1,7 @@
from dataclasses import dataclass
@dataclass(frozen=True)
class Application:
name: str
env: str

View 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

View 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 = "/"

View 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
View File

16
kernel/event/listener.py Normal file
View 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
View 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)

View File

View 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

View File

144
kernel/rabbitmq/client.py Normal file
View 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)

View File

View File

@@ -0,0 +1,5 @@
from enum import Enum
class ContentType(str, Enum):
APPLICATION_JSON = "application/json"

View File

View File

@@ -0,0 +1,7 @@
from aio_pika.exceptions import AMQPConnectionError
from kernel.rabbitmq.exception.rabbitmq_exception import RabbitMQException
class ConnectionNotEstablished(RabbitMQException, AMQPConnectionError):
pass

View File

@@ -0,0 +1,7 @@
from aio_pika.exceptions import AMQPConnectionError
from kernel.rabbitmq.exception.rabbitmq_exception import RabbitMQException
class ConnectionTimeout(RabbitMQException, AMQPConnectionError):
pass

View File

@@ -0,0 +1,5 @@
from aio_pika.exceptions import AMQPException
class RabbitMQException(AMQPException):
pass

View File

View File

@@ -0,0 +1,7 @@
from dataclasses import dataclass
@dataclass(frozen=True)
class AuthenticationParams:
username: str = 'guest'
password: str = 'guest'

View File

@@ -0,0 +1,8 @@
from dataclasses import dataclass
@dataclass(frozen=True)
class ConnectionParams:
host: str = "localhost"
port: int = 5672
vhost: str = "/"