Skeleton ready
This commit is contained in:
23
.editorconfig
Normal file
23
.editorconfig
Normal file
@@ -0,0 +1,23 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.py]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
max_line_length = 88
|
||||
insert_final_newline = true
|
||||
|
||||
[*.{yml,yaml,json,md}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[Makefile]
|
||||
indent_style = tab
|
||||
13
.env.example
Normal file
13
.env.example
Normal file
@@ -0,0 +1,13 @@
|
||||
APP_NAME=Skeleton
|
||||
APP_ENV=local
|
||||
|
||||
RABBITMQ_HOST=localhost
|
||||
RABBITMQ_PORT=5672
|
||||
RABBITMQ_USER=guest
|
||||
RABBITMQ_PASS=guest
|
||||
RABBITMQ_TIMEOUT=10
|
||||
RABBITMQ_VHOST=/
|
||||
|
||||
REDIS_HOST=application-skeleton-redis
|
||||
REDIS_PORT=6379
|
||||
REDIS_DB=0
|
||||
15
.gitignore
vendored
Normal file
15
.gitignore
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
.venv/
|
||||
.vscode/
|
||||
.idea/
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.pyd
|
||||
__pycache__/
|
||||
*.so
|
||||
*.egg
|
||||
*.egg-info
|
||||
dist/
|
||||
build/
|
||||
*.log
|
||||
*.env
|
||||
pyrightconfig.json
|
||||
35
Jenkinsfile
vendored
Normal file
35
Jenkinsfile
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
pipeline {
|
||||
agent {
|
||||
label "backend-agent"
|
||||
}
|
||||
|
||||
environment {
|
||||
PROJECT_NAME = "application-skeleton"
|
||||
PROJECT_CREDENTIALS = credentials("applicationSkeleton.credentials")
|
||||
}
|
||||
|
||||
stages {
|
||||
stage("Doing something") {
|
||||
steps() {
|
||||
script {
|
||||
pipe = setupWebhookPipeline()
|
||||
|
||||
def dateTag = sh(
|
||||
script: 'date -u +%Y%m%d%H%M%S',
|
||||
returnStdout: true
|
||||
).trim()
|
||||
|
||||
env.VERSION = "v1.2.3-${dateTag}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
post {
|
||||
success {
|
||||
|
||||
}
|
||||
unsuccessful {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 Diffhead
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
14
Makefile
Normal file
14
Makefile
Normal file
@@ -0,0 +1,14 @@
|
||||
Compose = docker compose --env-file .env -f deploy/local/compose.yaml
|
||||
|
||||
build:
|
||||
@$(Compose) build
|
||||
@$(Compose) up -d
|
||||
start:
|
||||
@$(Compose) start
|
||||
stop:
|
||||
@$(Compose) stop
|
||||
restart:
|
||||
@$(Compose) restart
|
||||
delete:
|
||||
@$(Compose) down
|
||||
|
||||
405
README.md
Normal file
405
README.md
Normal file
@@ -0,0 +1,405 @@
|
||||
# Feature-Oriented Event-Driven Application Skeleton
|
||||
|
||||
A Python-based asynchronous application framework designed for building scalable, event-driven systems using a feature-oriented architecture. This skeleton provides a solid foundation for building complex applications with clear separation of concerns, dependency injection, and event-driven communication patterns.
|
||||
|
||||
## Overview
|
||||
|
||||
This project implements a feature-oriented architecture where functionality is organized into independent, self-contained features that communicate through an event-driven model. Each feature is responsible for its own initialization, configuration, and bootstrap logic, making the system highly modular and testable.
|
||||
|
||||
### Key Concepts
|
||||
|
||||
- **Feature-Oriented**: Application logic is divided into independent features, each with its own provider for configuration and lifecycle management
|
||||
- **Event-Driven**: Features communicate asynchronously through an event emitter
|
||||
- **Async-First**: Built on Python's `asyncio` for high-performance concurrent operations
|
||||
- **Dependency Injection**: Uses the `punq` container for loose coupling and testability
|
||||
- **Kernel-Centric**: Core infrastructure (application lifecycle, messaging, events) lives in the `kernel` namespace
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
FeatureOrientedSkeleton/
|
||||
├── kernel/ # Core framework and infrastructure
|
||||
│ ├── application/ # Application lifecycle management
|
||||
│ │ ├── application.py # Abstract base application class
|
||||
│ │ ├── kernel.py # Abstract kernel (initialization point)
|
||||
│ │ ├── stop_event.py # Graceful shutdown signaling
|
||||
│ │ ├── enum/ # Core enumerations
|
||||
│ │ └── manager/ # Event managers
|
||||
│ ├── event/ # Event handling infrastructure
|
||||
│ │ ├── listener.py # Base listener interface
|
||||
│ │ └── registrar.py # Event listener registration mixin
|
||||
│ ├── feature/ # Feature provider interface
|
||||
│ │ └── provider.py # Abstract feature provider
|
||||
│ └── rabbitmq/ # RabbitMQ client and connection
|
||||
│ ├── client.py # RabbitMQ messaging client
|
||||
│ ├── enum/ # RabbitMQ-specific types
|
||||
│ └── exception/ # RabbitMQ exceptions
|
||||
│
|
||||
├── shared/ # Shared utilities and implementations
|
||||
│ ├── application/ # Concrete application implementations
|
||||
│ │ ├── rabbitmq_application.py # RabbitMQ-based application
|
||||
│ │ ├── rabbitmq_kernel.py # RabbitMQ kernel with DI setup
|
||||
│ │ └── factory/ # Factory patterns for common objects
|
||||
│ ├── event/ # Shared event types and structures
|
||||
│ ├── service/ # Cross-cutting services
|
||||
│ ├── manager/ # Utilities (argument parsing, etc)
|
||||
│ └── view/ # View models and configurations
|
||||
│
|
||||
├── feature/ # Application features (domain logic)
|
||||
│ ├── listen_rabbitmq/ # Feature: Listen to RabbitMQ messages
|
||||
│ │ ├── listen_rabbitmq.py # Feature provider
|
||||
│ │ ├── consumer/ # Message consumption logic
|
||||
│ │ ├── service/ # Business logic and services
|
||||
│ │ └── type/ # Domain types and models
|
||||
│ │
|
||||
│ ├── log_bus_message/ # Feature: Log received messages
|
||||
│ │ ├── log_bus_message.py # Feature provider
|
||||
│ │ └── listener/ # Event listeners
|
||||
│ │
|
||||
│ ├── prepare_error_notification/ # Feature: Prepare error notifications
|
||||
│ ├── send_telegram_notification/ # Feature: Send Telegram notifications
|
||||
│ └── [other-features]/ # Additional features
|
||||
│
|
||||
├── entry/ # Application entry points
|
||||
│ └── log_user_messages.py # CLI entry script
|
||||
│
|
||||
├── typings/ # Type stubs for third-party libraries
|
||||
│ ├── aioredis/
|
||||
│ ├── aioredlock/
|
||||
│ └── punq/
|
||||
│
|
||||
├── deploy/ # Deployment configurations
|
||||
│ ├── local/ # Local development (Docker Compose)
|
||||
│ │ ├── compose.yaml
|
||||
│ │ └── python.Dockerfile
|
||||
│ └── main/ # Production deployment
|
||||
│ ├── compose.yaml
|
||||
│ └── python.Dockerfile
|
||||
│
|
||||
├── examples/ # Example payloads and usage
|
||||
├── Makefile # Development commands
|
||||
├── pyrightconfig.json # Type checking configuration
|
||||
├── requirements.txt # Python dependencies
|
||||
├── .env.example # Environment variables template
|
||||
└── README.md # This file
|
||||
```
|
||||
|
||||
## Architecture Patterns
|
||||
|
||||
### 1. Feature Provider Pattern
|
||||
|
||||
Each feature must implement a **Feature Provider** - a class that inherits from `Provider` (and optionally `Registrar` mixin):
|
||||
|
||||
```python
|
||||
from typing import override
|
||||
from kernel.feature.provider import Provider
|
||||
from kernel.event.registrar import Registrar
|
||||
from shared.event.type import Type as EventType
|
||||
|
||||
class MyFeature(Provider, Registrar):
|
||||
@override
|
||||
def register(self) -> None:
|
||||
"""
|
||||
Called during DI container initialization.
|
||||
- Register classes in the container
|
||||
- Load configurations
|
||||
- Set up initial state
|
||||
"""
|
||||
self._container.register(MyService)
|
||||
|
||||
@override
|
||||
def bootstrap(self) -> None:
|
||||
"""
|
||||
Called after all features are registered.
|
||||
- Start background tasks
|
||||
- Attach event listeners
|
||||
- Initialize runtime state
|
||||
"""
|
||||
self.add_event_listener(EventType.MESSAGE_RECEIVED, MyEventListener)
|
||||
```
|
||||
|
||||
### 2. Event Listener Pattern
|
||||
|
||||
Create an event listener that inherits from `Listener[T]`:
|
||||
|
||||
```python
|
||||
from kernel.event.listener import Listener
|
||||
from shared.event.type import Type as EventType
|
||||
|
||||
class MyEventListener(Listener[MyEventType]):
|
||||
async def handle(self, event: str, payload: MyEventType) -> None:
|
||||
# Process the event
|
||||
pass
|
||||
```
|
||||
|
||||
Attach it in your feature's `bootstrap()` method using the `Registrar` mixin.
|
||||
|
||||
### 3. Dependency Injection
|
||||
|
||||
All dependencies are resolved through the `punq` Container:
|
||||
|
||||
```python
|
||||
from punq import Container
|
||||
|
||||
class MyService:
|
||||
def __init__(self, container: Container) -> None:
|
||||
self._container = container
|
||||
|
||||
def do_something(self) -> None:
|
||||
dependency = self._container.resolve(SomeDependency)
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Clone and Setup
|
||||
|
||||
```bash
|
||||
git clone <repository>
|
||||
cd FeatureOrientedSkeleton
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
### 2. Using Makefile (Recommended)
|
||||
|
||||
```bash
|
||||
# Build and start all services
|
||||
make up
|
||||
|
||||
# View application logs
|
||||
make logs
|
||||
|
||||
# Stop services
|
||||
make stop
|
||||
|
||||
# Clean up containers and volumes
|
||||
make clean
|
||||
```
|
||||
|
||||
### 3. Manual Docker Compose
|
||||
|
||||
```bash
|
||||
# Start services
|
||||
docker-compose -f deploy/local/compose.yaml up
|
||||
|
||||
# Stop services
|
||||
docker-compose -f deploy/local/compose.yaml down
|
||||
```
|
||||
|
||||
## Running the Example
|
||||
|
||||
The project includes an example application that listens to RabbitMQ messages and logs them:
|
||||
|
||||
```bash
|
||||
make up
|
||||
# Application starts and listens on queue 'application.skeleton'
|
||||
# Send messages and view logs
|
||||
make logs
|
||||
```
|
||||
|
||||
## Creating a New Feature
|
||||
|
||||
### Step 1: Create Feature Directory
|
||||
|
||||
```bash
|
||||
mkdir -p feature/my_feature/{service,listener,type,consumer}
|
||||
touch feature/my_feature/__init__.py
|
||||
```
|
||||
|
||||
### Step 2: Create Feature Provider
|
||||
|
||||
```python
|
||||
# feature/my_feature/my_feature.py
|
||||
from typing import override
|
||||
from kernel.feature.provider import Provider
|
||||
from kernel.event.registrar import Registrar
|
||||
|
||||
class MyFeature(Provider, Registrar):
|
||||
@override
|
||||
def register(self) -> None:
|
||||
# Register dependencies and services
|
||||
pass
|
||||
|
||||
@override
|
||||
def bootstrap(self) -> None:
|
||||
# Attach event listeners if needed
|
||||
pass
|
||||
```
|
||||
|
||||
### Step 3: Create Event Listener (if needed)
|
||||
|
||||
```python
|
||||
# feature/my_feature/listener/my_listener.py
|
||||
from kernel.event.listener import Listener
|
||||
|
||||
class MyListener(Listener[MyEventType]):
|
||||
async def handle(self, event: str, payload: MyEventType) -> None:
|
||||
# Handle the event
|
||||
pass
|
||||
```
|
||||
|
||||
### Step 4: Use in Entry Script
|
||||
|
||||
```python
|
||||
# entry/my_listener.py
|
||||
import asyncio
|
||||
from shared.application.factory.application import Application as ApplicationFactory
|
||||
from feature.my_feature import MyFeature
|
||||
|
||||
if __name__ == "__main__":
|
||||
factory = ApplicationFactory()
|
||||
|
||||
application = factory.create_rabbitmq([
|
||||
MyFeature,
|
||||
])
|
||||
|
||||
asyncio.run(application.launch(main))
|
||||
```
|
||||
|
||||
## Message Format
|
||||
|
||||
Messages must conform to a specific JSON format:
|
||||
|
||||
```json
|
||||
{
|
||||
"service": "service_name",
|
||||
"environment": "production",
|
||||
"exception": "ErrorType",
|
||||
"message": "Error message text",
|
||||
"type": "error",
|
||||
"datetime": "2024-01-01T12:00:00"
|
||||
}
|
||||
```
|
||||
|
||||
See `examples/` directory for sample messages.
|
||||
|
||||
## Environment Configuration
|
||||
|
||||
Copy `.env.example` to `.env` and configure:
|
||||
|
||||
```env
|
||||
# Application
|
||||
APP_NAME=application-skeleton
|
||||
APP_ENV=local
|
||||
|
||||
# RabbitMQ
|
||||
RABBITMQ_HOST=rabbitmq
|
||||
RABBITMQ_PORT=5672
|
||||
RABBITMQ_USER=guest
|
||||
RABBITMQ_PASS=guest
|
||||
RABBITMQ_VHOST=/
|
||||
RABBITMQ_TIMEOUT=10
|
||||
|
||||
# Redis
|
||||
REDIS_HOST=redis
|
||||
REDIS_PORT=6379
|
||||
REDIS_DB=0
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
### Makefile Commands
|
||||
|
||||
```bash
|
||||
make build # Build Docker images
|
||||
make up # Start services (builds if needed)
|
||||
make down # Stop services
|
||||
make restart # Restart services
|
||||
make logs # View application logs
|
||||
make stop # Stop and remove containers
|
||||
make clean # Remove all containers and volumes
|
||||
```
|
||||
|
||||
### Type Checking
|
||||
|
||||
```bash
|
||||
pyright
|
||||
```
|
||||
|
||||
### Code Style
|
||||
|
||||
Follow PEP 8 conventions. Configuration in `.editorconfig`.
|
||||
|
||||
## Key Design Principles
|
||||
|
||||
1. **Kernel is Core**: Infrastructure lives in `kernel/`, never import from `feature/` into `kernel/`
|
||||
2. **Shared for Common Code**: Cross-cutting concerns and shared implementations belong in `shared/`
|
||||
3. **Features are Independent**: Features should not depend on other features directly
|
||||
4. **DI Over Coupling**: Use the DI container to manage dependencies
|
||||
5. **Async-First**: Embrace async/await throughout the codebase
|
||||
6. **Events for Communication**: Features communicate through events, not direct calls
|
||||
7. **Provider Pattern**: Each feature must implement the Provider pattern with `register()` and `bootstrap()` methods
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
- **Graceful Shutdown**: Application responds to SIGTERM and SIGINT signals
|
||||
- **Resource Cleanup**: Set `stop_grace_period: 3s` in Docker Compose for clean shutdown
|
||||
- **Connection Pooling**: RabbitMQ and Redis connections are pooled for efficiency
|
||||
- **Event-Driven**: Non-blocking event handling allows processing many messages concurrently
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Application doesn't stop gracefully
|
||||
|
||||
Ensure `stop_grace_period` is set in `docker-compose.yaml`:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
application:
|
||||
stop_grace_period: 3s
|
||||
```
|
||||
|
||||
### Messages not being processed
|
||||
|
||||
1. Check RabbitMQ is healthy: `docker ps`
|
||||
2. Verify queue and routing key match the listeners
|
||||
3. View logs: `make logs`
|
||||
4. Check environment variables in `.env`
|
||||
|
||||
### Type checking errors
|
||||
|
||||
Ensure typings are installed:
|
||||
|
||||
```bash
|
||||
ls typings/
|
||||
# Should contain stubs for aioredis, aioredlock, punq
|
||||
```
|
||||
|
||||
### ConnectionError to RabbitMQ
|
||||
|
||||
1. Verify `depends_on` conditions in compose.yaml:
|
||||
```yaml
|
||||
depends_on:
|
||||
rabbitmq:
|
||||
condition: service_healthy
|
||||
```
|
||||
2. Check healthcheck passes: `docker ps`
|
||||
3. Verify credentials in `.env`
|
||||
|
||||
## Dependencies
|
||||
|
||||
Core dependencies include:
|
||||
|
||||
- `punq` - Dependency injection container
|
||||
- `aio-pika` - RabbitMQ client
|
||||
- `redis` - Redis client
|
||||
- `aioredlock` - Distributed locking
|
||||
- `pyee` - Event emitter
|
||||
|
||||
See `requirements.txt` for complete list and versions.
|
||||
|
||||
## Contributing
|
||||
|
||||
1. Follow the feature-oriented structure
|
||||
2. Each feature should be self-contained
|
||||
3. Use dependency injection for all dependencies
|
||||
4. Add type annotations to all functions
|
||||
5. Write docstrings for public APIs
|
||||
6. Test features in isolation using the DI container
|
||||
|
||||
## License
|
||||
|
||||
[Add your license here]
|
||||
|
||||
---
|
||||
|
||||
**For more information on feature-oriented architecture and event-driven systems, see the examples in the `examples/` directory.**
|
||||
57
deploy/local/compose.yaml
Normal file
57
deploy/local/compose.yaml
Normal file
@@ -0,0 +1,57 @@
|
||||
name: application-skeleton
|
||||
|
||||
services:
|
||||
application:
|
||||
build:
|
||||
context: ../../
|
||||
dockerfile: deploy/local/python.Dockerfile
|
||||
container_name: application-skeleton-application
|
||||
command: [
|
||||
"python", "-m", "entry.log_user_messages",
|
||||
"--queue=application.skeleton",
|
||||
"--exchange=amq.topic",
|
||||
"--routing-key=user.message.*"
|
||||
]
|
||||
stop_grace_period: 3s
|
||||
depends_on:
|
||||
rabbitmq:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
volumes:
|
||||
- ../../:/opt/app
|
||||
networks:
|
||||
- application-skeleton-network
|
||||
|
||||
rabbitmq:
|
||||
image: rabbitmq:4.3-management
|
||||
container_name: application-skeleton-rabbitmq
|
||||
environment:
|
||||
RABBITMQ_DEFAULT_USER: ${RABBITMQ_USER}
|
||||
RABBITMQ_DEFAULT_PASS: ${RABBITMQ_PASS}
|
||||
healthcheck:
|
||||
test: rabbitmq-diagnostics ping
|
||||
interval: 3s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
start_period: 10s
|
||||
networks:
|
||||
- application-skeleton-network
|
||||
ports:
|
||||
- "15672:15672"
|
||||
|
||||
redis:
|
||||
image: redis:8-alpine
|
||||
container_name: application-skeleton-redis
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 3s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
start_period: 10s
|
||||
networks:
|
||||
- application-skeleton-network
|
||||
|
||||
networks:
|
||||
application-skeleton-network:
|
||||
external: false
|
||||
11
deploy/local/python.Dockerfile
Normal file
11
deploy/local/python.Dockerfile
Normal file
@@ -0,0 +1,11 @@
|
||||
FROM python:3.12-slim
|
||||
|
||||
ENV PYTHONPATH=/opt/app
|
||||
|
||||
RUN pip install --upgrade pip
|
||||
|
||||
COPY requirements.txt .
|
||||
|
||||
RUN pip install -r requirements.txt
|
||||
|
||||
WORKDIR /opt/app
|
||||
77
deploy/main/compose.yaml
Normal file
77
deploy/main/compose.yaml
Normal file
@@ -0,0 +1,77 @@
|
||||
name: application-skeleton
|
||||
|
||||
services:
|
||||
application:
|
||||
build:
|
||||
context: ../../
|
||||
dockerfile: deploy/main/python.Dockerfile
|
||||
image: ${APP_IMAGE}
|
||||
container_name: application-skeleton-application
|
||||
depends_on:
|
||||
rabbitmq:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
command: [
|
||||
"python", "-m", "entry.log_user_messages",
|
||||
"--queue=application.skeleton",
|
||||
"--exchange=amq.topic",
|
||||
"--routing-key=user.message.*"
|
||||
]
|
||||
stop_grace_period: 3s
|
||||
volumes:
|
||||
- application-skeleton-data:/opt/app/data
|
||||
environment:
|
||||
- APP_NAME=${APP_NAME}
|
||||
- APP_ENV=${APP_ENV}
|
||||
- RABBITMQ_HOST=${RABBITMQ_HOST}
|
||||
- RABBITMQ_PORT=${RABBITMQ_PORT}
|
||||
- RABBITMQ_USER=${RABBITMQ_USER}
|
||||
- RABBITMQ_PASS=${RABBITMQ_PASS}
|
||||
- RABBITMQ_TIMEOUT=${RABBITMQ_TIMEOUT}
|
||||
- RABBITMQ_VHOST=${RABBITMQ_VHOST}
|
||||
- REDIS_HOST=${REDIS_HOST}
|
||||
- REDIS_PORT=${REDIS_PORT}
|
||||
- REDIS_DB=${REDIS_DB}
|
||||
networks:
|
||||
- application-skeleton-network
|
||||
restart: unless-stopped
|
||||
|
||||
rabbitmq:
|
||||
image: rabbitmq:4.3-management
|
||||
container_name: application-skeleton-rabbitmq
|
||||
environment:
|
||||
RABBITMQ_DEFAULT_USER: ${RABBITMQ_USER}
|
||||
RABBITMQ_DEFAULT_PASS: ${RABBITMQ_PASS}
|
||||
healthcheck:
|
||||
test: rabbitmq-diagnostics ping
|
||||
interval: 3s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
start_period: 10s
|
||||
networks:
|
||||
- application-skeleton-network
|
||||
ports:
|
||||
- "15672:15672"
|
||||
|
||||
redis:
|
||||
image: redis:8-alpine
|
||||
container_name: application-skeleton-redis
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 3s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
start_period: 10s
|
||||
networks:
|
||||
- application-skeleton-network
|
||||
restart: unless-stopped
|
||||
|
||||
networks:
|
||||
application-skeleton-network:
|
||||
external: false
|
||||
|
||||
volumes:
|
||||
application-skeleton-data:
|
||||
external: false
|
||||
|
||||
13
deploy/main/python.Dockerfile
Normal file
13
deploy/main/python.Dockerfile
Normal file
@@ -0,0 +1,13 @@
|
||||
FROM python:3.12-slim
|
||||
|
||||
ENV PYTHONPATH=/opt/app
|
||||
|
||||
RUN pip install --upgrade pip
|
||||
|
||||
COPY requirements.txt .
|
||||
|
||||
RUN pip install -r requirements.txt
|
||||
|
||||
COPY . /opt/app
|
||||
|
||||
WORKDIR /opt/app
|
||||
37
entry/log_user_messages.py
Normal file
37
entry/log_user_messages.py
Normal file
@@ -0,0 +1,37 @@
|
||||
import asyncio
|
||||
|
||||
from punq import Container
|
||||
from aio_pika.abc import AbstractIncomingMessage
|
||||
from pyee import EventEmitter
|
||||
|
||||
from shared.application.factory.application import Application as ApplicationFactory
|
||||
from shared.event.type import Type as EventType
|
||||
|
||||
from feature.listen_user_message.listen_user_message import ListenUserMessage
|
||||
from feature.log_bus_message.log_bus_message import LogBusMessage
|
||||
|
||||
from feature.listen_user_message.manager.message import Message as MessageManager
|
||||
|
||||
|
||||
async def main(container: Container, message: AbstractIncomingMessage) -> None:
|
||||
manager = container.resolve(MessageManager, message=message)
|
||||
emitter = container.resolve(EventEmitter)
|
||||
event = EventType.MESSAGE_RECEIVED
|
||||
|
||||
try:
|
||||
received = manager.get_message()
|
||||
emitter.emit(event, received)
|
||||
|
||||
await message.ack()
|
||||
except Exception:
|
||||
await message.reject()
|
||||
|
||||
if __name__ == "__main__":
|
||||
factory = ApplicationFactory()
|
||||
|
||||
application = factory.create_rabbitmq([
|
||||
ListenUserMessage,
|
||||
LogBusMessage
|
||||
])
|
||||
|
||||
asyncio.run(application.launch(main))
|
||||
6
examples/message.json
Normal file
6
examples/message.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"id":"019f42ff-de59-7a17-8189-a36b68191a71",
|
||||
"source":"019f42ff-dff9-7b68-832f-a6323a078105",
|
||||
"type":"user.message",
|
||||
"message":"Hello my friend, i'm there"
|
||||
}
|
||||
0
feature/__init__.py
Normal file
0
feature/__init__.py
Normal file
0
feature/listen_user_message/__init__.py
Normal file
0
feature/listen_user_message/__init__.py
Normal file
15
feature/listen_user_message/listen_user_message.py
Normal file
15
feature/listen_user_message/listen_user_message.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from typing import override
|
||||
|
||||
from kernel.feature.provider import Provider
|
||||
from kernel.event.registrar import Registrar
|
||||
|
||||
from feature.listen_user_message.manager.message import Message
|
||||
|
||||
class ListenUserMessage(Provider, Registrar):
|
||||
@override
|
||||
def register(self) -> None:
|
||||
self._container.register(Message, Message)
|
||||
|
||||
@override
|
||||
def bootstrap(self) -> None:
|
||||
pass
|
||||
0
feature/listen_user_message/manager/__init__.py
Normal file
0
feature/listen_user_message/manager/__init__.py
Normal file
37
feature/listen_user_message/manager/message.py
Normal file
37
feature/listen_user_message/manager/message.py
Normal file
@@ -0,0 +1,37 @@
|
||||
from aio_pika.abc import AbstractIncomingMessage
|
||||
from json import loads as json_loads
|
||||
|
||||
from kernel.application.enum.encoding import Encoding
|
||||
|
||||
from shared.bus.view.message import Message as BusMessage
|
||||
|
||||
|
||||
class Message:
|
||||
__message: AbstractIncomingMessage
|
||||
__required_columns = [
|
||||
"id",
|
||||
"type",
|
||||
"source",
|
||||
"message",
|
||||
]
|
||||
|
||||
def __init__(self, message: AbstractIncomingMessage) -> None:
|
||||
self.__message = message
|
||||
|
||||
def get_message(self) -> BusMessage:
|
||||
payload: str = self.__message.body.decode(Encoding.utf8)
|
||||
payload_json: dict = json_loads(payload)
|
||||
|
||||
for column in self.__required_columns:
|
||||
if column not in payload_json:
|
||||
raise ValueError(
|
||||
f"Missing required column: {column}"
|
||||
)
|
||||
|
||||
return BusMessage(
|
||||
id=payload_json["id"],
|
||||
type=payload_json["type"],
|
||||
source=payload_json["source"],
|
||||
message=payload_json["message"]
|
||||
)
|
||||
|
||||
0
feature/log_bus_message/__init__.py
Normal file
0
feature/log_bus_message/__init__.py
Normal file
0
feature/log_bus_message/listener/__init__.py
Normal file
0
feature/log_bus_message/listener/__init__.py
Normal file
25
feature/log_bus_message/listener/log_message.py
Normal file
25
feature/log_bus_message/listener/log_message.py
Normal file
@@ -0,0 +1,25 @@
|
||||
from typing import override
|
||||
from logging import Logger
|
||||
|
||||
from kernel.event.listener import Listener
|
||||
|
||||
from shared.bus.view.message import Message
|
||||
|
||||
|
||||
class LogMessage(Listener[Message]):
|
||||
@override
|
||||
async def handle(self, event: str, payload: Message) -> None:
|
||||
logger: Logger = self._container.resolve(Logger)
|
||||
|
||||
logger.info(f"Event: {event}\n")
|
||||
|
||||
content = (
|
||||
f"Received message\n"
|
||||
f"Id: {payload.id}\n"
|
||||
f"Type: {payload.type}\n"
|
||||
f"Source: {payload.source}\n"
|
||||
f"Message: {payload.message}"
|
||||
)
|
||||
|
||||
logger.debug(content)
|
||||
|
||||
18
feature/log_bus_message/log_bus_message.py
Normal file
18
feature/log_bus_message/log_bus_message.py
Normal file
@@ -0,0 +1,18 @@
|
||||
from typing import override
|
||||
|
||||
from kernel.feature.provider import Provider
|
||||
from kernel.event.registrar import Registrar
|
||||
|
||||
from shared.event.type import Type as EventType
|
||||
|
||||
from feature.log_bus_message.listener.log_message import LogMessage
|
||||
|
||||
|
||||
class LogBusMessage(Provider, Registrar):
|
||||
@override
|
||||
def register(self) -> None:
|
||||
pass
|
||||
|
||||
@override
|
||||
def bootstrap(self) -> None:
|
||||
self.add_event_listener(EventType.MESSAGE_RECEIVED, LogMessage)
|
||||
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 = "/"
|
||||
9
requirements.txt
Normal file
9
requirements.txt
Normal file
@@ -0,0 +1,9 @@
|
||||
aio-pika==9.5.7
|
||||
aioredis==1.3.1
|
||||
aioredlock==0.7.3
|
||||
pyee==13.0.1
|
||||
python-dotenv==1.1.1
|
||||
punq==0.7.0
|
||||
redis==7.4.0
|
||||
setuptools==82.0.1
|
||||
xxhash==3.6.0
|
||||
0
shared/__init__.py
Normal file
0
shared/__init__.py
Normal file
0
shared/application/__init__.py
Normal file
0
shared/application/__init__.py
Normal file
0
shared/application/factory/__init__.py
Normal file
0
shared/application/factory/__init__.py
Normal file
10
shared/application/factory/application.py
Normal file
10
shared/application/factory/application.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from typing import List
|
||||
|
||||
from kernel.feature.provider import Provider
|
||||
|
||||
from shared.application.rabbitmq_application import RabbitMQApplication
|
||||
from shared.application.rabbitmq_kernel import RabbitMQKernel
|
||||
|
||||
class Application:
|
||||
def create_rabbitmq(self, features: List[type[Provider]]) -> RabbitMQApplication:
|
||||
return RabbitMQApplication(RabbitMQKernel(), features)
|
||||
27
shared/application/factory/logger.py
Normal file
27
shared/application/factory/logger.py
Normal file
@@ -0,0 +1,27 @@
|
||||
import logging
|
||||
|
||||
from sys import stdout
|
||||
|
||||
|
||||
class Logger:
|
||||
def create_stdout(
|
||||
self,
|
||||
name: str,
|
||||
level: int = logging.INFO
|
||||
) -> logging.Logger:
|
||||
logger = logging.getLogger(name)
|
||||
logger.setLevel(level)
|
||||
|
||||
handler = logging.StreamHandler(stdout)
|
||||
handler.setLevel(level)
|
||||
handler.setFormatter(
|
||||
logging.Formatter(
|
||||
"%(asctime)s - %(levelname)s - %(message)s",
|
||||
datefmt="%Y-%m-%d %H:%M:%S"
|
||||
)
|
||||
)
|
||||
|
||||
logger.addHandler(handler)
|
||||
|
||||
return logger
|
||||
|
||||
0
shared/application/manager/__init__.py
Normal file
0
shared/application/manager/__init__.py
Normal file
40
shared/application/manager/listener_arguments_parser.py
Normal file
40
shared/application/manager/listener_arguments_parser.py
Normal file
@@ -0,0 +1,40 @@
|
||||
from typing import Any, Sequence
|
||||
from argparse import ArgumentParser, HelpFormatter
|
||||
|
||||
|
||||
class ListenerArgumentsParser(ArgumentParser):
|
||||
def __init__(
|
||||
self,
|
||||
prog: str | None = None,
|
||||
usage: str | None = None,
|
||||
description: str | None = None,
|
||||
epilog: str | None = None,
|
||||
parents: Sequence[ArgumentParser] = [],
|
||||
formatter_class: type[HelpFormatter] = HelpFormatter,
|
||||
prefix_chars: str = '-',
|
||||
fromfile_prefix_chars: str | None = None,
|
||||
argument_default: Any = None,
|
||||
conflict_handler: str = 'error',
|
||||
add_help: bool = True,
|
||||
allow_abbrev: bool = True,
|
||||
exit_on_error: bool = True
|
||||
) -> None:
|
||||
super().__init__(
|
||||
prog=prog,
|
||||
usage=usage,
|
||||
description=description,
|
||||
epilog=epilog,
|
||||
parents=parents,
|
||||
formatter_class=formatter_class,
|
||||
prefix_chars=prefix_chars,
|
||||
fromfile_prefix_chars=fromfile_prefix_chars,
|
||||
argument_default=argument_default,
|
||||
conflict_handler=conflict_handler,
|
||||
add_help=add_help,
|
||||
allow_abbrev=allow_abbrev,
|
||||
exit_on_error=exit_on_error
|
||||
)
|
||||
|
||||
self.add_argument('--queue', type=str, required=True)
|
||||
self.add_argument('--exchange', type=str, required=True)
|
||||
self.add_argument('--routing-key', type=str, default="")
|
||||
56
shared/application/rabbitmq_application.py
Normal file
56
shared/application/rabbitmq_application.py
Normal file
@@ -0,0 +1,56 @@
|
||||
from typing import Callable, Awaitable, override
|
||||
from asyncio import CancelledError
|
||||
from logging import Logger
|
||||
from punq import Container
|
||||
from aio_pika.abc import AbstractIncomingMessage
|
||||
|
||||
from kernel.application.stop_event import StopEvent
|
||||
from kernel.application.view.configuration import Configuration
|
||||
from kernel.application.application import Application
|
||||
from kernel.rabbitmq.client import Client
|
||||
|
||||
from shared.application.view.listener_arguments import ListenerArguments
|
||||
|
||||
|
||||
class RabbitMQApplication(Application):
|
||||
@override
|
||||
async def do(
|
||||
self,
|
||||
callback: Callable[[Container, AbstractIncomingMessage], Awaitable[None]]
|
||||
) -> None:
|
||||
rabbitmq = await self.__connect_rabbitmq_client()
|
||||
arguments = self.get_container().resolve(ListenerArguments)
|
||||
|
||||
async def consumer(message: AbstractIncomingMessage) -> None:
|
||||
await callback(self.get_container(), message)
|
||||
|
||||
queue = arguments.queue
|
||||
exchange = arguments.exchange
|
||||
routing_key = arguments.routing_key
|
||||
|
||||
await rabbitmq.consume(queue, exchange, consumer, routing_key)
|
||||
|
||||
logger = self.get_container().resolve(Logger)
|
||||
|
||||
logger.info(f"Queue: {queue}")
|
||||
logger.info(f"Exchange: `{exchange}`")
|
||||
|
||||
if routing_key != "":
|
||||
logger.info(f"Routing key: {routing_key}")
|
||||
|
||||
try:
|
||||
await self.get_container().resolve(StopEvent).wait()
|
||||
except CancelledError as e:
|
||||
logger.exception(e)
|
||||
|
||||
async def __connect_rabbitmq_client(self) -> Client:
|
||||
rabbitmq_client = self.get_container().resolve(Client)
|
||||
|
||||
stop_event = self.get_container().resolve(StopEvent)
|
||||
configuration = self.get_container().resolve(Configuration)
|
||||
|
||||
await rabbitmq_client\
|
||||
.connect(stop_event, configuration.rabbitmq().timeout)
|
||||
|
||||
return rabbitmq_client
|
||||
|
||||
139
shared/application/rabbitmq_kernel.py
Normal file
139
shared/application/rabbitmq_kernel.py
Normal file
@@ -0,0 +1,139 @@
|
||||
from os import getenv
|
||||
from logging import Logger
|
||||
from punq import Container
|
||||
from pyee import EventEmitter
|
||||
from pyee.asyncio import AsyncIOEventEmitter
|
||||
from aioredlock import Aioredlock
|
||||
from redis.asyncio import Redis as AsyncRedis
|
||||
|
||||
from kernel.application.manager.stop_event import StopEvent as StopEventManager
|
||||
from kernel.application.stop_event import StopEvent
|
||||
from kernel.application.view.application import Application
|
||||
from kernel.application.view.configuration import Configuration
|
||||
from kernel.application.view.rabbitmq import RabbitMQ
|
||||
from kernel.application.view.redis import Redis as RedisConfig
|
||||
|
||||
from kernel.application.kernel import Kernel
|
||||
from shared.application.service.environment_loader import EnvironmentLoader
|
||||
from shared.application.manager.listener_arguments_parser import ListenerArgumentsParser
|
||||
from shared.application.factory.logger import Logger as LoggerFactory
|
||||
from shared.application.view.listener_arguments import ListenerArguments
|
||||
from kernel.rabbitmq.client import Client
|
||||
from kernel.rabbitmq.view.authentication_params import AuthenticationParams
|
||||
from kernel.rabbitmq.view.connection_params import ConnectionParams
|
||||
|
||||
|
||||
class RabbitMQKernel(Kernel):
|
||||
__container: Container
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.__load_env()
|
||||
self.__init_container()
|
||||
self.__register_config()
|
||||
self.__register_arguments()
|
||||
self.__register_stop_event()
|
||||
self.__register_logger()
|
||||
self.__register_rabbitmq_client()
|
||||
self.__register_redis()
|
||||
self.__register_lock_provider()
|
||||
self.__register_event_emitter()
|
||||
|
||||
def get_container(self) -> Container:
|
||||
return self.__container
|
||||
|
||||
def __load_env(self) -> None:
|
||||
env_loader = EnvironmentLoader(override=False)
|
||||
env_loader.load()
|
||||
|
||||
def __init_container(self) -> None:
|
||||
self.__container = Container()
|
||||
|
||||
def __register_config(self) -> None:
|
||||
application = Application(
|
||||
name=getenv("APP_NAME", ""),
|
||||
env=getenv("APP_ENV", ""),
|
||||
)
|
||||
|
||||
rabbitmq = RabbitMQ(
|
||||
host=getenv("RABBITMQ_HOST", "localhost"),
|
||||
port=int(getenv("RABBITMQ_PORT", "5672")),
|
||||
user=getenv("RABBITMQ_USER", "guest"),
|
||||
password=getenv("RABBITMQ_PASS", "guest"),
|
||||
vhost=getenv("RABBITMQ_VHOST", "/")
|
||||
)
|
||||
|
||||
redis = RedisConfig(
|
||||
host=getenv("REDIS_HOST", "localhost"),
|
||||
port=int(getenv("REDIS_PORT", "6379")),
|
||||
db=int(getenv("REDIS_DB", "0"))
|
||||
)
|
||||
|
||||
config = Configuration(application, rabbitmq, redis)
|
||||
|
||||
self.__container.register(Configuration, instance=config)
|
||||
|
||||
def __register_arguments(self) -> None:
|
||||
argument_parser = ListenerArgumentsParser()
|
||||
arguments_parsed = argument_parser.parse_args()
|
||||
|
||||
arguments = ListenerArguments(
|
||||
queue=arguments_parsed.queue,
|
||||
exchange=arguments_parsed.exchange,
|
||||
routing_key=arguments_parsed.routing_key
|
||||
)
|
||||
|
||||
self.__container.register(ListenerArguments, instance=arguments)
|
||||
|
||||
def __register_stop_event(self) -> None:
|
||||
stop_event = StopEvent()
|
||||
|
||||
stop_event_manager = StopEventManager(stop_event)
|
||||
stop_event_manager.setup_stop_signals()
|
||||
|
||||
self.__container.register(StopEvent, instance=stop_event)
|
||||
|
||||
def __register_logger(self) -> None:
|
||||
logger_factory = LoggerFactory()
|
||||
logger = logger_factory.create_stdout(name="rbmq.listener")
|
||||
|
||||
self.__container.register(Logger, instance=logger)
|
||||
|
||||
def __register_rabbitmq_client(self) -> None:
|
||||
config = self.__container.resolve(Configuration).rabbitmq()
|
||||
|
||||
rabbitmq_client = Client(
|
||||
AuthenticationParams(config.user, config.password),
|
||||
ConnectionParams(config.host, config.port, config.vhost),
|
||||
self.__container.resolve(Logger)
|
||||
)
|
||||
|
||||
self.__container.register(Client, instance=rabbitmq_client)
|
||||
|
||||
def __register_redis(self) -> None:
|
||||
config = self.__container.resolve(Configuration).redis()
|
||||
|
||||
client = AsyncRedis(
|
||||
host=config.host,
|
||||
port=config.port,
|
||||
db=config.db,
|
||||
decode_responses=True
|
||||
)
|
||||
|
||||
self.__container.register(AsyncRedis, instance=client)
|
||||
|
||||
def __register_lock_provider(self) -> None:
|
||||
config = self.__container.resolve(Configuration).redis()
|
||||
|
||||
aioredlock = Aioredlock([
|
||||
{
|
||||
"host": config.host,
|
||||
"port": config.port,
|
||||
"db": config.db
|
||||
}
|
||||
])
|
||||
|
||||
self.__container.register(Aioredlock, instance=aioredlock)
|
||||
|
||||
def __register_event_emitter(self) -> None:
|
||||
event_emitter = AsyncIOEventEmitter()
|
||||
self.__container.register(EventEmitter, instance=event_emitter)
|
||||
0
shared/application/service/__init__.py
Normal file
0
shared/application/service/__init__.py
Normal file
29
shared/application/service/environment_loader.py
Normal file
29
shared/application/service/environment_loader.py
Normal file
@@ -0,0 +1,29 @@
|
||||
from dotenv import find_dotenv, load_dotenv
|
||||
|
||||
|
||||
class EnvironmentLoader:
|
||||
__name: str
|
||||
__usecwd: bool
|
||||
__override: bool
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str = ".env",
|
||||
usecwd: bool = True,
|
||||
override: bool = True
|
||||
) -> None:
|
||||
self.__name = name
|
||||
self.__usecwd = usecwd
|
||||
self.__override = override
|
||||
|
||||
def load(self) -> None:
|
||||
dotenv = find_dotenv(
|
||||
filename=self.__name,
|
||||
raise_error_if_not_found=True,
|
||||
usecwd=self.__usecwd
|
||||
)
|
||||
|
||||
load_dotenv(
|
||||
dotenv_path=dotenv,
|
||||
override=self.__override
|
||||
)
|
||||
0
shared/application/view/__init__.py
Normal file
0
shared/application/view/__init__.py
Normal file
8
shared/application/view/listener_arguments.py
Normal file
8
shared/application/view/listener_arguments.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ListenerArguments:
|
||||
queue: str
|
||||
exchange: str
|
||||
routing_key: str
|
||||
0
shared/bus/__init__.py
Normal file
0
shared/bus/__init__.py
Normal file
0
shared/bus/view/__init__.py
Normal file
0
shared/bus/view/__init__.py
Normal file
9
shared/bus/view/message.py
Normal file
9
shared/bus/view/message.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Message:
|
||||
id: str
|
||||
type: str
|
||||
source: str
|
||||
message: str
|
||||
0
shared/event/__init__.py
Normal file
0
shared/event/__init__.py
Normal file
5
shared/event/type.py
Normal file
5
shared/event/type.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class Type(str, Enum):
|
||||
MESSAGE_RECEIVED = "message.received"
|
||||
321
typings/aioredis/__init__.pyi
Normal file
321
typings/aioredis/__init__.pyi
Normal file
@@ -0,0 +1,321 @@
|
||||
from typing import Any, overload, Coroutine, Awaitable
|
||||
|
||||
|
||||
class RedisError(Exception): ...
|
||||
|
||||
|
||||
class Redis:
|
||||
"""High-level Redis client with all Redis commands."""
|
||||
|
||||
encoding: str | None
|
||||
|
||||
@overload
|
||||
def __init__(self, pool_or_conn: Any) -> None: ...
|
||||
|
||||
@overload
|
||||
def __init__(
|
||||
self,
|
||||
pool_or_conn: Any = ...,
|
||||
*,
|
||||
host: str,
|
||||
port: int,
|
||||
db: int = ...,
|
||||
password: str | None = ...,
|
||||
encoding: str | None = ...,
|
||||
ssl: Any = ...,
|
||||
) -> None: ...
|
||||
|
||||
# ==================== Connection Commands ====================
|
||||
def close(self) -> None: ...
|
||||
async def wait_closed(self) -> None: ...
|
||||
async def auth(self, password: str) -> bool: ...
|
||||
async def select(self, db: int) -> bool: ...
|
||||
async def quit(self) -> bool: ...
|
||||
async def swapdb(self, db1: int, db2: int) -> bool: ...
|
||||
|
||||
# ==================== String Commands ====================
|
||||
async def append(self, key: str | bytes, value: str | bytes) -> int: ...
|
||||
async def bitcount(self, key: str | bytes, start: int | None = ..., end: int | None = ...) -> int: ...
|
||||
async def bitop_and(self, destkey: str | bytes, key: str | bytes, *keys: str | bytes) -> int: ...
|
||||
async def bitop_or(self, destkey: str | bytes, key: str | bytes, *keys: str | bytes) -> int: ...
|
||||
async def bitop_xor(self, destkey: str | bytes, key: str | bytes, *keys: str | bytes) -> int: ...
|
||||
async def bitop_not(self, destkey: str | bytes, key: str | bytes) -> int: ...
|
||||
async def bitpos(self, key: str | bytes, bit: int, start: int | None = ..., end: int | None = ...) -> int: ...
|
||||
async def decr(self, key: str | bytes) -> int: ...
|
||||
async def decrby(self, key: str | bytes, decrement: int) -> int: ...
|
||||
async def get(self, key: str | bytes, *, encoding: str | None = ...) -> bytes | str | None: ...
|
||||
async def getbit(self, key: str | bytes, offset: int) -> int: ...
|
||||
async def getrange(self, key: str | bytes, start: int, end: int, *, encoding: str | None = ...) -> bytes | str: ...
|
||||
async def getset(self, key: str | bytes, value: str | bytes, *, encoding: str | None = ...) -> bytes | str | None: ...
|
||||
async def incr(self, key: str | bytes) -> int: ...
|
||||
async def incrby(self, key: str | bytes, increment: int) -> int: ...
|
||||
async def incrbyfloat(self, key: str | bytes, increment: float) -> float: ...
|
||||
async def mget(self, key: str | bytes, *keys: str | bytes, encoding: str | None = ...) -> list[Any]: ...
|
||||
async def mset(self, *args: Any) -> bool: ...
|
||||
async def msetnx(self, key: str | bytes, value: str | bytes, *pairs: Any) -> bool: ...
|
||||
async def psetex(self, key: str | bytes, milliseconds: int, value: str | bytes) -> bool: ...
|
||||
async def set(self, key: str | bytes, value: str | bytes, *, expire: int = ..., pexpire: int = ..., exist: str | None = ...) -> bool: ...
|
||||
async def setbit(self, key: str | bytes, offset: int, value: int) -> int: ...
|
||||
async def setex(self, key: str | bytes, seconds: int, value: str | bytes) -> bool: ...
|
||||
async def setnx(self, key: str | bytes, value: str | bytes) -> bool: ...
|
||||
async def setrange(self, key: str | bytes, offset: int, value: str | bytes) -> int: ...
|
||||
async def strlen(self, key: str | bytes) -> int: ...
|
||||
|
||||
# ==================== List Commands ====================
|
||||
async def blpop(self, key: str | bytes, *keys: str | bytes, timeout: int = ..., encoding: str | None = ...) -> list[Any] | None: ...
|
||||
async def brpop(self, key: str | bytes, *keys: str | bytes, timeout: int = ..., encoding: str | None = ...) -> list[Any] | None: ...
|
||||
async def brpoplpush(self, sourcekey: str | bytes, destkey: str | bytes, timeout: int = ..., encoding: str | None = ...) -> bytes | str | None: ...
|
||||
async def lindex(self, key: str | bytes, index: int, *, encoding: str | None = ...) -> bytes | str | None: ...
|
||||
async def linsert(self, key: str | bytes, pivot: str | bytes, value: str | bytes, before: bool = ...) -> int: ...
|
||||
async def llen(self, key: str | bytes) -> int: ...
|
||||
async def lpop(self, key: str | bytes, *, encoding: str | None = ...) -> bytes | str | None: ...
|
||||
async def lpush(self, key: str | bytes, value: str | bytes, *values: str | bytes) -> int: ...
|
||||
async def lpushx(self, key: str | bytes, value: str | bytes) -> int: ...
|
||||
async def lrange(self, key: str | bytes, start: int, stop: int, *, encoding: str | None = ...) -> list[Any]: ...
|
||||
async def lrem(self, key: str | bytes, count: int, value: str | bytes) -> int: ...
|
||||
async def lset(self, key: str | bytes, index: int, value: str | bytes) -> bool: ...
|
||||
async def ltrim(self, key: str | bytes, start: int, stop: int) -> bool: ...
|
||||
async def rpop(self, key: str | bytes, *, encoding: str | None = ...) -> bytes | str | None: ...
|
||||
async def rpoplpush(self, sourcekey: str | bytes, destkey: str | bytes, *, encoding: str | None = ...) -> bytes | str | None: ...
|
||||
async def rpush(self, key: str | bytes, value: str | bytes, *values: str | bytes) -> int: ...
|
||||
async def rpushx(self, key: str | bytes, value: str | bytes) -> int: ...
|
||||
|
||||
# ==================== Set Commands ====================
|
||||
async def sadd(self, key: str | bytes, member: str | bytes, *members: str | bytes) -> int: ...
|
||||
async def scard(self, key: str | bytes) -> int: ...
|
||||
async def sdiff(self, key: str | bytes, *keys: str | bytes, encoding: str | None = ...) -> list[Any]: ...
|
||||
async def sdiffstore(self, destkey: str | bytes, key: str | bytes, *keys: str | bytes) -> int: ...
|
||||
async def sinter(self, key: str | bytes, *keys: str | bytes, encoding: str | None = ...) -> list[Any]: ...
|
||||
async def sinterstore(self, destkey: str | bytes, key: str | bytes, *keys: str | bytes) -> int: ...
|
||||
async def sismember(self, key: str | bytes, member: str | bytes) -> bool: ...
|
||||
async def smembers(self, key: str | bytes, *, encoding: str | None = ...) -> list[Any]: ...
|
||||
async def smove(self, sourcekey: str | bytes, destkey: str | bytes, member: str | bytes) -> bool: ...
|
||||
async def spop(self, key: str | bytes, count: int | None = ..., *, encoding: str | None = ...) -> bytes | str | list[Any] | None: ...
|
||||
async def srandmember(self, key: str | bytes, count: int | None = ..., *, encoding: str | None = ...) -> bytes | str | list[Any] | None: ...
|
||||
async def srem(self, key: str | bytes, member: str | bytes, *members: str | bytes) -> int: ...
|
||||
async def sscan(self, key: str | bytes, cursor: int = ..., match: str | None = ..., count: int | None = ...) -> tuple[int, list[Any]]: ...
|
||||
|
||||
# ==================== Hash Commands ====================
|
||||
async def hdel(self, key: str | bytes, field: str | bytes, *fields: str | bytes) -> int: ...
|
||||
async def hexists(self, key: str | bytes, field: str | bytes) -> bool: ...
|
||||
async def hget(self, key: str | bytes, field: str | bytes, *, encoding: str | None = ...) -> bytes | str | None: ...
|
||||
async def hgetall(self, key: str | bytes, *, encoding: str | None = ...) -> dict[Any, Any]: ...
|
||||
async def hincrby(self, key: str | bytes, field: str | bytes, increment: int) -> int: ...
|
||||
async def hincrbyfloat(self, key: str | bytes, field: str | bytes, increment: float) -> float: ...
|
||||
async def hkeys(self, key: str | bytes, *, encoding: str | None = ...) -> list[Any]: ...
|
||||
async def hlen(self, key: str | bytes) -> int: ...
|
||||
async def hmget(self, key: str | bytes, field: str | bytes, *fields: str | bytes, encoding: str | None = ...) -> list[Any]: ...
|
||||
async def hmset(self, key: str | bytes, field: str | bytes, value: str | bytes, *fv_pairs: Any) -> bool: ...
|
||||
async def hmset_dict(self, key: str | bytes, mapping: dict[Any, Any]) -> bool: ...
|
||||
async def hset(self, key: str | bytes, field: str | bytes, value: str | bytes) -> int: ...
|
||||
async def hsetnx(self, key: str | bytes, field: str | bytes, value: str | bytes) -> bool: ...
|
||||
async def hstrlen(self, key: str | bytes, field: str | bytes) -> int: ...
|
||||
async def hvals(self, key: str | bytes, *, encoding: str | None = ...) -> list[Any]: ...
|
||||
async def hscan(self, key: str | bytes, cursor: int = ..., match: str | None = ..., count: int | None = ...) -> tuple[int, list[Any]]: ...
|
||||
|
||||
# ==================== Sorted Set Commands ====================
|
||||
async def zadd(self, key: str | bytes, score: float, member: str | bytes, *score_members: Any) -> int: ...
|
||||
async def zcard(self, key: str | bytes) -> int: ...
|
||||
async def zcount(self, key: str | bytes, min: float | str, max: float | str) -> int: ...
|
||||
async def zincrby(self, key: str | bytes, increment: float, member: str | bytes) -> float: ...
|
||||
async def zinterstore(self, destkey: str | bytes, numkeys: int, key: str | bytes, *keys: str | bytes, weights: list[int] | None = ..., aggregate: str | None = ...) -> int: ...
|
||||
async def zlexcount(self, key: str | bytes, min: str, max: str) -> int: ...
|
||||
async def zpopmax(self, key: str | bytes, count: int | None = ..., encoding: str | None = ...) -> list[Any]: ...
|
||||
async def zpopmin(self, key: str | bytes, count: int | None = ..., encoding: str | None = ...) -> list[Any]: ...
|
||||
async def zrange(self, key: str | bytes, start: int, stop: int, *, withscores: bool = ..., encoding: str | None = ...) -> list[Any]: ...
|
||||
async def zrangebylex(self, key: str | bytes, min: str, max: str, *, offset: int | None = ..., count: int | None = ..., encoding: str | None = ...) -> list[Any]: ...
|
||||
async def zrangebyscore(self, key: str | bytes, min: float | str, max: float | str, *, offset: int | None = ..., count: int | None = ..., withscores: bool = ..., encoding: str | None = ...) -> list[Any]: ...
|
||||
async def zrank(self, key: str | bytes, member: str | bytes) -> int | None: ...
|
||||
async def zrem(self, key: str | bytes, member: str | bytes, *members: str | bytes) -> int: ...
|
||||
async def zremrangebylex(self, key: str | bytes, min: str, max: str) -> int: ...
|
||||
async def zremrangebyrank(self, key: str | bytes, start: int, stop: int) -> int: ...
|
||||
async def zremrangebyscore(self, key: str | bytes, min: float | str, max: float | str) -> int: ...
|
||||
async def zrevrange(self, key: str | bytes, start: int, stop: int, *, withscores: bool = ..., encoding: str | None = ...) -> list[Any]: ...
|
||||
async def zrevrangebylex(self, key: str | bytes, max: str, min: str, *, offset: int | None = ..., count: int | None = ..., encoding: str | None = ...) -> list[Any]: ...
|
||||
async def zrevrangebyscore(self, key: str | bytes, max: float | str, min: float | str, *, offset: int | None = ..., count: int | None = ..., withscores: bool = ..., encoding: str | None = ...) -> list[Any]: ...
|
||||
async def zrevrank(self, key: str | bytes, member: str | bytes) -> int | None: ...
|
||||
async def zscan(self, key: str | bytes, cursor: int = ..., match: str | None = ..., count: int | None = ...) -> tuple[int, list[Any]]: ...
|
||||
async def zscore(self, key: str | bytes, member: str | bytes) -> float | None: ...
|
||||
async def zunionstore(self, destkey: str | bytes, numkeys: int, key: str | bytes, *keys: str | bytes, weights: list[int] | None = ..., aggregate: str | None = ...) -> int: ...
|
||||
async def bzpopmax(self, key: str | bytes, *keys: str | bytes, timeout: int = ..., encoding: str | None = ...) -> list[Any] | None: ...
|
||||
async def bzpopmin(self, key: str | bytes, *keys: str | bytes, timeout: int = ..., encoding: str | None = ...) -> list[Any] | None: ...
|
||||
|
||||
# ==================== Generic/Key Commands ====================
|
||||
async def delete(self, key: str | bytes, *keys: str | bytes) -> int: ...
|
||||
async def dump(self, key: str | bytes) -> bytes | None: ...
|
||||
async def exists(self, key: str | bytes, *keys: str | bytes) -> int: ...
|
||||
async def expire(self, key: str | bytes, timeout: int) -> bool: ...
|
||||
async def expireat(self, key: str | bytes, timestamp: int) -> bool: ...
|
||||
async def keys(self, pattern: str | bytes, *, encoding: str | None = ...) -> list[Any]: ...
|
||||
async def migrate(self, host: str, port: int, key: str | bytes, dest_db: int, timeout: int, *, copy: bool = ..., replace: bool = ...) -> bool: ...
|
||||
async def move(self, key: str | bytes, db: int) -> bool: ...
|
||||
async def object_encoding(self, key: str | bytes) -> bytes | str: ...
|
||||
async def object_idletime(self, key: str | bytes) -> int | None: ...
|
||||
async def object_refcount(self, key: str | bytes) -> int | None: ...
|
||||
async def persist(self, key: str | bytes) -> bool: ...
|
||||
async def pexpire(self, key: str | bytes, timeout: int) -> bool: ...
|
||||
async def pexpireat(self, key: str | bytes, timestamp: int) -> bool: ...
|
||||
async def pttl(self, key: str | bytes) -> int: ...
|
||||
async def randomkey(self, *, encoding: str | None = ...) -> bytes | str | None: ...
|
||||
async def rename(self, key: str | bytes, newkey: str | bytes) -> bool: ...
|
||||
async def renamenx(self, key: str | bytes, newkey: str | bytes) -> bool: ...
|
||||
async def restore(self, key: str | bytes, ttl: int, value: bytes) -> bool: ...
|
||||
async def scan(self, cursor: int = ..., match: str | None = ..., count: int | None = ...) -> tuple[int, list[Any]]: ...
|
||||
async def sort(self, key: str | bytes, *get_patterns: str | bytes, by: str | None = ..., offset: int | None = ..., count: int | None = ..., asc: bool | None = ..., alpha: bool = ..., store: str | bytes | None = ...) -> list[Any]: ...
|
||||
async def touch(self, key: str | bytes, *keys: str | bytes) -> int: ...
|
||||
async def ttl(self, key: str | bytes) -> int: ...
|
||||
async def type(self, key: str | bytes) -> bytes: ...
|
||||
async def unlink(self, key: str | bytes, *keys: str | bytes) -> int: ...
|
||||
async def wait(self, numslaves: int, timeout: int) -> int: ...
|
||||
|
||||
# ==================== Pub/Sub Commands ====================
|
||||
async def publish(self, channel: str | bytes, message: str | bytes) -> int: ...
|
||||
async def publish_json(self, channel: str | bytes, obj: Any) -> int: ...
|
||||
async def pubsub_channels(self, pattern: str | None = ..., *, encoding: str | None = ...) -> list[Any]: ...
|
||||
async def pubsub_numpat(self) -> int: ...
|
||||
async def pubsub_numsub(self, *channels: str | bytes) -> dict[Any, int]: ...
|
||||
async def subscribe(self, channel: str | bytes, *channels: str | bytes) -> list[Any]: ...
|
||||
async def unsubscribe(self, channel: str | bytes | None = ..., *channels: str | bytes) -> list[Any]: ...
|
||||
async def psubscribe(self, pattern: str | bytes, *patterns: str | bytes) -> list[Any]: ...
|
||||
async def punsubscribe(self, pattern: str | bytes | None = ..., *patterns: str | bytes) -> list[Any]: ...
|
||||
|
||||
# ==================== Transaction Commands ====================
|
||||
async def multi_exec(self, *commands: Any) -> list[Any]: ...
|
||||
async def watch(self, key: str | bytes, *keys: str | bytes) -> bool: ...
|
||||
async def unwatch(self) -> bool: ...
|
||||
|
||||
# ==================== Scripting Commands ====================
|
||||
async def eval(self, script: str | bytes, keys: list[str | bytes] | None = ..., args: list[Any] | None = ...) -> Any: ...
|
||||
async def evalsha(self, digest: str | bytes, keys: list[str | bytes] | None = ..., args: list[Any] | None = ...) -> Any: ...
|
||||
async def script_exists(self, digest: str | bytes, *digests: str | bytes) -> list[bool]: ...
|
||||
async def script_flush(self) -> bool: ...
|
||||
async def script_kill(self) -> bool: ...
|
||||
async def script_load(self, script: str | bytes) -> bytes: ...
|
||||
|
||||
# ==================== Server Commands ====================
|
||||
async def bgrewriteaof(self) -> bytes: ...
|
||||
async def bgsave(self) -> bytes: ...
|
||||
async def client_getname(self, *, encoding: str | None = ...) -> bytes | str | None: ...
|
||||
async def client_kill(self, *args: Any) -> int: ...
|
||||
async def client_list(self, *, encoding: str | None = ...) -> bytes | str: ...
|
||||
async def client_pause(self, timeout: int) -> bool: ...
|
||||
async def client_reply(self, *args: Any) -> bool: ...
|
||||
async def client_setname(self, name: str | bytes) -> bool: ...
|
||||
async def command(self) -> list[Any]: ...
|
||||
async def command_count(self) -> int: ...
|
||||
async def command_getkeys(self, command: str | bytes, *args: Any, encoding: str = ...) -> list[Any]: ...
|
||||
async def command_info(self, command: str | bytes, *commands: str | bytes) -> list[Any]: ...
|
||||
async def config_get(self, parameter: str | bytes = ..., *, encoding: str | None = ...) -> dict[Any, Any]: ...
|
||||
async def config_resetstat(self) -> bool: ...
|
||||
async def config_rewrite(self) -> bool: ...
|
||||
async def config_set(self, parameter: str | bytes, value: str | bytes) -> bool: ...
|
||||
async def dbsize(self) -> int: ...
|
||||
async def debug_object(self, key: str | bytes) -> bytes: ...
|
||||
async def debug_sleep(self, timeout: float) -> None: ...
|
||||
async def flushall(self, async_mode: bool = ...) -> bool: ...
|
||||
async def flushdb(self, async_mode: bool = ...) -> bool: ...
|
||||
async def info(self, section: str | None = ..., *, encoding: str | None = ...) -> bytes | str | dict[str, Any]: ...
|
||||
async def lastsave(self) -> int: ...
|
||||
async def monitor(self) -> Any: ...
|
||||
async def ping(self, message: str | bytes | None = ..., *, encoding: str | None = ...) -> bytes | str: ...
|
||||
async def save(self) -> bool: ...
|
||||
async def shutdown(self, save: bool = ...) -> None: ...
|
||||
async def slaveof(self, host: str, port: int) -> bool: ...
|
||||
async def slowlog_get(self, count: int | None = ...) -> list[Any]: ...
|
||||
async def slowlog_len(self) -> int: ...
|
||||
async def slowlog_reset(self) -> bool: ...
|
||||
async def sync(self) -> Any: ...
|
||||
async def time(self) -> tuple[int, int]: ...
|
||||
async def role(self) -> list[Any]: ...
|
||||
|
||||
# ==================== Geo Commands ====================
|
||||
async def geoadd(self, key: str | bytes, longitude: float, latitude: float, member: str | bytes, *latlng_member: Any) -> int: ...
|
||||
async def geodist(self, key: str | bytes, member1: str | bytes, member2: str | bytes, unit: str | None = ...) -> float | None: ...
|
||||
async def geohash(self, key: str | bytes, member: str | bytes, *members: str | bytes, encoding: str | None = ...) -> list[Any]: ...
|
||||
async def geopos(self, key: str | bytes, member: str | bytes, *members: str | bytes) -> list[Any]: ...
|
||||
async def georadius(self, key: str | bytes, longitude: float, latitude: float, radius: float, unit: str, *, count: int | None = ..., sort: str | None = ..., encoding: str | None = ...) -> list[Any]: ...
|
||||
async def georadiusbymember(self, key: str | bytes, member: str | bytes, radius: float, unit: str, *, count: int | None = ..., sort: str | None = ..., encoding: str | None = ...) -> list[Any]: ...
|
||||
|
||||
# ==================== HyperLogLog Commands ====================
|
||||
async def pfadd(self, key: str | bytes, element: str | bytes, *elements: str | bytes) -> int: ...
|
||||
async def pfcount(self, key: str | bytes, *keys: str | bytes) -> int: ...
|
||||
async def pfmerge(self, destkey: str | bytes, sourcekey: str | bytes, *sourcekeys: str | bytes) -> bool: ...
|
||||
|
||||
# ==================== Stream Commands ====================
|
||||
async def xack(self, key: str | bytes, group: str | bytes, id: str | bytes, *ids: str | bytes) -> int: ...
|
||||
async def xadd(self, key: str | bytes, id: str | bytes, *field_value_pairs: Any) -> bytes: ...
|
||||
async def xclaim(self, key: str | bytes, group: str | bytes, consumer: str | bytes, min_idle_time: int, id: str | bytes, *ids: str | bytes) -> list[Any]: ...
|
||||
async def xdel(self, key: str | bytes, id: str | bytes, *ids: str | bytes) -> int: ...
|
||||
async def xgroup_create(self, key: str | bytes, group: str | bytes, id: str | bytes = ...) -> bool: ...
|
||||
async def xgroup_delconsumer(self, key: str | bytes, group: str | bytes, consumer: str | bytes) -> int: ...
|
||||
async def xgroup_destroy(self, key: str | bytes, group: str | bytes) -> int: ...
|
||||
async def xgroup_setid(self, key: str | bytes, group: str | bytes, id: str | bytes) -> bool: ...
|
||||
async def xlen(self, key: str | bytes) -> int: ...
|
||||
async def xpending(self, key: str | bytes, group: str | bytes) -> list[Any]: ...
|
||||
async def xrange(self, key: str | bytes, start: str | bytes, end: str | bytes, count: int | None = ...) -> list[Any]: ...
|
||||
async def xread(self, *keys_ids: Any, count: int | None = ..., timeout: int | None = ...) -> list[Any]: ...
|
||||
async def xread_group(self, group: str | bytes, consumer: str | bytes, *keys_ids: Any, count: int | None = ..., timeout: int | None = ...) -> list[Any]: ...
|
||||
async def xrevrange(self, key: str | bytes, end: str | bytes, start: str | bytes, count: int | None = ...) -> list[Any]: ...
|
||||
async def xtrim(self, key: str | bytes, maxlen: int, approximate: bool = ...) -> int: ...
|
||||
|
||||
# ==================== Connection Pool Properties ====================
|
||||
@property
|
||||
def db(self) -> int: ...
|
||||
@property
|
||||
def closed(self) -> bool: ...
|
||||
@property
|
||||
def address(self) -> tuple[str, int] | str: ...
|
||||
@property
|
||||
def connection(self) -> Any: ...
|
||||
@property
|
||||
def in_transaction(self) -> bool: ...
|
||||
|
||||
|
||||
async def create_redis(
|
||||
address: str | tuple[str, int],
|
||||
*,
|
||||
db: int = ...,
|
||||
password: str | None = ...,
|
||||
encoding: str | None = ...,
|
||||
ssl: Any = ...,
|
||||
minsize: int = ...,
|
||||
maxsize: int = ...,
|
||||
parser: Any = ...,
|
||||
create_connection_timeout: float | None = ...,
|
||||
) -> Redis: ...
|
||||
|
||||
|
||||
async def create_redis_pool(
|
||||
address: str | tuple[str, int],
|
||||
*,
|
||||
db: int = ...,
|
||||
password: str | None = ...,
|
||||
encoding: str | None = ...,
|
||||
ssl: Any = ...,
|
||||
minsize: int = ...,
|
||||
maxsize: int = ...,
|
||||
parser: Any = ...,
|
||||
create_connection_timeout: float | None = ...,
|
||||
) -> Redis: ...
|
||||
|
||||
|
||||
# Errors and Exceptions
|
||||
class ProtocolError(RedisError): ...
|
||||
class ReplyError(RedisError): ...
|
||||
class ConnectionClosedError(RedisError): ...
|
||||
class ConnectionForcedCloseError(RedisError): ...
|
||||
class MasterNotFoundError(RedisError): ...
|
||||
class MultiExecError(RedisError): ...
|
||||
class PipelineError(RedisError): ...
|
||||
class ReadOnlyError(RedisError): ...
|
||||
class MaxClientsError(RedisError): ...
|
||||
class AuthError(RedisError): ...
|
||||
class ChannelClosedError(RedisError): ...
|
||||
class WatchVariableError(RedisError): ...
|
||||
class PoolClosedError(RedisError): ...
|
||||
class SlaveNotFoundError(RedisError): ...
|
||||
class MasterReplyError(RedisError): ...
|
||||
class SlaveReplyError(RedisError): ...
|
||||
|
||||
|
||||
__all__: tuple[str, ...]
|
||||
95
typings/aioredlock/__init__.pyi
Normal file
95
typings/aioredlock/__init__.pyi
Normal file
@@ -0,0 +1,95 @@
|
||||
from typing import Any, TypeAlias
|
||||
import ssl
|
||||
|
||||
_RedisAddress: TypeAlias = tuple[str, int] | list[tuple[str, int]]
|
||||
_RedisConnection: TypeAlias = dict[str, Any] | str | _RedisAddress | Any
|
||||
|
||||
|
||||
class AioredlockError(Exception): ...
|
||||
|
||||
|
||||
class LockError(AioredlockError): ...
|
||||
|
||||
|
||||
class LockAcquiringError(LockError): ...
|
||||
|
||||
|
||||
class LockRuntimeError(LockError): ...
|
||||
|
||||
|
||||
class SentinelConfigError(Exception): ...
|
||||
|
||||
|
||||
class Sentinel:
|
||||
master: str
|
||||
connection: list[tuple[str, int]]
|
||||
redis_kwargs: dict[str, Any]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
connection: dict[str, Any] | str | _RedisAddress,
|
||||
master: str | None = ...,
|
||||
password: str | None = ...,
|
||||
db: int | None = ...,
|
||||
ssl_context: ssl.SSLContext | bool | None = ...,
|
||||
) -> None: ...
|
||||
|
||||
async def get_sentinel(self) -> Any: ...
|
||||
async def get_master(self) -> Any: ...
|
||||
|
||||
|
||||
class Lock:
|
||||
lock_manager: Aioredlock
|
||||
resource: str
|
||||
id: str
|
||||
lock_timeout: float | None
|
||||
valid: bool
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
lock_manager: Aioredlock,
|
||||
resource: str,
|
||||
id: str,
|
||||
lock_timeout: float | None = ...,
|
||||
valid: bool = ...,
|
||||
) -> None: ...
|
||||
|
||||
async def __aenter__(self) -> Lock: ...
|
||||
async def __aexit__(self, exc_type: type[BaseException] | None, exc: BaseException | None, tb: Any) -> None: ...
|
||||
async def extend(self) -> None: ...
|
||||
async def release(self) -> None: ...
|
||||
async def is_locked(self) -> bool: ...
|
||||
|
||||
|
||||
class Aioredlock:
|
||||
redis_connections: list[_RedisConnection]
|
||||
retry_count: int
|
||||
retry_delay_min: float
|
||||
retry_delay_max: float
|
||||
internal_lock_timeout: float
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
redis_connections: list[_RedisConnection] = ...,
|
||||
retry_count: int = ...,
|
||||
retry_delay_min: float = ...,
|
||||
retry_delay_max: float = ...,
|
||||
internal_lock_timeout: float = ...,
|
||||
) -> None: ...
|
||||
|
||||
async def lock(
|
||||
self,
|
||||
resource: str,
|
||||
lock_timeout: float | None = ...,
|
||||
lock_identifier: str | None = ...,
|
||||
) -> Lock: ...
|
||||
|
||||
async def extend(self, lock: Lock, lock_timeout: float | None = ...) -> None: ...
|
||||
async def unlock(self, lock: Lock) -> None: ...
|
||||
async def is_locked(self, resource_or_lock: str | Lock) -> bool: ...
|
||||
async def destroy(self) -> None: ...
|
||||
async def get_active_locks(self) -> list[Lock]: ...
|
||||
async def get_lock(self, resource: str, lock_identifier: str) -> Lock: ...
|
||||
|
||||
|
||||
__all__: tuple[str, ...]
|
||||
55
typings/punq/__init__.pyi
Normal file
55
typings/punq/__init__.pyi
Normal file
@@ -0,0 +1,55 @@
|
||||
from enum import Enum
|
||||
from typing import Any, Callable, List, Type, TypeVar, overload
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
class MissingDependencyException(Exception): ...
|
||||
class MissingDependencyError(MissingDependencyException): ...
|
||||
class InvalidRegistrationException(Exception): ...
|
||||
class InvalidRegistrationError(InvalidRegistrationException): ...
|
||||
class InvalidForwardReferenceException(Exception): ...
|
||||
class InvalidForwardReferenceError(InvalidForwardReferenceException): ...
|
||||
|
||||
class Scope(Enum):
|
||||
transient = 0
|
||||
singleton = 1
|
||||
|
||||
class _Empty: ...
|
||||
|
||||
empty: _Empty
|
||||
|
||||
class Container:
|
||||
def __init__(self) -> None: ...
|
||||
|
||||
@overload
|
||||
def register(
|
||||
self,
|
||||
service: Type[T],
|
||||
factory: Type[T] | Callable[..., T] | _Empty = ...,
|
||||
instance: T | _Empty = ...,
|
||||
scope: Scope = ...,
|
||||
**kwargs: Any,
|
||||
) -> Container: ...
|
||||
|
||||
@overload
|
||||
def register(
|
||||
self,
|
||||
service: str,
|
||||
factory: Callable[..., Any] | _Empty = ...,
|
||||
instance: Any | _Empty = ...,
|
||||
scope: Scope = ...,
|
||||
**kwargs: Any,
|
||||
) -> Container: ...
|
||||
|
||||
def register(
|
||||
self,
|
||||
service: Type[T] | str,
|
||||
factory: Type[T] | Callable[..., T] | _Empty = ...,
|
||||
instance: T | _Empty = ...,
|
||||
scope: Scope = ...,
|
||||
**kwargs: Any,
|
||||
) -> Container: ...
|
||||
|
||||
def resolve(self, service_key: Type[T], **kwargs: Any) -> T: ...
|
||||
def resolve_all(self, service: Type[T], **kwargs: Any) -> List[T]: ...
|
||||
def instantiate(self, service_key: Type[T], **kwargs: Any) -> T: ...
|
||||
Reference in New Issue
Block a user