diff --git a/AMQ_ADAPTER_DEVELOPER_GUIDE.md b/AMQ_ADAPTER_DEVELOPER_GUIDE.md new file mode 100644 index 0000000..85309b4 --- /dev/null +++ b/AMQ_ADAPTER_DEVELOPER_GUIDE.md @@ -0,0 +1,663 @@ +# AMQ Adapter Developer Guide + +## Overview + +The AMQ Adapter library enables FastAPI-based Business Services to communicate with the CleverMicro platform via RabbitMQ. This guide demonstrates how to build your own service using the AMQ Adapter, following the pattern established by the OTDemo application. + +## Table of Contents + +1. [Architecture Overview](#architecture-overview) +2. [Prerequisites](#prerequisites) +3. [Project Structure](#project-structure) +4. [Implementation Steps](#implementation-steps) +5. [Configuration](#configuration) +6. [Creating Your Adapter](#creating-your-adapter) +7. [Frontend Integration](#frontend-integration) +8. [Backend Processing](#backend-processing) +9. [Testing and Deployment](#testing-and-deployment) +10. [Advanced Features](#advanced-features) + +## Architecture Overview + +The AMQ Adapter follows a layered architecture: + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Business Service │ +├─────────────────────────────────────────────────────────────┤ +│ FastAPI Endpoints │ Your Custom Adapter │ Backend │ +│ │ │ Processing │ +└─────────────────────┼───────────────────────┼───────────────┘ + │ │ +┌─────────────────────┼───────────────────────┼───────────────┐ +│ AMQ Adapter Library │ │ +├─────────────────────────────────────────────────────────────┤ +│ CleverThisServiceAdapter │ AMQService │ Router │ +│ (Base Class) │ │ │ +└─────────────────────┼───────────────────────┼───────────────┘ + │ │ +┌─────────────────────┼───────────────────────┼───────────────┐ +│ RabbitMQ │ │ +│ Exchanges │ Queues │ Routing Keys │ │ +└─────────────────────────────────────────────────────────────┘ +``` + +### Key Components + +- **Your Custom Adapter**: Extends `CleverThisServiceAdapter` to handle service-specific logic +- **AMQService**: Manages RabbitMQ connections and message routing +- **Router**: Routes incoming AMQP messages to appropriate endpoints +- **Configuration**: Defines service settings, routing, and RabbitMQ connection details + +## Prerequisites + +Before building your service, ensure you have: + +1. **Python 3.8+** installed +2. **RabbitMQ** server running (or access to CleverMicro platform) +3. **FastAPI** knowledge +4. **AMQ Adapter library** installed in your project + +### Dependencies + +Add the following to your `requirements.txt`: + +```txt +fastapi>=0.111.0 +uvicorn[standard]>=0.29.0 +python-multipart>=0.0.20 +aio-pika>=9.0.0 +pydantic>=2.0.0 +# Add your AMQ Adapter library dependency +``` + +## Project Structure + +Follow this recommended structure for your Business Service: + +``` +your-business-service/ +├── main.py # FastAPI application entry point +├── requirements.txt # Python dependencies +├── config/ +│ ├── frontend.properties # Frontend service configuration +│ └── backend.properties # Backend service configuration +├── adapter/ +│ └── your_adapter.py # Your custom AMQ adapter +├── backend/ +│ └── backend_processor.py # Backend message processing +└── tests/ + └── test_your_service.py +``` + +## Implementation Steps + +### Step 1: Create Your Configuration Files + +Create configuration files for both frontend and backend services. + +**`config/frontend.properties`**: +```properties +# ==================================================================== +# CleverMicro AMQ Adapter settings +# ==================================================================== +[CleverMicro-AMQ] +cm.amq-adapter.service-name=your-service +cm.amq-adapter.generator-id=1 +cm.amq-adapter.route-mapping=your-service:your-exchange:your-queue-1:api.cm.dev.cleverthis.localhost.test.python.frontend.#:5:0 +cm.amq-adapter.require-authenticated-user=false +cm.amq-adapter.callbacks-enabled=True + +# CleverMicro AMQ Adapter dispatcher settings +cm.dispatch.use-dlq=true +cm.dispatch.amq-host=localhost +cm.dispatch.amq-port=5672 +cm.dispatch.amq-port-tls=5671 +cm.dispatch.download-dir=/tmp/downloaded_files + +# Backpressure settings +cm.backpressure.threshold=1 +cm.backpressure.time-window=150 +cm.backpressure.cpu-overload-duration=300 +cm.backpressure.cpu-idle-duration=300 +``` + +**`config/backend.properties`**: +```properties +# ==================================================================== +# CleverMicro AMQ Adapter settings +# ==================================================================== +[CleverMicro-AMQ] +cm.amq-adapter.service-name=your-service-backend +cm.amq-adapter.generator-id=1 +cm.amq-adapter.route-mapping=your-service-backend:your-exchange:your-queue-2:your-service-backend.#:5:0 +cm.amq-adapter.require-authenticated-user=false +cm.amq-adapter.callbacks-enabled=False + +# CleverMicro AMQ Adapter dispatcher settings +cm.dispatch.use-dlq=true +cm.dispatch.amq-host=localhost +cm.dispatch.amq-port=5672 +cm.dispatch.amq-port-tls=5671 +cm.dispatch.download-dir=/tmp/downloaded_files + +# Backpressure settings +cm.backpressure.threshold=1 +cm.backpressure.time-window=150 +cm.backpressure.cpu-overload-duration=300 +cm.backpressure.cpu-idle-duration=300 +``` + +### Step 2: Create Your Custom Adapter + +Create your custom adapter by extending `CleverThisServiceAdapter`: + +```python +""" +Your Business Service AMQP Adapter for handling API requests via AMQP. +This adapter is specific to your business service and handles service-specific logic. +""" + +from threading import Thread +from typing import Any, List + +from fastapi.routing import APIRoute + +from amqp.adapter.cleverthis_service_adapter import CleverThisServiceAdapter +from amqp.config.amq_configuration import AMQConfiguration +from amqp.service.amq_service import AMQService + + +class YourServiceAmqpAdapter(CleverThisServiceAdapter): + """ + Your Business Service AMQP Adapter. Implements service-specific logic that overrides + common logic defined by the AMQ Adapter library. + + Key responsibilities: + 1. Handle authentication and user management + 2. Process incoming AMQP messages + 3. Route messages to appropriate endpoints + 4. Manage service-specific business logic + """ + + def __init__(self, amq_configuration: AMQConfiguration, routes: List[APIRoute]): + super().__init__(amq_configuration, routes=routes, session=None) + self.amq_service: AMQService = AMQService(amq_configuration, self) + self.thread = Thread(target=self.amq_service.run) + self.thread.start() + + async def get_current_user(self, token: str) -> str: + """ + Override the default get_current_user method to handle authentication. + In this example, we simply return the token, but you should implement + proper JWT token validation and user extraction. + + :param token: JWT token from the AMQP message + :return: User identifier or user object + """ + # TODO: Implement proper JWT token validation + # Example: return await your_auth_service.validate_token(token) + return token + + def get_endpoint(self, endpoint: Any) -> Any: + """ + Override to provide custom endpoint selection logic. + For example, you might want to use JSON wrapper endpoints + or implement custom routing logic. + + :param endpoint: The original endpoint callable + :return: The endpoint callable to use (original or modified) + """ + return endpoint +``` + +### Step 3: Create Your FastAPI Application + +Create your main FastAPI application with endpoints: + +```python +""" +Your Business Service FastAPI Application +""" + +import os +from fastapi import FastAPI, File, Form, UploadFile +from fastapi.routing import APIRoute + +from amqp.config.amq_configuration import AMQConfiguration +from adapter.your_adapter import YourServiceAmqpAdapter + + +# Create FastAPI application +app = FastAPI( + title="Your Business Service", + description="A FastAPI application using AMQ Adapter for CleverMicro communication", + version="1.0.0" +) + +# Initialize your AMQ adapter +# This should be done after all routes are defined +your_adapter = None + + +@app.post("/api/v1/items") +async def create_item( + name: str = Form(...), + description: str = Form(...), + file: UploadFile = File(...) +): + """ + Create a new item with file upload. + This endpoint will be available via both REST and AMQP. + """ + # Your business logic here + result = { + "message": "Item created successfully", + "name": name, + "description": description, + "filename": file.filename + } + + # Example: Send a command to backend via AMQP + if your_adapter: + your_adapter.amq_service.command( + "your-service-backend", + "process_item", + item_name=name, + item_description=description + ) + + return result + + +@app.get("/api/v1/items") +async def get_items(): + """ + Retrieve all items. + This endpoint will be available via both REST and AMQP. + """ + # Your business logic here + items = [ + {"id": "1", "name": "Item 1", "description": "Description 1"}, + {"id": "2", "name": "Item 2", "description": "Description 2"} + ] + + return {"items": items} + + +@app.get("/health") +async def health_check(): + """ + Health check endpoint. + """ + return {"status": "healthy", "service": "your-business-service"} + + +# Initialize the AMQ adapter after all routes are defined +def initialize_amq_adapter(): + global your_adapter + config = AMQConfiguration("config/frontend.properties") + your_adapter = YourServiceAmqpAdapter(config, app.routes) + + +# Initialize adapter when the application starts +@app.on_event("startup") +async def startup_event(): + initialize_amq_adapter() + + +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=8000) +``` + +### Step 4: Create Backend Processing + +Create a backend processor for handling AMQP messages: + +```python +""" +Backend processor for your business service. +Handles AMQP messages and performs background processing. +""" + +import asyncio +from amqp.config.amq_configuration import AMQConfiguration +from amqp.model.model import AMQRoute, DataMessage +from adapter.your_adapter import YourServiceAmqpAdapter + + +def process_item_static(item_name: str, item_description: str) -> str: + """ + Static function to process an item. + """ + print(f"STATIC Processing item: {item_name} - {item_description}") + return f"STATIC PROCESSED: {item_name}" + + +class BackendProcessor: + """ + Backend processor class for handling business logic. + """ + + def process_item(self, item_name: str, item_description: str) -> str: + """ + Process an item using instance method. + """ + print(f"CLASS Processing item: {item_name} - {item_description}") + return f"CLASS PROCESSED: {item_name}" + + +async def processing_loop(): + """ + Main processing loop for handling AMQP messages. + """ + # Wait for AMQ adapter to initialize + while not otadapter.amq_service.future.done(): + await asyncio.sleep(0.1) + + # Find the route for backend processing + route: AMQRoute = otadapter.amq_service.router.find_route_by_service("your-service-backend") + generator = otadapter.amq_service.data_message_generator(route) + + print(f"Data message generator created: {generator}") + + # Start the generator + message = await generator.asend(None) + + # Process messages + while True: + print(f"Received message: {message}") + if message is None: + break + + data_message: DataMessage = message + + # Extract message attributes + print(f"Message attributes: {data_message.method()} {data_message.domain()} {data_message.path()}") + + # Process based on message path + if data_message.path() == "process_item_static": + item_name = data_message._base64body.decode("utf-8") + process_item_static(item_name, "Static processing") + elif data_message.path() == "process_item": + item_name = data_message._base64body.decode("utf-8") + processor = BackendProcessor() + processor.process_item(item_name, "Class processing") + + # Get next message + message = await generator.asend(True) + + +if __name__ == "__main__": + # Initialize backend adapter + otadapter = YourServiceAmqpAdapter(AMQConfiguration("config/backend.properties"), []) + + # Run the processing loop in the AMQ service event loop + asyncio.run_coroutine_threadsafe(processing_loop(), otadapter.amq_service.loop) + + # Keep the program running + otadapter.thread.join() +``` + +## Configuration + +### Configuration Parameters + +The configuration file contains several key sections: + +#### AMQ Adapter Settings (`cm.amq-adapter.*`) + +- **`service-name`**: Unique identifier for your service +- **`generator-id`**: Unique ID for message generation (usually 1) +- **`route-mapping`**: RabbitMQ routing configuration +- **`require-authenticated-user`**: Whether authentication is required +- **`callbacks-enabled`**: Whether to enable callback functionality + +#### Dispatch Settings (`cm.dispatch.*`) + +- **`amq-host`**: RabbitMQ host address +- **`amq-port`**: RabbitMQ port (5672 for non-TLS, 5671 for TLS) +- **`use-dlq`**: Whether to use Dead Letter Queue +- **`download-dir`**: Directory for downloaded files + +#### Backpressure Settings (`cm.backpressure.*`) + +- **`threshold`**: Backpressure threshold value +- **`time-window`**: Time window for backpressure calculation +- **`cpu-overload-duration`**: Duration for CPU overload detection +- **`cpu-idle-duration`**: Duration for CPU idle detection + +### Route Mapping Format + +The route mapping follows this format: +``` +service-name:exchange:queue:routing-key:prefetch:durable +``` + +Example: +``` +your-service:your-exchange:your-queue-1:api.cm.dev.cleverthis.localhost.test.python.frontend.#:5:0 +``` + +## Frontend Integration + +### Key Integration Points + +1. **Adapter Initialization**: Initialize your adapter after defining all FastAPI routes +2. **Message Handling**: The adapter automatically routes AMQP messages to your endpoints +3. **Command Sending**: Use `amq_service.command()` to send commands to backend services + +### Example Integration + +```python +# In your FastAPI application +@app.post("/api/v1/process") +async def process_data(data: dict): + # Your business logic + result = {"status": "processing", "data": data} + + # Send command to backend + your_adapter.amq_service.command( + "your-service-backend", + "process_data", + **data + ) + + return result +``` + +## Backend Processing + +### Message Processing Pattern + +1. **Initialize Adapter**: Create adapter with backend configuration +2. **Wait for Connection**: Wait for RabbitMQ connection to be established +3. **Find Route**: Locate the route for your backend service +4. **Create Generator**: Create message generator for the route +5. **Process Messages**: Continuously process incoming messages + +### Example Processing + +```python +async def process_messages(): + # Wait for connection + while not adapter.amq_service.future.done(): + await asyncio.sleep(0.1) + + # Get route and generator + route = adapter.amq_service.router.find_route_by_service("your-service-backend") + generator = adapter.amq_service.data_message_generator(route) + + # Process messages + message = await generator.asend(None) + while message: + # Process the message + await handle_message(message) + message = await generator.asend(True) +``` + +## Testing and Deployment + +### Local Testing + +1. **Start RabbitMQ**: Ensure RabbitMQ is running locally +2. **Run Frontend**: Start your FastAPI application +3. **Run Backend**: Start your backend processor +4. **Test Endpoints**: Use tools like curl or Postman to test REST endpoints +5. **Monitor AMQP**: Check RabbitMQ management interface for message flow + +### Docker Deployment + +Create a `Dockerfile` for your service: + +```dockerfile +FROM python:3.9-slim + +WORKDIR /app + +COPY requirements.txt . +RUN pip install -r requirements.txt + +COPY . . + +EXPOSE 8000 + +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] +``` + +### Docker Compose + +Create a `docker-compose.yml` for local development: + +```yaml +version: '3.8' + +services: + rabbitmq: + image: rabbitmq:3-management + ports: + - "5672:5672" + - "15672:15672" + environment: + RABBITMQ_DEFAULT_USER: guest + RABBITMQ_DEFAULT_PASS: guest + + your-service-frontend: + build: . + ports: + - "8000:8000" + environment: + - RABBITMQ_HOST=rabbitmq + depends_on: + - rabbitmq + + your-service-backend: + build: . + command: ["python", "backend/backend_processor.py"] + environment: + - RABBITMQ_HOST=rabbitmq + depends_on: + - rabbitmq +``` + +## Advanced Features + +### Authentication + +Implement proper JWT token validation in your adapter: + +```python +async def get_current_user(self, token: str) -> User: + """ + Validate JWT token and extract user information. + """ + try: + # Decode and validate JWT token + payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"]) + user_id = payload.get("sub") + + # Fetch user from database + user = await get_user_by_id(user_id) + return user + except jwt.InvalidTokenError: + raise HTTPException(status_code=401, detail="Invalid token") +``` + +### Error Handling + +Implement comprehensive error handling: + +```python +async def handle_message(self, message: DataMessage): + """ + Handle incoming messages with proper error handling. + """ + try: + # Process message + result = await self.process_message(message) + return result + except ValidationError as e: + logging.error(f"Validation error: {e}") + # Handle validation errors + except ProcessingError as e: + logging.error(f"Processing error: {e}") + # Handle processing errors + except Exception as e: + logging.error(f"Unexpected error: {e}") + # Handle unexpected errors +``` + +### Monitoring and Logging + +Implement proper logging and monitoring: + +```python +import logging +from opentelemetry import trace, metrics + +# Configure logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +# Configure tracing +tracer = trace.get_tracer(__name__) + +# Configure metrics +meter = metrics.get_meter(__name__) +message_counter = meter.create_counter("messages_processed_total") + +async def process_message(self, message: DataMessage): + """ + Process message with tracing and metrics. + """ + with tracer.start_as_current_span("process_message"): + logger.info(f"Processing message: {message.id()}") + + # Your processing logic here + + message_counter.add(1, {"service": "your-service"}) + logger.info(f"Message processed successfully: {message.id()}") +``` + +## Troubleshooting + +### Common Issues + +1. **Connection Errors**: Ensure RabbitMQ is running and accessible +2. **Route Not Found**: Check route mapping configuration +3. **Message Not Processed**: Verify endpoint method signatures match expected parameters +4. **Authentication Failures**: Check JWT token validation logic + +### Debugging Tips + +1. **Enable Debug Logging**: Set logging level to DEBUG +2. **Check RabbitMQ Management**: Use RabbitMQ management interface to monitor queues +3. **Trace Message Flow**: Use OpenTelemetry tracing to follow message flow +4. **Validate Configuration**: Ensure all configuration parameters are correct + +## Conclusion + +This guide provides a comprehensive overview of building a Business Service using the AMQ Adapter library. By following the patterns established in the OTDemo application, you can create robust, scalable services that integrate seamlessly with the CleverMicro platform. + +For more detailed information, refer to the AMQ Adapter library documentation and the OTDemo source code examples. diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md new file mode 100644 index 0000000..4feef14 --- /dev/null +++ b/DEPLOYMENT.md @@ -0,0 +1,182 @@ +# FastAPI Demo - Docker Swarm Deployment + +This guide explains how to deploy the FastAPI demo application to an existing Docker Swarm cluster. + +## Prerequisites + +- Docker Swarm cluster already running +- Docker CLI installed and configured +- Access to the swarm manager node + +## Files Created + +1. **`Dockerfile.demo`** - Docker image for the FastAPI application +2. **`docker-stack.yml`** - Docker Swarm stack configuration +3. **`prometheus.yml`** - Prometheus configuration for metrics collection +4. **`deploy-stack.sh`** - Automated deployment script + +## Quick Deployment + +### Option 1: Using the deployment script (Recommended) + +```bash +./deploy-stack.sh +``` + +This script will: +- Check if Docker is running +- Initialize Docker Swarm if needed +- Build the Docker image +- Deploy the stack +- Show service status and access URLs + +### Option 2: Manual deployment + +1. **Build the Docker image:** + ```bash + docker build -f Dockerfile.demo -t fastapi-demo:latest . + ``` + +2. **Deploy the stack:** + ```bash + docker stack deploy -c docker-stack.yml fastapi-demo + ``` + +## Services Deployed + +The stack deploys the following services: + +### 1. FastAPI Demo Application (`fastapi-demo`) +- **Ports:** 8000 (app), 8001 (metrics) +- **Image:** `fastapi-demo:latest` +- **Features:** + - FastAPI application with OpenTelemetry integration + - Prometheus metrics endpoint + - File upload functionality + - Health checks + - Resource limits and reservations + +### 2. Jaeger Tracing (Optional) +- **Ports:** 16686 (UI), 4317 (OTLP gRPC) +- **Image:** `jaegertracing/all-in-one:latest` +- **Purpose:** Distributed tracing visualization + +### 3. Prometheus Monitoring (Optional) +- **Ports:** 9090 (UI) +- **Image:** `prom/prometheus:latest` +- **Purpose:** Metrics collection and visualization + +## Access URLs + +After successful deployment: + +- **FastAPI Application:** http://localhost:8000 +- **FastAPI Documentation:** http://localhost:8000/docs +- **Prometheus Metrics:** http://localhost:8001/metrics +- **Jaeger UI:** http://localhost:16686 +- **Prometheus UI:** http://localhost:9090 + +## Configuration + +### Environment Variables + +The FastAPI demo service uses these environment variables: + +- `OTEL_EXPORTER_OTLP_ENDPOINT`: Jaeger OTLP endpoint (default: http://jaeger:4317) +- `OTEL_DEBUG_CONSOLE_TRACES`: Enable console trace output (default: false) +- `OTEL_EXPORTER_PROMETHEUS_HOST`: Prometheus metrics host (default: 0.0.0.0) +- `OTEL_EXPORTER_PROMETHEUS_PORT`: Prometheus metrics port (default: 8001) + +### Properties File + +The `otdemo/otdemo.properties` file is automatically included in the Docker image and available at the relative path `./otdemo/otdemo.properties` within the container. + +## Management Commands + +### View Stack Status +```bash +docker stack services fastapi-demo +``` + +### View Service Logs +```bash +docker service logs fastapi-demo_fastapi-demo +``` + +### Scale Services +```bash +docker service scale fastapi-demo_fastapi-demo=2 +``` + +### Update Stack +```bash +docker stack deploy -c docker-stack.yml fastapi-demo +``` + +### Remove Stack +```bash +docker stack rm fastapi-demo +``` + +## Monitoring + +### Health Checks +The FastAPI service includes health checks that verify the application is responding: +- **Endpoint:** `/docs` +- **Interval:** 30 seconds +- **Timeout:** 10 seconds +- **Retries:** 3 + +### Resource Limits +- **CPU:** 0.5 cores (limit), 0.25 cores (reservation) +- **Memory:** 512MB (limit), 256MB (reservation) + +### Metrics +The application exposes Prometheus metrics at `/metrics` including: +- `documents_uploaded_total` +- `documents_metadata_processed_total` +- `documents_retrieved_total` +- `internal_api_call_duration_seconds` + +## Troubleshooting + +### Service Not Starting +1. Check service logs: `docker service logs fastapi-demo_fastapi-demo` +2. Verify image was built: `docker images | grep fastapi-demo` +3. Check resource availability on nodes + +### Port Conflicts +If ports 8000, 8001, 16686, or 9090 are already in use, modify the `docker-stack.yml` file to use different ports. + +### Network Issues +The stack creates an overlay network `demo-network`. If you need to connect to external services, ensure they're on the same network or configure network routing appropriately. + +## Customization + +### Adding New Services +To add new services to the stack: + +1. Add the service definition to `docker-stack.yml` +2. Include it in the `demo-network` +3. Configure appropriate resource limits and health checks + +### Modifying Configuration +- **Application config:** Modify `demo.py` and rebuild the image +- **Prometheus config:** Modify `prometheus.yml` +- **Stack config:** Modify `docker-stack.yml` + +### Environment-Specific Deployments +Create environment-specific stack files: +- `docker-stack.dev.yml` +- `docker-stack.prod.yml` +- `docker-stack.staging.yml` + +## Security Considerations + +- The demo runs with minimal security for development purposes +- For production, consider: + - Using secrets for sensitive configuration + - Implementing proper authentication + - Using HTTPS/TLS + - Restricting network access + - Running with non-root user diff --git a/README.md b/README.md index 36ba7ce..9d0bc09 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,7 @@ python_multipart ``` Go to amq-dapter-python repo source code, and copy the service specific directory, e.g. `/cleverswarm` -directory to your service's sources. For the CleverSwarm service, the file is `cleverswarm/clever_swarm.py`, +directory to your service's sources. For the CleverSwarm service, the file is `cleverswarm/clever_swarm.py`, and contains the AMQ Adapter to CleverSwarm service custom facade, implemented as ```python class CleverSwarmAmqpAdapter(CleverThisServiceAdapter): @@ -94,7 +94,7 @@ to instatiate and initialize the AMQ adapter, add following line to the init rou ``` and ensure that the `adapter` stays in the context of the service, so it is not garbage collected. -At version 0.2.21, AMQ Adapter uses local configuration file, relative path to which is passed +At version 0.2.21, AMQ Adapter uses local configuration file, relative path to which is passed to the AMQConfiguration constructor. This may change in the future, when the central configuration service will become available in CleverMicro, but for now please ensure correct relative path wrt the current working directory. @@ -104,8 +104,7 @@ For the config entries, please refer to the wiki page: [AMQ Adapter Config](htt The AMQ Adapter uses the following environment variables to configure the service: -PARALLEL_WORKERS - number of parallel workers to use for processing incoming requests. This is currently mandatory value, -shall be greater than 0. +MAX_AVAILABILITY - For backpressure management, indicates max capacity for processing incoming requests. If set, shall be greater than 0. AMQ_VERBOSE_LOGGING - if set to 1, the adapter will indicate in the log next to the message itself also module name, line and function name where the message was generated @@ -148,4 +147,3 @@ To remove volumes as well: ```bash docker-compose down -v ``` - diff --git a/api-doc/Makefile b/api-doc/Makefile new file mode 100644 index 0000000..d4bb2cb --- /dev/null +++ b/api-doc/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/api-doc/README.md b/api-doc/README.md new file mode 100644 index 0000000..9f0153a --- /dev/null +++ b/api-doc/README.md @@ -0,0 +1,51 @@ +# AMQ Adapter Python API Documentation + +This directory contains the generated HTML API documentation for the AMQ Adapter Python library. + +## Viewing the Documentation + +1. **Open the main page**: Open `_build/html/index.html` in your web browser +2. **Browse modules**: Use the navigation menu to explore different modules: + - **Configuration** (`amqp.config`) - Configuration classes and settings + - **Adapter** (`amqp.adapter`) - Core adapter classes and utilities + - **Service** (`amqp.service`) - Service layer and message handling + - **Router** (`amqp.router`) - Routing and service discovery + - **Model** (`amqp.model`) - Data models and message structures + - **RabbitMQ** (`amqp.rabbitmq`) - RabbitMQ client implementations + +## Features + +- **Search functionality**: Use the search box to find specific classes, methods, or functions +- **Module index**: Browse all modules alphabetically +- **Class inheritance**: View class hierarchies and inheritance relationships +- **Source code**: Click "View page source" to see the actual Python code +- **Cross-references**: Navigate between related classes and methods + +## Rebuilding the Documentation + +To regenerate the documentation after code changes: + +```bash +cd api-doc +make html +``` + +## Documentation Structure + +The documentation is organized by module: + +- **amqp.config**: Configuration management and settings +- **amqp.adapter**: Core adapter functionality and service integration +- **amqp.service**: Service layer, message handling, and tracing +- **amqp.router**: Routing, service discovery, and queue management +- **amqp.model**: Data models, message structures, and serialization +- **amqp.rabbitmq**: RabbitMQ client implementations and utilities + +## Key Classes + +- `CleverThisServiceAdapter`: Base class for service adapters +- `AMQService`: Main service class for AMQP operations +- `AMQConfiguration`: Configuration management +- `DataMessage`: Message structure for AMQP communication +- `RouterConsumer`: Service discovery and routing +- `RabbitMQClient`: RabbitMQ connection management diff --git a/api-doc/_build/doctrees/environment.pickle b/api-doc/_build/doctrees/environment.pickle new file mode 100644 index 0000000..f4d2aae Binary files /dev/null and b/api-doc/_build/doctrees/environment.pickle differ diff --git a/api-doc/_build/doctrees/index.doctree b/api-doc/_build/doctrees/index.doctree new file mode 100644 index 0000000..56064c1 Binary files /dev/null and b/api-doc/_build/doctrees/index.doctree differ diff --git a/api-doc/_build/doctrees/modules/amqp.adapter.doctree b/api-doc/_build/doctrees/modules/amqp.adapter.doctree new file mode 100644 index 0000000..b7f6ef8 Binary files /dev/null and b/api-doc/_build/doctrees/modules/amqp.adapter.doctree differ diff --git a/api-doc/_build/doctrees/modules/amqp.config.doctree b/api-doc/_build/doctrees/modules/amqp.config.doctree new file mode 100644 index 0000000..fdf65ec Binary files /dev/null and b/api-doc/_build/doctrees/modules/amqp.config.doctree differ diff --git a/api-doc/_build/doctrees/modules/amqp.doctree b/api-doc/_build/doctrees/modules/amqp.doctree new file mode 100644 index 0000000..fc642b7 Binary files /dev/null and b/api-doc/_build/doctrees/modules/amqp.doctree differ diff --git a/api-doc/_build/doctrees/modules/amqp.model.doctree b/api-doc/_build/doctrees/modules/amqp.model.doctree new file mode 100644 index 0000000..b5663ac Binary files /dev/null and b/api-doc/_build/doctrees/modules/amqp.model.doctree differ diff --git a/api-doc/_build/doctrees/modules/amqp.rabbitmq.doctree b/api-doc/_build/doctrees/modules/amqp.rabbitmq.doctree new file mode 100644 index 0000000..25753ad Binary files /dev/null and b/api-doc/_build/doctrees/modules/amqp.rabbitmq.doctree differ diff --git a/api-doc/_build/doctrees/modules/amqp.router.doctree b/api-doc/_build/doctrees/modules/amqp.router.doctree new file mode 100644 index 0000000..e3e79f2 Binary files /dev/null and b/api-doc/_build/doctrees/modules/amqp.router.doctree differ diff --git a/api-doc/_build/doctrees/modules/amqp.service.doctree b/api-doc/_build/doctrees/modules/amqp.service.doctree new file mode 100644 index 0000000..13a13f4 Binary files /dev/null and b/api-doc/_build/doctrees/modules/amqp.service.doctree differ diff --git a/api-doc/_build/html/.buildinfo b/api-doc/_build/html/.buildinfo new file mode 100644 index 0000000..4dd1202 --- /dev/null +++ b/api-doc/_build/html/.buildinfo @@ -0,0 +1,4 @@ +# Sphinx build info version 1 +# This file records the configuration used when building these files. When it is not found, a full rebuild will be done. +config: bf29a028b890480b74024cefba26652a +tags: 645f666f9bcd5a90fca523b33c5a78b7 diff --git a/api-doc/_build/html/_modules/amqp/adapter/amq_route_factory.html b/api-doc/_build/html/_modules/amqp/adapter/amq_route_factory.html new file mode 100644 index 0000000..295f6aa --- /dev/null +++ b/api-doc/_build/html/_modules/amqp/adapter/amq_route_factory.html @@ -0,0 +1,193 @@ + + + + + + + + amqp.adapter.amq_route_factory — AMQ Adapter Python 1.0 documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

Source code for amqp.adapter.amq_route_factory

+from amqp.adapter.logging_utils import logging_error
+from amqp.model.model import AMQRoute
+
+# record separator
+RS = ":"
+# null route, in lieu of None value
+NULL_ROUTE = {
+    "componentName": "",
+    "exchange": None,
+    "queue": None,
+    "routingKey": "#",
+    "timeout": 1,
+    "validUntil": 0,
+}
+
+
+
+[docs] +class AMQRouteFactory: + """ + Define the methods to create and validate AMQRoute objects. + """ + +
+[docs] + @staticmethod + def from_string(data): + """ + Builds the AMQRoute from its string form. + :param data: route string + :return: AMQRoute object + """ + try: + _keys = data.split(RS) + if len(_keys) >= 5: + _i = 0 + _component_name = _keys[_i] + _i += 1 + _exchange = _keys[_i] + _i += 1 + _queue = _keys[_i] if _i < len(_keys) else "" + _i += 1 + _routing_key = _keys[_i] if _i < len(_keys) else "" + _i += 1 + _timeout = int(_keys[_i]) if _i < len(_keys) else 0 + _i += 1 + _valid_until = int(_keys[_i]) if _i < len(_keys) else 0 + return AMQRoute( + component_name=_component_name, + exchange=_exchange, + queue=_queue, + key=_routing_key, + timeout=_timeout, + valid_until=_valid_until, + ) + logging_error( + "AMQRoute incorrect format. AMQRoute expects input as: " + "'Component-Name:AMQExchange-Name:Queue-Name:Routing-Key:Timeout:Valid-Until', " + "got: ['%s']", + data, + ) + except Exception as err: + logging_error( + "%s: AMQRoute incorrect format. AMQRoute expects input as: " + "'Component-Name:AMQExchange-Name:Queue-Name:Routing-Key:Timeout:Valid-Until', " + "got: ['%s']", + err, + data, + ) + return NULL_ROUTE
+ + +
+[docs] + @staticmethod + def validate(route_str): + """ + Validates the route string. + :param route_str: route string + :return: boolean validity indicator + """ + if route_str is not None: + return route_str.count(":") > 4 + return False
+
+ +
+ +
+
+ +
+
+
+
+ + + + diff --git a/api-doc/_build/html/_modules/amqp/adapter/backpressure_handler.html b/api-doc/_build/html/_modules/amqp/adapter/backpressure_handler.html new file mode 100644 index 0000000..b29f3b2 --- /dev/null +++ b/api-doc/_build/html/_modules/amqp/adapter/backpressure_handler.html @@ -0,0 +1,466 @@ + + + + + + + + amqp.adapter.backpressure_handler — AMQ Adapter Python 1.0 documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

Source code for amqp.adapter.backpressure_handler

+import asyncio
+import json
+import time
+from asyncio import AbstractEventLoop
+from threading import Thread
+from typing import Optional
+
+from aio_pika import Message
+from aio_pika.abc import AbstractRobustChannel
+
+from amqp.adapter.logging_utils import logging_info, logging_warning
+from amqp.config.amq_configuration import AMQConfiguration
+from amqp.model.model import ScalingRequestAlert
+from amqp.router.utils import await_future, await_result
+
+
+
+[docs] +class ScaleRequestV1: + +
+[docs] + def __init__( + self, + serviceId: str, + taskId: str, + max_availability: int, + current_availability: int, + requestType: ScalingRequestAlert, + ): + self.version = 1 # Version of the request, currently 1 + self.serviceId = serviceId + self.taskId = taskId + self.max_availability = max_availability + self.current_availability = current_availability + self.requestType = requestType + self.thread = None
+ + +
+[docs] + def to_json(self) -> str: + """Converts the object to a JSON string matching the Java structure""" + return json.dumps( + { + "version": self.version, + "serviceId": self.serviceId, + "taskId": self.taskId, + "maxAvailability": self.max_availability, + "currentAvailability": self.current_availability, + "requestType": self.requestType.value, # Using .value for Enum serialization + }, + indent=2, + )
+
+ + + +
+[docs] +class BackpressureHandler: + # Track the number of messages currently being processed + current_availability = 0 + # helps detect IDLE condition + # helps prevent flooding the system with backpressure events + last_backpressure_event_time = 0 + last_backpressure_event = ScalingRequestAlert.UPDATE + # _callback_list is used in unit tests to record the invoked callbacks + _callback_list = None + +
+[docs] + def __init__( + self, + channel: AbstractRobustChannel, + loop: AbstractEventLoop, + config: AMQConfiguration, + ): + self.lock = None + self.channel = channel + self.loop = loop + self.config = config + self.exchange = None + self.swarm_service_id = self.config.amq_adapter.swarm_service_id + self.swarm_task_id = self.config.amq_adapter.swarm_task_id + self.do_loop = -1 + self._resource_usage_changed = 0 + self._resource_average_value = 0 + self._last_resource_max_value = 100 # Default max value for CPU usage + self.max_availability = -1 + self.current_availability = 1 + self.max_load = 1 + self.current_load = 0 + self.idle_state_detected_time = 0
+ + +
+[docs] + def increase_current_load(self): + """Increase the number of parallel executions, or current_availability load""" + logging_info( + "Backpressure: Increase current load (%s) by 1", + self.current_load, + ) + self.current_load += 1
+ + +
+[docs] + def decrease_current_load(self): + """Decrease the number of parallel executions, or current load""" + logging_info( + "Backpressure: Decrease current load(%s) by 1", + self.current_load, + ) + if self.current_load > 0: + self.current_load -= 1
+ + +
+[docs] + def update_current_availability(self, count: int): + """Update the number of parallel executions, or current load""" + self.current_availability = count
+ + +
+[docs] + def update_last_backpressure_event_time(self): + # logging_info("Backpressure: Update last data message time") + """Update the last data message time""" + self.last_backpressure_event_time = time.time()
+ + +
+[docs] + async def update_backpressure_value(self, current_availability: int, maximum: int): + """ + Update the current backpressure value and check for overload conditions. + This method is called by the AMQService.backpressure() method to update + the current parallel executions count and check for overload conditions. + + Args: + current_availability: Current value of the backpressure metric + maximum: Maximum value of the backpressure metric + """ + logging_info( + "Backpressure: Updating backpressure value, current_availability=%s, maximum=%s", + current_availability, + maximum, + ) + if maximum > 0 and maximum > self.max_availability: + self.max_availability = maximum + # Update the current_availability / parallel executions count + self.update_current_availability(current_availability) + # Check for overload conditions + await self.check_overload_condition()
+ + +
+[docs] + def start_backpressure_monitor(self) -> Optional[Thread]: + # Start the Backpressure monitor loop + self.thread = Thread(target=self.backpressure_monitor_loop) + self.thread.daemon = True # This makes it a daemon thread + self.thread.start() + return self.thread
+ + +
+[docs] + async def check_overload_condition(self): + """ + Check if the current_availability availability is too low (OVERLOAD) + or if the service has been idle for too long with high availability (IDLE). + + Note: current_availability represents available capacity, not used capacity. + - Low availability (close to 0) means OVERLOAD + - High availability (close to maximum) with no activity means IDLE + """ + current_time = time.time() + last_event_delta: float = current_time - self.last_backpressure_event_time + + # logging_info( + # "Backpressure: Check conditions, current_availability=%s, max=%s, last_activity=%s", + # self.current_availability, + # self.max_availability, + # last_event_delta, + # ) + + # Check for OVERLOAD condition - low availability (less than 20% of maximum) + if self.current_availability <= round(0.1 * self.max_availability): + # Check if the last backpressure event was not an overload + if ( + self.last_backpressure_event != ScalingRequestAlert.OVERLOAD + or last_event_delta > self.config.backpressure.idle_duration + ): + # Trigger the overload event + await self.handle_backpressure_overload_event() + # update / reset time-window so that the OVERLOAD is not sent too often + self.last_backpressure_event_time = current_time + self.last_backpressure_event = ScalingRequestAlert.OVERLOAD + + # Check for IDLE condition - high availability (more than 80% of maximum) with no activity + elif self.current_availability >= round(0.8 * self.max_availability): + idle_duration = self.config.backpressure.idle_duration + # Check if service has been idle for longer than the configured duration + if ( + current_time - self.idle_state_detected_time > idle_duration + and self.last_backpressure_event == ScalingRequestAlert.IDLE + ): + logging_info( + "Backpressure: Service has been idle for %s seconds (threshold: %s seconds)", + last_event_delta, + idle_duration, + ) + + # Trigger the idle event + await self._handle_backpressure_idle_event() + # update / reset time-window so that the IDLE is not sent too often + self.last_backpressure_event_time = current_time + self.idle_state_detected_time = current_time + # send IDLE even only once + self.last_backpressure_event = ScalingRequestAlert.UPDATE + else: + # Don't send IDLE right away when the availability increases, but wait for a while, send UPDATE instead + self.last_backpressure_event = ScalingRequestAlert.IDLE + if current_time - self.idle_state_detected_time > idle_duration: + self.idle_state_detected_time = current_time + if last_event_delta > self.config.backpressure.time_window: + _scaling_request: ScaleRequestV1 = ScaleRequestV1( + self.swarm_service_id, + self.swarm_task_id, + self.max_availability, + self.current_availability, # Current availability is passed directly + ScalingRequestAlert.UPDATE, + ) + # Address the message to any adapter capable of supporting BACKPRESSURE request + await self.publish_backpressure_request(_scaling_request) + self.last_backpressure_event_time = current_time + + # If neither OVERLOAD nor IDLE, and it's time for an update, send UPDATE event + elif last_event_delta > self.config.backpressure.time_window: + _scaling_request: ScaleRequestV1 = ScaleRequestV1( + self.swarm_service_id, + self.swarm_task_id, + self.max_availability, + self.current_availability, # Current availability is passed directly + ScalingRequestAlert.UPDATE, + ) + # Address the message to any adapter capable of supporting BACKPRESSURE request + await self.publish_backpressure_request(_scaling_request) + self.last_backpressure_event_time = current_time + self.last_backpressure_event = ScalingRequestAlert.UPDATE
+ + + async def _backpressure_monitor(self): + """Periodically monitor the backpressure conditions and trigger events accordingly""" + _monitor_interval = 0.5 # Monitor every 500ms + while self._do_loop(): + await self.check_overload_condition() + await asyncio.sleep(_monitor_interval) + +
+[docs] + def backpressure_monitor_loop(self): + _loop = asyncio.new_event_loop() + _loop.run_until_complete(self._backpressure_monitor())
+ + +
+[docs] + async def handle_backpressure_overload_event(self): + logging_warning("Backpressure: Capacity close to depleted!") + # Send an Overload event to the Management service + scaling_request: ScaleRequestV1 = ScaleRequestV1( + self.swarm_service_id, + self.swarm_task_id, + self.max_availability, + self.current_availability, # Current availability is passed directly + ScalingRequestAlert.OVERLOAD, + ) + # Address the message to any management service instance capable of supporting BACKPRESSURE request + await self.publish_backpressure_request(scaling_request)
+ + + async def _handle_backpressure_idle_event(self): + logging_warning("Backpressure: Service is idle.") + # Send an Idle event to the Management service + scaling_request: ScaleRequestV1 = ScaleRequestV1( + self.swarm_service_id, + self.swarm_task_id, + self.max_availability, + self.current_availability, # Current availability is passed directly + ScalingRequestAlert.IDLE, + ) + # Address the message to any adapter capable of supporting BACKPRESSURE request + await self.publish_backpressure_request(scaling_request) + +
+[docs] + async def publish_backpressure_request(self, scaling_request: ScaleRequestV1): + # Publish the backpressure request to the management service + logging_info( + f"Publishing backpressure for {scaling_request.serviceId} with request type {scaling_request.requestType}" + ) + if not self.exchange: + + async def _wrap_rabbit_mq_api_init(channel): + _exchange = await channel.get_exchange(name="cleverthis.clevermicro.management") + return _exchange + + if BackpressureHandler._callback_list is not None: + BackpressureHandler._callback_list["_wrap_rabbit_mq_api_init"] = 1 + self.exchange = await await_result( + asyncio.run_coroutine_threadsafe(_wrap_rabbit_mq_api_init(self.channel), self.loop) + ) + + if self.exchange: + + async def _wrap_rabbit_mq_api(): + if not self.channel.is_closed: + binary_content: bytes = scaling_request.to_json().encode("utf-8") + pika_message: Message = Message( + body=binary_content, + content_encoding="utf-8", + delivery_mode=2, + content_type="application/octet-stream", + headers=None, + priority=0, + correlation_id=None, + ) + await self.exchange.publish( + message=pika_message, routing_key="backpressure-scaling-v1" + ) + logging_info( + "Service Message Published to %s, msg: %s", + self.exchange.name, + str(binary_content), + ) + return True + return False + + if BackpressureHandler._callback_list is not None: + BackpressureHandler._callback_list["_wrap_rabbit_mq_api"] = 1 + await await_future(asyncio.run_coroutine_threadsafe(_wrap_rabbit_mq_api(), self.loop))
+ + + def _do_loop(self) -> bool: + """ + Helper function for unit tests to perform several loops only. Check if the loop should continue running. + Positive value of do_loop indicates the number of iterations left. + Negative value indicates the loop should run indefinitely. + """ + _val = self.do_loop != 0 + if self.do_loop > 0: + self.do_loop -= 1 + return _val
+ +
+ +
+
+ +
+
+
+
+ + + + diff --git a/api-doc/_build/html/_modules/amqp/adapter/cleverthis_service_adapter.html b/api-doc/_build/html/_modules/amqp/adapter/cleverthis_service_adapter.html new file mode 100644 index 0000000..26e3179 --- /dev/null +++ b/api-doc/_build/html/_modules/amqp/adapter/cleverthis_service_adapter.html @@ -0,0 +1,948 @@ + + + + + + + + amqp.adapter.cleverthis_service_adapter — AMQ Adapter Python 1.0 documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

Source code for amqp.adapter.cleverthis_service_adapter

+"""
+The layer that interfaces with the CleverThis service that this Adapter is integrating with.
+Methods here will invoke the actual CleverThis Service code, which are passed as Callable object.
+"""
+
+import asyncio
+import enum
+import inspect
+import json
+import pathlib
+import re
+from asyncio import AbstractEventLoop
+from collections import defaultdict
+from typing import Any, Callable, Coroutine, Dict, List
+
+from aio_pika.abc import (
+    AbstractExchange,
+    AbstractRobustChannel,
+    AbstractRobustConnection,
+)
+from aiohttp import ClientResponse, ClientSession
+from fastapi import HTTPException, UploadFile
+from fastapi.params import File
+from fastapi.routing import APIRoute
+from fastapi.security import OAuth2PasswordRequestForm
+from pydantic import BaseModel
+from starlette.datastructures import Headers
+from starlette.responses import FileResponse
+
+from amqp.adapter import serializer
+from amqp.adapter.data_parser import AMQDataParser
+from amqp.adapter.data_response_factory import DataResponseFactory
+from amqp.adapter.file_handler import FileHandler, StreamingFileHandler
+from amqp.adapter.logging_utils import (
+    logging_debug,
+    logging_error,
+    logging_info,
+    logging_warning,
+)
+from amqp.adapter.pydantic_serializer import python_type_to_json
+from amqp.adapter.type_descriptor import TypeDescriptor
+from amqp.config.amq_configuration import AMQConfiguration
+from amqp.model.model import AMQErrorMessage, AMQMessage, DataMessage, DataResponse
+from amqp.router.router_base import AMQ_REPLY_TO_EXCHANGE
+from amqp.router.utils import await_result
+
+g_extra_init_data: dict = dict()
+
+
+async def _create_reply_exchange_and_queue(
+    connection: AbstractRobustConnection,
+) -> tuple[AbstractRobustChannel, AbstractExchange, str]:
+    """
+    Create objects for reply.
+    Return (channel for reply queue, exchange for reply, reply queue name)
+    """
+
+    # Wrap calls to RabbitMQ API to make it Awaitable, so it can be run thread-safe with
+    # correct event loop. This is needed because the RabbitMQ API is not thread-safe and needs
+    # to be called in the context of event loop the first RabbitMQ connection was created with.
+    _channel = await connection.channel()
+    _exchange_name: str = AMQ_REPLY_TO_EXCHANGE
+    _exchange = await _channel.get_exchange(name=_exchange_name, ensure=True)
+    _queue = await _channel.declare_queue(exclusive=False)  # Declare a unique, non-exclusive queue
+    _queue_name = _queue.name
+    await _queue.bind(
+        exchange=_exchange_name,
+        routing_key=_queue_name,  # Use queue name as routing key
+    )
+    logging_debug(f"Publishing to exchange: {_exchange_name}, q: {_queue_name}")
+    return _channel, _exchange, _queue_name
+
+
+async def _convert_file_response(
+    obj: FileResponse,
+    connection: AbstractRobustConnection,
+    file_handler: FileHandler,
+    loop: AbstractEventLoop | None,
+) -> str:
+    """
+    Converts a FileResponse object to a JSON string containing the filename and stream name.
+    The file content itself is transmitted through the named stream (which is implemented as
+    temporary dedicated queue.
+    """
+    # All calls to RabbitMQ API have to be called threadsafe, because those appear to be attached
+    # to parent event loop, and need to execute at that event loop, not the current_availability one
+    # allow coroutine to execute. calling _future.result() will block the entire thread
+    # and the coroutine will never execute
+    _channel, _exchange, _routing_key = await await_result(
+        asyncio.run_coroutine_threadsafe(_create_reply_exchange_and_queue(connection), loop)
+    )
+    # publish_file also needs RabbitMQ API, ensure the loop is passed as argument
+    (_stream_name, _file_size) = await file_handler.publish_file(
+        _exchange, obj.path, _routing_key, loop
+    )
+    # as above, but luckily this time we don't need to wait for the result
+    asyncio.run_coroutine_threadsafe(_channel.close(), loop)
+    return json.dumps(
+        {
+            "files": [
+                {
+                    "filename": obj.filename,
+                    "mediaType": "application/octet-stream",
+                    "streamName": _stream_name,
+                    "fileSize": _file_size,
+                }
+            ]
+        }
+    )
+
+
+
+[docs] +def is_likely_json(text: str) -> bool: + """Quick test to see if should convert the response string to JSON format.""" + if not isinstance(text, str): + return False + _text = text.strip() + return (_text.startswith("{") and _text.endswith("}")) or ( + _text.startswith("[") and _text.endswith("]") + )
+ + + +# ============= CleverThisServiceAdapter ============= +
+[docs] +class CleverThisServiceAdapter: + """ + A base class for CleverThis service adapter that handles the incoming AMQP messages and routes + them to the appropriate CleverThisService API endpoint. + IMPORTANT: This class is not intended to be used directly. It should be subclassed for + each CleverThis service and override the methods to handle the specific API calls. + """ + +
+[docs] + def __init__( + self, + amq_configuration: AMQConfiguration, + routes: List[APIRoute], + session: ClientSession = None, + authenticated_user_arg_name: str = "authenticated_user", + extra_init_data=None, + ): + """ + Initializes the CleverThisServiceAdapter with the given AMQ configuration and optional HTTP + session. The HTTP session is for the case when the adapter uses loose coupling with + the CleverThis service via HTTP REST API. + """ + if extra_init_data is None: + extra_init_data = dict() + self.authenticated_user_arg_name = authenticated_user_arg_name + self.amq_configuration = amq_configuration + self.session = session + self.service_host = self.amq_configuration.dispatch.service_host + self.service_port = self.amq_configuration.dispatch.service_port + self.loop: AbstractEventLoop | None = None + self.require_authenticated_user = ( + self.amq_configuration.amq_adapter.require_authenticated_user + ) + self.routes = self.group_routes_by_method(api_routes=routes) + global g_extra_init_data + g_extra_init_data = extra_init_data or {}
+ + +
+[docs] + def get_endpoint(self, endpoint: Any) -> Any: + """ + Optionally, Service can override this method to alter the endpoint mapping. + This is useful for example when the CleverThis service has a JSON wrapper + around the endpoint, like CleverSwarm, which might be the preferred endpoint to call. + + :param endpoint: The original endpoint to be called. + :return: The actual endpoint to be called. + """ + return endpoint
+ + +
+[docs] + def clone_and_adapt_route(self, original_route: APIRoute) -> APIRoute: + """ + Creates a clone of APIRoute. Checks if there's preferred endpoint with json serialization. + It also adjusts the path regex to ensure it matches the path in different API versions. + """ + + # Need to adapt also the path regex to ensure it matches the path in different API versions + # Ensures the leading '^' is followed by any match and there's no '/' before final '$' + _new_regex_string = "^.*" + original_route.path_regex.pattern.lstrip("^").rstrip("/$") + "$" + _new_route: APIRoute = APIRoute( + path=original_route.path, + endpoint=self.get_endpoint( + original_route.endpoint + ), # Service can alter the endpoint mapping here + response_model=original_route.response_model, + status_code=original_route.status_code, + tags=original_route.tags.copy() if original_route.tags else None, + dependencies=( + original_route.dependencies.copy() if original_route.dependencies else None + ), + summary=original_route.summary, + description=original_route.description, + response_description=original_route.response_description, + responses=(original_route.responses.copy() if original_route.responses else None), + deprecated=original_route.deprecated, + name=original_route.name, + methods=original_route.methods.copy() if original_route.methods else None, + operation_id=original_route.operation_id, + response_model_include=original_route.response_model_include, + response_model_exclude=original_route.response_model_exclude, + response_model_by_alias=original_route.response_model_by_alias, + response_model_exclude_unset=original_route.response_model_exclude_unset, + response_model_exclude_defaults=original_route.response_model_exclude_defaults, + response_model_exclude_none=original_route.response_model_exclude_none, + include_in_schema=original_route.include_in_schema, + response_class=original_route.response_class, + dependency_overrides_provider=original_route.dependency_overrides_provider, + callbacks=(original_route.callbacks.copy() if original_route.callbacks else None), + openapi_extra=( + original_route.openapi_extra.copy() if original_route.openapi_extra else None + ), + generate_unique_id_function=original_route.generate_unique_id_function, + ) + _new_route.path_regex = re.compile(_new_regex_string) + return _new_route
+ + +
+[docs] + def group_routes_by_method(self, api_routes: List[APIRoute]) -> Dict[str, List[APIRoute]]: + """ + Groups API routes by their HTTP method. This is important to ensure that URLPath is matched + correctly, as different methods may share the same URLPath (e.g. GET, POST PUT, DELETE). + """ + _method_to_routes = defaultdict(list) # Automatically initializes new lists for new keys + for _route in api_routes: + if ( + isinstance(_route, APIRoute) and _route.methods + ): # Ensure methods exist (should always be true for valid routes) + for method in _route.methods: + _method_to_routes[method].append(self.clone_and_adapt_route(_route)) + + return dict(_method_to_routes)
+ + +
+[docs] + def get_content_type(self, message_headers: Dict[str, List[str]]) -> str: + """ + Pull the value of the Content-Type header from the message headers. + :param message_headers: The headers of the message. + return: The content type of the message. + """ + for header, values in message_headers.items(): + if header.lower() == "content-type": + return values[0].split(";")[0] + return "application/json"
+ + +
+[docs] + async def get_current_user(self, token: str) -> object: + """ + To Be Overriden in subclass for given CleverThis Service, to return the current_availability user based + on the token, in format appropriate for the service. + + :param token: The token to be used for authentication. + + :return: A user object or None if the user is not authenticated. + """ + return None
+ + +
+[docs] + async def on_message(self, message: AMQMessage) -> DataResponse: + """ + Called when a message is received from the AMQP. + :param message: + :return: AMQP response class with operation status code and optional error details. + """ + _auth_user: Any | None = None + _amq_response: Any | None = None + + logging_debug(f"on_message: require_authenticated_user={self.require_authenticated_user}") + if self.require_authenticated_user: + _auth: str = message.headers().get("Authorization", "") + logging_debug(f"Auth: {_auth}") + if isinstance(_auth, List): + _auth = _auth[0] + if "Bearer" in _auth: + try: + token = _auth.split(" ")[1] + logging_info(f"Token: {token}") + _auth_user = await self.get_current_user(token) + except Exception as error: + logging_error(f"on_message: Error getting user: {error}") + + if _auth_user or not self.require_authenticated_user or message.path().endswith("/login"): + method = message.method().lower() + if method == "get": + _amq_response = await self.get(message, _auth_user) + elif method == "put": + _amq_response = await self.put(message, _auth_user) + elif method == "post": + _amq_response = await self.post(message, _auth_user) + elif method == "head": + _amq_response = await self.head(message, _auth_user) + elif method == "delete": + _amq_response = await self.delete(message, _auth_user) + else: + raise ValueError(f"Unexpected HTTP method: {message.method()}") + else: + _amq_response = DataResponseFactory.of_error_message( + message.id(), + AMQErrorMessage(401, "Unauthorized", "Unauthorized"), + message.trace_info(), + ) + logging_info( + f"ALL DONE in on_message id:{message.id()}, resp code:{_amq_response.response_code()}" + ) + return _amq_response
+ + + async def _authorize_forward_request( + self, message: DataMessage, request_coroutine, auth_user: Any + ) -> DataResponse: + """ + This method is used to authorize the request and forward it to the CleverThis Service API. + Used only for loose / 3rd party mode of coupling with the Service + """ + # AMQDataParser.parse_request_body(message) + # TODO: implement authorization logic here - ensure the security related headers are set + # This depends on what kind of security is required by the CleverThis service. + # However at this time there is no loosely coupled Service. + + return await request_coroutine + + # ============================================================================================== + # ===================== C l e v e r T h i s A P I m e t h o d s ======================== + # ============================================================================================== + +
+[docs] + async def get(self, message: AMQMessage, auth_user: Any) -> DataResponse: + """ + Handles the GET requests for the CleverThis API. In tight coupling (self.session is None), + use path, path params and query string to determine the endpoint method to invoke. + In loose coupling (self.session is not None), forward the request as REST request using + the session, i.e. attempt to call the REST endpoint directly using HTTP protocol. + + :param message: message from the AMQP that contains the GET request + :param auth_user: user authenticated by the API Gateway, as UserSchem + + :return: DataResponse to be sent back to the API Gateway via AMQP + """ + + if self.session is None: + return await self.handle_no_body_payload(message, auth_user, "GET") + + # Forwards as REST - try to invoke the service directly on this path + url = f"http://{self.service_host}:{self.service_port}{message.path()}" + headers = {**message.headers(), **message.trace_info()} + logging_info( + f"SEND HEADERS out: {', '.join(f'{k}={v}' for k, v in headers.items())}, REQ={url}" + ) + _http_resp: ClientResponse = await self.session.get(url, headers=headers) + return DataResponseFactory.of( + message.id(), + _http_resp.status, + _http_resp.content_type, + await _http_resp.read(), + message.trace_info(), + )
+ + +
+[docs] + async def put(self, message: AMQMessage, auth_user: Any) -> DataResponse: + """ + Handles the PUT requests for the CleverThis API. Uses path, path params, query string and + body payload to determine the endpoint method to invoke and its input arguments + In loose coupling (self.session is not None), attempt to call the REST endpoint directly + using HTTP protocol. + + :param message: message from the AMQP that contains the PUT request + :param auth_user: user authenticated by the API Gateway, as UserSchem + + :return: DataResponse to be sent back to the API Gateway via AMQP + """ + if self.session is None: + return await self.handle_body_payload(message, auth_user, "PUT") + + # Forwards as REST - try to invoke the service directly on this path + return await self._authorize_forward_request( + message, + self.session.put( + f"http://{self.service_host}:{self.service_port}{message.path()}", + data=message.body(), + headers={**message.headers(), **message.trace_info()}, + ), + auth_user, + )
+ + +
+[docs] + async def post(self, message: AMQMessage, auth_user: Any) -> DataResponse: + """ + Handles the POST requests for the CleverThis API. Uses path, path params, query string and + body payload to determine the endpoint method to invoke and its input arguments + In loose coupling (self.session is not None), attempt to call the REST endpoint directly + using HTTP protocol. + + :param message: message from AMQP that contains the POST request, including body and headers + :param auth_user: user authenticated by the API Gateway, as UserSchem + + :return: DataResponse to be sent back to the API Gateway via AMQP + """ + if self.session is None: + return await self.handle_body_payload(message, auth_user, "POST") + + return await self._authorize_forward_request( + message, + self.session.post( + f"http://{self.service_host}:{self.service_port}{message.path()}", + data=message.body(), + headers={**message.headers(), **message.trace_info()}, + ), + auth_user, + )
+ + +
+[docs] + async def head(self, message: AMQMessage, auth_user: Any) -> DataResponse: + """ + Handles the HEAD requests for the CleverThis API. In tight coupling (self.session is None), + use path, path params and query string to determine the endpoint method to invoke. + In loose coupling (self.session is not None), HEAD call is not supported. + + :param message: message from AMQP containing the HEAD request, including body and headers + :param auth_user: user authenticated by the API Gateway, as UserSchema + + :return: DataResponse to be sent back to the API Gateway via AMQP + """ + if self.session is None: + return await self.handle_no_body_payload(message, auth_user, "HEAD") + + raise NotImplementedError
+ + +
+[docs] + async def delete(self, message: AMQMessage, auth_user: Any) -> DataResponse: + """ + Handles the DELETE requests for the CleverThis API. + + :param message: message from AMQP that contains the DELETE request, including body & headers + :param auth_user: user authenticated by the API Gateway, as UserSchema + + :return: DataResponse to be sent back to the API Gateway via AMQP + """ + if self.session is None: + return await self.handle_no_body_payload(message, auth_user, "DELETE") + + return await self._authorize_forward_request( + message, + self.session.post( + f"http://{self.service_host}:{self.service_port}{message.path()}", + data=message.body(), + headers={**message.headers(), **message.trace_info()}, + ), + auth_user, + )
+ + +
+[docs] + async def options(self, message: AMQMessage, auth_user: Any) -> DataResponse: + """ + Handles the OPTIONS requests. In tight coupling (self.session is None), + use path, path params and query string to determine the endpoint method to invoke. + In loose coupling (self.session is not None), forward the request as REST request using + the session, i.e. attempt to call the REST endpoint directly using HTTP protocol. + + :param message: message from AMQP that contains the OPTIONS request, including headers + :param auth_user: user authenticated by the API Gateway, as UserSchema + + :return: DataResponse to be sent back to the API Gateway via AMQP + """ + if self.session is None: + return await self.handle_no_body_payload(message, auth_user, "OPTIONS") + + raise NotImplementedError
+ + +
+[docs] + async def patch(self, message: AMQMessage, auth_user: Any) -> DataResponse: + """ + Handles the PATCH requests for the CleverBRAG API. + + :param message: message from AMQP that contains the PATCH request, including body & headers + :param auth_user: user authenticated by the API Gateway, as UserSchema + + :return: DataResponse to be sent back to the API Gateway via AMQP + """ + if self.session is None: + return await self.handle_no_body_payload(message, auth_user, "PATCH") + + return await self._authorize_forward_request( + message, + self.session.post( + f"http://{self.service_host}:{self.service_port}{message.path()}", + data=message.body(), + headers={**message.headers(), **message.trace_info()}, + ), + auth_user, + )
+ + + # ============================================================================================== + # ================= C l e v e r S w a r m p a y l o a d h a n d l e r s ================== + # ============================================================================================== + +
+[docs] + async def handle_no_body_payload( + self, message: AMQMessage, auth_user: Any, method: str + ) -> DataResponse: + """ + Handles the requests that do not have a body payload. + :param message: message from the AMQP that contains the request + :param auth_user: user authenticated by the API Gateway, as UserSchem + :return: DataResponse to be sent back to the API Gateway via AMQP + """ + _routes = self.routes.get(method, []) + _path, _args = AMQDataParser.parse_get_url(message.path()) + for _route in _routes: + # TODO: here the regex will match with path prefix, + # but it might not be a stable feature that we could rely on. + # Need to figure out something stable for the prefix path. + _match = _route.path_regex.match(_path) + if _match: + _args.update(_match.groupdict()) + _signature_args = inspect.signature(_route.endpoint).parameters + if self.authenticated_user_arg_name in _signature_args.keys(): + _args[self.authenticated_user_arg_name] = auth_user + # If the path matches, call the corresponding function + return await self.call_cleverthis_api( + message, + _args, + api_method=_route.endpoint, + success_code=_route.status_code, + ) + + return DataResponseFactory.of( + message.id(), + 404, + "text/plain", + str(f"Destination location [{message.path()}] does not exists").encode("utf-8"), + message.trace_info(), + )
+ + +
+[docs] + async def handle_body_payload( + self, message: AMQMessage, auth_user: Any, method: str + ) -> DataResponse: + """ + Handles the requests that have a body payload, including Multipart data. + :param message: message from the AMQP that contains the request + :param auth_user: user authenticated by the API Gateway, as UserSchem + :param method: HTTP method (POST, PUT, etc.) + :return: DataResponse to be sent back to the API Gateway via AMQP + """ + _form_data = AMQDataParser.parse_request_body(message) + _routes = self.routes.get(method, []) + _path, _query_args = AMQDataParser.parse_get_url(message.path()) + for _route in _routes: + _match = _route.path_regex.match(_path) + if _match: + # If the path matches, call the corresponding function + _form_data.update(_match.groupdict()) + _form_data.update(_query_args) + for _body_param in _route.dependant.body_params: + if _body_param.name in _form_data: + _file_data = _form_data.get(_body_param.name, None) + actual_filepath = message.extra_data.get(_body_param.name, None) + _form_data[_body_param.name] = ( + ( + _body_param.type_.__name__ == "UploadFile" + or isinstance(_body_param.field_info, File) + ) + and _file_data is not None + and actual_filepath is not None + and UploadFile( + file=pathlib.Path(actual_filepath).open("rb"), + size=_file_data.get("size"), + filename=_file_data.get("filename"), + headers=Headers({"Content-Type": _file_data.get("mediaType")}), + ) + or _form_data[_body_param.name] + ) + + _verified_args = {} + _required_args = getattr(_route.endpoint, "__annotations__", {}) + _signature_args = inspect.signature(_route.endpoint).parameters + for arg in _required_args: + if arg in _form_data: + _verified_args[arg] = _form_data[arg] + else: + # needed for CleverSwarm /login input + if _required_args[arg].__name__ == "OAuth2PasswordRequestForm": + try: + _verified_args[arg] = OAuth2PasswordRequestForm(**_form_data) + except Exception as e: + print(f"Error: {e}") + if self.authenticated_user_arg_name in _signature_args.keys(): + _verified_args[self.authenticated_user_arg_name] = auth_user + + return await self.call_cleverthis_api( + message, + _verified_args, + api_method=_route.endpoint, + success_code=_route.status_code if _route.status_code else 200, + ) + + return DataResponseFactory.of( + message.id(), + 404, + "text/plain", + str(f"Destination location [{message.path()}] does not exists").encode("utf-8"), + message.trace_info(), + )
+ + + def _verify_and_instantiate_arguments( + self, + args: Any, + api_method: Callable[[Any, Any], Coroutine], + ) -> dict: + """ + Check the provided arguments against the API method signature and convert the incoming string value + into correct expected input object. + The logic used FastAPI annotation and would use the value from 'default_factory' invocation (where defined) + or 'default' value, as defined in the FastAPI annotation, to create default value in case the object is not in + the input values provided by the caller. + + """ + _verified_args = {} + try: + _sig = inspect.signature(api_method) + _required_args = list(_sig.parameters.values()) + # _required_args = getattr(api_method, '__annotations__', {}) + for arg in _required_args: + if arg.name in args: + _annotation = arg.annotation + _str_annotation = str(_annotation) + # convert to another type if arg is not a str + if arg.name == self.authenticated_user_arg_name: + _verified_args[arg.name] = args.get(arg.name) + elif _annotation == int: + _verified_args[arg.name] = int(args.get(arg.name)) + elif _annotation == float: + _verified_args[arg.name] = float(args.get(arg.name)) + elif _annotation == bool: + _verified_args[arg.name] = serializer.str_to_bool(args.get(arg.name)) + elif ( + "Optional" in _str_annotation + or _str_annotation.lower().startswith("list") + or _str_annotation.startswith("typing.List") + or "class" in _str_annotation + ): + _type_descr = TypeDescriptor( + _str_annotation, extra_init_data=g_extra_init_data + ) + try: + _payload = args.get(arg.name, "") + _verified_args[arg.name] = _type_descr.instantiate( + _payload, is_json=is_likely_json(_payload) + ) + except Exception as err: + logging_error( + f"Could not instantiate argument {arg}, defaulting to its string value", + err, + ) + _verified_args[arg.name] = args.get(arg.name, None) + else: + _temp = args.get(arg.name) + if isinstance(arg.annotation, enum.EnumType) or isinstance( + arg.annotation, enum.Enum + ): + try: + _temp = arg.annotation[_temp] + except KeyError as ke: + # some enums have string value different to key spelling, try different thing + try: + _temp = arg.annotation(_temp) + except ValueError as ve: + logging_warning( + f"cannot convert '{_temp}' to Enum type {arg.annotation}, {ke}, {ve}" + ) + print(f"ENUM -> {_temp}") + _verified_args[arg.name] = _temp + else: + if hasattr(arg, "default"): + _temp = arg.default + if hasattr(_temp, "default_factory") and _temp.default_factory is not None: + _temp = _temp.default_factory() + elif hasattr(_temp, "default"): + _temp = _temp.default + if not str(_temp) == "<class 'inspect._empty'>": + _verified_args[arg.name] = _temp + else: + logging_warning( + f"{api_method.__name__}: Missing required argument: {arg.name}" + ) + except Exception as e: + logging_error("Failed to validate and instantiate input args", e) + + return _verified_args + +
+[docs] + async def call_cleverthis_api( + self, + message: AMQMessage, + args: Any, + api_method: Callable[[Any, Any], Coroutine], + success_code: int, + ) -> DataResponse: + """ + Handles POST and PUT requests, which typically contain possibly complex payload data, + passed as a dictionary. Invokes the provided API method with the given arguments and + authenticated user, and returns the response as DataResponse. + + :param message: DataMessage containing the details of the call and the payload + :param args: map of the arguments to pass to the API method + :param api_method: Callable - the method to invoke. Last parameter is the authenticated user + :param success_code: HTTP code to be returned when operation suceeds + + :return: DataResponse class + """ + try: + _verified_args = self._verify_and_instantiate_arguments( + args=args, api_method=api_method + ) + logging_info( + "INVOKE ENDPOINT: [%s], ARGS: {%s}", + api_method.__name__, + ", ".join(f"{k}={v}" for k, v in _verified_args.items()), + ) + _result: Any = await api_method(**_verified_args) + logging_info(f"ENDPOINT RESULT: [{api_method.__name__}], RESULT: {str(_result)}") + + if isinstance(_result, FileResponse): + _json_result, _content_type = ( + await _convert_file_response( + _result, + message.connection, + file_handler=StreamingFileHandler(), + loop=self.loop, + ), + "application/octet-stream", + ) + elif isinstance(_result, BaseModel): + _json_result = _result.json() + _content_type = "application/json" + else: + if isinstance(_result, str): + _json_result = _result + else: + _json_result = python_type_to_json(_result) + _content_type = "application/json" + + _binary_result = ( + _json_result.encode("utf-8") + if hasattr(_json_result, "encode") + else ( + _json_result.body + if hasattr(_json_result, "body") + else (_json_result.read() if hasattr(_json_result, "read") else _json_result) + ) + ) + logging_info( + f"ALL DONE in call_cleverthis_api id:{message.id()}, resp code:{success_code}" + ) + return DataResponseFactory.of( + message.id(), + success_code, + _content_type, + _binary_result, + message.trace_info(), + ) + except HTTPException as http_error: + logging_error(f"Service REST endpoint invocation failed: {http_error}") + _content = str(http_error.detail) + if not is_likely_json(_content): + return DataResponseFactory.of_error_message( + message.id(), + AMQErrorMessage( + http_error.status_code, + "Service Invocation Failed", + _content, + ), + message.trace_info(), + ) + return DataResponseFactory.of( + message.id(), + http_error.status_code, + "application/json", + _content.encode("utf-8"), + message.trace_info(), + ) + except Exception as error: + logging_error(f"Service endpoint invocation failed: {error}") + _content = str(error) + if not is_likely_json(_content): + return DataResponseFactory.of_error_message( + message.id(), + AMQErrorMessage( + 500, + "Service Invocation Failed", + _content, + ), + message.trace_info(), + ) + return DataResponseFactory.of( + message.id(), + 500, + "application/json", + _content.encode("utf-8"), + message.trace_info(), + )
+
+ +
+ +
+
+ +
+
+
+
+ + + + diff --git a/api-doc/_build/html/_modules/amqp/adapter/consul_global_id_generator.html b/api-doc/_build/html/_modules/amqp/adapter/consul_global_id_generator.html new file mode 100644 index 0000000..ff838e1 --- /dev/null +++ b/api-doc/_build/html/_modules/amqp/adapter/consul_global_id_generator.html @@ -0,0 +1,214 @@ + + + + + + + + amqp.adapter.consul_global_id_generator — AMQ Adapter Python 1.0 documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

Source code for amqp.adapter.consul_global_id_generator

+import logging
+import os
+import random
+import socket
+import time
+
+import consul_kv
+
+from amqp.config.amq_configuration import AMQAdapter
+
+logger = logging.getLogger(__name__)
+
+
+
+[docs] +def get_config_values(config: AMQAdapter) -> tuple: + """ + Get configuration values from AMQConfiguration or environment variables. + + Returns: + tuple: (consul_host, consul_port, consul_counter_key, max_retries, retry_delay_seconds) + """ + try: + consul_host = config.consul_host + consul_port = config.consul_port + consul_counter_key = config.consul_counter_key + max_retries = config.consul_max_retries + retry_delay_seconds = config.consul_initial_retry_delay + return consul_host, consul_port, consul_counter_key, max_retries, retry_delay_seconds + except Exception as e: + logger.warning(f"Failed to load configuration: {str(e)}. Using default values.") + return ( + os.environ.get("CONSUL_HOST", consul_kv.settings.DEFAULT_ENDPOINT.split(":")[0]), + int(os.environ.get("CONSUL_PORT", consul_kv.settings.DEFAULT_ENDPOINT.split(":")[1])), + os.environ.get("CONSUL_COUNTER_KEY", "service/ids/counter"), + int(os.environ.get("CONSUL_MAX_RETRIES", 5)), + float(os.environ.get("CONSUL_RETRY_DELAY_SECONDS", 0.2)), + )
+ + + +
+[docs] +def generate_fallback_id() -> int: + """ + Generate a fallback ID when Consul is not available. + Uses a combination of timestamp, hostname hash, and random number. + + Returns: + int: A reasonably unique integer ID + """ + timestamp = int(time.time() * 1000) + hostname = socket.gethostname() + hostname_hash = hash(hostname) % 10000 + random_part = random.randint(0, 9999) + unique_id = (timestamp * 100000000) + (hostname_hash * 10000) + random_part + logger.warning(f"Using fallback ID generation method: {unique_id}") + return unique_id
+ + + +
+[docs] +def get_unique_instance_id(config: AMQAdapter) -> int: + """ + Get a globally unique ID from Consul. + Uses Consul's atomic Compare-And-Set operations to safely increment a counter. + Falls back to a local generation method if Consul is unavailable. + + Returns: + int: A globally unique integer ID + """ + consul_host, consul_port, consul_counter_key, max_retries, retry_delay_seconds = ( + get_config_values(config) + ) + try: + c = consul_kv.Connection(endpoint=f"{consul_host}:{consul_port}", timeout=5) + index, data = c.get(consul_counter_key) + if data is None: + logger.info(f"Initializing Consul counter at {consul_counter_key}") + if c.put(consul_counter_key, "1", cas=1): + return 1 + else: + logger.error("Failed to initialize Consul counter") + return generate_fallback_id() + current_value = int(data["Value"].decode("utf-8")) + new_value = current_value + 1 + for attempt in range(max_retries): + success = c.put(consul_counter_key, str(new_value), cas=data["ModifyIndex"]) + if success: + logger.debug(f"Successfully obtained unique ID: {new_value}") + return new_value + logger.debug(f"CAS update failed on attempt {attempt + 1}, retrying...") + if attempt < max_retries - 1: + time.sleep(retry_delay_seconds * (1 << attempt)) # Exponential backoff + index, data = c.get(consul_counter_key) + if data is None: + logger.error("Counter disappeared during update") + return generate_fallback_id() + current_value = int(data["Value"].decode("utf-8")) + new_value = current_value + 1 + logger.error(f"Failed to update counter after {max_retries} attempts") + return generate_fallback_id() + except Exception as e: + logger.error(f"Error accessing Consul: {str(e)}") + return generate_fallback_id()
+ +
+ +
+
+ +
+
+
+
+ + + + diff --git a/api-doc/_build/html/_modules/amqp/adapter/data_message_factory.html b/api-doc/_build/html/_modules/amqp/adapter/data_message_factory.html new file mode 100644 index 0000000..c59ac31 --- /dev/null +++ b/api-doc/_build/html/_modules/amqp/adapter/data_message_factory.html @@ -0,0 +1,449 @@ + + + + + + + + amqp.adapter.data_message_factory — AMQ Adapter Python 1.0 documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

Source code for amqp.adapter.data_message_factory

+"""
+methods to create, serialize and deserialize DataMessage objects.
+"""
+
+import base64
+import json
+import re
+from datetime import datetime, timezone
+from io import BytesIO
+from typing import Any, Dict, List
+
+import amqp.adapter.file_handler
+from amqp.adapter.file_handler import FileHandler, StreamingFileHandler
+from amqp.adapter.pydantic_serializer import python_type_to_json
+from amqp.adapter.serializer import (
+    map_as_string,
+    map_with_list_as_string,
+    parse_map_list_string,
+    parse_map_string,
+)
+from amqp.model.model import DataMessage, DataMessageMagicByte
+from amqp.model.snowflake_id import SnowflakeId
+from amqp.router.utils import sanitize_as_rabbitmq_name
+
+
+
+[docs] +class DataMessageFactory: + SNOWFLAKE_ID_BITS = 80 + SEQUENCE_BITS = 12 + MACHINE_ID_BITS = 24 + TIMESTAMP_BITS = SNOWFLAKE_ID_BITS - SEQUENCE_BITS - MACHINE_ID_BITS + + MACHINE_ID_MASK = (1 << MACHINE_ID_BITS) - 1 + SEQUENCE_MASK = (1 << SEQUENCE_BITS) - 1 + + _instance = None + snowflake_epoch = datetime(2024, 10, 1, 0, 0, tzinfo=timezone.utc).timestamp() + +
+[docs] + def __init__(self, machine_id: int): + self.machine_id = (machine_id & self.MACHINE_ID_MASK) << self.SEQUENCE_BITS + self.snowflake_sequence = 0 + self.snowflake_last_timestamp = 0
+ + +
+[docs] + @classmethod + def get_instance(cls, machine_id: int): + """ + return reusable factory instance + :param machine_id: a generator ID, should be unique for each component to prevent duplicate IDs + :return: factory instance + """ + if cls._instance is None: + cls._instance = DataMessageFactory(machine_id) + return cls._instance
+ + +
+[docs] + def generate_snowflake_id(self) -> SnowflakeId: + """ + Generate an unique ID following the SnowflakeID structure, see https://en.wikipedia.org/wiki/Snowflake_ID + :return: 80-bit wide ID + """ + timestamp = int(datetime.now(timezone.utc).timestamp() * 1000) + + if timestamp < self.snowflake_last_timestamp: + raise RuntimeError("Clock moved backwards. Refusing to generate id") + + if timestamp == self.snowflake_last_timestamp: + self.snowflake_sequence = (self.snowflake_sequence + 1) & self.SEQUENCE_MASK + if self.snowflake_sequence == 0: + while timestamp == self.snowflake_last_timestamp: + timestamp = int(datetime.now(timezone.utc).timestamp() * 1000) + self.snowflake_last_timestamp = timestamp + else: + self.snowflake_sequence = 0 + self.snowflake_last_timestamp = timestamp + + time = timestamp - 1000 * int(DataMessageFactory.snowflake_epoch) + return SnowflakeId( + time >> (64 - self.SEQUENCE_BITS - self.MACHINE_ID_BITS), + ( + (time << (self.SEQUENCE_BITS + self.MACHINE_ID_BITS)) + | self.machine_id + | self.snowflake_sequence + ) + & 0xFFFFFFFFFFFFFFFF, + )
+ + +
+[docs] + def create_request_message( + self, + method: str, + domain: str, + path: str, + headers: Dict[str, List[str]], + message, + ) -> DataMessage: + """ + Encapsulate / hide the system level inputs, create the AMQ message from business relevant input + :param method: a method requested (e.g. HTTP method like GET, POST in case of REST request) + :param domain: service host name + :param path: path at the service + :param headers: HTTP headers + :param message: payload + :return: DataMessage object + """ + serializable_headers = headers.copy() if headers else {} + payload = base64.b64encode(message.encode("utf-8") if isinstance(message, str) else message) + trace_info = {} + return DataMessage( + DataMessageMagicByte.HTTP_REQUEST.value, + self.generate_snowflake_id(), + method, + domain, + path, + serializable_headers, + trace_info, + payload, + )
+ + +
+[docs] + def safe_rsplit(self, fq_method_name, default="_"): + """ + Safely split a fully qualified method name into package, class, and method + + Args: + fq_method_name (str): Fully qualified method name + default (str, optional): Default value for missing tokens. Defaults to ''. + + Returns: + tuple: (package, class_name, method) + """ + if not fq_method_name: + return default, default, default + parts = fq_method_name.rsplit(".", 2) + # Pad with default values if insufficient tokens + while len(parts) < 3: + parts.insert(0, default) + return tuple(parts)
+ + +
+[docs] + def create_rpc_message( + self, + fq_method_name: str, + swarm_id: str, + args: Dict[str, Any] = None, + ) -> DataMessage: + """ + Create a RPC message with the given parameters. + :param fq_method_name: fully qualified method name (e.g. 'com.example.ServiceClass.method_name') + :param args: arguments whose serialized form will form the request body + :return: DataMessage object + """ + package, class_name, method = self.safe_rsplit(fq_method_name) + headers = {"Content-Type": ["application/octet-stream"], "Return-Address": [swarm_id]} + trace_info = {} + body = python_type_to_json(args).encode(encoding="utf-8") if args else b"" + print( + f"Creating RPC message for method: {fq_method_name}, args: {args} body: {body.decode('utf-8') if body else 'None'}" + ) + + return DataMessage( + DataMessageMagicByte.RPC_REQUEST.value, + self.generate_snowflake_id(), + package, + class_name, + method, + headers, + trace_info, + body, + )
+ + +
+[docs] + @staticmethod + def amq_message_routing_key(message: DataMessage) -> str: + """ + Constructs the message specific routing key. + :param message: DataMessage input + :return: routing key (as string) compliant to RabbitMQ allowed character set. + """ + return sanitize_as_rabbitmq_name(f"{message.domain()}.{message.path()}")
+ + +
+[docs] + @staticmethod + def get_timestamp_from_id(_id: SnowflakeId) -> int: + """ + Helper method - retrieve the timestamp from the ID + :param _id: SnowflakeID (includes encoded timestamp) + :return: the message timestamp + """ + bits = _id.get_bits() + low = bits[0] >> (DataMessageFactory.SEQUENCE_BITS + DataMessageFactory.MACHINE_ID_BITS) + high = bits[1] << ( + 64 - DataMessageFactory.SEQUENCE_BITS - DataMessageFactory.MACHINE_ID_BITS + ) + return (high | low) + DataMessageFactory.snowflake_epoch * 1000
+ + +
+[docs] + @staticmethod + def to_string(message: DataMessage) -> str: + """ + Human-readable representation. + :param message: DataMessage input + :return: as string + """ + return ( + f"DataMessage{{" + f"id={message.id()}, " + f"method='{message.method()}', " + f"domain='{message.domain()}', " + f"path='{message.path()}', " + f"headers={{{map_with_list_as_string(message.headers())}}}, " + f"traceInfo={{{map_as_string(message.trace_info())}}}, " + f"base64body={message.base64body().decode()}" + f"}}" + )
+ + +
+[docs] + @staticmethod + def serialize(message: DataMessage) -> bytes: + """ + Converts DataMessage to byte array + :param message: message to serialize + :return: messages as bytes + """ + id_bytes = str(message.id()).encode() + prolog = ( + f"|m={message.method()}|d={message.domain()}|p={message.path()}|h={{{map_with_list_as_string(message.headers())}}}|t={{{map_as_string(message.trace_info())}}}|b=" + ).encode("utf-8") + header_length = 1 + len(id_bytes) + len(prolog) + prolog_len = header_length.to_bytes(2, byteorder="big") + msg_length = 2 + header_length + len(message.base64body()) + baos = BytesIO(bytearray(msg_length)) + baos.write(prolog_len) + baos.write(message.magic().encode("utf-8")) + baos.write(id_bytes) + baos.write(prolog) + baos.write(message.base64body()) + return baos.getvalue()
+ + +
+[docs] + @staticmethod + def from_bytes(input_bytes: bytes) -> DataMessage: + """ + Builds the DataMessage from its serialized (byte array) form. + :param input_bytes: serialized message + :return: DataMessage instance + """ + prolog_len = (input_bytes[0] << 8) + input_bytes[1] + metadata = input_bytes[2 : prolog_len + 2].decode() + + pattern = re.compile( + r"([AR])" + r"([0-9A-Fa-f]+)\|" + r"m=([^|]*)\|" + r"d=([^|]*)\|" + r"p=([^|]*)\|" + r"h=(\{[^}]*\})\|" + r"t=(\{[^}]*\})\|" + r"b=" + ) + match = pattern.match(metadata) + if not match: + raise ValueError( + f"DataMessage format does not match expected format. " + f"Input string [{metadata}] does not match pattern: {pattern.pattern}" + ) + + id_str = match.group(2) + method = match.group(3) + domain = match.group(4) + path = match.group(5) + headers_str = match.group(6) + trace_info_str = match.group(7) + body_b64 = input_bytes[prolog_len + 2 :] + + headers = parse_map_list_string(headers_str) + trace_info = parse_map_string(trace_info_str) + + return DataMessage( + match.group(1), # Magic byte (R for RPC, A for HTTP) + SnowflakeId.from_hex(id_str), + method, + domain, + path, + headers, + trace_info, + body_b64, + )
+ + +
+[docs] + @staticmethod + async def from_stream( + stream: bytes, + rabbitmq_url: str, + loop, + file_handler: FileHandler = StreamingFileHandler(), + ) -> DataMessage | None: + """ + Builds the DataMessage from its serialized (byte array) form. + :param stream: serialized message + :param rabbitmq_url: RabbitMQ connection URL + :param loop: event loop + :param file_handler: file handler for downloading the message + :return: DataMessage instance + """ + # async implementation + _message_data = json.loads(stream.decode()) + _req_info = _message_data.get("req-info", "") + if _req_info: + (body, eof) = await file_handler.download_buffer( + loop=loop, rabbitmq_url=rabbitmq_url, queue_name=_req_info + ) + if eof == amqp.adapter.file_handler.END_OF_FILE: + return DataMessageFactory.from_bytes(body) + return None
+
+ +
+ +
+
+ +
+
+
+
+ + + + diff --git a/api-doc/_build/html/_modules/amqp/adapter/data_parser.html b/api-doc/_build/html/_modules/amqp/adapter/data_parser.html new file mode 100644 index 0000000..9c596f9 --- /dev/null +++ b/api-doc/_build/html/_modules/amqp/adapter/data_parser.html @@ -0,0 +1,325 @@ + + + + + + + + amqp.adapter.data_parser — AMQ Adapter Python 1.0 documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for amqp.adapter.data_parser

+import io
+import json
+from typing import Dict, Tuple
+from urllib.parse import parse_qs, urlparse
+from xml.etree import ElementTree
+
+import python_multipart
+from fastapi import UploadFile
+from python_multipart.multipart import Field, File
+
+from amqp.adapter.logging_utils import logging_debug, logging_error
+from amqp.model.model import AMQMessage
+
+
+
+[docs] +class MultipartFormDataParser: + """ + Parses multipart/form-data content and extracts form fields (key/value pairs) and files. + Note: multipart message that contains file(s) to upload are converted to CleverMicro JSON-based streaming format, + where files are transmitted separately via stream using dedicated queue, so the message content is no longer + the HTTP multipart format, but the Content-Type is being preserved for compatibility with the original message. + This class supports both formats (multipart and JSON) depending on the message content, because if no file is + included, the message is transmitted in the unchanged multipart/form-data format. + """ + +
+[docs] + def __init__(self, message: AMQMessage): + self.form_data: dict = {} + self.is_multipart = False + self.is_json = False + self.is_valid = True + # Convert each list of strings to a single string joined by newline, then to bytes + _headers = { + key: "\n".join(value).encode("utf-8") for key, value in message.headers().items() + } + try: + if ( + hasattr(message, "extra_data") + and isinstance(message.extra_data, dict) + and len(message.extra_data) > 0 + ): + try: + self.form_data = json.loads(message.body()) + self.is_json = True + if self.form_data["files"] is not None and self.form_data["form"] is not None: + self.form_data = AMQDataParser.process_form_data(self.form_data) + except Exception as jsonerror: + logging_error(f"Parsing JSON: {jsonerror}") + self.is_valid = False + else: + python_multipart.parse_form( + _headers, io.BytesIO(message.body()), self.on_field, self.on_file + ) + self.is_multipart = True + except Exception as e: + __error = f"Error parsing multipart/form-data: {e}. Trying parsing as JSON." + try: + self.form_data = json.loads(message.body()) + self.is_json = True + if self.form_data["files"] is not None and self.form_data["form"] is not None: + self.form_data = AMQDataParser.process_form_data(self.form_data) + except Exception as jsonerror: + logging_error(__error) + logging_error(f"Parsing JSON: {jsonerror}") + self.is_valid = False
+ + +
+[docs] + def on_field(self, field: Field): + logging_debug(str(field)) + self.form_data[field.field_name.decode("utf-8")] = field.value
+ + +
+[docs] + def on_file(self, file: File): + logging_debug(str(file)) + file.file_object.seek(0) + self.form_data[file.field_name.decode("utf-8")] = UploadFile( + file.file_object, size=file.size, filename=file.file_name.decode("utf-8") + )
+
+ + + +
+[docs] +class AMQDataParser: + """ + A class for parsing AMQ data messages. It provides methods to parse form data, + """ + +
+[docs] + @staticmethod + def process_form_data(json_input) -> dict: + """ + Processes the input JSON to extract form data. The JSON has two top entries: 'files' and 'form'. + This is part of the file stream implementation in AMQ adapter to stream large files in chunks. + 'files' contains information about the AMQ queue from which read the file, while 'form' contains + additional non-file form values. + This function combines the file and values at the top level in the returned dictionary. + Args: + json_input: JSON string or dict containing the form and files data + + Returns: + dict: Processed form data with files data overriding non-empty values + """ + # Load JSON if input is string + if isinstance(json_input, str): + data = json.loads(json_input) + else: + data = json_input + + result = {} + + # Process form data - take first element of each list + for key, value in data["form"].items(): + if value: # Only process if list is not empty + result[key] = value[0] + + # Override with files data where available + for key, value in data["files"].items(): + if value: # Only override if files data is non-empty + result[key] = value[0] + return result
+ + +
+[docs] + @staticmethod + def parse_get_url(url: str) -> Tuple[str, Dict[str, str]]: + """ + Parses an HTTP GET URL and returns (context_path, query_params_dict) + + Args: + url: The URL string to parse (e.g., "http://example.com/path?foo=1&bar=2&foo=3") + + Returns: + tuple: (context_path, query_params_dict) + - context_path: The path part of the URL (e.g., "/path") + - query_params_dict: Query parameters where values are str for single value or list of str for multivalue + """ + _parsed = urlparse(url) + _query_params = parse_qs(_parsed.query) + # Convert values from lists to single values when there's only one value + _simplified_params = {k: v[0] if len(v) == 1 else v for k, v in _query_params.items()} + return _parsed.path, _simplified_params
+ + +
+[docs] + @staticmethod + def parse_request_body(message: AMQMessage): + """ + Parses the HTTP POST request body based on the MIME type. + This is typically called fom the concrete CleverThis Service to extract parameters from the message body. + Args: + message (DataMessage): Contains raw request and MIME headers. + + Returns: + dict: Parsed data as a dictionary. + """ + _content_type = message.headers().get("Content-Type", "") + if not _content_type: + raise ValueError("Content-Type header is required") + content_type = _content_type[0] + # Parse JSON + if content_type == "application/json": + try: + return json.loads(message.body() or "{}") + except json.JSONDecodeError as e: + logging_error(f"Invalid JSON: {e}") + raise ValueError(f"Invalid JSON: {e}") + + # Parse URL-encoded form data + elif content_type == "application/x-www-form-urlencoded": + try: + _form_data: dict = { + k: v[0] if len(v) == 1 else v for k, v in parse_qs(message.body()).items() + } + if len(_form_data) == 0 and len(message.body()) > 0: + _form_data = json.loads(message.body()) + if _form_data["files"] is not None and _form_data["form"] is not None: + _form_data = AMQDataParser.process_form_data(_form_data) + return _form_data + except Exception as e: + logging_error(f"Invalid URL-encoded form data: {e}") + raise ValueError(f"Invalid URL-encoded form data: {e}") + + # Parse multipart/form-data + elif content_type.startswith("multipart/form-data"): + try: + parser: MultipartFormDataParser = MultipartFormDataParser(message) + return parser.form_data + except Exception as e: + logging_error(f"Invalid multipart/form-data: {e}") + raise ValueError(f"Invalid multipart/form-data: {e}") + + # Parse plain text + elif content_type == "text/plain": + return {"text": message.body()} + + # Parse XML + elif content_type == "application/xml": + try: + _root = ElementTree.fromstring(message.body()) + return {elem.tag: elem.text for elem in _root} + except ElementTree.ParseError as e: + raise ValueError(f"Invalid XML: {e}") + + else: + raise ValueError(f"Unsupported Content-Type: {content_type}")
+
+ +
+ +
+
+ +
+
+
+
+ + + + diff --git a/api-doc/_build/html/_modules/amqp/adapter/data_response_factory.html b/api-doc/_build/html/_modules/amqp/adapter/data_response_factory.html new file mode 100644 index 0000000..84d2a6f --- /dev/null +++ b/api-doc/_build/html/_modules/amqp/adapter/data_response_factory.html @@ -0,0 +1,276 @@ + + + + + + + + amqp.adapter.data_response_factory — AMQ Adapter Python 1.0 documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

Source code for amqp.adapter.data_response_factory

+"""
+method to create, serialize and deserialize DataResponse message
+"""
+
+import base64
+import re
+from io import BytesIO
+
+from amqp.adapter.serializer import map_as_string, parse_map_string
+from amqp.model.model import AMQErrorMessage, DataMessageMagicByte, DataResponse
+from amqp.model.snowflake_id import SnowflakeId
+
+
+
+[docs] +class DataResponseFactory: + +
+[docs] + @staticmethod + def create_async_response_message(_id: SnowflakeId) -> DataResponse: + """ + Helper method to create standard OK response. + :param _id: ID + :return: Response message + """ + return DataResponseFactory.of( + id=_id, + response_code=200, + content_type="application/text", + body="OK".encode("utf-8"), + trace_info={}, + error="", + error_cause="", + )
+ + +
+[docs] + @staticmethod + def of( + id: SnowflakeId, + response_code: int, + content_type: str, + body: bytes, + trace_info: dict[str, str], + error: str | None = None, + error_cause: str | None = None, + ) -> DataResponse: + """ + Create the DataResponse message supplying all key values + :param id: SnowflakeID + :param response_code: HTTP response code + :param content_type: MIME content type + :param body: payload + :param trace_info: Jaeger trace info (id) + :return: DataResponseMessage object + """ + return DataResponse( + magic=DataMessageMagicByte.HTTP_REQUEST.value, + id=id, + response_code=str(response_code), + content_type=content_type, + error=error, + error_cause=error_cause, + headers={}, + trace_info=trace_info, + base64body=base64.b64encode(body), + )
+ + +
+[docs] + @staticmethod + def of_error_message( + id: SnowflakeId, + error_message: AMQErrorMessage, + trace_info: dict[str, str], + ) -> DataResponse: + return DataResponseFactory.of( + id=id, + response_code=error_message.response_code, + content_type=error_message.content_type, + body=error_message.serialize(), + trace_info=trace_info, + error=error_message.error, + error_cause=error_message.detail, + )
+ + +
+[docs] + @staticmethod + def serialize(response: DataResponse) -> bytes: + """ + Serialize the message to byte array (to be sent over RabbitMQ) + :param response: Object to serialize + :return: byte array as serialized message + """ + id_bytes = response.id().as_string().encode("utf-8") + prolog = ( + f"|r={response.response_code()}|c={response.content_type()}|e={response.error()}|f={response.error_cause()}|t={{{map_as_string(response.trace_info())}}}|b=" + ).encode("utf-8") + header_length = 1 + len(id_bytes) + len(prolog) + prolog_len = header_length.to_bytes(2, byteorder="big") + msg_length = 2 + header_length + len(response.base64body()) + with BytesIO(bytearray(msg_length)) as baos: + baos.write(prolog_len) + baos.write(DataMessageMagicByte.HTTP_REQUEST.value.encode("utf-8")) + baos.write(id_bytes) + baos.write(prolog) + baos.write(response.base64body()) + return baos.getvalue()
+ + +
+[docs] + @staticmethod + def from_bytes(input_bytes: bytes) -> DataResponse: + """ + Buil;ds the DataResponse object from its serialized form + :param input_bytes: byte array - serialized response + :return: DataResponse object + """ + prolog_len = (input_bytes[0] << 8) + input_bytes[1] + metadata = input_bytes[2 : prolog_len + 2].decode() + + pattern = re.compile( + f"{DataMessageMagicByte.HTTP_REQUEST.value}" + r"([0-9A-Fa-f]+)\|" + r"r=([^|]*)\|" + r"c=([^|]*)\|" + r"e=([^|]*)\|" + r"f=([^|]*)\|" + r"t=(\{[^}]*\})\|" + r"b=" + ) + match = pattern.match(metadata) + if not match: + raise ValueError( + f"DataResponse format does not match expected format. " + f"Input string [{metadata}] does not match pattern: {pattern.pattern}" + ) + + id_str = match.group(1) + response_code = int(match.group(2)) + content_type = match.group(3) + error = match.group(4) + error_cause = match.group(5) + trace_info_str = match.group(6) + body_b64 = input_bytes[prolog_len + 2 :] + + trace_info = parse_map_string(trace_info_str) + + return DataResponseFactory.of( + id_str, + response_code, + content_type, + base64.b64decode(body_b64), + trace_info, + error, + error_cause, + )
+
+ + + +# Ensure backward compatibility +AMQResponseFactory = DataResponseFactory +
+ +
+
+ +
+
+
+
+ + + + diff --git a/api-doc/_build/html/_modules/amqp/adapter/file_handler.html b/api-doc/_build/html/_modules/amqp/adapter/file_handler.html new file mode 100644 index 0000000..bd66a50 --- /dev/null +++ b/api-doc/_build/html/_modules/amqp/adapter/file_handler.html @@ -0,0 +1,561 @@ + + + + + + + + amqp.adapter.file_handler — AMQ Adapter Python 1.0 documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for amqp.adapter.file_handler

+"""
+implements the file downloader for the amqp adapter, where file is sent in chunks over RabbitMQ
+"""
+
+import asyncio
+import json
+import logging
+import os
+from asyncio import AbstractEventLoop
+from collections import defaultdict
+from pathlib import Path
+
+from aio_pika import Message, connect_robust
+from aio_pika.abc import (
+    AbstractExchange,
+    AbstractIncomingMessage,
+    AbstractQueue,
+    AbstractRobustChannel,
+    DeliveryMode,
+)
+
+from amqp.adapter.logging_utils import logging_debug, logging_error, logging_info
+from amqp.router.utils import await_result
+
+# Global dictionary to track completed downloads
+download_locations = defaultdict(str)
+IN_PROGRESS = 0
+END_OF_FILE = 1
+CANCELLED = 2
+
+
+
+[docs] +class FileHandler: + """ + The abstract base class for file handling. This is mainly to make the code testable, as there are only 2 + options for pushing (uploading) a file(s) to a service: as HTTP multipart message delivered via RabbitMQ + or via stream in RabbitMQ. Downloading a file is always done via RabbitMQ stream. Thus the method signiture + is tighly coupled to the RabbitMQ API in v1. Only alternative implementation is a mock object in unittests. + """ + +
+[docs] + def __init__(self): + pass
+ + +
+[docs] + def ensure_directory_exists(self, filepath) -> str: + """ + Ensures that the directory for the given filepath exists, and handles file versioning. + + Args: + filepath: The path to the file. + + Returns: + The original filepath if the file does not exist, or a modified filepath + pointing to the next available version of the file if it does. + """ + # 1. Ensure directory exists + directory = os.path.dirname(filepath) + if directory and not os.path.exists(directory): + os.makedirs(directory) + + # 2. Handle file versioning + if not os.path.exists(filepath): + return filepath # File doesn't exist, return original path + + # File exists, find next available version + base, ext = os.path.splitext(filepath) + version = 1 + while True: + new_filepath = f"{base}.{version}{ext}" + if not os.path.exists(new_filepath): + return new_filepath # Found a free version, return this path + version += 1
+ + +
+[docs] + async def download_buffer(self, loop, rabbitmq_url, queue_name) -> (bytearray, int): + """ + Asynchronously downloads a bytearray from RabbitMQ based on the provided file information. + It assumes entire array is in the first (and only) chunk + Args: + loop: The asyncio event loop. + rabbitmq_url: The RabbitMQ connection URL. + queue_name (str): The directory where the file should be saved. + """ + raise NotImplementedError("Subclasses must implement this method.")
+ + +
+[docs] + async def download_file( + self, loop, rabbitmq_url, file_info, message_data, output_dir + ) -> (int, int): + """ + Asynchronously downloads a file from RabbitMQ based on the provided file information. + takes the file_info and message_data and output_dir, creates queue consumer and waits for the file + to be downloaded. File may consist of multiple chunks, so the function will wait until all chunks are received. + Args: + loop: The asyncio event loop. + rabbitmq_url: The RabbitMQ connection URL. + file_info (dict): A dictionary containing file metadata + message_data (dict): The entire message + output_dir (str): The directory where the file should be saved. + """ + raise NotImplementedError("Subclasses must implement this method.")
+ + +
+[docs] + async def on_message( + self, + message: AbstractIncomingMessage, + json_data: bytes, + rabbitmq_url: str, + loop, + output_dir="downloaded_files", + ) -> defaultdict: + """ + Asynchronous callback function for handling messages from the 'amq-cleverswarm' queue. + + Args: + message: The incoming RabbitMQ message. + rabbitmq_url: The RabbitMQ connection URL. + """ + raise NotImplementedError("Subclasses must implement this method.")
+ + +
+[docs] + async def publish_file( + self, + exchange: AbstractExchange, + file_path: Path, + routing_key: str, + loop: AbstractEventLoop, + max_chunk_size: int = 2**20, # 1MB default chunk size + content_type: str = "application/octet-stream", # Default content type + delivery_mode: DeliveryMode = DeliveryMode.PERSISTENT, + ) -> tuple[str, int]: + """ + Reads a file from the file system and publishes its content to a RabbitMQ exchange + in chunks. It declares a queue with a random name, binds it to the specified + exchange, and uses the queue name as the routing key. + + Args: + exchange: The aio_pika RobustChannel to use for communication with RabbitMQ. + file_path: The path to the file to publish. + max_chunk_size: The maximum size of each chunk (in bytes). + routing_key: The name of the RabbitMQ exchange routing key to publish to correct queue. + loop: correct event loop + max_chunk_size: chunk size to send + content_type: The content type of the file data. + delivery_mode: The delivery mode (e.g., PERSISTENT or TRANSIENT). + + Returns: + The name of the randomly generated queue where the file content was published. + """ + raise NotImplementedError("Subclasses must implement this method.")
+
+ + + +# The StreamingFileHandler class is a subclass of FileHandler that implements the download_file method +
+[docs] +class StreamingFileHandler(FileHandler): + """ + Handles file downloads from RabbitMQ streams. + """ + +
+[docs] + def __init__(self): + super().__init__()
+ + + # This method MUST NOT be static. + # For testing purpose, we will mock this method and inject a mock connection. + async def _get_connection(self, rabbitmq_url, loop): + return await connect_robust(rabbitmq_url, loop=loop) + + async def _download_from_queue_to_file( + self, rabbitmq_url, queue_name, filepath, loop + ) -> tuple[int, int] | None: + """ + Internal method for downloading a file from a queue to disk. + Return (file size, eof status). + """ + # Here the normal mock doesn't work since this method + # is async and will be run in another loop, + # out of the scope of method mock. + connection = await self._get_connection(rabbitmq_url, loop=loop) + _file_size = 0 + _eof = IN_PROGRESS + try: + channel: AbstractRobustChannel = await connection.channel() + queue: AbstractQueue = await channel.get_queue(queue_name, ensure=False) + logging_debug(f"Awaiting all file chunks being downloaded for {filepath}") + async with queue.iterator() as queue_iter: + async for message in queue_iter: + async with message.process(): + with open(filepath, "ab") as f: + f.write(message.body) + _file_size = _file_size + message.body_size + _eof = int(message.headers.get("eof", END_OF_FILE)) + if _eof != IN_PROGRESS: + break + except Exception as e: + logging_error(f"File download failed: {e}") + finally: + await connection.close() + + return _file_size, _eof + +
+[docs] + async def download_file( + self, loop, rabbitmq_url, file_info, message_data, output_dir + ) -> (int, int): + """ + Asynchronously downloads a file from RabbitMQ based on the provided file information. + takes the file_info and message_data and output_dir, creates queue consumer and waits for the file to be downloaded + Args: + loop: The asyncio event loop. + rabbitmq_url: The RabbitMQ connection URL. + file_info (dict): A dictionary containing file metadata + message_data (dict): The entire message + output_dir (str): The directory where the file should be saved. + """ + filename = file_info["filename"] + size = file_info["size"] + stream_name = file_info["streamName"] + queue_name = file_info["queue_name"] + filepath = os.path.join(output_dir, filename) + filepath = self.ensure_directory_exists( + filepath + ) # if filename exists, create a new (next) version + download_locations[file_info["key"]] = filepath + filename = os.path.basename( + filepath + ) # if a new version was created, ensure to use that new version + file_info["filename"] = filename + + logging_debug( + f"Downloading file: {filename} (size: {size} bytes) from stream: {stream_name}:{queue_name} to {filepath}" + ) + + if queue_name: + _local_file_size, _local_eof = await await_result( + asyncio.run_coroutine_threadsafe( + self._download_from_queue_to_file(rabbitmq_url, queue_name, filepath, loop), + loop=loop, + ) + ) + return _local_file_size, _local_eof + + return 0, CANCELLED
+ + + async def _download_from_queue_to_buffer( + self, rabbitmq_url, queue_name, loop + ) -> tuple[bytearray, int]: + """ + Internal method for downloading a file from a queue to RAM. + Return (bytearray, eof status). + """ + _connection = await self._get_connection(rabbitmq_url, loop=loop) + _eof = IN_PROGRESS + _res = bytearray() + try: + channel: AbstractRobustChannel = await _connection.channel() + queue: AbstractQueue = await channel.get_queue(queue_name, ensure=False) + logging_debug(f"Awaiting all buffer chunks being downloaded for {queue_name}") + async with queue.iterator() as queue_iter: + async for message in queue_iter: + async with message.process(): + _res.extend(message.body) + _eof = message.headers.get("eof", END_OF_FILE) + if _eof != IN_PROGRESS: + break + logging_info(f"Finished downloading buffer {queue_name}") + except Exception as e: + logging_error(f"Downloading buffer: {e}") + + await _connection.close() + return _res, _eof + +
+[docs] + async def download_buffer(self, loop, rabbitmq_url, queue_name: str | None) -> (bytearray, int): + """ + Asynchronously downloads a bytearay from RabbitMQ based on the provided file information. + It assumes entire array is in the first (and only) chunk + Args: + loop: The asyncio event loop. + rabbitmq_url: The RabbitMQ connection URL. + queue_name (str): The directory where the file should be saved. + """ + logging_debug(f"Downloading buffer from stream: {queue_name}") + if queue_name: + _local_res, _local_eof = await await_result( + asyncio.run_coroutine_threadsafe( + self._download_from_queue_to_buffer(rabbitmq_url, queue_name, loop), loop=loop + ) + ) + return _local_res, _local_eof + return 0, CANCELLED
+ + +
+[docs] + async def on_message( + self, + message: AbstractIncomingMessage, + json_data: bytes, + rabbitmq_url: str, + loop, + output_dir="downloaded_files", + ) -> defaultdict: + """ + Asynchronous callback function for handling messages from the 'amq-cleverswarm' queue. + + Args: + message: The incoming RabbitMQ message. + json_data: The JSON data from the message. + rabbitmq_url: The RabbitMQ connection URL. + loop: The asyncio event loop of the RabbitMQ API + output_dir + """ + try: + _message_data = json.loads(message.body.decode()) + _amq_message_data = json.loads(json_data.decode()) + + files_to_download = [] + for _key, _file_type in _amq_message_data["files"].items(): + if len(_file_type) > 0: + files_info = _file_type[0] + stream_name = files_info["streamName"] + files_info["queue_name"] = ( + _message_data.get(stream_name, "") if stream_name else "" + ) + files_info["key"] = _key + files_to_download.append(files_info) + + if not files_to_download: + logging_info(" [%s] No files to download in this message.", message.delivery_tag) + # await message.ack() + return defaultdict() + + # Create a task for each file download + _tasks = [] + for file_info in files_to_download: + logging_info(f" [{message.delivery_tag}] about to create task for {file_info}") + _task = asyncio.create_task( + self.download_file(loop, rabbitmq_url, file_info, _message_data, output_dir) + ) + _tasks.append(_task) + logging_info( + f" [{message.delivery_tag}] Waiting for all files to download...gather(*tasks)" + ) + await asyncio.gather(*_tasks) # Wait for all downloads to complete + + logging_info(f"All files downloaded. d-tag: {message.delivery_tag}") + except Exception as e: + logging_error(f"Building DataMessage: {e}") + asyncio.run_coroutine_threadsafe( + message.ack(), loop=loop + ) # ACK -> ignore the message (do not redeliver) + + return download_locations
+ + +
+[docs] + async def publish_file( + self, + exchange: AbstractExchange, + file_path: Path, + routing_key: str, + loop: AbstractEventLoop, + max_chunk_size: int = 2**20, # 1MB default chunk size + content_type: str = "application/octet-stream", # Default content type + delivery_mode: DeliveryMode = DeliveryMode.PERSISTENT, + ) -> tuple[str, int]: + """ + Reads a file from the file system and publishes its content to a RabbitMQ exchange + in chunks. It declares a queue with a random name, binds it to the specified + exchange, and uses the queue name as the routing key. + + Args: + exchange: The aio_pika RobustChannel to use for communication with RabbitMQ. + file_path: The path to the file to publish. + max_chunk_size: The maximum size of each chunk (in bytes). + routing_key: The name of the RabbitMQ exchange routing key to publish to correct queue. + loop: correct event loop + max_chunk_size: chunk size to send + content_type: The content type of the file data. + delivery_mode: The delivery mode (e.g., PERSISTENT or TRANSIENT). + + Returns: + The name of the randomly generated queue where the file content was published. + """ + if not file_path.is_file(): + raise FileNotFoundError(f"File not found: {file_path}") + _file_size = os.path.getsize(file_path) + + with open(file_path, "rb") as f: + _offset = 0 + while True: + _chunk = f.read(max_chunk_size) + if not _chunk: + break # End of file + + message = Message( + body=_chunk, + content_type=content_type, + delivery_mode=delivery_mode, + headers={ + "file_name": file_path.name, + "pos": _offset, + "total_size": _file_size, + "chunk_size": len(_chunk), + "eof": ( + END_OF_FILE if max_chunk_size + _offset >= _file_size else IN_PROGRESS + ), + }, + ) + logging.debug( + f"Going to Publish chunk {_offset}/{_file_size} to {exchange.name}:{routing_key}" + ) + _future = asyncio.run_coroutine_threadsafe( + exchange.publish(message, routing_key=routing_key), loop + ) + while not _future.done(): + # sleep allows coroutine to execute. must wait for completion to ensure all chunks written in right order + await asyncio.sleep(0.02) + logging_debug(f"Waiting for chunk {_offset}/{_file_size} future= {_future}") + + _offset += len(_chunk) + logging_debug( + f"Published chunk {_offset}/{_file_size} to {exchange.name}:{routing_key}" + ) + + logging_debug( + f"File {file_path} published to RabbitMQ exchange {exchange.name}, queue {routing_key}" + ) + return routing_key, _file_size
+
+ +
+ +
+
+ +
+
+
+
+ + + + diff --git a/api-doc/_build/html/_modules/amqp/adapter/logging_utils.html b/api-doc/_build/html/_modules/amqp/adapter/logging_utils.html new file mode 100644 index 0000000..0f1b5d3 --- /dev/null +++ b/api-doc/_build/html/_modules/amqp/adapter/logging_utils.html @@ -0,0 +1,250 @@ + + + + + + + + amqp.adapter.logging_utils — AMQ Adapter Python 1.0 documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for amqp.adapter.logging_utils

+import inspect
+import logging
+import os
+import sys
+import threading
+import traceback
+from importlib.metadata import version
+
+# Get version from installed package metadata
+__version__ = version("amq_adapter")  # Name must match pyproject.toml [project] name
+__verbose__ = os.environ.get("AMQ_VERBOSE_LOGGING", "0") == "1"
+
+
+
+[docs] +def get_context_info(): + """ + Retrieves the current_availability module, line number, and function name. + + Returns: + A tuple containing (module name, line number, function name). + Returns (None, None, None) if it fails to retrieve the information. + """ + try: + # Get the frame object for the current_availability function call + frame = inspect.currentframe() + if frame is not None: + # Get the frame object for the caller of the current_availability function + frame = frame.f_back.f_back + if frame: + code_context = frame.f_code + module_name = code_context.co_filename + module_name = ( + "amqp" + module_name.split("/amqp", 1)[-1] + if "/amqp" in module_name + else module_name + ) + line_number = frame.f_lineno + function_name = code_context.co_name + return module_name, line_number, function_name + else: + return None, None, None + except Exception: + return None, None, None
+ + + +
+[docs] +def logging_error(msg: str, *args) -> None: + """ + Log an error message according to current_availability logging for 'ERROR' level. + Add module name, line and function name where the error occurred, on single line. + Args: + msg (str): The error message to log. + """ + _exec_info = sys.exc_info()[2] + _thread_id = threading.get_ident() + if _exec_info: + _tb = traceback.extract_tb(sys.exc_info()[2])[-1] + logging.error( + f"{_thread_id}:{__version__} [E] '{msg}' ---> '{_tb.filename}':{_tb.lineno}, '{_tb.name}()'", + *args, + ) + else: + module, line_number, function_name = get_context_info() + if module and line_number and function_name: + logging.error( + f"{_thread_id}:{__version__} [E] '{msg}' ---> '{module}':{line_number}, '{function_name}()'", + *args, + ) + else: + logging.error(f"{_thread_id}:{__version__} [E] {msg}", *args)
+ + + +
+[docs] +def logging_warning(msg: str, *args) -> None: + """ + Log a warning message according to current_availability logging for 'WARN' level. + Add module name, line and function name where the print occurred, on single line. + Args: + msg (str): The warning message to log. + """ + _thread_id = threading.get_ident() + if not __verbose__: + logging.warning(f"{_thread_id}:{__version__} [W] {msg}", *args) + return + module, line_number, function_name = get_context_info() + if module and line_number and function_name: + logging.warning( + f"{_thread_id}:{__version__} [W] '{msg}' ---> '{module}':{line_number}, '{function_name}()'", + *args, + ) + else: + logging.warning(f"{_thread_id}:{__version__} [W] {msg}", *args)
+ + + +
+[docs] +def logging_info(msg: str, *args) -> None: + """ + Log an info message according to current_availability logging for 'INFO' level. + Add module name, line and function name where the print occurred, on single line. + Args: + msg (str): The info message to log. + """ + _thread_id = threading.get_ident() + if not __verbose__: + logging.info(f"{_thread_id}:{__version__} [*] {msg}", *args) + return + module, line_number, function_name = get_context_info() + if module and line_number and function_name: + logging.info( + f"{_thread_id}:{__version__} [*] '{msg}' ---> '{module}':{line_number}, '{function_name}()'", + *args, + ) + else: + logging.info(f"{_thread_id}:{__version__} [*] {msg}", *args)
+ + + +
+[docs] +def logging_debug(msg: str, *args) -> None: + """ + Log a debug message according to current_availability logging for 'DEBUG' level. + Add module name, line and function name where the print occurred, on single line. + Args: + msg (str): The debug message to log. + """ + _thread_id = threading.get_ident() + module, line_number, function_name = get_context_info() + if module and line_number and function_name: + logging.debug( + f"{_thread_id}:{__version__} [DBG] '{msg}' ---> '{module}':{line_number}, '{function_name}()'", + *args, + ) + else: + logging.debug(f"{_thread_id}:{__version__} [DBG] {msg}", *args)
+ +
+ +
+
+ +
+
+
+
+ + + + diff --git a/api-doc/_build/html/_modules/amqp/adapter/pydantic_serializer.html b/api-doc/_build/html/_modules/amqp/adapter/pydantic_serializer.html new file mode 100644 index 0000000..bcca3b5 --- /dev/null +++ b/api-doc/_build/html/_modules/amqp/adapter/pydantic_serializer.html @@ -0,0 +1,304 @@ + + + + + + + + amqp.adapter.pydantic_serializer — AMQ Adapter Python 1.0 documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

Source code for amqp.adapter.pydantic_serializer

+"""
+This module provides serialization and deserialization functions for Pydantic models that extend BaseModel
+"""
+
+import importlib
+import json
+import uuid
+from datetime import date, datetime
+from types import UnionType
+from typing import Any, Dict, Union, get_args, get_origin
+from urllib.parse import parse_qs, urlparse
+
+from pydantic import BaseModel
+
+from amqp.adapter.logging_utils import logging_debug
+
+
+
+[docs] +def serialize_object(obj: BaseModel) -> str: + """ + Serializes an R2RSerializable object to a string in the format + "<ClassName>:{key1:value1, key2:value2,...}". Handles UUIDs and datetimes, + and nested Pydantic models. + + Args: + obj: The BaseModel object to serialize. + + Returns: + A string representation of the object. + """ + class_name = obj.__class__.__name__ + module_name = obj.__class__.__module__ + obj_dict = {} + for key, _ in obj.model_dump().items(): + # here we use the original value + # the one returned by model_dump will convert everything into dict recursively + value = getattr(obj, key) + if isinstance(value, uuid.UUID): + obj_dict[key] = str(value) + elif isinstance(value, datetime): + obj_dict[key] = value.isoformat() + elif isinstance(value, dict): # Ensure dictionaries are handled + obj_dict[key] = value + elif isinstance(value, BaseModel): + obj_dict[key] = serialize_object(value) # Recursively serialize + elif isinstance(value, list): + serialized_list = [] + for item in value: + if isinstance(item, BaseModel): + serialized_list.append(serialize_object(item)) + else: + serialized_list.append(item) + obj_dict[key] = serialized_list + else: + obj_dict[key] = value + + dict_string = json.dumps(obj_dict) + return f"{module_name}.{class_name}:{dict_string}"
+ + + +
+[docs] +def python_type_to_json(data): + """ + Convert Python objects (dict, list, tuple, set, datetime, etc.) to a JSON string. + Handles common non-JSON-serializable types like datetime, date, and sets. + + Args: + data: Python object to be converted (dict, list, tuple, set, etc.) + + Returns: + str: JSON string representation of the input data + + Raises: + TypeError: If the object contains non-serializable types (e.g., custom classes) + """ + + def default_serializer(obj): + """Custom serializer for non-JSON-serializable objects.""" + if isinstance(obj, (datetime, date)): + return obj.isoformat() # Convert datetime/date to ISO string + elif isinstance(obj, set): + return list(obj) # Convert sets to lists + elif hasattr(obj, "__dict__"): + return obj.__dict__ # Attempt to serialize custom objects + else: + raise TypeError(f"Object of type {type(obj)} is not JSON serializable") + + return json.dumps(data, default=default_serializer, indent=0, ensure_ascii=False)
+ + + +
+[docs] +def try_deserialize_object(data: str) -> BaseModel | str: + """ + Try deserialize object, if failed, return the input string as-is. + """ + if not isinstance(data, str): + return data + try: + return deserialize_object(data) + except Exception as e: + logging_debug(f"Failed to deserialize object: {e}") + return data
+ + + +
+[docs] +def deserialize_object(data: str) -> BaseModel: + """ + Deserializes a string in the format "<ClassName>:{key1:value1, key2:value2,...}" + back into an BaseModel object. Handles UUIDs, datetimes, and + nested Pydantic models. + + Args: + data: The string to deserialize. + + Returns: + An R2RSerializable object. + """ + class_path, dict_str = data.split(":", 1) + module_path, class_name = class_path.rsplit(".", 1) + + obj_dict: Dict[str, Any] = json.loads(dict_str) + + logging_debug(f"Deserialized object: {class_path}") + if "[" in class_name: + class_name = class_name.split("[")[0] + logging_debug(f"trying object: {class_name}") + module = importlib.import_module(module_path) + cls = getattr(module, class_name) + if cls is None: + raise ValueError(f"Unknown class: {class_name}") + + # process nested model, dict and list + for key, value in obj_dict.items(): + key_type: type[Any] = cls.model_fields[key].annotation + # handle union with none, this will give the true type + # for example, return `A` if the type is `A | None` + if get_origin(key_type) is UnionType or get_origin(key_type) is Union: + union_types = get_args(key_type) + if len(union_types) > 2: + # if there are multiple unions, like `A | B | None` + # then we have no way to figure out which one to use + raise ValueError(f"Unsupported Union type: {key_type}") + # return the none-None one as the type for processing + if union_types[0] is type(None): + key_type = union_types[1] + else: + key_type = union_types[0] + # skip if the value is None + if value is None: + continue + + if key_type == uuid.UUID: + obj_dict[key] = uuid.UUID(value) + elif key_type == datetime: + obj_dict[key] = datetime.fromisoformat(value) + elif key_type == dict: + obj_dict[key] = value + elif issubclass(key_type, BaseModel): + obj_dict[key] = deserialize_object(value) + elif key_type == list: + obj_dict[key] = list(map(try_deserialize_object, value)) + + return cls(**obj_dict)
+ + + +
+[docs] +def parse_url_query(url): + """ + Parses a HTTP GET URL and returns (context_path, query_params_dict) + + Args: + url: The URL string to parse (e.g., "http://example.com/path?foo=1&bar=2&foo=3") + + Returns: + tuple: (context_path, query_params_dict) + - context_path: The path part of the URL (e.g., "/path") + - query_params_dict: Dictionary of query parameters where values are always lists + (e.g., {"foo": ["1", "3"], "bar": ["2"]}) + """ + parsed = urlparse(url) + query_params = parse_qs(parsed.query) + + # Convert values from lists to single values when there's only one value + # If you prefer to always keep values as lists, remove this comprehension + simplified_params = {k: v[0] if len(v) == 1 else v for k, v in query_params.items()} + + return (parsed.path, simplified_params)
+ +
+ +
+
+ +
+
+
+
+ + + + diff --git a/api-doc/_build/html/_modules/amqp/adapter/serializer.html b/api-doc/_build/html/_modules/amqp/adapter/serializer.html new file mode 100644 index 0000000..0525751 --- /dev/null +++ b/api-doc/_build/html/_modules/amqp/adapter/serializer.html @@ -0,0 +1,216 @@ + + + + + + + + amqp.adapter.serializer — AMQ Adapter Python 1.0 documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for amqp.adapter.serializer

+"""
+Helper functions used to aid with (de)serialization of messages sent over AMQP.
+"""
+
+import struct
+from io import BytesIO
+from typing import Dict, List, Union
+
+
+
+[docs] +def long_to_bytes(x: int) -> bytes: + return struct.pack("q", x)
+ + + +
+[docs] +def bytes_to_long(b: bytes) -> int: + return struct.unpack("q", b)[0]
+ + + +
+[docs] +def read_long(bis: BytesIO) -> int: + try: + return struct.unpack("q", bis.read(8))[0] + except struct.error: + return 0
+ + + +
+[docs] +def object_or_list_as_string(value: Union[str, List[str]]) -> str: + if isinstance(value, list): + return f"[{','.join(value)}]" + return str(value)
+ + + +
+[docs] +def map_as_string(map_: Dict[str, str]) -> str: + return ",".join(f"{k}={object_or_list_as_string(v)}" for k, v in map_.items())
+ + + +
+[docs] +def map_with_list_as_string(map_: Dict[str, List[str]]) -> str: + return ",".join(f"{k}={object_or_list_as_string(v)}" for k, v in map_.items())
+ + + +
+[docs] +def parse_map_string(input_str: str) -> Dict[str, str]: + map_ = {} + if input_str.startswith("{") and input_str.endswith("}"): + for entry in str(input_str[1:-1]).replace("\n", "").split(","): + if len(entry) > 1: + if entry.startswith('"'): + key, value = entry.split(":") + map_[key.strip('"')] = value.strip('"') + else: + key, value = entry.split("=") + map_[key] = value + return map_
+ + + +
+[docs] +def csv_as_list(input_str: str) -> List[str]: + return [item.strip() for item in input_str.strip("[]").split(",")]
+ + + +
+[docs] +def add_to_map(map_: Dict[str, List[str]], token: str) -> None: + try: + eq_idx = token.index("=") + except ValueError: + eq_idx = -1 # set to -1 when not found + if eq_idx > 0: + key = token[:eq_idx] + map_[key] = csv_as_list(token[eq_idx + 1 :])
+ + + +
+[docs] +def parse_map_list_string(input_str: str) -> Dict[str, List[str]]: + map_ = {} + if input_str.startswith("{") and input_str.endswith("}"): + for token in input_str[1:-1].split("],"): + add_to_map(map_, token) + return map_
+ + + +
+[docs] +def str_to_bool(value: str) -> bool: + return value.lower() in ("true", "yes", "1", "t", "y", "on")
+ +
+ +
+
+ +
+
+
+
+ + + + diff --git a/api-doc/_build/html/_modules/amqp/adapter/service_message_factory.html b/api-doc/_build/html/_modules/amqp/adapter/service_message_factory.html new file mode 100644 index 0000000..760c151 --- /dev/null +++ b/api-doc/_build/html/_modules/amqp/adapter/service_message_factory.html @@ -0,0 +1,278 @@ + + + + + + + + amqp.adapter.service_message_factory — AMQ Adapter Python 1.0 documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

Source code for amqp.adapter.service_message_factory

+import base64
+
+from amqp.adapter.data_message_factory import DataMessageFactory
+from amqp.adapter.logging_utils import logging_error, logging_info
+from amqp.adapter.serializer import map_as_string, parse_map_string
+from amqp.adapter.trace_info_adapter import TraceInfoAdapter
+from amqp.model.model import ServiceMessage
+from amqp.model.service_message_type import ServiceMessageType
+from amqp.model.snowflake_id import SnowflakeId
+
+RS = "|"
+SPLIT_RS = r"\|"
+
+
+INVALID_MESSAGE = ServiceMessage()  # default values mean invalid message
+
+
+
+[docs] +class ServiceMessageFactory: + """ + build/serialize/deserialize CleverMicro Service Message class + """ + +
+[docs] + def __init__(self, name: str): + self.process_name = name
+ + +
+[docs] + def to_string(self, message: ServiceMessage) -> str: + """ + Convert ServiceMessage to human-readable format + :param message: ServiceMessage object + :return: as string + """ + if message is not None: + return ( + f"ServiceMessage{{id={message.id.as_string()}|type={self.format_message_type(message)}|" + f"recipient={message.recipient_name}|replyTo={message.reply_to}|" + f"traceInfo={message.trace_info}|body={base64.b64encode(message.message.encode()).decode() if message.payload_type & 0x80 else message.message}}}" + ) + return "null"
+ + +
+[docs] + def of( + self, message_type: ServiceMessageType, payload: str, recipient_name: str + ) -> ServiceMessage: + """ + Builds ServiceMessage object from relevant values. + :param message_type: enum Message type + :param payload: Message content, stored as-is + :param recipient_name: Recipient name (from the routing expression) + :return: ServiceMessage object + """ + _id = DataMessageFactory.get_instance(1).generate_snowflake_id() + return ServiceMessage( + id=_id, + payload_type=0, + message_type=message_type, + recipient_name=recipient_name, + reply_to=self.process_name, + trace_info=TraceInfoAdapter.create_produce_span(_id), + message=payload, + )
+ + +
+[docs] + def as_base64( + self, message_type: ServiceMessageType, payload: str, recipient_name: str + ) -> ServiceMessage: + """ + Builds ServiceMessage object from relevant values, stores content Base64 encoded. + :param message_type: enum Message type + :param payload: Message content, stored as Base64 string + :param recipient_name: Recipient name (from the routing expression) + :return: ServiceMessage object + """ + _id = DataMessageFactory.get_instance(1).generate_snowflake_id() + return ServiceMessage( + id=_id, + payload_type=0x80, + message_type=message_type, + recipient_name=recipient_name, + reply_to=self.process_name, + trace_info=TraceInfoAdapter.create_produce_span(_id.as_string()), + message=payload, + )
+ + +
+[docs] + def from_bytes(self, input_bytes: bytes) -> ServiceMessage: + """ + Deserialize ServiceMessage from byte array serialized form. + :param input_bytes: serialized ServiceMessage + :return: ServiceMessage object represented by the input. + """ + logging_info(f"ServiceMessage >>> [{input_bytes.decode()}]") + input_str_array = input_bytes.decode().split(RS) + try: + i = 0 + _id = input_str_array[i] + i += 1 + payload_type = int(input_str_array[i], 16) + i += 1 + message_type = ServiceMessageType(payload_type + 1 & 0x7F) + recipient_name = input_str_array[i] if i < len(input_str_array) else "" + i += 1 + reply_to = input_str_array[i] if i < len(input_str_array) else "" + i += 1 + trace_info = parse_map_string(input_str_array[i]) if i < len(input_str_array) else {} + i += 1 + message = input_str_array[i] if i < len(input_str_array) else "" + return ServiceMessage( + id=SnowflakeId.from_hex(_id), + payload_type=payload_type, + message_type=message_type, + recipient_name=recipient_name, + reply_to=reply_to, + trace_info=trace_info, + message=(base64.b64decode(message).decode() if payload_type & 0x80 else message), + ) + except Exception as e: + logging_error( + "Unexpected ServiceMessage format. Expected 6 tokens separated by '|' in format: " + "[ID(64bit number)|MessageType(8bit number)|RecipientName(String)|" + "ReplyTo(String)|TraceInfo|Payload(String or Base64)], got: " + "[%s], reported problem: %s", + input_bytes.decode(), + str(e), + ) + return INVALID_MESSAGE
+ + +
+[docs] + def serialize(self, message: ServiceMessage) -> bytes: + """ + Serialize ServiceMessage to byte array. + :param message: ServiceMessage to serialize. + :return: serialized object. + """ + return ( + f"{message.id.as_string()}{RS}{self.format_message_type(message)}{RS}{message.recipient_name}{RS}" + f"{message.reply_to}{RS}{map_as_string(message.trace_info)}{RS}" + f"{base64.b64encode(message.message.encode()).decode() if message.payload_type != 0 else message.message}" + ).encode("utf-8")
+ + +
+[docs] + def format_message_type(self, message: ServiceMessage) -> str: + """ + Formats the Enum message type and decorates it with base64 flag at highest bit. + For compatibility with 0-based enum ordinals in Java subtract 1 from ordinal value. + :param message: Service message to send + :return: formatted message type + """ + num_type = ( + message.message_type.value if message.message_type else ServiceMessageType.UNKNOWN.value + ) - 1 + fmt = (num_type & 0x7F) | (message.payload_type & 0x80) + return f"{fmt:02X}"
+
+ +
+ +
+
+ +
+
+
+
+ + + + diff --git a/api-doc/_build/html/_modules/amqp/adapter/trace_info_adapter.html b/api-doc/_build/html/_modules/amqp/adapter/trace_info_adapter.html new file mode 100644 index 0000000..bc312c4 --- /dev/null +++ b/api-doc/_build/html/_modules/amqp/adapter/trace_info_adapter.html @@ -0,0 +1,126 @@ + + + + + + + + amqp.adapter.trace_info_adapter — AMQ Adapter Python 1.0 documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

Source code for amqp.adapter.trace_info_adapter

+from dataclasses import dataclass
+from typing import Dict
+
+
+
+[docs] +@dataclass +class TraceInfoAdapter: + """ + TODO This class should set up the Open Telemetry tracing stack. To be done later. + """ + +
+[docs] + @staticmethod + def create_produce_span(message_id: str) -> Dict[str, str]: + return {"trace-id": message_id}
+
+ +
+ +
+
+ +
+
+
+
+ + + + diff --git a/api-doc/_build/html/_modules/amqp/adapter/type_descriptor.html b/api-doc/_build/html/_modules/amqp/adapter/type_descriptor.html new file mode 100644 index 0000000..63294ec --- /dev/null +++ b/api-doc/_build/html/_modules/amqp/adapter/type_descriptor.html @@ -0,0 +1,324 @@ + + + + + + + + amqp.adapter.type_descriptor — AMQ Adapter Python 1.0 documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for amqp.adapter.type_descriptor

+""" """
+
+import importlib
+import json
+from typing import Any
+
+
+
+[docs] +class TypeDescriptor: +
+[docs] + def __init__(self, type_string: str, extra_init_data: dict): + self.type_string = type_string + self.extra_init_data = extra_init_data or {} + self.is_json: bool = False + self.type: str = self._extract_type()
+ + + def _extract_type(self) -> str: + if self.type_string.startswith("<class '"): + return self.type_string[8:-2] + # Handle Annotated case of FastAPI annotation + if "typing.Optional" in self.type_string: + annotated = _extract_type_of(self.type_string) + if "typing.Annotated" in annotated: + # Extract content between the first square brackets after Annotated + match = _extract_type_of(annotated) + if match: + # Split by comma and get first part, then strip whitespace + parts = [p.strip() for p in match.split(",")] + if len(parts) > 1: + self.is_json = parts[1].lower() == "json" + return parts[0] if parts else "" + else: + # Handle simple Optional case + return annotated + return _extract_type_of(self.type_string) + +
+[docs] + def instantiate(self, payload: Any, is_json: bool = False) -> Any: + return _instantiate( + ( + self.type + if "typing.Optional" in self.type_string or self.type_string.startswith("<class ") + else self.type_string + ), + payload, + self.is_json or is_json, + extra_init_data=self.extra_init_data, + )
+ + + def __repr__(self) -> str: + return f"TypeDescriptor(type='{self.type}', is_json={self.is_json})"
+ + + +def _extract_type_of(input_string: str) -> str: + stack = [] + start_index = -1 + end_index = -1 + + for i, char in enumerate(input_string): + if i > 0: # this assumes paramerized type, that must be at lest 1 char long + if char == "[": + if start_index == -1: + start_index = i + stack.append(char) + elif char == "]": + if len(stack) == 1: + end_index = i + break + if len(stack) > 0: + stack.pop() + + if start_index != -1 and end_index != -1: + return input_string[start_index + 1 : end_index].strip() + else: + return input_string.strip() if start_index == 1 and end_index == -1 else "" + + +
+[docs] +def merge_extra_init_data(cls, json_payload, extra_init_data: dict): + """ + Merges extra_init_data into json_payload only when: + 1. cls has an attribute named like the key + 2. json_payload doesn't already have the key + """ + return { + **json_payload, + **{ + k: v + for k, v in extra_init_data.items() + if (hasattr(cls, k) or (hasattr(cls, "model_fields") and k in cls.model_fields)) + and k not in json_payload + }, + }
+ + + +def _instantiate( + type_string: str, payload: Any, is_json: bool = False, extra_init_data: dict = None +) -> Any: + """ + Dynamically imports and instantiates a class from a TypeDescriptor's type_string attribute. + + Args: + type_string: TypeDescriptor instance with the class path + payload: Argument to pass to the constructor + + Returns: + Instance of the target class initialized with payload + + Raises: + ImportError: If the module/class cannot be imported + TypeError: If instantiation fails + """ + # Handle JSON payload first + json_payload = json.loads(payload) if is_json and isinstance(payload, str) else None + + # Handle basic types + if type_string == "str": + return str(payload) + if type_string == "int": + return int(payload) + if type_string == "float": + return float(payload) + if type_string == "bool": + return bool(payload) + + # Handle typing module types + if type_string.startswith("typing.") or type_string.startswith("list"): + if ( + type_string.startswith("typing.List") + or type_string.startswith("typing.Sequence") + or type_string.startswith("list") + ): + if json_payload is not None: + return json_payload + inner_type = _extract_type_of(type_string) + if inner_type: + return [ + _instantiate(inner_type, p, extra_init_data=extra_init_data) + for p in (payload if isinstance(payload, list) else payload.split(",")) + ] + return payload if isinstance(payload, list) else payload.split(",") + + if type_string.startswith("typing.Dict") or type_string.startswith("typing.Mapping"): + if json_payload is not None: + return json_payload + return dict(payload) + + if type_string.startswith("typing.Set"): + if json_payload is not None: + return set(json_payload) + _set_type = _extract_type_of(type_string) + return set( + payload + if isinstance(payload, list) + else [_instantiate(_set_type, p) for p in payload.split(",")] + ) + + if type_string.startswith("typing.Tuple"): + if json_payload is not None: + return tuple(json_payload) + _tuple_type = _extract_type_of(type_string) + return tuple( + payload + if isinstance(payload, list) + else [_instantiate(_tuple_type, p) for p in payload.split(",")] + ) + + if type_string.startswith("typing.Optional"): + inner_type = _extract_type_of(type_string) + if inner_type: + return _instantiate(inner_type, payload, is_json, extra_init_data) + return payload + + # Handle regular class instantiation + parts = type_string.split(".") + module_path = ".".join(parts[:-1]) + class_name = parts[-1] + + try: + # Import the module + module = importlib.import_module(module_path) + # Get the class + cls = getattr(module, class_name) + if isinstance(payload, cls): + return payload + # Instantiate with payload + if json_payload is not None: + _args = merge_extra_init_data(cls, json_payload, extra_init_data or {}) + if hasattr(cls, "from_dict"): + return cls.from_dict(_args) + elif hasattr(cls, "create"): + return cls.create(**_args) + return cls(**_args) + + if isinstance(payload, dict): + _args = merge_extra_init_data(cls, payload, extra_init_data or {}) + if hasattr(cls, "from_dict"): + return cls.from_dict(_args) + elif hasattr(cls, "create"): + return cls.create(**_args) + return cls(**_args) + + return cls(payload) + except ImportError as e: + raise ImportError(f"Could not import module '{module_path}'") from e + except AttributeError as e: + raise ImportError(f"Module '{module_path}' has no class '{class_name}'") from e + except TypeError as e: + raise TypeError(f"Failed to instantiate {type_string} with payload {payload}") from e +
+ +
+
+ +
+
+
+
+ + + + diff --git a/api-doc/_build/html/_modules/amqp/config/amq_configuration.html b/api-doc/_build/html/_modules/amqp/config/amq_configuration.html new file mode 100644 index 0000000..f72d00d --- /dev/null +++ b/api-doc/_build/html/_modules/amqp/config/amq_configuration.html @@ -0,0 +1,383 @@ + + + + + + + + amqp.config.amq_configuration — AMQ Adapter Python 1.0 documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

Source code for amqp.config.amq_configuration

+import configparser
+import inspect
+import os
+
+from amqp.adapter.logging_utils import logging_error, logging_warning
+
+"""
+Convert configuration from properties file to an object.
+"""
+
+
+
+[docs] +class AMQAdapter: + """ + configuration with prefix 'cm.amq-adapter' + """ + + PREFIX: str = "cm.amq-adapter." + +
+[docs] + def __init__(self, config: configparser.ConfigParser): + """ + cm.amq-adapter.service-name=amq-adapter + cm.amq-adapter.generator-id=164 + cm.amq-adapter.route-mapping= + + :param config: config parser to read configuration values + """ + self.generator_id = 0 + self.service_name = config.get( + "CleverMicro-AMQ", + self.PREFIX + "service-name", + fallback="amq-python-adapter-" + str(self.generator_id), + ) + self.route_mapping = config.get( + "CleverMicro-AMQ", self.PREFIX + "route-mapping", fallback=None + ) + self.require_authenticated_user = config.getboolean( + "CleverMicro-AMQ", + self.PREFIX + "require-authenticated-user", + fallback=False, + ) + self.backpressure_enabled = config.getboolean( + "CleverMicro-AMQ", + self.PREFIX + "default-backpressure-enabled", + fallback=False, + ) + self.callbacks_enabled = config.getboolean( + "CleverMicro-AMQ", + self.PREFIX + "callbacks-enabled", + fallback=False, + ) + # these 2 shall be provided by Docker swarm, format is a random alphanumeric string + self.swarm_service_id = os.getenv("SWARM_SERVICE_ID", default="clever-amqp-service") + self.swarm_task_id = os.getenv("SWARM_TASK_ID", default="python-adapter") + # + # Consul related configuration + self.consul_port = ( + int( + os.getenv( + "CONSUL_PORT", + config.get("CleverMicro-AMQ", self.PREFIX + "consul-port", fallback="8500"), + ) + ) + if os.getenv("CONSUL_PORT") is not None + else config.getint("CleverMicro-AMQ", self.PREFIX + "consul-port", fallback=8500) + ) + self.consul_host = os.getenv( + "CONSUL_HOST", + config.get("CleverMicro-AMQ", self.PREFIX + "consul-host", fallback="consul"), + ) + self.consul_max_retries = ( + int( + os.getenv( + "CONSUL_MAX_RETRIES", + config.get("CleverMicro-AMQ", self.PREFIX + "consul-max-retries", fallback="5"), + ) + ) + if os.getenv("CONSUL_MAX_RETRIES") is not None + else config.getint("CleverMicro-AMQ", self.PREFIX + "consul-max-retries", fallback=5) + ) + self.consul_initial_retry_delay = ( + float( + os.getenv( + "CONSUL_INITIAL_RETRY_DELAY", + config.get( + "CleverMicro-AMQ", + self.PREFIX + "consul-initial-retry-delay", + fallback="0.2", + ), + ) + ) + if os.getenv("CONSUL_INITIAL_RETRY_DELAY") is not None + else config.getfloat( + "CleverMicro-AMQ", self.PREFIX + "consul-initial-retry-delay", fallback=0.2 + ) + ) + self.consul_counter_key = ( + os.getenv( + "CONSUL_COUNTER_KEY", + config.get( + "CleverMicro-AMQ", + self.PREFIX + "consul-counter-key", + fallback="service/ids/counter", + ), + ) + if os.getenv("CONSUL_COUNTER_KEY") is not None + else config.get( + "CleverMicro-AMQ", + self.PREFIX + "consul-counter-key", + fallback="service/ids/counter", + ) + )
+ + +
+[docs] + def adapter_prefix(self) -> str: + return f"{self.service_name}-{self.generator_id}-"
+
+ + + +
+[docs] +class Dispatch: + """ + configuration with prefix 'cm.dispatch' + """ + + PREFIX: str = "cm.dispatch." + +
+[docs] + def __init__(self, config: configparser.ConfigParser): + """ + cm.dispatch.use-dlq=true + cm.dispatch.amq-host=rabbitmq + cm.dispatch.amq-port=5672 + cm.dispatch.amq-port-tls=5671 + cm.dispatch.service-host= + cm.dispatch.service-port=8080 + + :param config: config parser to read configuration values + """ + self.use_dlq = config.getboolean("CleverMicro-AMQ", self.PREFIX + "use-dlq", fallback=False) + self.amq_host = config.get("CleverMicro-AMQ", self.PREFIX + "amq-host", fallback="rabbitmq") + self.amq_port = config.getint("CleverMicro-AMQ", self.PREFIX + "amq-port", fallback=5672) + self.amq_port_tls = config.getint( + "CleverMicro-AMQ", self.PREFIX + "amq-port-tls", fallback=5671 + ) + self.service_host = config.get( + "CleverMicro-AMQ", self.PREFIX + "service-host", fallback="localhost" + ) + self.service_port = config.getint( + "CleverMicro-AMQ", self.PREFIX + "service-port", fallback=8080 + ) + self.rabbit_mq_user = os.getenv("RABBIT_MQ_USER", "springuser") + self.rabbit_mq_password = os.getenv("RABBIT_MQ_PASSWORD", "TheCleverWho") + self.rabbit_mq_url = ( + f"amqp://{self.rabbit_mq_user}:{self.rabbit_mq_password}@{self.amq_host}/" + ) + self.download_dir = config.get( + "CleverMicro-AMQ", self.PREFIX + "download-dir", fallback="downloaded_files" + )
+
+ + + +
+[docs] +class Backpressure: + """ + configuration with prefix 'cm.backpressure' + """ + + PREFIX: str = "cm.backpressure." + +
+[docs] + def __init__(self, config: configparser.ConfigParser): + """ + cm.backpressure.threshold=20 + cm.backpressure.time-window=3000 + + :param config: config parser to read configuration values + """ + self.threshold_load = config.getint( + "CleverMicro-AMQ", + self.PREFIX + "threshold", + fallback=int(os.getenv("PARALLEL_WORKERS", 1)), + ) + # time-window in seconds, duration between backpressure reports + self.time_window = config.getint( + "CleverMicro-AMQ", self.PREFIX + "time-window", fallback=10 + ) + + # Define maximum CPU usage before triggering backpressure OVERLOAD alert + self.threshold_cpu = config.getint( + "CleverMicro-AMQ", self.PREFIX + "threshold-cpu", fallback=90 + ) + # Define the time window length, in seconds, during which, if CPU usage is consistently above the threshold, the system will trigger OVERLOAD alert + self.cpu_overload_duration = config.getint( + "CleverMicro-AMQ", self.PREFIX + "cpu-overload-duration", fallback=30 + ) + # Define the time window length, in seconds, during which, if no request is made to the Service, the system will trigger IDLE alert + self.idle_duration = config.getint( + "CleverMicro-AMQ", self.PREFIX + "idle-duration", fallback=30 + ) + # Flag to enable/disable CPU monitoring + self.cpu_monitoring_enabled = config.getboolean( + "CleverMicro-AMQ", self.PREFIX + "cpu-monitoring-enabled", fallback=False + )
+
+ + + +
+[docs] +class AMQConfiguration: + """ + Top level configuration context holder / object + """ + +
+[docs] + def __init__(self, config_file_name: str = None): + config = configparser.ConfigParser() + # Read the application.properties file + if os.path.exists(config_file_name): + config.read(config_file_name) + else: + # Get the directory of the caller (the module where this function is called) + caller_frame = inspect.stack()[1] # 0 is current_availability frame, 1 is caller + caller_module = inspect.getmodule(caller_frame[0]) + if caller_module: + caller_dir = os.path.dirname(os.path.abspath(caller_module.__file__)) + config_file_path = os.path.join(caller_dir, config_file_name) + config.read(config_file_path) + else: + logging_error(f"Configuration file not found: {config_file_name}") + logging_warning( + "Defaulting to preset values, which will likely be wrong, causing errors." + ) + + # Override values with environment variables + for section in config.sections(): + for option in config.options(section): + # env_var = f"{section.upper()}_{option.upper()}" + if option in os.environ: + config.set(section, option, os.environ[option]) + + # + # Add individual config areas + # + self.amq_adapter: AMQAdapter = AMQAdapter(config) + self.dispatch: Dispatch = Dispatch(config) + self.backpressure: Backpressure = Backpressure(config)
+ + +
+[docs] + def get_unique_instance_id(self) -> int: + """ + Get a unique instance ID for this AMQ service. + + Returns: + int: A unique instance ID + """ + if self.instance_id == -1: + logging_warning(f"Generated unique instance ID: {self.instance_id}") + return self.instance_id
+
+ +
+ +
+
+ +
+
+
+
+ + + + diff --git a/api-doc/_build/html/_modules/amqp/model/model.html b/api-doc/_build/html/_modules/amqp/model/model.html new file mode 100644 index 0000000..e6481fa --- /dev/null +++ b/api-doc/_build/html/_modules/amqp/model/model.html @@ -0,0 +1,589 @@ + + + + + + + + amqp.model.model — AMQ Adapter Python 1.0 documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for amqp.model.model

+import base64
+import json
+from dataclasses import dataclass, field
+from enum import Enum
+from typing import Dict, List
+
+from aio_pika.abc import AbstractRobustConnection
+
+from amqp.model.service_message_type import ServiceMessageType
+from amqp.model.snowflake_id import SnowflakeId
+from amqp.router.utils import sanitize_as_rabbitmq_name
+
+RS = ":"
+
+
+"""
+Define the structure of messages used in Clever Micro
+"""
+
+
+
+[docs] +class DataMessageMagicByte(Enum): + HTTP_REQUEST = "A" + RPC_REQUEST = "R"
+ + + +
+[docs] +@dataclass +class CleverMicroMessage: + DEFAULT_CONTENT_TYPE = ["application/octet-stream"] + + """ + The common structure of a message that transfers the user request to the Service and back. All data related messages + conform to this base structure: + + magic - identifies the message type (REST, RPC, DataResponse) + id - unique message ID + m_field - method name (for REST style calls) or service name (for RPC style calls) or response code for DataResponse + d_field - domain name (for REST style calls) or module name (for RPC style calls) or error code for DataResponse + p_field - path (for REST style calls) or method name (for RPC style calls) or error cause for DataResponse + headers - headers of the message (e.g. Content-Type, Accept, etc.) + trace_info - trace information (e.g. trace ID, span ID, etc. for Jaeger trace monitor) + base64body - the body of the message encoded in base64 + """ + +
+[docs] + def __init__( + self, + magic: str, + id: SnowflakeId, + m_field: str, + d_field: str, + p_field: str, + headers: Dict[str, List[str]], + trace_info: Dict[str, str], + base64body: bytes, + ): + self._magic = magic + self._id: SnowflakeId = id + self._m_field = m_field + self._d_field = d_field + self._p_field = p_field + self._headers: Dict[str, List[str]] = headers + self._trace_info: Dict[str, str] = trace_info + self._base64body: bytes = base64body
+ + +
+[docs] + def magic(self): + return self._magic
+ + +
+[docs] + def id(self): + return self._id
+ + +
+[docs] + def m_field(self) -> str: + return self._m_field
+ + +
+[docs] + def d_field(self) -> str: + return self._d_field
+ + +
+[docs] + def p_field(self) -> str: + return self._p_field
+ + +
+[docs] + def headers(self) -> Dict[str, List[str]]: + return self._headers
+ + +
+[docs] + def trace_info(self) -> Dict[str, str]: + return self._trace_info
+ + +
+[docs] + def base64body(self) -> bytes: + return self._base64body
+ + +
+[docs] + def body(self) -> bytes: + return base64.b64decode(self.base64body())
+ + +
+[docs] + def content_type(self) -> str: + return self.headers().get("Content-Type", self.DEFAULT_CONTENT_TYPE)[0]
+ + +
+[docs] + def return_address(self) -> str: + return self.headers().get("Return-Address", "")[0]
+
+ + + +
+[docs] +class DataMessage(CleverMicroMessage): + """ + The class represents a structure of a message that transfers the user request to the Service. + It may also be used to make a call/request between services. + The response to this call is in format of DataResponse class. + + See DataMessageFactory for code to instantiate, serialize and deserialize the class + """ + +
+[docs] + def __init__( + self, + magic: str, + id: SnowflakeId, + method: str, + domain: str, + path: str, + headers: Dict[str, List[str]], + trace_info: Dict[str, str], + base64body: bytes, + ): + super().__init__(magic, id, method, domain, path, headers, trace_info, base64body)
+ + +
+[docs] + def method(self) -> str: + return self.m_field()
+ + +
+[docs] + def domain(self) -> str: + return self.d_field()
+ + +
+[docs] + def path(self) -> str: + return self.p_field()
+ + +
+[docs] + def routing_key(self) -> str: + return sanitize_as_rabbitmq_name(f"{self.d_field()}.{self.p_field()}")
+
+ + + +
+[docs] +class MultipartDataMessage(DataMessage): +
+[docs] + def __init__(self, message: DataMessage, extra_data: dict): + super().__init__( + message.magic(), + message.id(), + message.method(), + message.domain(), + message.path(), + message.headers(), + message.trace_info(), + message.base64body(), + ) + self.extra_data = extra_data
+
+ + + +
+[docs] +@dataclass +class AMQMessage(DataMessage): + """ + Expand the core DataMessage type to include the channel, which is required to create a dedicated queue + in case the response should initiate file download. + """ + + _connection: AbstractRobustConnection = field(repr=False) # Don't show in repr + +
+[docs] + def __init__(self, data_message: DataMessage, connection: AbstractRobustConnection): + # Initialize the parent DataMessage with all properties from the input data_message + super().__init__( + magic=data_message.magic(), + id=data_message.id(), + method=data_message.method(), + domain=data_message.domain(), + path=data_message.path(), + headers=data_message.headers(), + trace_info=data_message.trace_info(), + base64body=data_message.base64body(), + ) + self._connection = connection + self.extra_data = data_message.extra_data if hasattr(data_message, "extra_data") else {}
+ + + @property + def connection(self) -> AbstractRobustConnection: + """Get the AMQP connection associated with this message""" + return self._connection
+ + + +
+[docs] +class DataResponse(CleverMicroMessage): + """ + The response message to REST & RPC style calls (note: the request is made with DataMessage) + + See DataResponseFactory for code to instantiate, serialize and deserialize the class + """ + +
+[docs] + def __init__( + self, + magic: str, + id: SnowflakeId, + response_code: str, + content_type: str, + error: str, + error_cause: str, + headers: Dict[str, List[str]], + trace_info: Dict[str, str], + base64body: bytes, + ): + if headers is None: + headers = {} + headers["Content-Type"] = [content_type] + super().__init__( + magic, + id, + response_code, + error, + error_cause, + headers, + trace_info, + base64body, + )
+ + +
+[docs] + def response_code(self) -> str: + return self.m_field()
+ + +
+[docs] + def error(self) -> str: + return self.d_field()
+ + +
+[docs] + def error_cause(self) -> str: + return self.p_field()
+
+ + + +
+[docs] +@dataclass(frozen=True) +class AMQRoute: + """ + Configuration item representing one AMQRoute. Adapter uses this to declare the Exchange + and/or queue and bind the key as routing expression. + + :param component_name: a service/component name that this route points to (owner of the route) + :param exchange: RabbitMQ exchange name + :param queue: RabbitMQ queue name + :param key: RabbitMQ TOPIC Exchange routing key + :param timeout: Timeout in milliseconds, how long to wait for reply, or not waiting if less then 1 + :param valid_until: optional timestamp (millis from epoch) after which the route is ignored + """ + + component_name: str + exchange: str + queue: str + key: str + timeout: int + valid_until: int + + FORMAT = "Component-Name:Exchange-Name:Queue-Name:Routing-Key:Timeout:Valid-Until" + + def __str__(self): + return ( + f"{self.component_name}{RS}{self.exchange}{RS}{self.queue}{RS}{self.key}" + f"{RS}{self.timeout}{RS}{self.valid_until}" + ) + + def __eq__(self, other): + if isinstance(other, AMQRoute): + return ( + self.component_name == other.component_name + and self.queue == other.queue + and self.key == other.key + ) + return False + + @property + def primary_key(self): + """ + Build the unique identifier of the route + :return primary key + """ + return f"{self.component_name}{RS}{self.queue}{RS}{self.key}" + +
+[docs] + def as_string(self) -> str: + return self.__str__()
+
+ + + +# Ensure backward compatibility +AMQResponse = DataResponse + + +
+[docs] +@dataclass(frozen=True) +class ServiceMessage: + """ + The CleverMicro service message that is used to communicate the internal state of the AMQ channel. + In v0.2 it supports service discovery / route registration and Backpressure status notification. + """ + + id: SnowflakeId = None + payload_type: int = 0 + message_type: ServiceMessageType = ServiceMessageType.UNKNOWN + recipient_name: str = None + reply_to: str = None + trace_info: Dict[str, str] = field(default_factory=dict) + message: str = None + +
+[docs] + def is_valid(self) -> bool: + return self.message_type is not None
+
+ + + +
+[docs] +class ScalingRequestAlert(Enum): + OVERLOAD = "OVERLOAD" + IDLE = "IDLE" + UPDATE = "UPDATE" + SCALE_UP = "SCALE_UP" + SCALE_DOWN = "SCALE_DOWN"
+ + + +
+[docs] +@dataclass(frozen=True) +class ScalingRequest: + service_id: str + task_id: str + max_availability: int + current_availability: int + request_type: ScalingRequestAlert + version: int = 1 + +
+[docs] + def to_json(self) -> str: + """Converts the object to a JSON string matching the Java structure""" + return json.dumps( + { + "version": self.version, + "serviceId": self.service_id, + "taskId": self.task_id, + "maxAvailability": self.max_availability, + "currentAvailability": self.current_availability, + "requestType": self.request_type.value, # Using .value for Enum serialization + }, + indent=2, + )
+ + +
+[docs] + @classmethod + def from_json(cls, json_str: str) -> "ScalingRequest": + """Creates a ScalingRequest from a JSON string""" + data = json.loads(json_str) + return cls( + service_id=data["serviceId"], + task_id=data["taskId"], + max_availability=data["maxAvailability"], + current_availability=data["currentAvailability"], + request_type=ScalingRequestAlert(data["requestType"]), # Convert string to Enum + )
+
+ + + +
+[docs] +class AMQErrorMessage: + """ + The error response message to RPC style call (note: the request is made with DataMessage) + """ + +
+[docs] + def __init__( + self, + response_code: int, + error: str, + detail: str, + content_type: str = "application/json", + ): + self.response_code = response_code + self.error = error + self.detail = detail + self.content_type = content_type
+ + +
+[docs] + def serialize(self) -> bytes: + return json.dumps( + { + "response_code": self.response_code, + "error": self.error, + "detail": self.detail, + } + ).encode("utf-8")
+
+ +
+ +
+
+ +
+
+
+
+ + + + diff --git a/api-doc/_build/html/_modules/amqp/model/service_message_type.html b/api-doc/_build/html/_modules/amqp/model/service_message_type.html new file mode 100644 index 0000000..6848143 --- /dev/null +++ b/api-doc/_build/html/_modules/amqp/model/service_message_type.html @@ -0,0 +1,120 @@ + + + + + + + + amqp.model.service_message_type — AMQ Adapter Python 1.0 documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

Source code for amqp.model.service_message_type

+from enum import Enum, auto
+
+
+
+[docs] +class ServiceMessageType(Enum): + ROUTING_DATA_REQUEST = auto() + ROUTING_DATA = auto() + ROUTING_DATA_REMOVE = auto() + ROUTING_DATA_REPLACE = auto() + BACKPRESSURE = auto() + UNKNOWN = auto()
+ +
+ +
+
+ +
+
+
+
+ + + + diff --git a/api-doc/_build/html/_modules/amqp/model/snowflake_id.html b/api-doc/_build/html/_modules/amqp/model/snowflake_id.html new file mode 100644 index 0000000..ec1ad2c --- /dev/null +++ b/api-doc/_build/html/_modules/amqp/model/snowflake_id.html @@ -0,0 +1,204 @@ + + + + + + + + amqp.model.snowflake_id — AMQ Adapter Python 1.0 documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for amqp.model.snowflake_id

+import struct
+
+
+
+[docs] +class SnowflakeId: + """ + see https://en.wikipedia.org/wiki/Snowflake_ID + """ + + SNOWFLAKE_ID_WIDTH = ( + 10 # with in bytes (8 bits each), can be more than long int width (8 bytes) + ) + +
+[docs] + def __init__(self, hi_bits_or_bytes=None, lo_bits=None): + self.bits = [0, 0] + if lo_bits is None: + if hi_bits_or_bytes is not None: + i = 0 + shift = 8 # starting from Hi value, which is only 2 bytes wide + n = 1 + while i < len(hi_bits_or_bytes) and i <= self.SNOWFLAKE_ID_WIDTH: + self.bits[n] |= hi_bits_or_bytes[i] << shift + if shift == 0: + n -= 1 + shift = 56 + else: + shift -= 8 + i += 1 + else: + self.bits = [lo_bits, hi_bits_or_bytes]
+ + +
+[docs] + @staticmethod + def from_hex(_input): + expected_length = 2 * SnowflakeId.SNOWFLAKE_ID_WIDTH + if _input is None or len(_input) < expected_length: + raise RuntimeError( + f"MessageId is {SnowflakeId.SNOWFLAKE_ID_WIDTH * 8} bits wide, it is represented by HEX string " + f"of length: {expected_length}, real input: [{_input}] of length: {len(_input) if _input else 0}" + ) + + bits = [0, 0] + n = 1 + for i in range(0, expected_length, 2): + if i == expected_length - (2 * 8): + n = 0 + bits[n] = (bits[n] << 8) | int(_input[i : i + 2], 16) + + return SnowflakeId(bits[1], bits[0])
+ + + def __str__(self): + _hex = f"{self.bits[1]:016X}{self.bits[0]:016X}" + _val = ( + _hex + if len(_hex) <= 2 * SnowflakeId.SNOWFLAKE_ID_WIDTH + else _hex[(-2 * SnowflakeId.SNOWFLAKE_ID_WIDTH) :] + ) + return _val.lower() + + def __eq__(self, other): + if isinstance(other, SnowflakeId): + return self.bits[1] == other.bits[1] and self.bits[0] == other.bits[0] + return False + + def __lt__(self, other): + if self.bits[1] < other.bits[1]: + return True + elif self.bits[1] > other.bits[1]: + return False + return self.bits[0] < other.bits[0] + +
+[docs] + def get_bytes(self): + high = struct.pack("!Q", self.bits[1]) + low = struct.pack("!Q", self.bits[0]) + return high[-2:] + low
+ + +
+[docs] + def get_bits(self): + return self.bits
+ + +
+[docs] + def as_string(self) -> str: + return self.__str__()
+
+ +
+ +
+
+ +
+
+
+
+ + + + diff --git a/api-doc/_build/html/_modules/amqp/rabbitmq/rabbit_mq_client.html b/api-doc/_build/html/_modules/amqp/rabbitmq/rabbit_mq_client.html new file mode 100644 index 0000000..4d77373 --- /dev/null +++ b/api-doc/_build/html/_modules/amqp/rabbitmq/rabbit_mq_client.html @@ -0,0 +1,374 @@ + + + + + + + + amqp.rabbitmq.rabbit_mq_client — AMQ Adapter Python 1.0 documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

Source code for amqp.rabbitmq.rabbit_mq_client

+import logging
+from typing import Optional
+
+import aio_pika
+from aio_pika.abc import AbstractRobustExchange, AbstractRobustQueue
+from pika.exchange_type import ExchangeType
+
+from amqp.adapter.data_message_factory import DataMessageFactory
+from amqp.adapter.logging_utils import (
+    logging_debug,
+    logging_error,
+    logging_info,
+    logging_warning,
+)
+from amqp.model.model import AMQRoute, DataMessage
+from amqp.router.router_base import (
+    AMQ_REPLY_TO_EXCHANGE,
+    AMQ_RPC_EXCHANGE,
+    DLQ_EXCHANGE,
+)
+from amqp.router.utils import sanitize_as_rabbitmq_name
+
+
+
+[docs] +class RabbitMQClient: + PREFETCH_COUNT = 1 + DLQ_ARGS = {"x-dead-letter-exchange": DLQ_EXCHANGE} + +
+[docs] + def __init__(self, configuration, router): + self.channel: aio_pika.RobustChannel | None = None + self.connection: aio_pika.RobustConnection | None = None + self.rpc_exchange: aio_pika.RobustExchange | None = None + self.reply_exchange: aio_pika.RobustExchange | None = None + self.amq_configuration = configuration + logging.info("*******************************************") + logging.info( + "******** RabbitMQClient.init(): %s", + self.amq_configuration.dispatch.amq_host, + ) + logging.info("*******************************************") + self.router = router + # this.meterRegistry = new PrometheusMeterRegistry(PrometheusConfig.DEFAULT); + + self.reply_to_exchange: AbstractRobustExchange | None = ( + None # when sending response to RPC request + ) + self.reply_to_queue: AbstractRobustQueue | None = ( + None # when receiving response to RPC request + )
+ + +
+[docs] + async def init(self, loop) -> AbstractRobustExchange: + await self.setup_amq_channel(loop) + await self.router.init_routing_paths_consumer(self.router.channel) + cm_reply_to_queue = self.router.get_reply_to_queue_name() + self.reply_to_queue = await self.channel.declare_queue( + name=cm_reply_to_queue, durable=True, exclusive=False, auto_delete=False + ) + self.reply_to_exchange = await self.channel.declare_exchange( + AMQ_REPLY_TO_EXCHANGE, type=ExchangeType.direct + ) + await self.reply_to_queue.bind( + exchange=self.reply_to_exchange, routing_key=cm_reply_to_queue + ) + return self.reply_to_exchange
+ + +
+[docs] + def cleanup(self): + logging_info("RabbitMQClient.cleanup()") + self.router.cleanup() # de-register route(s) + if self.router.is_open(self.channel): + try: + self.channel.close() + if not self.connection.is_closed: + self.connection.close() + except Exception as e: + logging_error( + "RabbitMQClient.PreDestroy cleanup - connection close error: %s", + e, + )
+ + +
+[docs] + async def register_inbound_routes(self, route_mapping: str, callback): + """ + Register inbound routes for the AMQ adapter. The route mapping is a comma-separated string that contains + list of routes. A route is the mapping of domain and path to the exchange and queue name. + This method declare (and thus create) the queue and exchange, bind them together using the + regex-like routing expression given in the route. + After this the Service is listening on (consuming from) the queue for incoming messages. + """ + if self.router.is_open(self.channel): + queue_args = self.DLQ_ARGS if self.amq_configuration.dispatch.use_dlq else None + logging_info("RabbitMQClient register routes, cfg mapping: [%s]", route_mapping) + # convert the comma-separated string into list of AMQRoute objects + requested_routes = self.router.unwrap_route_list(route_mapping) + for route in requested_routes: + try: + # Note: AMQRoute allows one of the exchange or queue to be None, in which case the default is used + queue_name = ( + route.queue if route.queue else self.router.get_consume_queue_name() + ) + logging_info("RabbitMQClient register route: [%s]", route) + if self.amq_configuration.amq_adapter.callbacks_enabled: + queue: Optional[AbstractRobustQueue] = await self.channel.declare_queue( + name=queue_name, + durable=True, + exclusive=False, + auto_delete=False, + arguments=queue_args, + ) + else: + queue = None + _exchange_name: str = route.exchange if route.exchange else AMQ_RPC_EXCHANGE + exchange: AbstractRobustExchange = await self.channel.declare_exchange( + name=_exchange_name, type=ExchangeType.topic + ) + if self.amq_configuration.amq_adapter.callbacks_enabled: + await queue.bind(exchange, route.key) + _c_tag = await queue.consume(callback, no_ack=False) + else: + _c_tag = None + # Register the route with the router and publish its detail to other adapters + await self.router.register_route( + AMQRoute( + route.component_name, + _exchange_name, + queue_name, + sanitize_as_rabbitmq_name(route.key), + route.timeout, + route.valid_until, + ) + ) + logging_info( + " [%s] AMQ connection initialized for route: %s, listens on: %s/%s", + _c_tag, + route, + exchange, + queue_name, + ) + except Exception as err: + logging_error("Callback registration INCOMPLETE, %s", err) + else: + logging_warning("Channel is null, cannot register routes.")
+ + +
+[docs] + async def register_data_reply_callback(self, rpc_callback): + _c_tag = await self.reply_to_queue.consume(callback=rpc_callback, no_ack=False) + logging_info( + " [%s] RabbitMQClient register data reply callback on queue: %s", + _c_tag, + self.reply_to_queue.name, + )
+ + +
+[docs] + async def setup_amq_channel(self, loop): + """ + Set up the RabbitMQ connection + :return: nothing, it applies changes to instance variables + """ + logging.info( + f"RabbitMQClient.setupAMQ, connecting to RabbitMQ at {self.amq_configuration.dispatch.amq_host}:{self.amq_configuration.dispatch.amq_port}, loop={loop}" + ) + self.connection = await aio_pika.connect_robust( + host=self.amq_configuration.dispatch.amq_host, + port=self.amq_configuration.dispatch.amq_port, + login=self.amq_configuration.dispatch.rabbit_mq_user, + password=self.amq_configuration.dispatch.rabbit_mq_password, + loop=loop, + ) + logging_debug( + "RabbitMQClient.setupAMQ, got connection, open=%s", + self.connection.is_closed, + ) + self.channel = await self.connection.channel() + await self.channel.set_qos(prefetch_count=self.PREFETCH_COUNT) + await self.router.init_internal_routing_paths(self.channel) + self.router.set_route_added_notifier( + lambda route: logging_debug( + " RouteAdded, setting up metrics: amqp.adapter.exchange.%s", + route.exchange, + ) + )
+ + +
+[docs] + async def send_message(self, message: DataMessage): + """ + The API routine that sends a datagram message (DataMessage) to the destination. The Destination + is determined from the DataMessage metadata section, using `domain` and `path` as key. + :param message: message to send + :return: nothing (void) as submitting message to RabbitMQ exchange does not return anything + meaningful. There will be asynch ACK later that will confirm the message was sent to RMQ. + """ + try: + route = self.router.find_route_by_message(message) + if route and self.rpc_exchange: + if route.timeout <= 0: + # No response expected + pika_message: aio_pika.message.Message = aio_pika.message.Message( + body=DataMessageFactory.serialize(message), + content_encoding="utf-8", + delivery_mode=2, + ) + await self.rpc_exchange.publish( + message=pika_message, + routing_key=self.router.get_routing_key( + message + ), # context path is a routing key + ) + logging_info(f"Msg Publish (no-reply), messageId: {message.id()}, to: {route}") + else: + # This is RPC call, there should be response + pika_message: aio_pika.message.Message = aio_pika.message.Message( + body=DataMessageFactory.serialize(message), + content_encoding="utf-8", + delivery_mode=2, + correlation_id=str(message.id()), + reply_to=self.router.get_reply_to_queue_name(), + ) + await self.rpc_exchange.publish( + message=pika_message, + routing_key=self.router.get_routing_key( + message + ), # context path is a routing key + ) + logging_info( + "Msg Publish (RPC call), messageId: %s, to: %s", + message.id().as_string(), + route.as_string(), + ) + else: + logging_warning( + "Message not sent, no route for %s/%s", + message.domain(), + message.path(), + ) + + except Exception as err: + logging_error("Send message failed: %s", err)
+ + +
+[docs] + async def request_routes(self): + await self.router.request_routes_from_adapters()
+ + +
+[docs] + def find_route(self, domain: str, path: str) -> AMQRoute: + return self.router.find_route(domain, path)
+
+ +
+ +
+
+ +
+
+
+
+ + + + diff --git a/api-doc/_build/html/_modules/amqp/rabbitmq/user_management_service_client.html b/api-doc/_build/html/_modules/amqp/rabbitmq/user_management_service_client.html new file mode 100644 index 0000000..ef59ea1 --- /dev/null +++ b/api-doc/_build/html/_modules/amqp/rabbitmq/user_management_service_client.html @@ -0,0 +1,397 @@ + + + + + + + + amqp.rabbitmq.user_management_service_client — AMQ Adapter Python 1.0 documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

Source code for amqp.rabbitmq.user_management_service_client

+import asyncio
+import json
+import logging
+import uuid
+from typing import Any, Dict, List, Optional
+
+import aio_pika
+from aio_pika import Message
+from aio_pika.abc import AbstractIncomingMessage, AbstractRobustChannel, AbstractRobustConnection
+
+from amqp.adapter.logging_utils import logging_debug, logging_error, logging_info
+from amqp.config.amq_configuration import AMQConfiguration
+
+
+
+[docs] +class UserManagementServiceException(Exception): + """Exception raised for errors in the User Management Service.""" + +
+[docs] + def __init__(self, exception_type: str, message: str, additional_info: Dict[str, Any] = None): + self.exception_type = exception_type + self.message = message + self.additional_info = additional_info or {} + super().__init__(f"{exception_type}: {message}")
+
+ + + +
+[docs] +class UserManagementServiceClient: + """ + Client for the User Management Service that communicates over RabbitMQ. + Supports operations for token, user, group, tenant, and permission services. + """ + + # Exchange name for User Management Service + EXCHANGE_NAME = "cleverthis.clevermicro.auth.endpoints" + + # Service routing keys + TOKEN_SERVICE = "token-service-v1" + USER_SERVICE = "user-service-v1" + GROUP_SERVICE = "group-service-v1" + TENANT_SERVICE = "tenant-service-v1" + PERMISSION_SERVICE = "permission-service-v1" + + # Response queue name + RESPONSE_QUEUE_NAME = "user-management-service-cleverswarm" + +
+[docs] + def __init__(self, configuration: AMQConfiguration, loop: asyncio.AbstractEventLoop = None): + """ + Initialize the User Management Service client. + + Args: + configuration: AMQ configuration + loop: Event loop to use (optional) + """ + self.amq_configuration = configuration + self.loop = loop or asyncio.get_event_loop() + self.connection: Optional[AbstractRobustConnection] = None + self.channel: Optional[AbstractRobustChannel] = None + self.exchange: Optional[aio_pika.RobustExchange] = None + self.response_queue: Optional[aio_pika.RobustQueue] = None + self.response_consumer_tag: Optional[str] = None + self.futures: Dict[str, asyncio.Future] = {} + self.initialized = False
+ + +
+[docs] + async def initialize(self) -> None: + """Initialize the RabbitMQ connection and set up the exchange and response queue.""" + if self.initialized: + return + + try: + # Connect to RabbitMQ + self.connection = await aio_pika.connect_robust( + host=self.amq_configuration.dispatch.amq_host, + port=self.amq_configuration.dispatch.amq_port, + login=self.amq_configuration.dispatch.rabbit_mq_user, + password=self.amq_configuration.dispatch.rabbit_mq_password, + loop=self.loop, + ) + + # Create channel + self.channel = await self.connection.channel() + await self.channel.set_qos(prefetch_count=1) + + # Declare exchange + self.exchange = await self.channel.declare_exchange( + name=self.EXCHANGE_NAME, + type=aio_pika.ExchangeType.TOPIC, + durable=True, + ) + + # Declare response queue + self.response_queue = await self.channel.declare_queue( + name=self.RESPONSE_QUEUE_NAME, + durable=True, + exclusive=False, + auto_delete=False, + ) + + # Start consuming responses + self.response_consumer_tag = await self.response_queue.consume( + self._on_response_callback, + no_ack=False, + ) + + self.initialized = True + logging_info("User Management Service client initialized") + + except Exception as e: + logging_error(f"Failed to initialize User Management Service client: {e}") + if self.connection and not self.connection.is_closed: + await self.connection.close() + raise
+ + + async def _on_response_callback(self, message: AbstractIncomingMessage) -> None: + """ + Handle responses from the User Management Service. + + Args: + message: The incoming message + """ + try: + correlation_id = message.correlation_id + if not correlation_id: + logging_error("Received response without correlation ID") + await message.ack() + return + + future = self.futures.get(correlation_id) + if not future: + logging_error(f"No pending request found for correlation ID: {correlation_id}") + await message.ack() + return + + try: + response_data = json.loads(message.body.decode('utf-8')) + logging_debug(f"Received response: {response_data}") + future.set_result(response_data) + except Exception as e: + future.set_exception(e) + + await message.ack() + + except Exception as e: + logging_error(f"Error processing response: {e}") + await message.ack() + +
+[docs] + async def close(self) -> None: + """Close the RabbitMQ connection.""" + if self.connection and not self.connection.is_closed: + await self.connection.close() + self.initialized = False + logging_info("User Management Service client closed")
+ + +
+[docs] + async def call_service( + self, service: str, method: str, args: Dict[str, Any] = None + ) -> Dict[str, Any]: + """ + Call a method on a User Management Service. + + Args: + service: Service routing key (e.g., 'token-service-v1') + method: Method name to call + args: Method arguments + + Returns: + Response data + + Raises: + UserManagementServiceException: If the service returns an error + """ + if not self.initialized: + await self.initialize() + + correlation_id = str(uuid.uuid4()) + future = self.loop.create_future() + self.futures[correlation_id] = future + + request_data = { + "method": method, + "args": args or {}, + } + + message = Message( + body=json.dumps(request_data).encode('utf-8'), + content_type='application/json', + correlation_id=correlation_id, + reply_to=self.RESPONSE_QUEUE_NAME, + ) + + try: + await self.exchange.publish(message, routing_key=service) + logging_info(f"Sent request to {service}.{method}: {args}") + + # Wait for response + response = await future + + # Process response + if response.get("type") == "OK": + return response.get("result", []) + else: + raise UserManagementServiceException( + response.get("exception", "unknown_error"), + response.get("message", "Unknown error"), + response.get("additional_info", {}) + ) + + except asyncio.CancelledError: + raise + except UserManagementServiceException: + raise + except Exception as e: + logging_error(f"Error calling service {service}.{method}: {e}") + raise + finally: + self.futures.pop(correlation_id, None)
+ + +
+[docs] + async def validate_token(self, token: str) -> Dict[str, Any]: + """ + Validate a JWT token. + + Args: + token: JWT token to validate + + Returns: + Token information + """ + return await self.call_service( + service=self.TOKEN_SERVICE, + method="validateToken", + args={"token": token} + )
+ + +
+[docs] + async def query_user_by_id(self, user_id: str) -> Dict[str, Any]: + """ + Query user information by user ID. + + Args: + user_id: User ID + + Returns: + User information + """ + return await self.call_service( + service=self.USER_SERVICE, + method="queryUserById", + args={"userId": user_id} + )
+ + +
+[docs] + async def list_users_in_group(self, group_id: str) -> List[Dict[str, Any]]: + """ + List users in a group. + + Args: + group_id: Group ID + + Returns: + List of users + """ + return await self.call_service( + service=self.GROUP_SERVICE, + method="listUsersInGroup", + args={"groupId": group_id} + )
+
+ +
+ +
+
+ +
+
+
+
+ + + + diff --git a/api-doc/_build/html/_modules/amqp/router/route_database.html b/api-doc/_build/html/_modules/amqp/router/route_database.html new file mode 100644 index 0000000..d102fe4 --- /dev/null +++ b/api-doc/_build/html/_modules/amqp/router/route_database.html @@ -0,0 +1,209 @@ + + + + + + + + amqp.router.route_database — AMQ Adapter Python 1.0 documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for amqp.router.route_database

+import re
+from typing import Optional, Set
+
+from amqp.adapter.logging_utils import logging_info
+from amqp.model.model import AMQRoute
+from amqp.router.utils import sanitize_as_rabbitmq_name
+
+
+
+[docs] +class RouteDatabase: +
+[docs] + def __init__(self): + self.routes: Set[AMQRoute] = set()
+ + +
+[docs] + def get_routes(self) -> Set[AMQRoute]: + return self.routes
+ + +
+[docs] + def pattern(self, routing_key: str) -> re.Pattern: + breaker = False + tokens = routing_key.split(".") + counter = len(tokens) + regex_parts = [] + + for token in tokens: + counter -= 1 + if not breaker: + if token == "": + res = "[^\\.]*" + elif token == "#": + breaker = True + res = ".*$" + else: + res = token + if counter > 0 and not breaker: + res += "\\." + regex_parts.append(res) + else: + regex_parts.append("") + + regex = "".join(filter(lambda x: x != "", regex_parts)) + if not routing_key: + return re.compile(".*") + return re.compile(regex)
+ + +
+[docs] + def matches(self, routing_key_from_route: str, routing_key_from_message: str) -> bool: + route_pattern = self.pattern(routing_key_from_route) + return bool(route_pattern.match(sanitize_as_rabbitmq_name(routing_key_from_message)))
+ + +
+[docs] + def wrap_routes(self) -> str: + return ",".join(str(route) for route in self.routes)
+ + +
+[docs] + def find_route(self, domain: str, path: str) -> AMQRoute | None: + routing_key = sanitize_as_rabbitmq_name(f"{domain}.{path}") + logging_info(f"findRoute key:{routing_key} in:{self.wrap_routes()}") + for route in self.routes: + if self.matches(route.key, routing_key): + return route + return None
+ + +
+[docs] + def find_route_by_service(self, service_name: str) -> Optional[AMQRoute]: + logging_info(f"findRoute service_name:{service_name} in:{self.wrap_routes()}") + for route in self.routes: + if route.component_name == service_name: + return route + return None
+ + +
+[docs] + def add_route(self, route: AMQRoute) -> bool: + if route not in self.routes: + self.routes.add(route) + return True + return False
+ + +
+[docs] + def remove_route(self, route: AMQRoute) -> bool: + return route in self.routes and self.routes.remove(route)
+
+ +
+ +
+
+ +
+
+
+
+ + + + diff --git a/api-doc/_build/html/_modules/amqp/router/router_base.html b/api-doc/_build/html/_modules/amqp/router/router_base.html new file mode 100644 index 0000000..f870aca --- /dev/null +++ b/api-doc/_build/html/_modules/amqp/router/router_base.html @@ -0,0 +1,258 @@ + + + + + + + + amqp.router.router_base — AMQ Adapter Python 1.0 documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for amqp.router.router_base

+from typing import Callable, List, Optional, Set
+
+from amqp.adapter.amq_route_factory import NULL_ROUTE, AMQRouteFactory
+from amqp.adapter.logging_utils import logging_debug, logging_error, logging_info
+from amqp.model.model import AMQRoute, DataMessage
+from amqp.router.route_database import RouteDatabase
+from amqp.router.utils import sanitize_as_rabbitmq_name
+
+CM_REQUEST_EXCHANGE = "CM-Request"
+CM_RESPONSE_EXCHANGE = "CM-Response"
+CM_C_REQ_QUEUE = "C-REQ"
+CM_C_RESP_QUEUE = "C-RESP"
+CM_C_AMQ_QUEUE = "C-AMQ"
+CM_P_RESP_QUEUE = "P-RESP"
+CM_P_REPLY_TO = "P-REPLY-TO"
+ANY_RECIPIENT = "*"
+AMQ_RPC_EXCHANGE = "AMQ-RPC"
+AMQ_REPLY_TO_EXCHANGE = "AMQ-REPLY-TO"
+DLQ_EXCHANGE = "DLX"
+
+
+
+[docs] +class RouterBase: +
+[docs] + def __init__(self): + self.route_database = RouteDatabase() + self.own_routes: Set[AMQRoute] = set() + self.route_added_notifier: Callable[[AMQRoute], None] | None = None + self.route_removed_notifier: Callable[[AMQRoute], None] | None = None
+ + +
+[docs] + def get_routing_key(self, message: DataMessage) -> str: + return sanitize_as_rabbitmq_name(f"{message.domain()}.{message.path()}")
+ + +
+[docs] + def wrap_routes(self) -> str: + return self.route_database.wrap_routes()
+ + +
+[docs] + def wrap_own_routes(self) -> str: + return ",".join(str(route) for route in self.own_routes)
+ + +
+[docs] + def unwrap_route_list(self, route_string: str) -> List[AMQRoute]: + logging_debug("RouterMaster.unwrapRoutes, route(s): %s", route_string) + routes = [AMQRouteFactory.from_string(route_str) for route_str in route_string.split(",")] + return [route for route in routes if route != NULL_ROUTE]
+ + +
+[docs] + def get_routes(self) -> Set[AMQRoute]: + return self.route_database.get_routes()
+ + +
+[docs] + def find_route(self, domain: str, path: str) -> AMQRoute | None: + return self.route_database.find_route(domain, path)
+ + +
+[docs] + def find_route_by_message(self, message: DataMessage) -> AMQRoute | None: + return self.route_database.find_route(message.domain(), message.path())
+ + +
+[docs] + def find_route_by_service(self, service_name: str) -> Optional[AMQRoute]: + _route: Optional[AMQRoute] = self.route_database.find_route_by_service(service_name) + if _route is None: + for route in self.own_routes: + if route.component_name == service_name: + _route = route + return _route
+ + +
+[docs] + def add_routes(self, message: str) -> None: + try: + amq_route_list = self.unwrap_route_list(message) + logging_info(" [DBG] RouterMaster.addRoute [%d]: %s", len(amq_route_list), message) + for route in amq_route_list: + logging_info("RouterMaster.addRoute, adding route: %s", route) + if not route.exchange and not route.queue and not route.key: + logging_error( + "Cannot setup AMQ Exchange for '%s', at least one of " + "Exchange, Queue and/or Key must be present in the routing string. " + "Expected format is '%s'", + route, + AMQRoute.FORMAT, + ) + else: + if route.exchange != DLQ_EXCHANGE: + is_new_route = self.route_database.add_route(route) + if is_new_route and self.route_added_notifier: + self.route_added_notifier(route) + except Exception as err: + logging_error("add_routes error: %s", err)
+ + +
+[docs] + def set_route_added_notifier(self, callback: Callable[[AMQRoute], None]): + self.route_added_notifier = callback
+ + +
+[docs] + def remove_routes(self, message: str) -> None: + try: + to_remove = [AMQRouteFactory.from_string(route_str) for route_str in message.split(",")] + removed_count = sum(1 for route in to_remove if self.remove_route(route)) + logging_info(f"RouterMaster.removeRoutes(): {message}, removed={removed_count}") + except IOError as err: + logging_error(f"Can't remove route(s): {err}")
+ + +
+[docs] + def remove_route(self, route: AMQRoute) -> bool: + is_route_removed = self.route_database.remove_route(route) + if is_route_removed and self.route_removed_notifier: + self.route_removed_notifier(route) + return is_route_removed
+ + +
+[docs] + def add_route(self, route: AMQRoute) -> None: + self.route_database.add_route(route)
+ + +
+[docs] + def add_own_route(self, route: AMQRoute) -> None: + self.own_routes.add(route)
+
+ +
+ +
+
+ +
+
+
+
+ + + + diff --git a/api-doc/_build/html/_modules/amqp/router/router_consumer.html b/api-doc/_build/html/_modules/amqp/router/router_consumer.html new file mode 100644 index 0000000..947f9ea --- /dev/null +++ b/api-doc/_build/html/_modules/amqp/router/router_consumer.html @@ -0,0 +1,328 @@ + + + + + + + + amqp.router.router_consumer — AMQ Adapter Python 1.0 documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for amqp.router.router_consumer

+from typing import Set
+
+from aio_pika.abc import (
+    AbstractIncomingMessage,
+    AbstractRobustChannel,
+    AbstractRobustQueue,
+)
+
+from amqp.adapter.amq_route_factory import AMQRouteFactory
+from amqp.adapter.logging_utils import (
+    logging_debug,
+    logging_error,
+    logging_info,
+    logging_warning,
+)
+from amqp.config.amq_configuration import AMQConfiguration
+from amqp.model.model import AMQRoute, ServiceMessage
+from amqp.model.service_message_type import ServiceMessageType
+from amqp.router.router_base import (
+    ANY_RECIPIENT,
+    CM_C_REQ_QUEUE,
+    CM_REQUEST_EXCHANGE,
+    CM_RESPONSE_EXCHANGE,
+)
+from amqp.router.router_producer import RouterProducer
+
+
+
+[docs] +class RouterConsumer(RouterProducer): + """ + * Contains Consumer logic. The logic here: + * Sets a listener on CM_C_REQ_QUEUE queue to receive routing data requests + * Upon receipt of routing data request, responds on CM_RESPONSE_EXCHANGE with local routing data. + * It also proactively pushes the local routing data to CM_RESPONSE_EXCHANGE on startup to + * notify the other (excluding self) Producers about its presence. + * + * Consumer is also Producer (Consumer sits alongside the CM Service, and anytime the Service needs + * to communicate with other Service, it can do so via the Adapter). Therefore, the Producer + * capability is also required. + """ + +
+[docs] + def __init__(self, configuration: AMQConfiguration): + super().__init__(configuration) + self.amqRouteFactory = AMQRouteFactory() + logging_info("RouterConsumer.<init>")
+ + +
+[docs] + async def init_routing_paths_consumer(self, _channel: AbstractRobustChannel): + """ + * Initialize router according to Consumer mode. + """ + logging_info( + "RouterConsumer.initConsumerRouter, channel open: %s", + self.is_open(_channel), + ) + if self.is_open(_channel): + try: + # 1. declare a Queue to receive the RoutingData requests + _cm_request_queue: str = ( + self.amq_configuration.amq_adapter.adapter_prefix() + CM_C_REQ_QUEUE + ) + _queue: AbstractRobustQueue = await self.channel.declare_queue( + name=_cm_request_queue, + durable=True, + exclusive=False, + auto_delete=False, + ) + # 1a. bind queue to CM_REQUEST_EXCHANGE w/o any conditions (empty routing key) + await _queue.bind(exchange=CM_REQUEST_EXCHANGE, routing_key="") + # 1b. Service adapter mode - listens on Request queue and replies to Response queue + _c_tag = await _queue.consume( + callback=self.handle_routing_data_request_message, no_ack=False + ) + logging_info( + " [%s] RouterConsumer listens for RouteDataRequests on: %s", + _c_tag, + _cm_request_queue, + ) + except Exception as ioe: + logging_error("RouterConsumer FATAL: cannot initialize, %s", ioe) + else: + logging_error("RouterConsumer FATAL: cannot initialize, RabbitMQ channel is not open.")
+ + + """ + * ServiceMessageType.ROUTING_DATA_REQUEST handler listening on queue 'CleverMicroRequest' + """ + +
+[docs] + async def handle_routing_data_request_message(self, message: AbstractIncomingMessage): + # some debug statements, TODO - remove for prod release + delivery_tag = message.delivery_tag + debug = "Delivery.Properties = " + message.properties.__str__() + logging_debug( + " ******[%s] (ROUTING_DATA_REQ) tag: %s, env: %s, props: %s", + delivery_tag, + message.consumer_tag, + message.properties, + debug, + ) + + await message.ack(False) + + cm_message: ServiceMessage = self.service_message_factory.from_bytes(message.body) + route_mapping: str = self.wrap_own_routes() + logging_debug( + "******[%s] (ROUTING_DATA_REQ) msg=%s, routeMap=[%s]", + delivery_tag, + self.service_message_factory.to_string(cm_message), + route_mapping, + ) + # Reply only if self adapter has own routeMapping (e.g. API-gateway-side adapter has none) + if ( + cm_message.is_valid() + and cm_message.message_type == ServiceMessageType.ROUTING_DATA_REQUEST + and route_mapping + ): + if cm_message.reply_to != self.amq_configuration.amq_adapter.service_name: + try: + service_message: ServiceMessage = self.service_message_factory.of( + ServiceMessageType.ROUTING_DATA, + route_mapping, + cm_message.reply_to, + ) + await self.publish_service_message(service_message, self.cm_response_exchange) + except Exception as err: + logging_error( + "******[%s] (ROUTING_DATA_REQ) Response to queue: %s failed: %s", + delivery_tag, + CM_RESPONSE_EXCHANGE, + err, + ) + else: + logging_info( + "******[%s] (ROUTING_DATA_REQ) [%s] Ignore request originated by ourselves.", + delivery_tag, + str(message.body), + ) + else: + logging_error( + "******[%s] (ROUTING_DATA_REQ) [%s] produces invalid or no CM msg type.", + delivery_tag, + str(message.body), + )
+ + +
+[docs] + async def register_route(self, route: AMQRoute): + """ + * Called in Consumer (Service Adapter) context. Registers the routing data for self Service + * with the Master router by sending own routing data to Master via CleverMicroResponse queue. + * :param route: route to register with Master + """ + try: + if self.is_open(self.channel) and route.queue: + serviceMessage: ServiceMessage = self.service_message_factory.of( + ServiceMessageType.ROUTING_DATA, route.as_string(), ANY_RECIPIENT + ) + await self.publish_service_message(serviceMessage, self.cm_response_exchange) + self.add_own_route(route) + logging_info("RouterConsumer registered Route: %s", route) + else: + logging_warning( + "RouterConsumer couldn't register route data: %s channel isn't open.", + CM_RESPONSE_EXCHANGE, + ) + except Exception as ioe: + logging_error("RouterConsumer failed register route data: %s", ioe)
+ + +
+[docs] + async def remove_route_on_cleanup(self, route: AMQRoute) -> bool: + """ + * Unregister / remove route from the list + * :param route: route to remove + * :return result of remove operation + """ + logging_info("RouterConsumer.removeRoute, route: %s", route.as_string()) + try: + service_message: ServiceMessage = self.service_message_factory.of( + ServiceMessageType.ROUTING_DATA_REMOVE, route.as_string(), ANY_RECIPIENT + ) + if not await self.publish_service_message(service_message, self.cm_response_exchange): + logging_warning( + "RouterConsumer couldn't remove current_availability route: %s, channel not open.", + route, + ) + + except Exception as ioe: + logging_error("RouterConsumer failed remove current_availability route: %s", ioe) + + result: bool = self.remove_route(route) + return result
+ + +
+[docs] + def cleanup(self): + """ + * Cleanup - de-register routes with master. + """ + routes: Set[AMQRoute] = self.get_routes() + logging_info("RouterConsumer.cleanup() [%d]: %s", len(routes), self.wrap_routes()) + not_removed = sum( + 1 for r in (self.remove_route_on_cleanup(route) for route in routes) if not r + ) + if not_removed > 0: + logging_warning( + "%d route(s) not removed. self may lead to failed delivery on self route", + not_removed, + )
+
+ +
+ +
+
+ +
+
+
+
+ + + + diff --git a/api-doc/_build/html/_modules/amqp/router/router_producer.html b/api-doc/_build/html/_modules/amqp/router/router_producer.html new file mode 100644 index 0000000..cbd3e79 --- /dev/null +++ b/api-doc/_build/html/_modules/amqp/router/router_producer.html @@ -0,0 +1,362 @@ + + + + + + + + amqp.router.router_producer — AMQ Adapter Python 1.0 documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for amqp.router.router_producer

+"""
+* Maps the request to RabbitMQ exchange. Handles the service discovery logic.
+*
+* Router can be either in 'Master' or 'Service (Consumer)' modes. 'Master' mode is the outward
+* facing side that receives the requests and routes it to queue associated with requested service.
+* 'Service' mode is consumer side for given service, which receives the message from AMQ and
+* forwards it to the application.
+"""
+
+from aio_pika import Message
+from aio_pika.abc import (
+    AbstractIncomingMessage,
+    AbstractRobustChannel,
+    AbstractRobustConnection,
+    AbstractRobustExchange,
+    AbstractRobustQueue,
+)
+from pika.exchange_type import ExchangeType
+
+from amqp.adapter.logging_utils import (
+    logging_debug,
+    logging_error,
+    logging_info,
+    logging_warning,
+)
+from amqp.adapter.service_message_factory import ServiceMessageFactory
+from amqp.config.amq_configuration import AMQConfiguration
+from amqp.model.model import ServiceMessage
+from amqp.model.service_message_type import ServiceMessageType
+from amqp.router.router_base import (
+    ANY_RECIPIENT,
+    CM_C_AMQ_QUEUE,
+    CM_P_REPLY_TO,
+    CM_P_RESP_QUEUE,
+    CM_REQUEST_EXCHANGE,
+    CM_RESPONSE_EXCHANGE,
+    RouterBase,
+)
+
+
+
+[docs] +class RouterProducer(RouterBase): + +
+[docs] + def __init__(self, configuration: AMQConfiguration): + """ + * Initialize router - need to supply the config values and RabbitMQ channel. + * :param configuration: adapter's configuration reference. + """ + super().__init__() + self.connection: AbstractRobustConnection | None = None + self.service_message_factory = ServiceMessageFactory(configuration.amq_adapter.service_name) + self.amq_configuration: AMQConfiguration = configuration + self.channel: AbstractRobustChannel | None = None + self.cm_request_exchange: AbstractRobustExchange | None = None + self.cm_response_exchange: AbstractRobustExchange | None = None + + logging_info( + "RouterProducer.<init>: %s, route: %s", + self.amq_configuration.amq_adapter.service_name, + self.amq_configuration.amq_adapter.route_mapping, + )
+ + +
+[docs] + async def init_internal_routing_paths(self, channel: AbstractRobustChannel | None): + """ + * Initialize internal routes for ServiceMessages. + * The logic here sets a producer and listener on CleverMicroRequest/CleverMicroResponse queues + * created to enable the service discovery. + * The Producer mode sends out routing data request on CleverMicroRequest and listens + * on CleverMicroResponse for any service responding with its routing data. + """ + + self.channel = channel + logging_info( + "RouterProducer.initRoutingPaths, channel open: %s", + self.is_open(self.channel), + ) + if self.is_open(self.channel): + try: + # **** set up the service discovery internal exchanges **** + # 1. declare RequestExchange to where to publish the RoutingData requests + self.cm_request_exchange = await self.channel.declare_exchange( + name=CM_REQUEST_EXCHANGE, type=ExchangeType.fanout + ) + # + # 2. declare ResponseExchange where to publish current_availability RoutingData (response to + # a RoutingData request) + self.cm_response_exchange = await self.channel.declare_exchange( + name=CM_RESPONSE_EXCHANGE, type=ExchangeType.fanout + ) + # 2a. declare a Response queue for Routing Data replies from Services + _cm_response_queue: str = self.get_response_queue_name() + _response_queue: AbstractRobustQueue = await self.channel.declare_queue( + name=_cm_response_queue, + durable=True, + exclusive=False, + auto_delete=False, + arguments={}, + ) + # 2b. bind queue to CM_RESPONSE_EXCHANGE w/o any conditions (empty routing key) + await _response_queue.bind(exchange=CM_RESPONSE_EXCHANGE, routing_key="#") + # 2c. listen for incoming RoutingData messages + _c_tag = await _response_queue.consume( + callback=self.handle_routing_data_message, no_ack=False + ) + logging_info( + " [%s] Routing master listens for Route updates on %s", + _c_tag, + _cm_response_queue, + ) + except Exception as ioe: + logging_error("RouterProducer FATAL: cannot initialize, %s", ioe) + + else: + logging_error("RouterProducer FATAL: cannot initialize, RabbitMQ channel is not open.")
+ + +
+[docs] + async def handle_routing_data_message(self, message: AbstractIncomingMessage): + """ + * Handler for CLEVER_MICRO_RESPONSE queue messages. Used in Master mode to handle receipt of + * routing data, invokes addRoute or removeRoute depending on the type of the message. + """ + logging_info( + "[%s] Received ROUTING DATA: [%s], msgId=%s", + message.delivery_tag, + message.body, + message.message_id, + ) + await message.ack(multiple=False) + cm_message: ServiceMessage = self.service_message_factory.from_bytes(message.body) + # ignore the message if it comes from ourselves (even if from different instance) + logging_debug( + "******[%s] ROUTING_DATA, msg=%s, routeMap=[%s] sn=%s, replyTo=%s", + message.delivery_tag, + self.service_message_factory.to_string(cm_message), + self.amq_configuration.amq_adapter.route_mapping, + self.amq_configuration.amq_adapter.service_name, + cm_message.reply_to, + ) + if ( + cm_message.is_valid() + and not self.amq_configuration.amq_adapter.service_name == cm_message.reply_to + ): + if cm_message.message_type == ServiceMessageType.ROUTING_DATA: + self.add_routes(cm_message.message) + if cm_message.message_type == ServiceMessageType.ROUTING_DATA_REMOVE: + self.remove_routes(cm_message.message)
+ + +
+[docs] + async def request_routes_from_adapters(self): + """ + * Send a broadcast message prompting all listening adapters to reply with own routing data. + """ + logging_info( + "RouterProducer.requestRoutesFromAdapters, queue name: %s", + CM_REQUEST_EXCHANGE, + ) + try: + serviceMessage: ServiceMessage = self.service_message_factory.of( + ServiceMessageType.ROUTING_DATA_REQUEST, "", ANY_RECIPIENT + ) + if not await self.publish_service_message(serviceMessage, self.cm_request_exchange): + logging_warning( + "RouterProducer could not request route data: %s channel is not open.", + CM_REQUEST_EXCHANGE, + ) + + except Exception as ioe: + logging_error("RouterProducer failed request routing data: %s", ioe)
+ + +
+[docs] + async def publish_service_message( + self, message: ServiceMessage, exchange: AbstractRobustExchange + ) -> bool: + """ + * Publishes a service message to the service queue and logs success log entry. + * + * :param message: Service Message to be sent + * :param exchange: target exchange name where to publish the message + * :return True if channel is open, False otherwise + * @throws IOException if something else goes wrong + """ + if self.is_open(self.channel): + binary_content: bytes = self.service_message_factory.serialize(message) + pika_message: Message = Message( + body=binary_content, + content_encoding="utf-8", + delivery_mode=2, + content_type="application/octet-stream", + headers=None, + priority=0, + correlation_id=None, + ) + await exchange.publish(message=pika_message, routing_key="") + logging_info( + "Service Message Published to %s, msg: %s", + exchange.name, + str(binary_content), + ) + return True + return False
+ + +
+[docs] + def get_reply_to_queue_name(self) -> str: + """ + * self should create a unique name for self process and also when there's several instances. + * @return unique reply-to queue name. + """ + return self.amq_configuration.amq_adapter.adapter_prefix() + CM_P_REPLY_TO
+ + +
+[docs] + def get_response_queue_name(self) -> str: + """ + * self should create a unique name for self process and also when there's several instances. + * @return unique reply-to queue name. + """ + return self.amq_configuration.amq_adapter.adapter_prefix() + CM_P_RESP_QUEUE
+ + +
+[docs] + def get_consume_queue_name(self) -> str: + """ + * self should create a unique name for self process and also when there's several instances. + * @return unique reply-to queue name. + """ + return self.amq_configuration.amq_adapter.adapter_prefix() + CM_C_AMQ_QUEUE
+ + +
+[docs] + def is_open(self, _channel: AbstractRobustChannel) -> bool: + """ + * Test if the channel is usable (opened). + * :param _channel: the channel + * :return True if the channel is opened. + """ + return _channel is not None and not _channel.is_closed
+
+ +
+ +
+
+ +
+
+
+
+ + + + diff --git a/api-doc/_build/html/_modules/amqp/router/utils.html b/api-doc/_build/html/_modules/amqp/router/utils.html new file mode 100644 index 0000000..757ee20 --- /dev/null +++ b/api-doc/_build/html/_modules/amqp/router/utils.html @@ -0,0 +1,156 @@ + + + + + + + + amqp.router.utils — AMQ Adapter Python 1.0 documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for amqp.router.utils

+import asyncio
+import re
+
+pattern = re.compile(r"[^a-zA-Z0-9.#/\-]")
+AWAIT_SLEEP = 0.05  # seconds
+
+
+
+[docs] +def sanitize_as_rabbitmq_name(input_str: str) -> str: + matcher = pattern.search(input_str) + token = input_str[: matcher.start()] if matcher else input_str + return sanitize_as_rabbitmq_domain_name(token)
+ + + +
+[docs] +def sanitize_as_rabbitmq_domain_name(token: str) -> str: + # Step 1: Replace non-alphanumeric characters (except periods) with a period + step1 = re.sub(r"[^A-Za-z0-9.#]", ".", token) + # Step 2: Replace multiple consecutive periods with a single period + step2 = re.sub(r"\.+", ".", step1) + # Step 3: Remove trailing '.' character, if applicable + step_last_char = len(step2) - 1 + return step2[:-1] if step_last_char > 0 and step2[step_last_char] == "." else step2
+ + + +
+[docs] +async def await_future(future: asyncio.Future, sleep_time=AWAIT_SLEEP): + """ + Wait for a future to complete. + """ + while not future.done(): + await asyncio.sleep(sleep_time)
+ + + +
+[docs] +async def await_result(future: asyncio.Future, sleep_time=AWAIT_SLEEP): + """ + Wait for a future to complete and return its result. + """ + await await_future(future, sleep_time=sleep_time) + return future.result()
+ +
+ +
+
+ +
+
+
+
+ + + + diff --git a/api-doc/_build/html/_modules/amqp/service/amq_message_handler.html b/api-doc/_build/html/_modules/amqp/service/amq_message_handler.html new file mode 100644 index 0000000..6f29167 --- /dev/null +++ b/api-doc/_build/html/_modules/amqp/service/amq_message_handler.html @@ -0,0 +1,692 @@ + + + + + + + + amqp.service.amq_message_handler — AMQ Adapter Python 1.0 documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

Source code for amqp.service.amq_message_handler

+import asyncio
+import importlib
+import inspect
+import os
+import threading
+import time
+from asyncio import AbstractEventLoop, Future
+from typing import Any, Dict
+
+from aio_pika import Message
+from aio_pika.abc import AbstractIncomingMessage, AbstractRobustExchange
+from opentelemetry import trace
+from opentelemetry.trace import Tracer, TraceState
+from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator
+from pika.exchange_type import ExchangeType
+
+from amqp.adapter.backpressure_handler import BackpressureHandler
+from amqp.adapter.cleverthis_service_adapter import AMQMessage, CleverThisServiceAdapter
+from amqp.adapter.data_message_factory import DataMessageFactory
+from amqp.adapter.data_response_factory import DataResponseFactory
+from amqp.adapter.file_handler import FileHandler, StreamingFileHandler
+from amqp.adapter.logging_utils import logging_debug, logging_error, logging_info
+from amqp.adapter.serializer import map_as_string, parse_map_string
+from amqp.adapter.service_message_factory import ServiceMessageFactory
+from amqp.config.amq_configuration import AMQConfiguration
+from amqp.model.model import (
+    DataMessage,
+    DataMessageMagicByte,
+    DataResponse,
+    MultipartDataMessage,
+)
+from amqp.rabbitmq.rabbit_mq_client import RabbitMQClient
+from amqp.router.utils import sanitize_as_rabbitmq_name
+
+
+
+[docs] +def collect_trace_info(): + trace_map = {} + TraceContextTextMapPropagator().inject( + carrier=trace_map, context=trace.context_api.get_current() + ) + return trace_map
+ + + +
+[docs] +def get_default_trace_state(): + return TraceState(entries={})
+ + + +
+[docs] +def set_text(carrier, key, value): + carrier[key] = value
+ + + +
+[docs] +async def invoke_command( + package_name: str, class_name: str, function_name: str, msg_id: str = "", kwargs=None +) -> Any: + if kwargs is None: + kwargs = {} + try: + # 1. Dynamically import the package + logging_info(f"Attempting to import package: {package_name}") + package = importlib.import_module(package_name) + logging_info(f"Package '{package_name}' imported successfully.") + + target_callable = None + + if class_name == "__global__": + # 2. Handle global function invocation + logging_info(f"Attempting to find global function: {function_name} in {package_name}") + target_callable = getattr(package, function_name) + logging_info(f"Global function '{function_name}' found.") + else: + # 3. Handle class method invocation + logging_info(f"Attempting to find class: {class_name} in {package_name}") + target_class = getattr(package, class_name) + logging_info(f"Class '{class_name}' found. Instantiating...") + + # Instantiate the class. Assuming a default constructor or one that doesn't + # require arguments from kwargs for initialization. + # If the class __init__ needs specific arguments, this part might need adjustment + # based on how those arguments are provided. + instance = target_class() + logging_info(f"Instance of '{class_name}' created.") + + logging_info(f"Attempting to find method: {function_name} in class {class_name}") + target_callable = getattr(instance, function_name) + logging_info(f"Method '{function_name}' found.") + + # 4. Invoke the function/method + logging_info(f"Invoking '{function_name}' with arguments: {kwargs}") + if inspect.iscoroutinefunction(target_callable): + result = await target_callable(**kwargs) + logging_info(f"Asynchronous function '{function_name}' invoked. Result: {result}") + else: + result = target_callable(**kwargs) + logging_info(f"Synchronous function '{function_name}' invoked. Result: {result}") + + response: DataResponse = DataResponseFactory.of( + id=msg_id, + response_code=200, + content_type="text/plain", + body=result.encode("utf-8"), + trace_info=collect_trace_info(), + error=None, + error_cause=None, + ) + response._magic = DataMessageMagicByte.RPC_REQUEST.value + return response + + except ModuleNotFoundError as e: + logging_error(f"Error: Package '{package_name}' not found. {e}") + raise + except AttributeError as e: + logging_error( + f"Error: Class '{class_name}' or function/method '{function_name}' not found in '{package_name}'. {e}" + ) + raise + except Exception as e: + logging_error(f"An unexpected error occurred during invocation: {e}") + raise
+ + + +
+[docs] +class DataMessageHandler: +
+[docs] + def __init__( + self, + service_adapter: CleverThisServiceAdapter, + tracer: Tracer, + message_factory: DataMessageFactory, + service_message_factory: ServiceMessageFactory, + reply_to_exchange: AbstractRobustExchange, + rabbit_mq_client: RabbitMQClient, + amq_configuration: AMQConfiguration, + loop, + backpressure_handler: BackpressureHandler, + file_handler: FileHandler = StreamingFileHandler(), + ): + self.service_adapter: CleverThisServiceAdapter = service_adapter + self.tracer = tracer + self.message_factory: DataMessageFactory = message_factory + self.service_message_factory: ServiceMessageFactory = service_message_factory + self.reply_to_exchange: AbstractRobustExchange = reply_to_exchange + self.rabbit_mq_client: RabbitMQClient = rabbit_mq_client + self.rabbit_mq_url = amq_configuration.dispatch.rabbit_mq_url + self.output_dir = amq_configuration.dispatch.download_dir + self.loop = loop + self.service_adapter.loop = loop + # Dictionary to store outstanding Futures + self.outstanding: Dict[str, asyncio.Future] = {} + self.reply_to: str | None = None + self.file_handler: FileHandler = file_handler + self.backpressure_handler: BackpressureHandler = backpressure_handler
+ + +
+[docs] + async def inbound_data_message_callback_as_thread(self, message: AbstractIncomingMessage): + """ + The callback method that is called when a DataMessage is received from RabbitMQ, starts new Thread. + """ + threading.Thread( + target=lambda: asyncio.run(self.inbound_data_message_callback_no_thread(message)) + ).start()
+ + +
+[docs] + async def inbound_data_message_callback_no_thread(self, message: AbstractIncomingMessage): + """ + Receives the DataMessage as bytes from RabbitMQ queue and deserializes it to the DataMessage object + Ensures Jaeger trace-id propagation. + Invokes custom handler/mapper/adapter method that corresponds to this message + :param message: + :return: + """ + start = time.time() + amq_message = await self.reconstitute_data_message(message) + if amq_message is not None: + self.ensure_trace_info(amq_message) + if amq_message.magic() == DataMessageMagicByte.HTTP_REQUEST.value: + await self.run_http_request_magic(message, amq_message, self.loop) + elif amq_message.magic() == DataMessageMagicByte.RPC_REQUEST.value: + await self.run_rpc_request(message, amq_message, self.loop) + else: + logging_error( + "Unknown MAGIC value: [%s], don't know how to interpret the message", + amq_message.magic(), + ) + asyncio.run_coroutine_threadsafe(message.nack(requeue=False), self.loop) + return + else: + logging_error("######[%s] No valid AMQ Message present.", message.delivery_tag) + self.record_duration(start, message.delivery_tag) + asyncio.run_coroutine_threadsafe(message.ack(multiple=False), self.loop)
+ + +
+[docs] + async def run_http_request_magic( + self, + message: AbstractIncomingMessage, + amq_message: DataMessage, + loop: AbstractEventLoop, + ) -> None: + """ + Synchronous version of on_message method. This is a wrapper around the async on_message method. + It also counts parallel executions and handles backpressure. + :param message: RabbitMQ raw message + :param amq_message: DataMessage + :param loop: Event loop + :return: DataResponse + """ + # Increment the counter for parallel executions and check resource availability + self.backpressure_handler.increase_current_load() + await self.backpressure_handler.check_overload_condition() + logging_info( + f"PARALLEL: Current Capacity [{self.backpressure_handler.current_availability}] of [{self.backpressure_handler.max_availability}]" + ) + if message.reply_to: + self.reply_to = message.reply_to + + try: + _a_message = AMQMessage(amq_message, self.rabbit_mq_client.connection) + _response: DataResponse = await self.service_adapter.on_message(_a_message) + logging_debug(f"Result in thread: {_response}") + try: + await self.send_reply(message.reply_to, _response) + except Exception as e: + logging_error(f"Processing message: {e}") + asyncio.run_coroutine_threadsafe(message.nack(requeue=False), loop=loop) + + if isinstance(amq_message, MultipartDataMessage): + # remove the downloaded files from the output_dir + for file_id, filename in amq_message.extra_data.items(): + try: + logging_info( + f"[{message.delivery_tag}] Deleting file: {file_id} in {filename}" + ) + os.remove(filename) + except OSError as e: + logging_error(f"Deleting file {filename}: {e}") + except Exception as e: + logging_error(f"Request handler: {e}") + + self.backpressure_handler.decrease_current_load()
+ + +
+[docs] + async def run_rpc_request( + self, + message: AbstractIncomingMessage, + amq_message: DataMessage, + loop: AbstractEventLoop, + ) -> None: + """ + Synchronous version of on_message method. This is a wrapper around the async on_message method. + It also counts parallel executions and handles backpressure. + :param message: RabbitMQ raw message + :param amq_message: DataMessage + :param loop: Event loop + :return: DataResponse + """ + # Increment the counter for parallel executions and check resource availability + self.backpressure_handler.increase_current_load() + await self.backpressure_handler.check_overload_condition() + logging_info( + f"PARALLEL: Current Capacity [{self.backpressure_handler.current_availability}] of [{self.backpressure_handler.max_availability}]" + ) + if message.reply_to: + self.reply_to = message.reply_to + + try: + _a_message = AMQMessage(amq_message, self.rabbit_mq_client.connection) + _response: DataResponse = await self.on_rpc(_a_message) + logging_debug(f"Result in thread: {_response}") + try: + await self.send_reply(message.reply_to, _response) + except Exception as e: + logging_error(f"Processing message: {e}") + asyncio.run_coroutine_threadsafe(message.nack(requeue=False), loop=loop) + + if isinstance(amq_message, MultipartDataMessage): + # remove the downloaded files from the output_dir + for file_id, filename in amq_message.extra_data.items(): + try: + logging_info( + f"[{message.delivery_tag}] Deleting file: {file_id} in {filename}" + ) + os.remove(filename) + except OSError as e: + logging_error(f"Deleting file {filename}: {e}") + except Exception as e: + logging_error(f"Request handler: {e}") + + self.backpressure_handler.decrease_current_load()
+ + +
+[docs] + async def on_rpc(self, a_message: AMQMessage) -> DataResponse: + """ + Dynamically invokes a function or a class method from a specified package. + + Args: + a_message (AMQMessage): The message containing the package, class, and function details. + Args encoded in the AMQMessage: + package_name (str): The name of the Python package to import. + class_name (str): The name of the class within the package. + Use '__global__' if the function_name refers to a global function + in the package. + function_name (str): The name of the function or method to invoke. + kwargs (Dict[str, Any]): A dictionary of keyword arguments to pass to the function/method (in the body). + + Returns: + Any: The return value of the invoked function or method. + + Raises: + ModuleNotFoundError: If the specified package cannot be imported. + AttributeError: If the specified class or function/method does not exist. + Exception: For any other errors during invocation. + """ + package_name: str = a_message.method() + class_name: str = a_message.domain() + function_name: str = a_message.path() + kwargs: Dict[str, Any] = parse_map_string(a_message._base64body.decode("utf-8")) + + return await invoke_command( + package_name, class_name, function_name, a_message.id().as_string(), kwargs + )
+ + +
+[docs] + async def reconstitute_data_message( + self, message: AbstractIncomingMessage + ) -> DataMessage | None: + """ + DataMessage may be serialized as string in separate queue (addressing == 1) or be a MultiPart message with + files streamed over individual queues, one per file. + This function uses 'addressing' mode from message headers to determine how to deserialize (reconstitute) + the original message. + """ + amq_message: DataMessage | None = None + delivery_tag = message.delivery_tag + addressing: int = message.headers.get("clevermicro.addressing", 0) + if addressing == 0: + amq_message = DataMessageFactory.from_bytes(message.body) + else: + amq_message = await DataMessageFactory.from_stream( + message.body, rabbitmq_url=self.rabbit_mq_url, loop=self.loop + ) + logging_info(f"######[{delivery_tag}] AMQ Message from stream: {amq_message}") + if amq_message is not None: + extra_data = await self.file_handler.on_message( + message=message, + json_data=amq_message.body(), + rabbitmq_url=self.rabbit_mq_url, + loop=self.loop, + output_dir=self.output_dir, + ) + logging_info(f"######[{delivery_tag}] Multipart download result: {extra_data}") + amq_message = MultipartDataMessage(amq_message, extra_data) + if amq_message: + logging_info( + "######[%s] AMQ Message received, ctag=%s, msgId=%s, %s:%s, traceInfo=%s", + message.delivery_tag, + message.consumer_tag, + amq_message.id(), + amq_message.method(), + amq_message.path(), + map_as_string(amq_message.trace_info()), + ) + return amq_message
+ + +
+[docs] + def ensure_trace_info(self, amq_message: DataMessage): + propagator = TraceContextTextMapPropagator() + context = propagator.extract( + carrier=amq_message.trace_info(), context=trace.context_api.get_current() + ) + self.tracer.start_span(name="AMQ-Adapter-Consumer", context=context)
+ + # logging_info("CTX=%s", context) + # current_span = self.tracer.start_span("AMQ-Adapter.Processing.Request", parent=parent_span) + # context_with_span = trace.set_span_in_context(current_span, context) + # logging.info(" [*] CTX2=%s", context_with_span) + # propagator.inject(context_with_span, amq_message.trace_info, self.set_text) + # logging.info(" [***] NEW TI=%s CTX=%s SCOPE=%s", + # map_as_string(amq_message.trace_info()), context_with_span, context) + +
+[docs] + def record_duration(self, start, delivery_tag): + global last_backpressure_event_time + last_backpressure_event_time = time.time() + logging_info( + "######[%s] Done, single ACK: %sms.", + delivery_tag, + last_backpressure_event_time - start, + )
+ + +
+[docs] + async def send_reply(self, reply_to: str, response: DataResponse): + """ + The Service side code. When the CleverXXX service has output ready, in DataResponse object, + call this method to return the reply back to the caller + :param reply_to:connection.channel + :param response: + :return: + """ + if reply_to: + pika_message: Message = Message( + body=DataResponseFactory.serialize(response), + correlation_id=response.id(), + content_type=( + response.content_type()[0] + if isinstance(response.content_type(), list) + else response.content_type() + ), + ) + _res = asyncio.run_coroutine_threadsafe( + self.reply_to_exchange.publish(message=pika_message, routing_key=reply_to), + self.loop, + ) + logging_debug( + "###### DONE Reply Published to exchg:(%s) To=%s, res=%s", + self.reply_to_exchange.name, + reply_to, + _res, + )
+ + +
+[docs] + async def reply_received_callback(self, message: AbstractIncomingMessage): + """ + The handler for reply for DataMessage that we sent out earlier - + the code matches the received response with the original request and passes the response to the caller. + :param message: + :return: + """ + reply_id = message.correlation_id + delivery_tag = message.delivery_tag + debug = f"######[{delivery_tag}] (RPC REPLY) env: {message.headers}, props: {message.properties}, BODY: {message.body.decode()}" + logging_debug(debug) + # TODO - figure out implementation of this part, as this is sending reply back either to a service, + # or to the user + logging_info(f"OUTSTANDING RECV: {self.outstanding}, size={len(self.outstanding)}") + + future: Future = self.outstanding.pop(reply_id, None) + # if reply is None: + # logging.warn(" [W] ######[%s] IGNORING REPLY. No outstanding request for ID %s", + # delivery_tag, reply_id) + # else: + # result = DataResponseFactory.from_bytes(body) + # reply.tracing_span().end() + # TODO - figure out the right response format (not needed for Clever Swarm v0.1) + # that currently runs Java version, this method here is for the future compatibility to enable + # internal, service-2-service communication, and the proper response type will be decided than + # reply.response().set_result(Response.ok(result.body, content_type).build()) + if future: + response: DataResponse = DataResponseFactory.from_bytes(message.body) + # Complete the Future with the response data + future.set_result(response.body()) + + await message.ack(multiple=False)
+ + +
+[docs] + async def send_rpc(self, route, message: DataMessage) -> Future: + """ + Sends an RPC message to the specified route and returns a Future that will be completed + when the response is received. + + :param route: AMQRoute object containing exchange and routing information + :param message: DataMessage to be sent + :return: Future that will be completed with the response payload + """ + # Create a Future to be completed when the response is received + future = asyncio.Future() + + # Store the future in the outstanding dictionary using the message ID as the key + message_id = message.id().as_string() + self.outstanding[message_id] = future + + # Create a Pika message with the correlation ID set to the message ID + pika_message = Message( + body=DataMessageFactory.serialize(message), + correlation_id=message_id, + reply_to=self.rabbit_mq_client.router.get_reply_to_queue_name(), + content_type=( + message.content_type()[0] + if isinstance(message.content_type(), list) + else message.content_type() + ), + ) + + # Get the exchange from the route + rpc_exchange_name = route.exchange + exchange: AbstractRobustExchange = await self.rabbit_mq_client.channel.declare_exchange( + name=rpc_exchange_name, type=ExchangeType.topic + ) + # Publish the message to the exchange with the component name as the routing key + await exchange.publish( + message=pika_message, routing_key=sanitize_as_rabbitmq_name(route.component_name) + ) + + logging_debug( + "###### RPC Request Published to exchg:(%s) routing_key=%s, msgId=%s", + rpc_exchange_name, + route.component_name, + message_id, + ) + + return future
+ + +
+[docs] + async def send_command(self, route, message: DataMessage) -> bool: + """ + Sends an RPC message to the specified route and returns a Future that will be completed + when the response is received. + + :param route: AMQRoute object containing exchange and routing information + :param message: DataMessage to be sent + :return: Future that will be completed with the response payload + """ + rmq_body = DataMessageFactory.serialize(message) + cid = message.id().as_string() + ctype = ( + message.content_type()[0] + if isinstance(message.content_type(), list) + else message.content_type() + ) + print( + f"Sending RPC command to route: {route}, message ID: {cid} with body: {rmq_body} correlation ID: {cid}, content type: {ctype}" + ) + # Create a Pika message with the correlation ID set to the message ID + pika_message = Message( + body=rmq_body, + correlation_id=cid, + content_type=ctype, + ) + print(f"SENDING RPC COMMAND: {pika_message.body} with ID: {message.id().as_string()}") + + # Get the exchange from the route + rpc_exchange_name = route.exchange + exchange: AbstractRobustExchange = await self.rabbit_mq_client.channel.declare_exchange( + name=rpc_exchange_name, type=ExchangeType.topic + ) + # Publish the message to the exchange with the component name as the routing key + await exchange.publish( + message=pika_message, routing_key=sanitize_as_rabbitmq_name(route.component_name) + ) + + logging_info( + "###### RPC Request Published to exchg:(%s) routing_key=%s, msgId=%s", + rpc_exchange_name, + route.component_name, + message.id().as_string(), + ) + + return True
+
+ +
+ +
+
+ +
+
+
+
+ + + + diff --git a/api-doc/_build/html/_modules/amqp/service/amq_service.html b/api-doc/_build/html/_modules/amqp/service/amq_service.html new file mode 100644 index 0000000..6fe48e6 --- /dev/null +++ b/api-doc/_build/html/_modules/amqp/service/amq_service.html @@ -0,0 +1,431 @@ + + + + + + + + amqp.service.amq_service — AMQ Adapter Python 1.0 documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for amqp.service.amq_service

+import asyncio
+import logging
+import logging.config
+import os
+from asyncio import Future
+from threading import Thread
+from typing import AsyncIterator, Optional
+
+from aio_pika.abc import AbstractRobustExchange
+
+from amqp.adapter.amq_route_factory import AMQRouteFactory
+from amqp.adapter.backpressure_handler import BackpressureHandler
+from amqp.adapter.consul_global_id_generator import get_unique_instance_id
+from amqp.adapter.data_message_factory import DataMessageFactory
+from amqp.adapter.logging_utils import logging_error, logging_info, logging_warning
+from amqp.adapter.service_message_factory import ServiceMessageFactory
+from amqp.config.amq_configuration import AMQConfiguration
+from amqp.model.model import AMQRoute, DataMessage
+from amqp.rabbitmq.rabbit_mq_client import RabbitMQClient
+from amqp.router.router_consumer import RouterConsumer
+from amqp.service.amq_message_handler import DataMessageHandler
+from amqp.service.tracing import TracingConfig, initialize_telemetry
+
+logging.basicConfig(level=logging.DEBUG)
+logging.info(f"CWD = {os.getcwd()}")
+logging_conf_path = os.path.join(os.getcwd(), "amqp", "service", "logging.conf")
+if os.path.exists(logging_conf_path):
+    logging.config.fileConfig(logging_conf_path)
+else:
+    logging_conf_path = os.path.join(os.getcwd(), "logging.conf")
+    if os.path.exists(logging_conf_path):
+        logging.config.fileConfig(logging_conf_path)
+
+
+
+[docs] +class AMQService: + CLEVER_AMQP_CLIENT_VERSION = os.getenv("CLEVER_AMQP_CLIENT_VERSION") + MAX_WAIT = 300 # defaults to 5 minutes (300 seconds) + +
+[docs] + def __init__(self, amq_configuration: AMQConfiguration, service_adapter=None): + logging.info("***********************************************") + logging.info("************** AMQ Service ***************") + logging.info("***********************************************") + self.loop = asyncio.new_event_loop() + + self.amq_configuration = amq_configuration + self.service_adapter = service_adapter + self.unique_id = get_unique_instance_id(amq_configuration.amq_adapter) + amq_configuration.amq_adapter.generator_id = self.unique_id + + self.data_message_factory = DataMessageFactory.get_instance( + self.unique_id & DataMessageFactory.MACHINE_ID_MASK + ) + self.router = RouterConsumer(self.amq_configuration) + self.rabbit_mq_client = RabbitMQClient(self.amq_configuration, self.router) + self.tracer = initialize_telemetry( + tracing_config=TracingConfig( + service_name=amq_configuration.amq_adapter.service_name, + service_version=os.getenv("SERVICE_VERSION", "unknown"), + ) + ) + self.amq_data_message_handler = None + self.service_message_factory = ServiceMessageFactory( + self.amq_configuration.amq_adapter.service_name + ) + self.backpressure_thread: Optional[Thread] = None + self.backpressure_handler: Optional[BackpressureHandler] = None + self.maximum_availability = -1 + self.future: Future = asyncio.Future()
+ + + # ======================================================================= + # ======================================================================= + # ============== P u b l i c A P I M e t h o d s =================== + # ======================================================================= + # ======================================================================= +
+[docs] + def run(self): + """ + Start the AMQ service, initializing RabbitMQ connection and message handlers. + """ + # self.init(loop) will initialize RabbitMQ connection + asyncio.set_event_loop(self.loop) + self.loop.run_until_complete(self.init(self.loop, self.service_adapter)) + + logging.info("***********************************************") + logging.info("****** AMQ Service Init Complete *********") + logging.info("***********************************************") + self.future.set_result(True) + self.loop.run_forever()
+ + +
+[docs] + def backpressure(self, current_availability: int, maximum: int = -1): + """ + Send Backpressure event to the Management Service. The Backpressure logic determines the type of the event + based on the current_availability and maximum values. + - OVERLOAD event is triggered immediately when the current_availability value is below threshold + typically set to 10% of the maximum value, or 0 if no maximum is provided. + - IDLE event is triggered when the current_availability value exeeds 80% of the maximum value or is greater + than 0 if no maximum is provided. + - UPDATE event is sent if neither OVERLOAD nor IDLE conditions are met, indicating that the backpressure is + within acceptable limits. + :param current_availability: Currently available capacity. + :param maximum: Maximum value of the backpressure metric. If undefined or set to lt 0 (e.g. -1), + the maximum is than determined from max value of current_availability seen over the time. + """ + if self.backpressure_handler is not None: + # watch maximum & current value to always have some valid maximum reference + if maximum < 0: + if current_availability > self.maximum_availability: + self.maximum_availability = current_availability + else: + if maximum > self.maximum_availability: + self.maximum_availability = maximum + asyncio.run_coroutine_threadsafe( + self.backpressure_handler.update_backpressure_value( + current_availability, maximum if maximum > 0 else self.maximum_availability + ), + loop=self.backpressure_handler.loop, + )
+ + +
+[docs] + def rpc(self, service_name: str, fq_method_name: str, **kwargs) -> Future: + """ + Send a Remote Procedure Call (RPC) message to the AMQ service. + :param service_name: The service name to receive the RPC message. + :param fq_method_name: The fully qualified method name to execute the RPC message. + :param kwargs: Additional keyword arguments to be passed to the RPC method. + :return: A Future that resolves when the RPC response is received. + """ + route: Optional[AMQRoute] = self.router.find_route_by_service(service_name) + if route is not None: + message: DataMessage = self.data_message_factory.create_rpc_message( + fq_method_name=fq_method_name, + swarm_id=self.amq_configuration.amq_adapter.swarm_task_id, + **kwargs, + ) + result: Future = asyncio.run_coroutine_threadsafe( + self.amq_data_message_handler.send_rpc(route, message), + loop=self.backpressure_handler.loop, + ) + self.set_outstanding_future(message, result) + return result + + result = asyncio.Future() + result.set_exception( + NameError(f"No route exists. Service '{service_name}' not found in routing table.") + ) + return result
+ + +
+[docs] + def command(self, service_name: str, fq_method_name: str, **kwargs) -> bool: + """ + Send a unidirectional Command message to the AMQ service. + :param service_name: The service name to receive the RPC message. + :param fq_method_name: The fully qualified method name to execute the RPC message. + :param kwargs: Additional keyword arguments to be passed to the RPC method. + :return: A Future that resolves when the RPC response is received. + """ + route: Optional[AMQRoute] = self.router.find_route_by_service(service_name) + if route is not None: + m: DataMessage = self.data_message_factory.create_rpc_message( + fq_method_name=fq_method_name, + swarm_id=self.amq_configuration.amq_adapter.swarm_task_id, + args=kwargs, + ) + asyncio.run_coroutine_threadsafe( + self.amq_data_message_handler.send_command(route, m), + loop=self.backpressure_handler.loop, + ) + return True + return False
+ + +
+[docs] + async def data_message_generator(self, route: AMQRoute) -> AsyncIterator[DataMessage]: + """ + An async iterator that blocks until messages arrive, processes them, + and continues in a loop. + + :param queue_name: The name of the queue to consume messages from + :yield: The processed DataResponse for each message + """ + # Get a channel from the connection + channel = self.rabbit_mq_client.channel + queue_args = ( + self.rabbit_mq_client.DLQ_ARGS if self.amq_configuration.dispatch.use_dlq else None + ) + queue = await channel.declare_queue( + route.queue, + durable=True, + exclusive=False, + auto_delete=False, + arguments=queue_args, + ) + await queue.bind(route.exchange, route.key) + # Start consuming without a callback + async with queue.iterator() as queue_iter: + async for message in queue_iter: + try: + # Process the message using the same logic as in inbound_data_message_callback_no_thread + data_message: DataMessage = ( + await self.amq_data_message_handler.reconstitute_data_message(message) + ) + + success = yield data_message + if not success: + logging_warning(f"Message processing failed for {data_message.id()}.") + # Nack the message without requeuing in case of failure + await message.nack(requeue=False) + else: + await message.ack() + except Exception as e: + logging_error(f"Error processing message: {e}") + # Nack the message without requeuing in case of error + await message.nack(requeue=False)
+ + +
+[docs] + def get_unique_instance_id(self) -> int: + """ + Provides cluster-wide unique instance ID for this AMQ service instance. + This ID is used to identify the service instance in the cluster and is unique across all instances. + IDs are managed by shared Consul KV store, ensuring that even if multiple instances of the service are running, + they will each have unique ID. + """ + return self.unique_id
+ + + # ======================================================================= + # ====================== Internal Methods ============================ + # ======================================================================= + +
+[docs] + async def init(self, loop, service_adapter): + _reply_to_exchange: AbstractRobustExchange = await self.rabbit_mq_client.init(loop) + self.backpressure_handler = BackpressureHandler( + channel=self.rabbit_mq_client.channel, + loop=loop, + config=self.amq_configuration, + ) + self.amq_data_message_handler = DataMessageHandler( + service_adapter, + self.tracer, + self.data_message_factory, + self.service_message_factory, + _reply_to_exchange, + self.rabbit_mq_client, + self.amq_configuration, + loop=loop, + backpressure_handler=self.backpressure_handler, + ) + await self.register_routes() + await self.rabbit_mq_client.request_routes() + logging_info("Starting backpressure monitor thread") + self.backpressure_thread = self.backpressure_handler.start_backpressure_monitor()
+ + +
+[docs] + def cleanup(self): + logging_info("AMQService.cleanup() rmqc=%s", self.rabbit_mq_client) + if self.rabbit_mq_client: + self.rabbit_mq_client.cleanup()
+ + +
+[docs] + async def reinitialize_if_disconnected(self): + if not self.rabbit_mq_client.channel or self.rabbit_mq_client.channel.is_closed: + self.rabbit_mq_client = RabbitMQClient(self.amq_configuration, self.router) + await self.rabbit_mq_client.init(loop=self.loop) + await self.register_routes() + await self.rabbit_mq_client.request_routes()
+ + +
+[docs] + async def register_routes(self) -> AbstractRobustExchange | None: + route_mapping = self.amq_configuration.amq_adapter.route_mapping + if AMQRouteFactory.validate(route_mapping): + try: + await self.rabbit_mq_client.register_inbound_routes( + route_mapping, + self.amq_data_message_handler.inbound_data_message_callback_no_thread, + ) + return await self.rabbit_mq_client.register_data_reply_callback( + self.amq_data_message_handler.reply_received_callback + ) + except Exception as e: + raise RuntimeError(e) + elif route_mapping: + logging_warning( + "route [%s] failed validation, no routes were registered.", + route_mapping, + ) + return None
+ + +
+[docs] + def set_outstanding_future(self, message: DataMessage, future: Future): + self.amq_data_message_handler.outstanding[str(message.id())] = future
+ + +
+[docs] + def remove_outstanding_future(self, message_id: str): + self.amq_data_message_handler.outstanding.pop(message_id, None)
+
+ +
+ +
+
+ +
+
+
+
+ + + + diff --git a/api-doc/_build/html/_modules/amqp/service/tracing.html b/api-doc/_build/html/_modules/amqp/service/tracing.html new file mode 100644 index 0000000..65e075f --- /dev/null +++ b/api-doc/_build/html/_modules/amqp/service/tracing.html @@ -0,0 +1,327 @@ + + + + + + + + amqp.service.tracing — AMQ Adapter Python 1.0 documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for amqp.service.tracing

+"""
+OpenTelemetry configuration for tracing and metrics.
+
+This module provides functionality to initialize and configure OpenTelemetry
+for both tracing (Jaeger via OTLP) and metrics (Prometheus) in the AMQ adapter.
+"""
+
+import logging
+import os
+from dataclasses import dataclass
+from typing import Any, Dict, Optional
+
+from opentelemetry import metrics, trace
+from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
+from opentelemetry.exporter.prometheus import PrometheusMetricReader
+from opentelemetry.sdk.metrics import MeterProvider
+from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
+from opentelemetry.sdk.resources import SERVICE_NAME, SERVICE_VERSION, Resource
+from opentelemetry.sdk.trace import TracerProvider
+from opentelemetry.sdk.trace.export import BatchSpanProcessor, ConsoleSpanExporter
+from opentelemetry.trace import Tracer
+from prometheus_client import start_http_server
+
+logger = logging.getLogger(__name__)
+
+
+
+[docs] +@dataclass +class TracingConfig: + """Configuration for OpenTelemetry tracing.""" + + service_name: str + service_version: str + otlp_endpoint: str = "http://localhost:4317" # Default Jaeger OTLP endpoint + insecure: bool = True + debug: bool = False + additional_attributes: Optional[Dict[str, Any]] = None
+ + + +
+[docs] +@dataclass +class MetricsConfig: + """Configuration for OpenTelemetry metrics.""" + + prometheus_port: int = 9464 + prometheus_host: str = "localhost" + export_interval_millis: int = 30000 + additional_attributes: Optional[Dict[str, Any]] = None
+ + + +
+[docs] +def create_resource(config: TracingConfig) -> Resource: + """Create an OpenTelemetry resource with service information.""" + attributes = { + SERVICE_NAME: config.service_name, + SERVICE_VERSION: config.service_version, + } + + if config.additional_attributes: + attributes.update(config.additional_attributes) + + return Resource.create(attributes)
+ + + +
+[docs] +def setup_otlp_exporter(config: TracingConfig) -> OTLPSpanExporter: + """Configure and create an OTLP exporter for Jaeger.""" + return OTLPSpanExporter(endpoint=config.otlp_endpoint, insecure=config.insecure)
+ + + +
+[docs] +def setup_trace_provider(config: TracingConfig, resource: Resource) -> TracerProvider: + """Set up the TracerProvider with OTLP exporter.""" + provider = TracerProvider(resource=resource) + + # Add OTLP exporter for Jaeger + otlp_exporter = setup_otlp_exporter(config) + provider.add_span_processor(BatchSpanProcessor(otlp_exporter)) + + # Add console exporter in debug mode + if config.debug: + provider.add_span_processor(BatchSpanProcessor(ConsoleSpanExporter())) + + return provider
+ + + +
+[docs] +def setup_metrics_provider(config: MetricsConfig, resource: Resource) -> MeterProvider: + """Set up the MeterProvider with Prometheus export.""" + try: + # Start Prometheus HTTP server + start_http_server(port=config.prometheus_port, addr=config.prometheus_host) + logger.info( + f"Started Prometheus metrics server on {config.prometheus_host}:{config.prometheus_port}" + ) + + # Create Prometheus reader + prometheus_reader = PrometheusMetricReader() + + # Create periodic reader for custom metrics + periodic_reader = PeriodicExportingMetricReader( + prometheus_reader, export_interval_millis=config.export_interval_millis + ) + + # Create and configure meter provider + return MeterProvider(resource=resource, metric_readers=[prometheus_reader, periodic_reader]) + except Exception as e: + logger.error(f"Failed to set up metrics provider: {str(e)}") + # Return a no-op meter provider + return MeterProvider(resource=resource)
+ + + +
+[docs] +def initialize_telemetry( + tracing_config: Optional[TracingConfig] = None, + metrics_config: Optional[MetricsConfig] = None, +) -> Tracer: + """ + Initialize OpenTelemetry with tracing and metrics. + + Args: + tracing_config: Configuration for tracing + metrics_config: Configuration for metrics + + Returns: + Tracer: Configured OpenTelemetry tracer + + Example: + ```python + config = TracingConfig( + service_name="amq-adapter", + service_version="1.0.0", + otlp_endpoint="http://jaeger:4317", + debug=True + ) + + metrics_config = MetricsConfig( + prometheus_port=9464, + prometheus_host="0.0.0.0" + ) + + tracer = initialize_telemetry(config, metrics_config) + ``` + """ + # Use default configs if none provided + if tracing_config is None: + tracing_config = TracingConfig( + service_name=os.getenv("SERVICE_NAME", "amq-adapter"), + service_version=os.getenv("SERVICE_VERSION", "unknown"), + otlp_endpoint=os.getenv("OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4317"), + insecure=os.getenv("OTEL_INSECURE_MODE", "true").lower() == "true", + debug=os.getenv("OTEL_DEBUG", "false").lower() == "true", + ) + + if metrics_config is None: + metrics_config = MetricsConfig( + prometheus_port=int(os.getenv("PROMETHEUS_PORT", "9464")), + prometheus_host=os.getenv("PROMETHEUS_HOST", "localhost"), + export_interval_millis=int(os.getenv("METRICS_EXPORT_INTERVAL", "30000")), + ) + + try: + # Create resource + resource = create_resource(tracing_config) + + # Set up tracing + provider = setup_trace_provider(tracing_config, resource) + trace.set_tracer_provider(provider) + logger.info(f"Initialized tracing for service {tracing_config.service_name}") + + # Set up metrics + metrics_provider = setup_metrics_provider(metrics_config, resource) + metrics.set_meter_provider(metrics_provider) + logger.info("Initialized metrics export") + + # Return configured tracer + return trace.get_tracer( + tracing_config.service_name, + tracing_config.service_version, + ) + + except Exception as e: + logger.error(f"Failed to initialize telemetry: {str(e)}") + # Return a no-op tracer in case of failure + return trace.get_tracer("no-op")
+ + + +
+[docs] +def create_span(tracer: Tracer, name: str, attributes: Optional[Dict[str, Any]] = None): + """ + Create a new span with the given name and attributes. + + Args: + tracer: The OpenTelemetry tracer + name: Name of the span + attributes: Optional attributes to add to the span + + Returns: + A context manager that creates and manages a span + """ + return tracer.start_as_current_span( + name, + attributes=attributes or {}, + )
+ +
+ +
+
+ +
+
+
+
+ + + + diff --git a/api-doc/_build/html/_modules/index.html b/api-doc/_build/html/_modules/index.html new file mode 100644 index 0000000..bcb76fa --- /dev/null +++ b/api-doc/_build/html/_modules/index.html @@ -0,0 +1,134 @@ + + + + + + + + Overview: module code — AMQ Adapter Python 1.0 documentation + + + + + + + + + + + + + + + +
+ + +
+ + +
+
+ + + + diff --git a/api-doc/_build/html/_sources/index.rst.txt b/api-doc/_build/html/_sources/index.rst.txt new file mode 100644 index 0000000..4fc59f2 --- /dev/null +++ b/api-doc/_build/html/_sources/index.rst.txt @@ -0,0 +1,23 @@ +AMQ Adapter Python API Documentation +==================================== + +Welcome to the AMQ Adapter Python API documentation. This library provides a Python implementation of the AMQ adapter for routing traffic over RabbitMQ to intended services with automated service discovery. + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + modules/amqp + modules/amqp.config + modules/amqp.adapter + modules/amqp.service + modules/amqp.router + modules/amqp.model + modules/amqp.rabbitmq + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/api-doc/_build/html/_sources/modules/amqp.adapter.rst.txt b/api-doc/_build/html/_sources/modules/amqp.adapter.rst.txt new file mode 100644 index 0000000..d7802b5 --- /dev/null +++ b/api-doc/_build/html/_sources/modules/amqp.adapter.rst.txt @@ -0,0 +1,77 @@ +Adapter Module +============= + +.. automodule:: amqp.adapter + :members: + :undoc-members: + :show-inheritance: + +.. automodule:: amqp.adapter.cleverthis_service_adapter + :members: + :undoc-members: + :show-inheritance: + +.. automodule:: amqp.adapter.amq_route_factory + :members: + :undoc-members: + :show-inheritance: + +.. automodule:: amqp.adapter.backpressure_handler + :members: + :undoc-members: + :show-inheritance: + +.. automodule:: amqp.adapter.consul_global_id_generator + :members: + :undoc-members: + :show-inheritance: + +.. automodule:: amqp.adapter.data_message_factory + :members: + :undoc-members: + :show-inheritance: + +.. automodule:: amqp.adapter.data_parser + :members: + :undoc-members: + :show-inheritance: + +.. automodule:: amqp.adapter.data_response_factory + :members: + :undoc-members: + :show-inheritance: + +.. automodule:: amqp.adapter.file_handler + :members: + :undoc-members: + :show-inheritance: + +.. automodule:: amqp.adapter.logging_utils + :members: + :undoc-members: + :show-inheritance: + +.. automodule:: amqp.adapter.pydantic_serializer + :members: + :undoc-members: + :show-inheritance: + +.. automodule:: amqp.adapter.serializer + :members: + :undoc-members: + :show-inheritance: + +.. automodule:: amqp.adapter.service_message_factory + :members: + :undoc-members: + :show-inheritance: + +.. automodule:: amqp.adapter.trace_info_adapter + :members: + :undoc-members: + :show-inheritance: + +.. automodule:: amqp.adapter.type_descriptor + :members: + :undoc-members: + :show-inheritance: diff --git a/api-doc/_build/html/_sources/modules/amqp.config.rst.txt b/api-doc/_build/html/_sources/modules/amqp.config.rst.txt new file mode 100644 index 0000000..986f01b --- /dev/null +++ b/api-doc/_build/html/_sources/modules/amqp.config.rst.txt @@ -0,0 +1,12 @@ +Configuration Module +=================== + +.. automodule:: amqp.config + :members: + :undoc-members: + :show-inheritance: + +.. automodule:: amqp.config.amq_configuration + :members: + :undoc-members: + :show-inheritance: diff --git a/api-doc/_build/html/_sources/modules/amqp.model.rst.txt b/api-doc/_build/html/_sources/modules/amqp.model.rst.txt new file mode 100644 index 0000000..93a83a0 --- /dev/null +++ b/api-doc/_build/html/_sources/modules/amqp.model.rst.txt @@ -0,0 +1,22 @@ +Model Module +=========== + +.. automodule:: amqp.model + :members: + :undoc-members: + :show-inheritance: + +.. automodule:: amqp.model.model + :members: + :undoc-members: + :show-inheritance: + +.. automodule:: amqp.model.service_message_type + :members: + :undoc-members: + :show-inheritance: + +.. automodule:: amqp.model.snowflake_id + :members: + :undoc-members: + :show-inheritance: diff --git a/api-doc/_build/html/_sources/modules/amqp.rabbitmq.rst.txt b/api-doc/_build/html/_sources/modules/amqp.rabbitmq.rst.txt new file mode 100644 index 0000000..a7acf76 --- /dev/null +++ b/api-doc/_build/html/_sources/modules/amqp.rabbitmq.rst.txt @@ -0,0 +1,17 @@ +RabbitMQ Module +============== + +.. automodule:: amqp.rabbitmq + :members: + :undoc-members: + :show-inheritance: + +.. automodule:: amqp.rabbitmq.rabbit_mq_client + :members: + :undoc-members: + :show-inheritance: + +.. automodule:: amqp.rabbitmq.user_management_service_client + :members: + :undoc-members: + :show-inheritance: diff --git a/api-doc/_build/html/_sources/modules/amqp.router.rst.txt b/api-doc/_build/html/_sources/modules/amqp.router.rst.txt new file mode 100644 index 0000000..d107b4a --- /dev/null +++ b/api-doc/_build/html/_sources/modules/amqp.router.rst.txt @@ -0,0 +1,32 @@ +Router Module +============ + +.. automodule:: amqp.router + :members: + :undoc-members: + :show-inheritance: + +.. automodule:: amqp.router.router_base + :members: + :undoc-members: + :show-inheritance: + +.. automodule:: amqp.router.router_consumer + :members: + :undoc-members: + :show-inheritance: + +.. automodule:: amqp.router.router_producer + :members: + :undoc-members: + :show-inheritance: + +.. automodule:: amqp.router.route_database + :members: + :undoc-members: + :show-inheritance: + +.. automodule:: amqp.router.utils + :members: + :undoc-members: + :show-inheritance: diff --git a/api-doc/_build/html/_sources/modules/amqp.rst.txt b/api-doc/_build/html/_sources/modules/amqp.rst.txt new file mode 100644 index 0000000..df1b2b0 --- /dev/null +++ b/api-doc/_build/html/_sources/modules/amqp.rst.txt @@ -0,0 +1,7 @@ +AMQP Package +=========== + +.. automodule:: amqp + :members: + :undoc-members: + :show-inheritance: diff --git a/api-doc/_build/html/_sources/modules/amqp.service.rst.txt b/api-doc/_build/html/_sources/modules/amqp.service.rst.txt new file mode 100644 index 0000000..a81f753 --- /dev/null +++ b/api-doc/_build/html/_sources/modules/amqp.service.rst.txt @@ -0,0 +1,22 @@ +Service Module +============= + +.. automodule:: amqp.service + :members: + :undoc-members: + :show-inheritance: + +.. automodule:: amqp.service.amq_service + :members: + :undoc-members: + :show-inheritance: + +.. automodule:: amqp.service.amq_message_handler + :members: + :undoc-members: + :show-inheritance: + +.. automodule:: amqp.service.tracing + :members: + :undoc-members: + :show-inheritance: diff --git a/api-doc/_build/html/_static/_sphinx_javascript_frameworks_compat.js b/api-doc/_build/html/_static/_sphinx_javascript_frameworks_compat.js new file mode 100644 index 0000000..8141580 --- /dev/null +++ b/api-doc/_build/html/_static/_sphinx_javascript_frameworks_compat.js @@ -0,0 +1,123 @@ +/* Compatability shim for jQuery and underscores.js. + * + * Copyright Sphinx contributors + * Released under the two clause BSD licence + */ + +/** + * small helper function to urldecode strings + * + * See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/decodeURIComponent#Decoding_query_parameters_from_a_URL + */ +jQuery.urldecode = function(x) { + if (!x) { + return x + } + return decodeURIComponent(x.replace(/\+/g, ' ')); +}; + +/** + * small helper function to urlencode strings + */ +jQuery.urlencode = encodeURIComponent; + +/** + * This function returns the parsed url parameters of the + * current request. Multiple values per key are supported, + * it will always return arrays of strings for the value parts. + */ +jQuery.getQueryParameters = function(s) { + if (typeof s === 'undefined') + s = document.location.search; + var parts = s.substr(s.indexOf('?') + 1).split('&'); + var result = {}; + for (var i = 0; i < parts.length; i++) { + var tmp = parts[i].split('=', 2); + var key = jQuery.urldecode(tmp[0]); + var value = jQuery.urldecode(tmp[1]); + if (key in result) + result[key].push(value); + else + result[key] = [value]; + } + return result; +}; + +/** + * highlight a given string on a jquery object by wrapping it in + * span elements with the given class name. + */ +jQuery.fn.highlightText = function(text, className) { + function highlight(node, addItems) { + if (node.nodeType === 3) { + var val = node.nodeValue; + var pos = val.toLowerCase().indexOf(text); + if (pos >= 0 && + !jQuery(node.parentNode).hasClass(className) && + !jQuery(node.parentNode).hasClass("nohighlight")) { + var span; + var isInSVG = jQuery(node).closest("body, svg, foreignObject").is("svg"); + if (isInSVG) { + span = document.createElementNS("http://www.w3.org/2000/svg", "tspan"); + } else { + span = document.createElement("span"); + span.className = className; + } + span.appendChild(document.createTextNode(val.substr(pos, text.length))); + node.parentNode.insertBefore(span, node.parentNode.insertBefore( + document.createTextNode(val.substr(pos + text.length)), + node.nextSibling)); + node.nodeValue = val.substr(0, pos); + if (isInSVG) { + var rect = document.createElementNS("http://www.w3.org/2000/svg", "rect"); + var bbox = node.parentElement.getBBox(); + rect.x.baseVal.value = bbox.x; + rect.y.baseVal.value = bbox.y; + rect.width.baseVal.value = bbox.width; + rect.height.baseVal.value = bbox.height; + rect.setAttribute('class', className); + addItems.push({ + "parent": node.parentNode, + "target": rect}); + } + } + } + else if (!jQuery(node).is("button, select, textarea")) { + jQuery.each(node.childNodes, function() { + highlight(this, addItems); + }); + } + } + var addItems = []; + var result = this.each(function() { + highlight(this, addItems); + }); + for (var i = 0; i < addItems.length; ++i) { + jQuery(addItems[i].parent).before(addItems[i].target); + } + return result; +}; + +/* + * backward compatibility for jQuery.browser + * This will be supported until firefox bug is fixed. + */ +if (!jQuery.browser) { + jQuery.uaMatch = function(ua) { + ua = ua.toLowerCase(); + + var match = /(chrome)[ \/]([\w.]+)/.exec(ua) || + /(webkit)[ \/]([\w.]+)/.exec(ua) || + /(opera)(?:.*version|)[ \/]([\w.]+)/.exec(ua) || + /(msie) ([\w.]+)/.exec(ua) || + ua.indexOf("compatible") < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec(ua) || + []; + + return { + browser: match[ 1 ] || "", + version: match[ 2 ] || "0" + }; + }; + jQuery.browser = {}; + jQuery.browser[jQuery.uaMatch(navigator.userAgent).browser] = true; +} diff --git a/api-doc/_build/html/_static/basic.css b/api-doc/_build/html/_static/basic.css new file mode 100644 index 0000000..9c0c9b8 --- /dev/null +++ b/api-doc/_build/html/_static/basic.css @@ -0,0 +1,906 @@ +/* + * Sphinx stylesheet -- basic theme. + */ + +/* -- main layout ----------------------------------------------------------- */ + +div.clearer { + clear: both; +} + +div.section::after { + display: block; + content: ''; + clear: left; +} + +/* -- relbar ---------------------------------------------------------------- */ + +div.related { + width: 100%; + font-size: 90%; +} + +div.related h3 { + display: none; +} + +div.related ul { + margin: 0; + padding: 0 0 0 10px; + list-style: none; +} + +div.related li { + display: inline; +} + +div.related li.right { + float: right; + margin-right: 5px; +} + +/* -- sidebar --------------------------------------------------------------- */ + +div.sphinxsidebarwrapper { + padding: 10px 5px 0 10px; +} + +div.sphinxsidebar { + float: left; + width: 230px; + margin-left: -100%; + font-size: 90%; + word-wrap: break-word; + overflow-wrap : break-word; +} + +div.sphinxsidebar ul { + list-style: none; +} + +div.sphinxsidebar ul ul, +div.sphinxsidebar ul.want-points { + margin-left: 20px; + list-style: square; +} + +div.sphinxsidebar ul ul { + margin-top: 0; + margin-bottom: 0; +} + +div.sphinxsidebar form { + margin-top: 10px; +} + +div.sphinxsidebar input { + border: 1px solid #98dbcc; + font-family: sans-serif; + font-size: 1em; +} + +div.sphinxsidebar #searchbox form.search { + overflow: hidden; +} + +div.sphinxsidebar #searchbox input[type="text"] { + float: left; + width: 80%; + padding: 0.25em; + box-sizing: border-box; +} + +div.sphinxsidebar #searchbox input[type="submit"] { + float: left; + width: 20%; + border-left: none; + padding: 0.25em; + box-sizing: border-box; +} + + +img { + border: 0; + max-width: 100%; +} + +/* -- search page ----------------------------------------------------------- */ + +ul.search { + margin-top: 10px; +} + +ul.search li { + padding: 5px 0; +} + +ul.search li a { + font-weight: bold; +} + +ul.search li p.context { + color: #888; + margin: 2px 0 0 30px; + text-align: left; +} + +ul.keywordmatches li.goodmatch a { + font-weight: bold; +} + +/* -- index page ------------------------------------------------------------ */ + +table.contentstable { + width: 90%; + margin-left: auto; + margin-right: auto; +} + +table.contentstable p.biglink { + line-height: 150%; +} + +a.biglink { + font-size: 1.3em; +} + +span.linkdescr { + font-style: italic; + padding-top: 5px; + font-size: 90%; +} + +/* -- general index --------------------------------------------------------- */ + +table.indextable { + width: 100%; +} + +table.indextable td { + text-align: left; + vertical-align: top; +} + +table.indextable ul { + margin-top: 0; + margin-bottom: 0; + list-style-type: none; +} + +table.indextable > tbody > tr > td > ul { + padding-left: 0em; +} + +table.indextable tr.pcap { + height: 10px; +} + +table.indextable tr.cap { + margin-top: 10px; + background-color: #f2f2f2; +} + +img.toggler { + margin-right: 3px; + margin-top: 3px; + cursor: pointer; +} + +div.modindex-jumpbox { + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + margin: 1em 0 1em 0; + padding: 0.4em; +} + +div.genindex-jumpbox { + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + margin: 1em 0 1em 0; + padding: 0.4em; +} + +/* -- domain module index --------------------------------------------------- */ + +table.modindextable td { + padding: 2px; + border-collapse: collapse; +} + +/* -- general body styles --------------------------------------------------- */ + +div.body { + min-width: 360px; + max-width: 800px; +} + +div.body p, div.body dd, div.body li, div.body blockquote { + -moz-hyphens: auto; + -ms-hyphens: auto; + -webkit-hyphens: auto; + hyphens: auto; +} + +a.headerlink { + visibility: hidden; +} + +a:visited { + color: #551A8B; +} + +h1:hover > a.headerlink, +h2:hover > a.headerlink, +h3:hover > a.headerlink, +h4:hover > a.headerlink, +h5:hover > a.headerlink, +h6:hover > a.headerlink, +dt:hover > a.headerlink, +caption:hover > a.headerlink, +p.caption:hover > a.headerlink, +div.code-block-caption:hover > a.headerlink { + visibility: visible; +} + +div.body p.caption { + text-align: inherit; +} + +div.body td { + text-align: left; +} + +.first { + margin-top: 0 !important; +} + +p.rubric { + margin-top: 30px; + font-weight: bold; +} + +img.align-left, figure.align-left, .figure.align-left, object.align-left { + clear: left; + float: left; + margin-right: 1em; +} + +img.align-right, figure.align-right, .figure.align-right, object.align-right { + clear: right; + float: right; + margin-left: 1em; +} + +img.align-center, figure.align-center, .figure.align-center, object.align-center { + display: block; + margin-left: auto; + margin-right: auto; +} + +img.align-default, figure.align-default, .figure.align-default { + display: block; + margin-left: auto; + margin-right: auto; +} + +.align-left { + text-align: left; +} + +.align-center { + text-align: center; +} + +.align-default { + text-align: center; +} + +.align-right { + text-align: right; +} + +/* -- sidebars -------------------------------------------------------------- */ + +div.sidebar, +aside.sidebar { + margin: 0 0 0.5em 1em; + border: 1px solid #ddb; + padding: 7px; + background-color: #ffe; + width: 40%; + float: right; + clear: right; + overflow-x: auto; +} + +p.sidebar-title { + font-weight: bold; +} + +nav.contents, +aside.topic, +div.admonition, div.topic, blockquote { + clear: left; +} + +/* -- topics ---------------------------------------------------------------- */ + +nav.contents, +aside.topic, +div.topic { + border: 1px solid #ccc; + padding: 7px; + margin: 10px 0 10px 0; +} + +p.topic-title { + font-size: 1.1em; + font-weight: bold; + margin-top: 10px; +} + +/* -- admonitions ----------------------------------------------------------- */ + +div.admonition { + margin-top: 10px; + margin-bottom: 10px; + padding: 7px; +} + +div.admonition dt { + font-weight: bold; +} + +p.admonition-title { + margin: 0px 10px 5px 0px; + font-weight: bold; +} + +div.body p.centered { + text-align: center; + margin-top: 25px; +} + +/* -- content of sidebars/topics/admonitions -------------------------------- */ + +div.sidebar > :last-child, +aside.sidebar > :last-child, +nav.contents > :last-child, +aside.topic > :last-child, +div.topic > :last-child, +div.admonition > :last-child { + margin-bottom: 0; +} + +div.sidebar::after, +aside.sidebar::after, +nav.contents::after, +aside.topic::after, +div.topic::after, +div.admonition::after, +blockquote::after { + display: block; + content: ''; + clear: both; +} + +/* -- tables ---------------------------------------------------------------- */ + +table.docutils { + margin-top: 10px; + margin-bottom: 10px; + border: 0; + border-collapse: collapse; +} + +table.align-center { + margin-left: auto; + margin-right: auto; +} + +table.align-default { + margin-left: auto; + margin-right: auto; +} + +table caption span.caption-number { + font-style: italic; +} + +table caption span.caption-text { +} + +table.docutils td, table.docutils th { + padding: 1px 8px 1px 5px; + border-top: 0; + border-left: 0; + border-right: 0; + border-bottom: 1px solid #aaa; +} + +th { + text-align: left; + padding-right: 5px; +} + +table.citation { + border-left: solid 1px gray; + margin-left: 1px; +} + +table.citation td { + border-bottom: none; +} + +th > :first-child, +td > :first-child { + margin-top: 0px; +} + +th > :last-child, +td > :last-child { + margin-bottom: 0px; +} + +/* -- figures --------------------------------------------------------------- */ + +div.figure, figure { + margin: 0.5em; + padding: 0.5em; +} + +div.figure p.caption, figcaption { + padding: 0.3em; +} + +div.figure p.caption span.caption-number, +figcaption span.caption-number { + font-style: italic; +} + +div.figure p.caption span.caption-text, +figcaption span.caption-text { +} + +/* -- field list styles ----------------------------------------------------- */ + +table.field-list td, table.field-list th { + border: 0 !important; +} + +.field-list ul { + margin: 0; + padding-left: 1em; +} + +.field-list p { + margin: 0; +} + +.field-name { + -moz-hyphens: manual; + -ms-hyphens: manual; + -webkit-hyphens: manual; + hyphens: manual; +} + +/* -- hlist styles ---------------------------------------------------------- */ + +table.hlist { + margin: 1em 0; +} + +table.hlist td { + vertical-align: top; +} + +/* -- object description styles --------------------------------------------- */ + +.sig { + font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace; +} + +.sig-name, code.descname { + background-color: transparent; + font-weight: bold; +} + +.sig-name { + font-size: 1.1em; +} + +code.descname { + font-size: 1.2em; +} + +.sig-prename, code.descclassname { + background-color: transparent; +} + +.optional { + font-size: 1.3em; +} + +.sig-paren { + font-size: larger; +} + +.sig-param.n { + font-style: italic; +} + +/* C++ specific styling */ + +.sig-inline.c-texpr, +.sig-inline.cpp-texpr { + font-family: unset; +} + +.sig.c .k, .sig.c .kt, +.sig.cpp .k, .sig.cpp .kt { + color: #0033B3; +} + +.sig.c .m, +.sig.cpp .m { + color: #1750EB; +} + +.sig.c .s, .sig.c .sc, +.sig.cpp .s, .sig.cpp .sc { + color: #067D17; +} + + +/* -- other body styles ----------------------------------------------------- */ + +ol.arabic { + list-style: decimal; +} + +ol.loweralpha { + list-style: lower-alpha; +} + +ol.upperalpha { + list-style: upper-alpha; +} + +ol.lowerroman { + list-style: lower-roman; +} + +ol.upperroman { + list-style: upper-roman; +} + +:not(li) > ol > li:first-child > :first-child, +:not(li) > ul > li:first-child > :first-child { + margin-top: 0px; +} + +:not(li) > ol > li:last-child > :last-child, +:not(li) > ul > li:last-child > :last-child { + margin-bottom: 0px; +} + +ol.simple ol p, +ol.simple ul p, +ul.simple ol p, +ul.simple ul p { + margin-top: 0; +} + +ol.simple > li:not(:first-child) > p, +ul.simple > li:not(:first-child) > p { + margin-top: 0; +} + +ol.simple p, +ul.simple p { + margin-bottom: 0; +} + +aside.footnote > span, +div.citation > span { + float: left; +} +aside.footnote > span:last-of-type, +div.citation > span:last-of-type { + padding-right: 0.5em; +} +aside.footnote > p { + margin-left: 2em; +} +div.citation > p { + margin-left: 4em; +} +aside.footnote > p:last-of-type, +div.citation > p:last-of-type { + margin-bottom: 0em; +} +aside.footnote > p:last-of-type:after, +div.citation > p:last-of-type:after { + content: ""; + clear: both; +} + +dl.field-list { + display: grid; + grid-template-columns: fit-content(30%) auto; +} + +dl.field-list > dt { + font-weight: bold; + word-break: break-word; + padding-left: 0.5em; + padding-right: 5px; +} + +dl.field-list > dd { + padding-left: 0.5em; + margin-top: 0em; + margin-left: 0em; + margin-bottom: 0em; +} + +dl { + margin-bottom: 15px; +} + +dd > :first-child { + margin-top: 0px; +} + +dd ul, dd table { + margin-bottom: 10px; +} + +dd { + margin-top: 3px; + margin-bottom: 10px; + margin-left: 30px; +} + +.sig dd { + margin-top: 0px; + margin-bottom: 0px; +} + +.sig dl { + margin-top: 0px; + margin-bottom: 0px; +} + +dl > dd:last-child, +dl > dd:last-child > :last-child { + margin-bottom: 0; +} + +dt:target, span.highlighted { + background-color: #fbe54e; +} + +rect.highlighted { + fill: #fbe54e; +} + +dl.glossary dt { + font-weight: bold; + font-size: 1.1em; +} + +.versionmodified { + font-style: italic; +} + +.system-message { + background-color: #fda; + padding: 5px; + border: 3px solid red; +} + +.footnote:target { + background-color: #ffa; +} + +.line-block { + display: block; + margin-top: 1em; + margin-bottom: 1em; +} + +.line-block .line-block { + margin-top: 0; + margin-bottom: 0; + margin-left: 1.5em; +} + +.guilabel, .menuselection { + font-family: sans-serif; +} + +.accelerator { + text-decoration: underline; +} + +.classifier { + font-style: oblique; +} + +.classifier:before { + font-style: normal; + margin: 0 0.5em; + content: ":"; + display: inline-block; +} + +abbr, acronym { + border-bottom: dotted 1px; + cursor: help; +} + +/* -- code displays --------------------------------------------------------- */ + +pre { + overflow: auto; + overflow-y: hidden; /* fixes display issues on Chrome browsers */ +} + +pre, div[class*="highlight-"] { + clear: both; +} + +span.pre { + -moz-hyphens: none; + -ms-hyphens: none; + -webkit-hyphens: none; + hyphens: none; + white-space: nowrap; +} + +div[class*="highlight-"] { + margin: 1em 0; +} + +td.linenos pre { + border: 0; + background-color: transparent; + color: #aaa; +} + +table.highlighttable { + display: block; +} + +table.highlighttable tbody { + display: block; +} + +table.highlighttable tr { + display: flex; +} + +table.highlighttable td { + margin: 0; + padding: 0; +} + +table.highlighttable td.linenos { + padding-right: 0.5em; +} + +table.highlighttable td.code { + flex: 1; + overflow: hidden; +} + +.highlight .hll { + display: block; +} + +div.highlight pre, +table.highlighttable pre { + margin: 0; +} + +div.code-block-caption + div { + margin-top: 0; +} + +div.code-block-caption { + margin-top: 1em; + padding: 2px 5px; + font-size: small; +} + +div.code-block-caption code { + background-color: transparent; +} + +table.highlighttable td.linenos, +span.linenos, +div.highlight span.gp { /* gp: Generic.Prompt */ + user-select: none; + -webkit-user-select: text; /* Safari fallback only */ + -webkit-user-select: none; /* Chrome/Safari */ + -moz-user-select: none; /* Firefox */ + -ms-user-select: none; /* IE10+ */ +} + +div.code-block-caption span.caption-number { + padding: 0.1em 0.3em; + font-style: italic; +} + +div.code-block-caption span.caption-text { +} + +div.literal-block-wrapper { + margin: 1em 0; +} + +code.xref, a code { + background-color: transparent; + font-weight: bold; +} + +h1 code, h2 code, h3 code, h4 code, h5 code, h6 code { + background-color: transparent; +} + +.viewcode-link { + float: right; +} + +.viewcode-back { + float: right; + font-family: sans-serif; +} + +div.viewcode-block:target { + margin: -1px -10px; + padding: 0 10px; +} + +/* -- math display ---------------------------------------------------------- */ + +img.math { + vertical-align: middle; +} + +div.body div.math p { + text-align: center; +} + +span.eqno { + float: right; +} + +span.eqno a.headerlink { + position: absolute; + z-index: 1; +} + +div.math:hover a.headerlink { + visibility: visible; +} + +/* -- printout stylesheet --------------------------------------------------- */ + +@media print { + div.document, + div.documentwrapper, + div.bodywrapper { + margin: 0 !important; + width: 100%; + } + + div.sphinxsidebar, + div.related, + div.footer, + #top-link { + display: none; + } +} diff --git a/api-doc/_build/html/_static/css/badge_only.css b/api-doc/_build/html/_static/css/badge_only.css new file mode 100644 index 0000000..eb61d1e --- /dev/null +++ b/api-doc/_build/html/_static/css/badge_only.css @@ -0,0 +1 @@ +.clearfix{*zoom:1}.clearfix:after,.clearfix:before{display:table;content:""}.clearfix:after{clear:both}@font-face{font-family:FontAwesome;font-style:normal;font-weight:400;src:url(fonts/fontawesome-webfont.eot?674f50d287a8c48dc19ba404d20fe713?#iefix) format("embedded-opentype"),url(fonts/fontawesome-webfont.woff2?af7ae505a9eed503f8b8e6982036873e) format("woff2"),url(fonts/fontawesome-webfont.woff?fee66e712a8a08eef5805a46892932ad) format("woff"),url(fonts/fontawesome-webfont.ttf?b06871f281fee6b241d60582ae9369b9) format("truetype"),url(fonts/fontawesome-webfont.svg?912ec66d7572ff821749319396470bde#FontAwesome) format("svg")}.fa:before{font-family:FontAwesome;font-style:normal;font-weight:400;line-height:1}.fa:before,a .fa{text-decoration:inherit}.fa:before,a .fa,li .fa{display:inline-block}li .fa-large:before{width:1.875em}ul.fas{list-style-type:none;margin-left:2em;text-indent:-.8em}ul.fas li .fa{width:.8em}ul.fas li .fa-large:before{vertical-align:baseline}.fa-book:before,.icon-book:before{content:"\f02d"}.fa-caret-down:before,.icon-caret-down:before{content:"\f0d7"}.fa-caret-up:before,.icon-caret-up:before{content:"\f0d8"}.fa-caret-left:before,.icon-caret-left:before{content:"\f0d9"}.fa-caret-right:before,.icon-caret-right:before{content:"\f0da"}.rst-versions{position:fixed;bottom:0;left:0;width:300px;color:#fcfcfc;background:#1f1d1d;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;z-index:400}.rst-versions a{color:#2980b9;text-decoration:none}.rst-versions .rst-badge-small{display:none}.rst-versions .rst-current-version{padding:12px;background-color:#272525;display:block;text-align:right;font-size:90%;cursor:pointer;color:#27ae60}.rst-versions .rst-current-version:after{clear:both;content:"";display:block}.rst-versions .rst-current-version .fa{color:#fcfcfc}.rst-versions .rst-current-version .fa-book,.rst-versions .rst-current-version .icon-book{float:left}.rst-versions .rst-current-version.rst-out-of-date{background-color:#e74c3c;color:#fff}.rst-versions .rst-current-version.rst-active-old-version{background-color:#f1c40f;color:#000}.rst-versions.shift-up{height:auto;max-height:100%;overflow-y:scroll}.rst-versions.shift-up .rst-other-versions{display:block}.rst-versions .rst-other-versions{font-size:90%;padding:12px;color:grey;display:none}.rst-versions .rst-other-versions hr{display:block;height:1px;border:0;margin:20px 0;padding:0;border-top:1px solid #413d3d}.rst-versions .rst-other-versions dd{display:inline-block;margin:0}.rst-versions .rst-other-versions dd a{display:inline-block;padding:6px;color:#fcfcfc}.rst-versions .rst-other-versions .rtd-current-item{font-weight:700}.rst-versions.rst-badge{width:auto;bottom:20px;right:20px;left:auto;border:none;max-width:300px;max-height:90%}.rst-versions.rst-badge .fa-book,.rst-versions.rst-badge .icon-book{float:none;line-height:30px}.rst-versions.rst-badge.shift-up .rst-current-version{text-align:right}.rst-versions.rst-badge.shift-up .rst-current-version .fa-book,.rst-versions.rst-badge.shift-up .rst-current-version .icon-book{float:left}.rst-versions.rst-badge>.rst-current-version{width:auto;height:30px;line-height:30px;padding:0 6px;display:block;text-align:center}@media screen and (max-width:768px){.rst-versions{width:85%;display:none}.rst-versions.shift{display:block}}#flyout-search-form{padding:6px} diff --git a/api-doc/_build/html/_static/css/fonts/Roboto-Slab-Bold.woff b/api-doc/_build/html/_static/css/fonts/Roboto-Slab-Bold.woff new file mode 100644 index 0000000..6cb6000 Binary files /dev/null and b/api-doc/_build/html/_static/css/fonts/Roboto-Slab-Bold.woff differ diff --git a/api-doc/_build/html/_static/css/fonts/Roboto-Slab-Bold.woff2 b/api-doc/_build/html/_static/css/fonts/Roboto-Slab-Bold.woff2 new file mode 100644 index 0000000..4e12420 Binary files /dev/null and b/api-doc/_build/html/_static/css/fonts/Roboto-Slab-Bold.woff2 differ diff --git a/api-doc/_build/html/_static/css/fonts/Roboto-Slab-Regular.woff b/api-doc/_build/html/_static/css/fonts/Roboto-Slab-Regular.woff new file mode 100644 index 0000000..125af8f Binary files /dev/null and b/api-doc/_build/html/_static/css/fonts/Roboto-Slab-Regular.woff differ diff --git a/api-doc/_build/html/_static/css/fonts/Roboto-Slab-Regular.woff2 b/api-doc/_build/html/_static/css/fonts/Roboto-Slab-Regular.woff2 new file mode 100644 index 0000000..f2c76e5 Binary files /dev/null and b/api-doc/_build/html/_static/css/fonts/Roboto-Slab-Regular.woff2 differ diff --git a/api-doc/_build/html/_static/css/fonts/fontawesome-webfont.eot b/api-doc/_build/html/_static/css/fonts/fontawesome-webfont.eot new file mode 100644 index 0000000..7c8959e Binary files /dev/null and b/api-doc/_build/html/_static/css/fonts/fontawesome-webfont.eot differ diff --git a/api-doc/_build/html/_static/css/fonts/fontawesome-webfont.svg b/api-doc/_build/html/_static/css/fonts/fontawesome-webfont.svg new file mode 100644 index 0000000..52c0773 --- /dev/null +++ b/api-doc/_build/html/_static/css/fonts/fontawesome-webfont.svg @@ -0,0 +1,2671 @@ + + + + +Created by FontForge 20120731 at Mon Oct 24 17:37:40 2016 + By ,,, +Copyright Dave Gandy 2016. All rights reserved. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/api-doc/_build/html/_static/css/fonts/fontawesome-webfont.ttf b/api-doc/_build/html/_static/css/fonts/fontawesome-webfont.ttf new file mode 100644 index 0000000..db4b3b1 Binary files /dev/null and b/api-doc/_build/html/_static/css/fonts/fontawesome-webfont.ttf differ diff --git a/api-doc/_build/html/_static/css/fonts/fontawesome-webfont.woff b/api-doc/_build/html/_static/css/fonts/fontawesome-webfont.woff new file mode 100644 index 0000000..e3a086c Binary files /dev/null and b/api-doc/_build/html/_static/css/fonts/fontawesome-webfont.woff differ diff --git a/api-doc/_build/html/_static/css/fonts/fontawesome-webfont.woff2 b/api-doc/_build/html/_static/css/fonts/fontawesome-webfont.woff2 new file mode 100644 index 0000000..693064c Binary files /dev/null and b/api-doc/_build/html/_static/css/fonts/fontawesome-webfont.woff2 differ diff --git a/api-doc/_build/html/_static/css/fonts/lato-bold-italic.woff b/api-doc/_build/html/_static/css/fonts/lato-bold-italic.woff new file mode 100644 index 0000000..dd2d870 Binary files /dev/null and b/api-doc/_build/html/_static/css/fonts/lato-bold-italic.woff differ diff --git a/api-doc/_build/html/_static/css/fonts/lato-bold-italic.woff2 b/api-doc/_build/html/_static/css/fonts/lato-bold-italic.woff2 new file mode 100644 index 0000000..8ae44fa Binary files /dev/null and b/api-doc/_build/html/_static/css/fonts/lato-bold-italic.woff2 differ diff --git a/api-doc/_build/html/_static/css/fonts/lato-bold.woff b/api-doc/_build/html/_static/css/fonts/lato-bold.woff new file mode 100644 index 0000000..07bc142 Binary files /dev/null and b/api-doc/_build/html/_static/css/fonts/lato-bold.woff differ diff --git a/api-doc/_build/html/_static/css/fonts/lato-bold.woff2 b/api-doc/_build/html/_static/css/fonts/lato-bold.woff2 new file mode 100644 index 0000000..94eb5c1 Binary files /dev/null and b/api-doc/_build/html/_static/css/fonts/lato-bold.woff2 differ diff --git a/api-doc/_build/html/_static/css/fonts/lato-normal-italic.woff b/api-doc/_build/html/_static/css/fonts/lato-normal-italic.woff new file mode 100644 index 0000000..439cd13 Binary files /dev/null and b/api-doc/_build/html/_static/css/fonts/lato-normal-italic.woff differ diff --git a/api-doc/_build/html/_static/css/fonts/lato-normal-italic.woff2 b/api-doc/_build/html/_static/css/fonts/lato-normal-italic.woff2 new file mode 100644 index 0000000..6285a12 Binary files /dev/null and b/api-doc/_build/html/_static/css/fonts/lato-normal-italic.woff2 differ diff --git a/api-doc/_build/html/_static/css/fonts/lato-normal.woff b/api-doc/_build/html/_static/css/fonts/lato-normal.woff new file mode 100644 index 0000000..9519242 Binary files /dev/null and b/api-doc/_build/html/_static/css/fonts/lato-normal.woff differ diff --git a/api-doc/_build/html/_static/css/fonts/lato-normal.woff2 b/api-doc/_build/html/_static/css/fonts/lato-normal.woff2 new file mode 100644 index 0000000..cd49ad9 Binary files /dev/null and b/api-doc/_build/html/_static/css/fonts/lato-normal.woff2 differ diff --git a/api-doc/_build/html/_static/css/theme.css b/api-doc/_build/html/_static/css/theme.css new file mode 100644 index 0000000..aeaf63d --- /dev/null +++ b/api-doc/_build/html/_static/css/theme.css @@ -0,0 +1,4 @@ +html{box-sizing:border-box}*,:after,:before{box-sizing:inherit}article,aside,details,figcaption,figure,footer,header,hgroup,nav,section{display:block}audio,canvas,video{display:inline-block;*display:inline;*zoom:1}[hidden],audio:not([controls]){display:none}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:100%;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}blockquote{margin:0}dfn{font-style:italic}ins{background:#ff9;text-decoration:none}ins,mark{color:#000}mark{background:#ff0;font-style:italic;font-weight:700}.rst-content code,.rst-content tt,code,kbd,pre,samp{font-family:monospace,serif;_font-family:courier new,monospace;font-size:1em}pre{white-space:pre}q{quotes:none}q:after,q:before{content:"";content:none}small{font-size:85%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}dl,ol,ul{margin:0;padding:0;list-style:none;list-style-image:none}li{list-style:none}dd{margin:0}img{border:0;-ms-interpolation-mode:bicubic;vertical-align:middle;max-width:100%}svg:not(:root){overflow:hidden}figure,form{margin:0}label{cursor:pointer}button,input,select,textarea{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle}button,input{line-height:normal}button,input[type=button],input[type=reset],input[type=submit]{cursor:pointer;-webkit-appearance:button;*overflow:visible}button[disabled],input[disabled]{cursor:default}input[type=search]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}textarea{resize:vertical}table{border-collapse:collapse;border-spacing:0}td{vertical-align:top}.chromeframe{margin:.2em 0;background:#ccc;color:#000;padding:.2em 0}.ir{display:block;border:0;text-indent:-999em;overflow:hidden;background-color:transparent;background-repeat:no-repeat;text-align:left;direction:ltr;*line-height:0}.ir br{display:none}.hidden{display:none!important;visibility:hidden}.visuallyhidden{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.visuallyhidden.focusable:active,.visuallyhidden.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.invisible{visibility:hidden}.relative{position:relative}big,small{font-size:100%}@media print{body,html,section{background:none!important}*{box-shadow:none!important;text-shadow:none!important;filter:none!important;-ms-filter:none!important}a,a:visited{text-decoration:underline}.ir a:after,a[href^="#"]:after,a[href^="javascript:"]:after{content:""}blockquote,pre{page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}img{max-width:100%!important}@page{margin:.5cm}.rst-content .toctree-wrapper>p.caption,h2,h3,p{orphans:3;widows:3}.rst-content .toctree-wrapper>p.caption,h2,h3{page-break-after:avoid}}.btn,.fa:before,.icon:before,.rst-content .admonition,.rst-content .admonition-title:before,.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .code-block-caption .headerlink:before,.rst-content .danger,.rst-content .eqno .headerlink:before,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .note,.rst-content .seealso,.rst-content .tip,.rst-content .warning,.rst-content code.download span:first-child:before,.rst-content dl dt .headerlink:before,.rst-content h1 .headerlink:before,.rst-content h2 .headerlink:before,.rst-content h3 .headerlink:before,.rst-content h4 .headerlink:before,.rst-content h5 .headerlink:before,.rst-content h6 .headerlink:before,.rst-content p.caption .headerlink:before,.rst-content p .headerlink:before,.rst-content table>caption .headerlink:before,.rst-content tt.download span:first-child:before,.wy-alert,.wy-dropdown .caret:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before,.wy-menu-vertical li.current>a button.toctree-expand:before,.wy-menu-vertical li.on a button.toctree-expand:before,.wy-menu-vertical li button.toctree-expand:before,input[type=color],input[type=date],input[type=datetime-local],input[type=datetime],input[type=email],input[type=month],input[type=number],input[type=password],input[type=search],input[type=tel],input[type=text],input[type=time],input[type=url],input[type=week],select,textarea{-webkit-font-smoothing:antialiased}.clearfix{*zoom:1}.clearfix:after,.clearfix:before{display:table;content:""}.clearfix:after{clear:both}/*! + * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome + * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) + */@font-face{font-family:FontAwesome;src:url(fonts/fontawesome-webfont.eot?674f50d287a8c48dc19ba404d20fe713);src:url(fonts/fontawesome-webfont.eot?674f50d287a8c48dc19ba404d20fe713?#iefix&v=4.7.0) format("embedded-opentype"),url(fonts/fontawesome-webfont.woff2?af7ae505a9eed503f8b8e6982036873e) format("woff2"),url(fonts/fontawesome-webfont.woff?fee66e712a8a08eef5805a46892932ad) format("woff"),url(fonts/fontawesome-webfont.ttf?b06871f281fee6b241d60582ae9369b9) format("truetype"),url(fonts/fontawesome-webfont.svg?912ec66d7572ff821749319396470bde#fontawesomeregular) format("svg");font-weight:400;font-style:normal}.fa,.icon,.rst-content .admonition-title,.rst-content .code-block-caption .headerlink,.rst-content .eqno .headerlink,.rst-content code.download span:first-child,.rst-content dl dt .headerlink,.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content p.caption .headerlink,.rst-content p .headerlink,.rst-content table>caption .headerlink,.rst-content tt.download span:first-child,.wy-menu-vertical li.current>a button.toctree-expand,.wy-menu-vertical li.on a button.toctree-expand,.wy-menu-vertical li button.toctree-expand{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14286em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14286em;width:2.14286em;top:.14286em;text-align:center}.fa-li.fa-lg{left:-1.85714em}.fa-border{padding:.2em .25em .15em;border:.08em solid #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa-pull-left.icon,.fa.fa-pull-left,.rst-content .code-block-caption .fa-pull-left.headerlink,.rst-content .eqno .fa-pull-left.headerlink,.rst-content .fa-pull-left.admonition-title,.rst-content code.download span.fa-pull-left:first-child,.rst-content dl dt .fa-pull-left.headerlink,.rst-content h1 .fa-pull-left.headerlink,.rst-content h2 .fa-pull-left.headerlink,.rst-content h3 .fa-pull-left.headerlink,.rst-content h4 .fa-pull-left.headerlink,.rst-content h5 .fa-pull-left.headerlink,.rst-content h6 .fa-pull-left.headerlink,.rst-content p .fa-pull-left.headerlink,.rst-content table>caption .fa-pull-left.headerlink,.rst-content tt.download span.fa-pull-left:first-child,.wy-menu-vertical li.current>a button.fa-pull-left.toctree-expand,.wy-menu-vertical li.on a button.fa-pull-left.toctree-expand,.wy-menu-vertical li button.fa-pull-left.toctree-expand{margin-right:.3em}.fa-pull-right.icon,.fa.fa-pull-right,.rst-content .code-block-caption .fa-pull-right.headerlink,.rst-content .eqno .fa-pull-right.headerlink,.rst-content .fa-pull-right.admonition-title,.rst-content code.download span.fa-pull-right:first-child,.rst-content dl dt .fa-pull-right.headerlink,.rst-content h1 .fa-pull-right.headerlink,.rst-content h2 .fa-pull-right.headerlink,.rst-content h3 .fa-pull-right.headerlink,.rst-content h4 .fa-pull-right.headerlink,.rst-content h5 .fa-pull-right.headerlink,.rst-content h6 .fa-pull-right.headerlink,.rst-content p .fa-pull-right.headerlink,.rst-content table>caption .fa-pull-right.headerlink,.rst-content tt.download span.fa-pull-right:first-child,.wy-menu-vertical li.current>a button.fa-pull-right.toctree-expand,.wy-menu-vertical li.on a button.fa-pull-right.toctree-expand,.wy-menu-vertical li button.fa-pull-right.toctree-expand{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left,.pull-left.icon,.rst-content .code-block-caption .pull-left.headerlink,.rst-content .eqno .pull-left.headerlink,.rst-content .pull-left.admonition-title,.rst-content code.download span.pull-left:first-child,.rst-content dl dt .pull-left.headerlink,.rst-content h1 .pull-left.headerlink,.rst-content h2 .pull-left.headerlink,.rst-content h3 .pull-left.headerlink,.rst-content h4 .pull-left.headerlink,.rst-content h5 .pull-left.headerlink,.rst-content h6 .pull-left.headerlink,.rst-content p .pull-left.headerlink,.rst-content table>caption .pull-left.headerlink,.rst-content tt.download span.pull-left:first-child,.wy-menu-vertical li.current>a button.pull-left.toctree-expand,.wy-menu-vertical li.on a button.pull-left.toctree-expand,.wy-menu-vertical li button.pull-left.toctree-expand{margin-right:.3em}.fa.pull-right,.pull-right.icon,.rst-content .code-block-caption .pull-right.headerlink,.rst-content .eqno .pull-right.headerlink,.rst-content .pull-right.admonition-title,.rst-content code.download span.pull-right:first-child,.rst-content dl dt .pull-right.headerlink,.rst-content h1 .pull-right.headerlink,.rst-content h2 .pull-right.headerlink,.rst-content h3 .pull-right.headerlink,.rst-content h4 .pull-right.headerlink,.rst-content h5 .pull-right.headerlink,.rst-content h6 .pull-right.headerlink,.rst-content p .pull-right.headerlink,.rst-content table>caption .pull-right.headerlink,.rst-content tt.download span.pull-right:first-child,.wy-menu-vertical li.current>a button.pull-right.toctree-expand,.wy-menu-vertical li.on a button.pull-right.toctree-expand,.wy-menu-vertical li button.pull-right.toctree-expand{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s linear infinite;animation:fa-spin 2s linear infinite}.fa-pulse{-webkit-animation:fa-spin 1s steps(8) infinite;animation:fa-spin 1s steps(8) infinite}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scaleX(-1);-ms-transform:scaleX(-1);transform:scaleX(-1)}.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";-webkit-transform:scaleY(-1);-ms-transform:scaleY(-1);transform:scaleY(-1)}:root .fa-flip-horizontal,:root .fa-flip-vertical,:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:""}.fa-music:before{content:""}.fa-search:before,.icon-search:before{content:""}.fa-envelope-o:before{content:""}.fa-heart:before{content:""}.fa-star:before{content:""}.fa-star-o:before{content:""}.fa-user:before{content:""}.fa-film:before{content:""}.fa-th-large:before{content:""}.fa-th:before{content:""}.fa-th-list:before{content:""}.fa-check:before{content:""}.fa-close:before,.fa-remove:before,.fa-times:before{content:""}.fa-search-plus:before{content:""}.fa-search-minus:before{content:""}.fa-power-off:before{content:""}.fa-signal:before{content:""}.fa-cog:before,.fa-gear:before{content:""}.fa-trash-o:before{content:""}.fa-home:before,.icon-home:before{content:""}.fa-file-o:before{content:""}.fa-clock-o:before{content:""}.fa-road:before{content:""}.fa-download:before,.rst-content code.download span:first-child:before,.rst-content tt.download span:first-child:before{content:""}.fa-arrow-circle-o-down:before{content:""}.fa-arrow-circle-o-up:before{content:""}.fa-inbox:before{content:""}.fa-play-circle-o:before{content:""}.fa-repeat:before,.fa-rotate-right:before{content:""}.fa-refresh:before{content:""}.fa-list-alt:before{content:""}.fa-lock:before{content:""}.fa-flag:before{content:""}.fa-headphones:before{content:""}.fa-volume-off:before{content:""}.fa-volume-down:before{content:""}.fa-volume-up:before{content:""}.fa-qrcode:before{content:""}.fa-barcode:before{content:""}.fa-tag:before{content:""}.fa-tags:before{content:""}.fa-book:before,.icon-book:before{content:""}.fa-bookmark:before{content:""}.fa-print:before{content:""}.fa-camera:before{content:""}.fa-font:before{content:""}.fa-bold:before{content:""}.fa-italic:before{content:""}.fa-text-height:before{content:""}.fa-text-width:before{content:""}.fa-align-left:before{content:""}.fa-align-center:before{content:""}.fa-align-right:before{content:""}.fa-align-justify:before{content:""}.fa-list:before{content:""}.fa-dedent:before,.fa-outdent:before{content:""}.fa-indent:before{content:""}.fa-video-camera:before{content:""}.fa-image:before,.fa-photo:before,.fa-picture-o:before{content:""}.fa-pencil:before{content:""}.fa-map-marker:before{content:""}.fa-adjust:before{content:""}.fa-tint:before{content:""}.fa-edit:before,.fa-pencil-square-o:before{content:""}.fa-share-square-o:before{content:""}.fa-check-square-o:before{content:""}.fa-arrows:before{content:""}.fa-step-backward:before{content:""}.fa-fast-backward:before{content:""}.fa-backward:before{content:""}.fa-play:before{content:""}.fa-pause:before{content:""}.fa-stop:before{content:""}.fa-forward:before{content:""}.fa-fast-forward:before{content:""}.fa-step-forward:before{content:""}.fa-eject:before{content:""}.fa-chevron-left:before{content:""}.fa-chevron-right:before{content:""}.fa-plus-circle:before{content:""}.fa-minus-circle:before{content:""}.fa-times-circle:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before{content:""}.fa-check-circle:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before{content:""}.fa-question-circle:before{content:""}.fa-info-circle:before{content:""}.fa-crosshairs:before{content:""}.fa-times-circle-o:before{content:""}.fa-check-circle-o:before{content:""}.fa-ban:before{content:""}.fa-arrow-left:before{content:""}.fa-arrow-right:before{content:""}.fa-arrow-up:before{content:""}.fa-arrow-down:before{content:""}.fa-mail-forward:before,.fa-share:before{content:""}.fa-expand:before{content:""}.fa-compress:before{content:""}.fa-plus:before{content:""}.fa-minus:before{content:""}.fa-asterisk:before{content:""}.fa-exclamation-circle:before,.rst-content .admonition-title:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before{content:""}.fa-gift:before{content:""}.fa-leaf:before{content:""}.fa-fire:before,.icon-fire:before{content:""}.fa-eye:before{content:""}.fa-eye-slash:before{content:""}.fa-exclamation-triangle:before,.fa-warning:before{content:""}.fa-plane:before{content:""}.fa-calendar:before{content:""}.fa-random:before{content:""}.fa-comment:before{content:""}.fa-magnet:before{content:""}.fa-chevron-up:before{content:""}.fa-chevron-down:before{content:""}.fa-retweet:before{content:""}.fa-shopping-cart:before{content:""}.fa-folder:before{content:""}.fa-folder-open:before{content:""}.fa-arrows-v:before{content:""}.fa-arrows-h:before{content:""}.fa-bar-chart-o:before,.fa-bar-chart:before{content:""}.fa-twitter-square:before{content:""}.fa-facebook-square:before{content:""}.fa-camera-retro:before{content:""}.fa-key:before{content:""}.fa-cogs:before,.fa-gears:before{content:""}.fa-comments:before{content:""}.fa-thumbs-o-up:before{content:""}.fa-thumbs-o-down:before{content:""}.fa-star-half:before{content:""}.fa-heart-o:before{content:""}.fa-sign-out:before{content:""}.fa-linkedin-square:before{content:""}.fa-thumb-tack:before{content:""}.fa-external-link:before{content:""}.fa-sign-in:before{content:""}.fa-trophy:before{content:""}.fa-github-square:before{content:""}.fa-upload:before{content:""}.fa-lemon-o:before{content:""}.fa-phone:before{content:""}.fa-square-o:before{content:""}.fa-bookmark-o:before{content:""}.fa-phone-square:before{content:""}.fa-twitter:before{content:""}.fa-facebook-f:before,.fa-facebook:before{content:""}.fa-github:before,.icon-github:before{content:""}.fa-unlock:before{content:""}.fa-credit-card:before{content:""}.fa-feed:before,.fa-rss:before{content:""}.fa-hdd-o:before{content:""}.fa-bullhorn:before{content:""}.fa-bell:before{content:""}.fa-certificate:before{content:""}.fa-hand-o-right:before{content:""}.fa-hand-o-left:before{content:""}.fa-hand-o-up:before{content:""}.fa-hand-o-down:before{content:""}.fa-arrow-circle-left:before,.icon-circle-arrow-left:before{content:""}.fa-arrow-circle-right:before,.icon-circle-arrow-right:before{content:""}.fa-arrow-circle-up:before{content:""}.fa-arrow-circle-down:before{content:""}.fa-globe:before{content:""}.fa-wrench:before{content:""}.fa-tasks:before{content:""}.fa-filter:before{content:""}.fa-briefcase:before{content:""}.fa-arrows-alt:before{content:""}.fa-group:before,.fa-users:before{content:""}.fa-chain:before,.fa-link:before,.icon-link:before{content:""}.fa-cloud:before{content:""}.fa-flask:before{content:""}.fa-cut:before,.fa-scissors:before{content:""}.fa-copy:before,.fa-files-o:before{content:""}.fa-paperclip:before{content:""}.fa-floppy-o:before,.fa-save:before{content:""}.fa-square:before{content:""}.fa-bars:before,.fa-navicon:before,.fa-reorder:before{content:""}.fa-list-ul:before{content:""}.fa-list-ol:before{content:""}.fa-strikethrough:before{content:""}.fa-underline:before{content:""}.fa-table:before{content:""}.fa-magic:before{content:""}.fa-truck:before{content:""}.fa-pinterest:before{content:""}.fa-pinterest-square:before{content:""}.fa-google-plus-square:before{content:""}.fa-google-plus:before{content:""}.fa-money:before{content:""}.fa-caret-down:before,.icon-caret-down:before,.wy-dropdown .caret:before{content:""}.fa-caret-up:before{content:""}.fa-caret-left:before{content:""}.fa-caret-right:before{content:""}.fa-columns:before{content:""}.fa-sort:before,.fa-unsorted:before{content:""}.fa-sort-desc:before,.fa-sort-down:before{content:""}.fa-sort-asc:before,.fa-sort-up:before{content:""}.fa-envelope:before{content:""}.fa-linkedin:before{content:""}.fa-rotate-left:before,.fa-undo:before{content:""}.fa-gavel:before,.fa-legal:before{content:""}.fa-dashboard:before,.fa-tachometer:before{content:""}.fa-comment-o:before{content:""}.fa-comments-o:before{content:""}.fa-bolt:before,.fa-flash:before{content:""}.fa-sitemap:before{content:""}.fa-umbrella:before{content:""}.fa-clipboard:before,.fa-paste:before{content:""}.fa-lightbulb-o:before{content:""}.fa-exchange:before{content:""}.fa-cloud-download:before{content:""}.fa-cloud-upload:before{content:""}.fa-user-md:before{content:""}.fa-stethoscope:before{content:""}.fa-suitcase:before{content:""}.fa-bell-o:before{content:""}.fa-coffee:before{content:""}.fa-cutlery:before{content:""}.fa-file-text-o:before{content:""}.fa-building-o:before{content:""}.fa-hospital-o:before{content:""}.fa-ambulance:before{content:""}.fa-medkit:before{content:""}.fa-fighter-jet:before{content:""}.fa-beer:before{content:""}.fa-h-square:before{content:""}.fa-plus-square:before{content:""}.fa-angle-double-left:before{content:""}.fa-angle-double-right:before{content:""}.fa-angle-double-up:before{content:""}.fa-angle-double-down:before{content:""}.fa-angle-left:before{content:""}.fa-angle-right:before{content:""}.fa-angle-up:before{content:""}.fa-angle-down:before{content:""}.fa-desktop:before{content:""}.fa-laptop:before{content:""}.fa-tablet:before{content:""}.fa-mobile-phone:before,.fa-mobile:before{content:""}.fa-circle-o:before{content:""}.fa-quote-left:before{content:""}.fa-quote-right:before{content:""}.fa-spinner:before{content:""}.fa-circle:before{content:""}.fa-mail-reply:before,.fa-reply:before{content:""}.fa-github-alt:before{content:""}.fa-folder-o:before{content:""}.fa-folder-open-o:before{content:""}.fa-smile-o:before{content:""}.fa-frown-o:before{content:""}.fa-meh-o:before{content:""}.fa-gamepad:before{content:""}.fa-keyboard-o:before{content:""}.fa-flag-o:before{content:""}.fa-flag-checkered:before{content:""}.fa-terminal:before{content:""}.fa-code:before{content:""}.fa-mail-reply-all:before,.fa-reply-all:before{content:""}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:""}.fa-location-arrow:before{content:""}.fa-crop:before{content:""}.fa-code-fork:before{content:""}.fa-chain-broken:before,.fa-unlink:before{content:""}.fa-question:before{content:""}.fa-info:before{content:""}.fa-exclamation:before{content:""}.fa-superscript:before{content:""}.fa-subscript:before{content:""}.fa-eraser:before{content:""}.fa-puzzle-piece:before{content:""}.fa-microphone:before{content:""}.fa-microphone-slash:before{content:""}.fa-shield:before{content:""}.fa-calendar-o:before{content:""}.fa-fire-extinguisher:before{content:""}.fa-rocket:before{content:""}.fa-maxcdn:before{content:""}.fa-chevron-circle-left:before{content:""}.fa-chevron-circle-right:before{content:""}.fa-chevron-circle-up:before{content:""}.fa-chevron-circle-down:before{content:""}.fa-html5:before{content:""}.fa-css3:before{content:""}.fa-anchor:before{content:""}.fa-unlock-alt:before{content:""}.fa-bullseye:before{content:""}.fa-ellipsis-h:before{content:""}.fa-ellipsis-v:before{content:""}.fa-rss-square:before{content:""}.fa-play-circle:before{content:""}.fa-ticket:before{content:""}.fa-minus-square:before{content:""}.fa-minus-square-o:before,.wy-menu-vertical li.current>a button.toctree-expand:before,.wy-menu-vertical li.on a button.toctree-expand:before{content:""}.fa-level-up:before{content:""}.fa-level-down:before{content:""}.fa-check-square:before{content:""}.fa-pencil-square:before{content:""}.fa-external-link-square:before{content:""}.fa-share-square:before{content:""}.fa-compass:before{content:""}.fa-caret-square-o-down:before,.fa-toggle-down:before{content:""}.fa-caret-square-o-up:before,.fa-toggle-up:before{content:""}.fa-caret-square-o-right:before,.fa-toggle-right:before{content:""}.fa-eur:before,.fa-euro:before{content:""}.fa-gbp:before{content:""}.fa-dollar:before,.fa-usd:before{content:""}.fa-inr:before,.fa-rupee:before{content:""}.fa-cny:before,.fa-jpy:before,.fa-rmb:before,.fa-yen:before{content:""}.fa-rouble:before,.fa-rub:before,.fa-ruble:before{content:""}.fa-krw:before,.fa-won:before{content:""}.fa-bitcoin:before,.fa-btc:before{content:""}.fa-file:before{content:""}.fa-file-text:before{content:""}.fa-sort-alpha-asc:before{content:""}.fa-sort-alpha-desc:before{content:""}.fa-sort-amount-asc:before{content:""}.fa-sort-amount-desc:before{content:""}.fa-sort-numeric-asc:before{content:""}.fa-sort-numeric-desc:before{content:""}.fa-thumbs-up:before{content:""}.fa-thumbs-down:before{content:""}.fa-youtube-square:before{content:""}.fa-youtube:before{content:""}.fa-xing:before{content:""}.fa-xing-square:before{content:""}.fa-youtube-play:before{content:""}.fa-dropbox:before{content:""}.fa-stack-overflow:before{content:""}.fa-instagram:before{content:""}.fa-flickr:before{content:""}.fa-adn:before{content:""}.fa-bitbucket:before,.icon-bitbucket:before{content:""}.fa-bitbucket-square:before{content:""}.fa-tumblr:before{content:""}.fa-tumblr-square:before{content:""}.fa-long-arrow-down:before{content:""}.fa-long-arrow-up:before{content:""}.fa-long-arrow-left:before{content:""}.fa-long-arrow-right:before{content:""}.fa-apple:before{content:""}.fa-windows:before{content:""}.fa-android:before{content:""}.fa-linux:before{content:""}.fa-dribbble:before{content:""}.fa-skype:before{content:""}.fa-foursquare:before{content:""}.fa-trello:before{content:""}.fa-female:before{content:""}.fa-male:before{content:""}.fa-gittip:before,.fa-gratipay:before{content:""}.fa-sun-o:before{content:""}.fa-moon-o:before{content:""}.fa-archive:before{content:""}.fa-bug:before{content:""}.fa-vk:before{content:""}.fa-weibo:before{content:""}.fa-renren:before{content:""}.fa-pagelines:before{content:""}.fa-stack-exchange:before{content:""}.fa-arrow-circle-o-right:before{content:""}.fa-arrow-circle-o-left:before{content:""}.fa-caret-square-o-left:before,.fa-toggle-left:before{content:""}.fa-dot-circle-o:before{content:""}.fa-wheelchair:before{content:""}.fa-vimeo-square:before{content:""}.fa-try:before,.fa-turkish-lira:before{content:""}.fa-plus-square-o:before,.wy-menu-vertical li button.toctree-expand:before{content:""}.fa-space-shuttle:before{content:""}.fa-slack:before{content:""}.fa-envelope-square:before{content:""}.fa-wordpress:before{content:""}.fa-openid:before{content:""}.fa-bank:before,.fa-institution:before,.fa-university:before{content:""}.fa-graduation-cap:before,.fa-mortar-board:before{content:""}.fa-yahoo:before{content:""}.fa-google:before{content:""}.fa-reddit:before{content:""}.fa-reddit-square:before{content:""}.fa-stumbleupon-circle:before{content:""}.fa-stumbleupon:before{content:""}.fa-delicious:before{content:""}.fa-digg:before{content:""}.fa-pied-piper-pp:before{content:""}.fa-pied-piper-alt:before{content:""}.fa-drupal:before{content:""}.fa-joomla:before{content:""}.fa-language:before{content:""}.fa-fax:before{content:""}.fa-building:before{content:""}.fa-child:before{content:""}.fa-paw:before{content:""}.fa-spoon:before{content:""}.fa-cube:before{content:""}.fa-cubes:before{content:""}.fa-behance:before{content:""}.fa-behance-square:before{content:""}.fa-steam:before{content:""}.fa-steam-square:before{content:""}.fa-recycle:before{content:""}.fa-automobile:before,.fa-car:before{content:""}.fa-cab:before,.fa-taxi:before{content:""}.fa-tree:before{content:""}.fa-spotify:before{content:""}.fa-deviantart:before{content:""}.fa-soundcloud:before{content:""}.fa-database:before{content:""}.fa-file-pdf-o:before{content:""}.fa-file-word-o:before{content:""}.fa-file-excel-o:before{content:""}.fa-file-powerpoint-o:before{content:""}.fa-file-image-o:before,.fa-file-photo-o:before,.fa-file-picture-o:before{content:""}.fa-file-archive-o:before,.fa-file-zip-o:before{content:""}.fa-file-audio-o:before,.fa-file-sound-o:before{content:""}.fa-file-movie-o:before,.fa-file-video-o:before{content:""}.fa-file-code-o:before{content:""}.fa-vine:before{content:""}.fa-codepen:before{content:""}.fa-jsfiddle:before{content:""}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-ring:before,.fa-life-saver:before,.fa-support:before{content:""}.fa-circle-o-notch:before{content:""}.fa-ra:before,.fa-rebel:before,.fa-resistance:before{content:""}.fa-empire:before,.fa-ge:before{content:""}.fa-git-square:before{content:""}.fa-git:before{content:""}.fa-hacker-news:before,.fa-y-combinator-square:before,.fa-yc-square:before{content:""}.fa-tencent-weibo:before{content:""}.fa-qq:before{content:""}.fa-wechat:before,.fa-weixin:before{content:""}.fa-paper-plane:before,.fa-send:before{content:""}.fa-paper-plane-o:before,.fa-send-o:before{content:""}.fa-history:before{content:""}.fa-circle-thin:before{content:""}.fa-header:before{content:""}.fa-paragraph:before{content:""}.fa-sliders:before{content:""}.fa-share-alt:before{content:""}.fa-share-alt-square:before{content:""}.fa-bomb:before{content:""}.fa-futbol-o:before,.fa-soccer-ball-o:before{content:""}.fa-tty:before{content:""}.fa-binoculars:before{content:""}.fa-plug:before{content:""}.fa-slideshare:before{content:""}.fa-twitch:before{content:""}.fa-yelp:before{content:""}.fa-newspaper-o:before{content:""}.fa-wifi:before{content:""}.fa-calculator:before{content:""}.fa-paypal:before{content:""}.fa-google-wallet:before{content:""}.fa-cc-visa:before{content:""}.fa-cc-mastercard:before{content:""}.fa-cc-discover:before{content:""}.fa-cc-amex:before{content:""}.fa-cc-paypal:before{content:""}.fa-cc-stripe:before{content:""}.fa-bell-slash:before{content:""}.fa-bell-slash-o:before{content:""}.fa-trash:before{content:""}.fa-copyright:before{content:""}.fa-at:before{content:""}.fa-eyedropper:before{content:""}.fa-paint-brush:before{content:""}.fa-birthday-cake:before{content:""}.fa-area-chart:before{content:""}.fa-pie-chart:before{content:""}.fa-line-chart:before{content:""}.fa-lastfm:before{content:""}.fa-lastfm-square:before{content:""}.fa-toggle-off:before{content:""}.fa-toggle-on:before{content:""}.fa-bicycle:before{content:""}.fa-bus:before{content:""}.fa-ioxhost:before{content:""}.fa-angellist:before{content:""}.fa-cc:before{content:""}.fa-ils:before,.fa-shekel:before,.fa-sheqel:before{content:""}.fa-meanpath:before{content:""}.fa-buysellads:before{content:""}.fa-connectdevelop:before{content:""}.fa-dashcube:before{content:""}.fa-forumbee:before{content:""}.fa-leanpub:before{content:""}.fa-sellsy:before{content:""}.fa-shirtsinbulk:before{content:""}.fa-simplybuilt:before{content:""}.fa-skyatlas:before{content:""}.fa-cart-plus:before{content:""}.fa-cart-arrow-down:before{content:""}.fa-diamond:before{content:""}.fa-ship:before{content:""}.fa-user-secret:before{content:""}.fa-motorcycle:before{content:""}.fa-street-view:before{content:""}.fa-heartbeat:before{content:""}.fa-venus:before{content:""}.fa-mars:before{content:""}.fa-mercury:before{content:""}.fa-intersex:before,.fa-transgender:before{content:""}.fa-transgender-alt:before{content:""}.fa-venus-double:before{content:""}.fa-mars-double:before{content:""}.fa-venus-mars:before{content:""}.fa-mars-stroke:before{content:""}.fa-mars-stroke-v:before{content:""}.fa-mars-stroke-h:before{content:""}.fa-neuter:before{content:""}.fa-genderless:before{content:""}.fa-facebook-official:before{content:""}.fa-pinterest-p:before{content:""}.fa-whatsapp:before{content:""}.fa-server:before{content:""}.fa-user-plus:before{content:""}.fa-user-times:before{content:""}.fa-bed:before,.fa-hotel:before{content:""}.fa-viacoin:before{content:""}.fa-train:before{content:""}.fa-subway:before{content:""}.fa-medium:before{content:""}.fa-y-combinator:before,.fa-yc:before{content:""}.fa-optin-monster:before{content:""}.fa-opencart:before{content:""}.fa-expeditedssl:before{content:""}.fa-battery-4:before,.fa-battery-full:before,.fa-battery:before{content:""}.fa-battery-3:before,.fa-battery-three-quarters:before{content:""}.fa-battery-2:before,.fa-battery-half:before{content:""}.fa-battery-1:before,.fa-battery-quarter:before{content:""}.fa-battery-0:before,.fa-battery-empty:before{content:""}.fa-mouse-pointer:before{content:""}.fa-i-cursor:before{content:""}.fa-object-group:before{content:""}.fa-object-ungroup:before{content:""}.fa-sticky-note:before{content:""}.fa-sticky-note-o:before{content:""}.fa-cc-jcb:before{content:""}.fa-cc-diners-club:before{content:""}.fa-clone:before{content:""}.fa-balance-scale:before{content:""}.fa-hourglass-o:before{content:""}.fa-hourglass-1:before,.fa-hourglass-start:before{content:""}.fa-hourglass-2:before,.fa-hourglass-half:before{content:""}.fa-hourglass-3:before,.fa-hourglass-end:before{content:""}.fa-hourglass:before{content:""}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:""}.fa-hand-paper-o:before,.fa-hand-stop-o:before{content:""}.fa-hand-scissors-o:before{content:""}.fa-hand-lizard-o:before{content:""}.fa-hand-spock-o:before{content:""}.fa-hand-pointer-o:before{content:""}.fa-hand-peace-o:before{content:""}.fa-trademark:before{content:""}.fa-registered:before{content:""}.fa-creative-commons:before{content:""}.fa-gg:before{content:""}.fa-gg-circle:before{content:""}.fa-tripadvisor:before{content:""}.fa-odnoklassniki:before{content:""}.fa-odnoklassniki-square:before{content:""}.fa-get-pocket:before{content:""}.fa-wikipedia-w:before{content:""}.fa-safari:before{content:""}.fa-chrome:before{content:""}.fa-firefox:before{content:""}.fa-opera:before{content:""}.fa-internet-explorer:before{content:""}.fa-television:before,.fa-tv:before{content:""}.fa-contao:before{content:""}.fa-500px:before{content:""}.fa-amazon:before{content:""}.fa-calendar-plus-o:before{content:""}.fa-calendar-minus-o:before{content:""}.fa-calendar-times-o:before{content:""}.fa-calendar-check-o:before{content:""}.fa-industry:before{content:""}.fa-map-pin:before{content:""}.fa-map-signs:before{content:""}.fa-map-o:before{content:""}.fa-map:before{content:""}.fa-commenting:before{content:""}.fa-commenting-o:before{content:""}.fa-houzz:before{content:""}.fa-vimeo:before{content:""}.fa-black-tie:before{content:""}.fa-fonticons:before{content:""}.fa-reddit-alien:before{content:""}.fa-edge:before{content:""}.fa-credit-card-alt:before{content:""}.fa-codiepie:before{content:""}.fa-modx:before{content:""}.fa-fort-awesome:before{content:""}.fa-usb:before{content:""}.fa-product-hunt:before{content:""}.fa-mixcloud:before{content:""}.fa-scribd:before{content:""}.fa-pause-circle:before{content:""}.fa-pause-circle-o:before{content:""}.fa-stop-circle:before{content:""}.fa-stop-circle-o:before{content:""}.fa-shopping-bag:before{content:""}.fa-shopping-basket:before{content:""}.fa-hashtag:before{content:""}.fa-bluetooth:before{content:""}.fa-bluetooth-b:before{content:""}.fa-percent:before{content:""}.fa-gitlab:before,.icon-gitlab:before{content:""}.fa-wpbeginner:before{content:""}.fa-wpforms:before{content:""}.fa-envira:before{content:""}.fa-universal-access:before{content:""}.fa-wheelchair-alt:before{content:""}.fa-question-circle-o:before{content:""}.fa-blind:before{content:""}.fa-audio-description:before{content:""}.fa-volume-control-phone:before{content:""}.fa-braille:before{content:""}.fa-assistive-listening-systems:before{content:""}.fa-american-sign-language-interpreting:before,.fa-asl-interpreting:before{content:""}.fa-deaf:before,.fa-deafness:before,.fa-hard-of-hearing:before{content:""}.fa-glide:before{content:""}.fa-glide-g:before{content:""}.fa-sign-language:before,.fa-signing:before{content:""}.fa-low-vision:before{content:""}.fa-viadeo:before{content:""}.fa-viadeo-square:before{content:""}.fa-snapchat:before{content:""}.fa-snapchat-ghost:before{content:""}.fa-snapchat-square:before{content:""}.fa-pied-piper:before{content:""}.fa-first-order:before{content:""}.fa-yoast:before{content:""}.fa-themeisle:before{content:""}.fa-google-plus-circle:before,.fa-google-plus-official:before{content:""}.fa-fa:before,.fa-font-awesome:before{content:""}.fa-handshake-o:before{content:""}.fa-envelope-open:before{content:""}.fa-envelope-open-o:before{content:""}.fa-linode:before{content:""}.fa-address-book:before{content:""}.fa-address-book-o:before{content:""}.fa-address-card:before,.fa-vcard:before{content:""}.fa-address-card-o:before,.fa-vcard-o:before{content:""}.fa-user-circle:before{content:""}.fa-user-circle-o:before{content:""}.fa-user-o:before{content:""}.fa-id-badge:before{content:""}.fa-drivers-license:before,.fa-id-card:before{content:""}.fa-drivers-license-o:before,.fa-id-card-o:before{content:""}.fa-quora:before{content:""}.fa-free-code-camp:before{content:""}.fa-telegram:before{content:""}.fa-thermometer-4:before,.fa-thermometer-full:before,.fa-thermometer:before{content:""}.fa-thermometer-3:before,.fa-thermometer-three-quarters:before{content:""}.fa-thermometer-2:before,.fa-thermometer-half:before{content:""}.fa-thermometer-1:before,.fa-thermometer-quarter:before{content:""}.fa-thermometer-0:before,.fa-thermometer-empty:before{content:""}.fa-shower:before{content:""}.fa-bath:before,.fa-bathtub:before,.fa-s15:before{content:""}.fa-podcast:before{content:""}.fa-window-maximize:before{content:""}.fa-window-minimize:before{content:""}.fa-window-restore:before{content:""}.fa-times-rectangle:before,.fa-window-close:before{content:""}.fa-times-rectangle-o:before,.fa-window-close-o:before{content:""}.fa-bandcamp:before{content:""}.fa-grav:before{content:""}.fa-etsy:before{content:""}.fa-imdb:before{content:""}.fa-ravelry:before{content:""}.fa-eercast:before{content:""}.fa-microchip:before{content:""}.fa-snowflake-o:before{content:""}.fa-superpowers:before{content:""}.fa-wpexplorer:before{content:""}.fa-meetup:before{content:""}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}.fa,.icon,.rst-content .admonition-title,.rst-content .code-block-caption .headerlink,.rst-content .eqno .headerlink,.rst-content code.download span:first-child,.rst-content dl dt .headerlink,.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content p.caption .headerlink,.rst-content p .headerlink,.rst-content table>caption .headerlink,.rst-content tt.download span:first-child,.wy-dropdown .caret,.wy-inline-validate.wy-inline-validate-danger .wy-input-context,.wy-inline-validate.wy-inline-validate-info .wy-input-context,.wy-inline-validate.wy-inline-validate-success .wy-input-context,.wy-inline-validate.wy-inline-validate-warning .wy-input-context,.wy-menu-vertical li.current>a button.toctree-expand,.wy-menu-vertical li.on a button.toctree-expand,.wy-menu-vertical li button.toctree-expand{font-family:inherit}.fa:before,.icon:before,.rst-content .admonition-title:before,.rst-content .code-block-caption .headerlink:before,.rst-content .eqno .headerlink:before,.rst-content code.download span:first-child:before,.rst-content dl dt .headerlink:before,.rst-content h1 .headerlink:before,.rst-content h2 .headerlink:before,.rst-content h3 .headerlink:before,.rst-content h4 .headerlink:before,.rst-content h5 .headerlink:before,.rst-content h6 .headerlink:before,.rst-content p.caption .headerlink:before,.rst-content p .headerlink:before,.rst-content table>caption .headerlink:before,.rst-content tt.download span:first-child:before,.wy-dropdown .caret:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before,.wy-menu-vertical li.current>a button.toctree-expand:before,.wy-menu-vertical li.on a button.toctree-expand:before,.wy-menu-vertical li button.toctree-expand:before{font-family:FontAwesome;display:inline-block;font-style:normal;font-weight:400;line-height:1;text-decoration:inherit}.rst-content .code-block-caption a .headerlink,.rst-content .eqno a .headerlink,.rst-content a .admonition-title,.rst-content code.download a span:first-child,.rst-content dl dt a .headerlink,.rst-content h1 a .headerlink,.rst-content h2 a .headerlink,.rst-content h3 a .headerlink,.rst-content h4 a .headerlink,.rst-content h5 a .headerlink,.rst-content h6 a .headerlink,.rst-content p.caption a .headerlink,.rst-content p a .headerlink,.rst-content table>caption a .headerlink,.rst-content tt.download a span:first-child,.wy-menu-vertical li.current>a button.toctree-expand,.wy-menu-vertical li.on a button.toctree-expand,.wy-menu-vertical li a button.toctree-expand,a .fa,a .icon,a .rst-content .admonition-title,a .rst-content .code-block-caption .headerlink,a .rst-content .eqno .headerlink,a .rst-content code.download span:first-child,a .rst-content dl dt .headerlink,a .rst-content h1 .headerlink,a .rst-content h2 .headerlink,a .rst-content h3 .headerlink,a .rst-content h4 .headerlink,a .rst-content h5 .headerlink,a .rst-content h6 .headerlink,a .rst-content p.caption .headerlink,a .rst-content p .headerlink,a .rst-content table>caption .headerlink,a .rst-content tt.download span:first-child,a .wy-menu-vertical li button.toctree-expand{display:inline-block;text-decoration:inherit}.btn .fa,.btn .icon,.btn .rst-content .admonition-title,.btn .rst-content .code-block-caption .headerlink,.btn .rst-content .eqno .headerlink,.btn .rst-content code.download span:first-child,.btn .rst-content dl dt .headerlink,.btn .rst-content h1 .headerlink,.btn .rst-content h2 .headerlink,.btn .rst-content h3 .headerlink,.btn .rst-content h4 .headerlink,.btn .rst-content h5 .headerlink,.btn .rst-content h6 .headerlink,.btn .rst-content p .headerlink,.btn .rst-content table>caption .headerlink,.btn .rst-content tt.download span:first-child,.btn .wy-menu-vertical li.current>a button.toctree-expand,.btn .wy-menu-vertical li.on a button.toctree-expand,.btn .wy-menu-vertical li button.toctree-expand,.nav .fa,.nav .icon,.nav .rst-content .admonition-title,.nav .rst-content .code-block-caption .headerlink,.nav .rst-content .eqno .headerlink,.nav .rst-content code.download span:first-child,.nav .rst-content dl dt .headerlink,.nav .rst-content h1 .headerlink,.nav .rst-content h2 .headerlink,.nav .rst-content h3 .headerlink,.nav .rst-content h4 .headerlink,.nav .rst-content h5 .headerlink,.nav .rst-content h6 .headerlink,.nav .rst-content p .headerlink,.nav .rst-content table>caption .headerlink,.nav .rst-content tt.download span:first-child,.nav .wy-menu-vertical li.current>a button.toctree-expand,.nav .wy-menu-vertical li.on a button.toctree-expand,.nav .wy-menu-vertical li button.toctree-expand,.rst-content .btn .admonition-title,.rst-content .code-block-caption .btn .headerlink,.rst-content .code-block-caption .nav .headerlink,.rst-content .eqno .btn .headerlink,.rst-content .eqno .nav .headerlink,.rst-content .nav .admonition-title,.rst-content code.download .btn span:first-child,.rst-content code.download .nav span:first-child,.rst-content dl dt .btn .headerlink,.rst-content dl dt .nav .headerlink,.rst-content h1 .btn .headerlink,.rst-content h1 .nav .headerlink,.rst-content h2 .btn .headerlink,.rst-content h2 .nav .headerlink,.rst-content h3 .btn .headerlink,.rst-content h3 .nav .headerlink,.rst-content h4 .btn .headerlink,.rst-content h4 .nav .headerlink,.rst-content h5 .btn .headerlink,.rst-content h5 .nav .headerlink,.rst-content h6 .btn .headerlink,.rst-content h6 .nav .headerlink,.rst-content p .btn .headerlink,.rst-content p .nav .headerlink,.rst-content table>caption .btn .headerlink,.rst-content table>caption .nav .headerlink,.rst-content tt.download .btn span:first-child,.rst-content tt.download .nav span:first-child,.wy-menu-vertical li .btn button.toctree-expand,.wy-menu-vertical li.current>a .btn button.toctree-expand,.wy-menu-vertical li.current>a .nav button.toctree-expand,.wy-menu-vertical li .nav button.toctree-expand,.wy-menu-vertical li.on a .btn button.toctree-expand,.wy-menu-vertical li.on a .nav button.toctree-expand{display:inline}.btn .fa-large.icon,.btn .fa.fa-large,.btn .rst-content .code-block-caption .fa-large.headerlink,.btn .rst-content .eqno .fa-large.headerlink,.btn .rst-content .fa-large.admonition-title,.btn .rst-content code.download span.fa-large:first-child,.btn .rst-content dl dt .fa-large.headerlink,.btn .rst-content h1 .fa-large.headerlink,.btn .rst-content h2 .fa-large.headerlink,.btn .rst-content h3 .fa-large.headerlink,.btn .rst-content h4 .fa-large.headerlink,.btn .rst-content h5 .fa-large.headerlink,.btn .rst-content h6 .fa-large.headerlink,.btn .rst-content p .fa-large.headerlink,.btn .rst-content table>caption .fa-large.headerlink,.btn .rst-content tt.download span.fa-large:first-child,.btn .wy-menu-vertical li button.fa-large.toctree-expand,.nav .fa-large.icon,.nav .fa.fa-large,.nav .rst-content .code-block-caption .fa-large.headerlink,.nav .rst-content .eqno .fa-large.headerlink,.nav .rst-content .fa-large.admonition-title,.nav .rst-content code.download span.fa-large:first-child,.nav .rst-content dl dt .fa-large.headerlink,.nav .rst-content h1 .fa-large.headerlink,.nav .rst-content h2 .fa-large.headerlink,.nav .rst-content h3 .fa-large.headerlink,.nav .rst-content h4 .fa-large.headerlink,.nav .rst-content h5 .fa-large.headerlink,.nav .rst-content h6 .fa-large.headerlink,.nav .rst-content p .fa-large.headerlink,.nav .rst-content table>caption .fa-large.headerlink,.nav .rst-content tt.download span.fa-large:first-child,.nav .wy-menu-vertical li button.fa-large.toctree-expand,.rst-content .btn .fa-large.admonition-title,.rst-content .code-block-caption .btn .fa-large.headerlink,.rst-content .code-block-caption .nav .fa-large.headerlink,.rst-content .eqno .btn .fa-large.headerlink,.rst-content .eqno .nav .fa-large.headerlink,.rst-content .nav .fa-large.admonition-title,.rst-content code.download .btn span.fa-large:first-child,.rst-content code.download .nav span.fa-large:first-child,.rst-content dl dt .btn .fa-large.headerlink,.rst-content dl dt .nav .fa-large.headerlink,.rst-content h1 .btn .fa-large.headerlink,.rst-content h1 .nav .fa-large.headerlink,.rst-content h2 .btn .fa-large.headerlink,.rst-content h2 .nav .fa-large.headerlink,.rst-content h3 .btn .fa-large.headerlink,.rst-content h3 .nav .fa-large.headerlink,.rst-content h4 .btn .fa-large.headerlink,.rst-content h4 .nav .fa-large.headerlink,.rst-content h5 .btn .fa-large.headerlink,.rst-content h5 .nav .fa-large.headerlink,.rst-content h6 .btn .fa-large.headerlink,.rst-content h6 .nav .fa-large.headerlink,.rst-content p .btn .fa-large.headerlink,.rst-content p .nav .fa-large.headerlink,.rst-content table>caption .btn .fa-large.headerlink,.rst-content table>caption .nav .fa-large.headerlink,.rst-content tt.download .btn span.fa-large:first-child,.rst-content tt.download .nav span.fa-large:first-child,.wy-menu-vertical li .btn button.fa-large.toctree-expand,.wy-menu-vertical li .nav button.fa-large.toctree-expand{line-height:.9em}.btn .fa-spin.icon,.btn .fa.fa-spin,.btn .rst-content .code-block-caption .fa-spin.headerlink,.btn .rst-content .eqno .fa-spin.headerlink,.btn .rst-content .fa-spin.admonition-title,.btn .rst-content code.download span.fa-spin:first-child,.btn .rst-content dl dt .fa-spin.headerlink,.btn .rst-content h1 .fa-spin.headerlink,.btn .rst-content h2 .fa-spin.headerlink,.btn .rst-content h3 .fa-spin.headerlink,.btn .rst-content h4 .fa-spin.headerlink,.btn .rst-content h5 .fa-spin.headerlink,.btn .rst-content h6 .fa-spin.headerlink,.btn .rst-content p .fa-spin.headerlink,.btn .rst-content table>caption .fa-spin.headerlink,.btn .rst-content tt.download span.fa-spin:first-child,.btn .wy-menu-vertical li button.fa-spin.toctree-expand,.nav .fa-spin.icon,.nav .fa.fa-spin,.nav .rst-content .code-block-caption .fa-spin.headerlink,.nav .rst-content .eqno .fa-spin.headerlink,.nav .rst-content .fa-spin.admonition-title,.nav .rst-content code.download span.fa-spin:first-child,.nav .rst-content dl dt .fa-spin.headerlink,.nav .rst-content h1 .fa-spin.headerlink,.nav .rst-content h2 .fa-spin.headerlink,.nav .rst-content h3 .fa-spin.headerlink,.nav .rst-content h4 .fa-spin.headerlink,.nav .rst-content h5 .fa-spin.headerlink,.nav .rst-content h6 .fa-spin.headerlink,.nav .rst-content p .fa-spin.headerlink,.nav .rst-content table>caption .fa-spin.headerlink,.nav .rst-content tt.download span.fa-spin:first-child,.nav .wy-menu-vertical li button.fa-spin.toctree-expand,.rst-content .btn .fa-spin.admonition-title,.rst-content .code-block-caption .btn .fa-spin.headerlink,.rst-content .code-block-caption .nav .fa-spin.headerlink,.rst-content .eqno .btn .fa-spin.headerlink,.rst-content .eqno .nav .fa-spin.headerlink,.rst-content .nav .fa-spin.admonition-title,.rst-content code.download .btn span.fa-spin:first-child,.rst-content code.download .nav span.fa-spin:first-child,.rst-content dl dt .btn .fa-spin.headerlink,.rst-content dl dt .nav .fa-spin.headerlink,.rst-content h1 .btn .fa-spin.headerlink,.rst-content h1 .nav .fa-spin.headerlink,.rst-content h2 .btn .fa-spin.headerlink,.rst-content h2 .nav .fa-spin.headerlink,.rst-content h3 .btn .fa-spin.headerlink,.rst-content h3 .nav .fa-spin.headerlink,.rst-content h4 .btn .fa-spin.headerlink,.rst-content h4 .nav .fa-spin.headerlink,.rst-content h5 .btn .fa-spin.headerlink,.rst-content h5 .nav .fa-spin.headerlink,.rst-content h6 .btn .fa-spin.headerlink,.rst-content h6 .nav .fa-spin.headerlink,.rst-content p .btn .fa-spin.headerlink,.rst-content p .nav .fa-spin.headerlink,.rst-content table>caption .btn .fa-spin.headerlink,.rst-content table>caption .nav .fa-spin.headerlink,.rst-content tt.download .btn span.fa-spin:first-child,.rst-content tt.download .nav span.fa-spin:first-child,.wy-menu-vertical li .btn button.fa-spin.toctree-expand,.wy-menu-vertical li .nav button.fa-spin.toctree-expand{display:inline-block}.btn.fa:before,.btn.icon:before,.rst-content .btn.admonition-title:before,.rst-content .code-block-caption .btn.headerlink:before,.rst-content .eqno .btn.headerlink:before,.rst-content code.download span.btn:first-child:before,.rst-content dl dt .btn.headerlink:before,.rst-content h1 .btn.headerlink:before,.rst-content h2 .btn.headerlink:before,.rst-content h3 .btn.headerlink:before,.rst-content h4 .btn.headerlink:before,.rst-content h5 .btn.headerlink:before,.rst-content h6 .btn.headerlink:before,.rst-content p .btn.headerlink:before,.rst-content table>caption .btn.headerlink:before,.rst-content tt.download span.btn:first-child:before,.wy-menu-vertical li button.btn.toctree-expand:before{opacity:.5;-webkit-transition:opacity .05s ease-in;-moz-transition:opacity .05s ease-in;transition:opacity .05s ease-in}.btn.fa:hover:before,.btn.icon:hover:before,.rst-content .btn.admonition-title:hover:before,.rst-content .code-block-caption .btn.headerlink:hover:before,.rst-content .eqno .btn.headerlink:hover:before,.rst-content code.download span.btn:first-child:hover:before,.rst-content dl dt .btn.headerlink:hover:before,.rst-content h1 .btn.headerlink:hover:before,.rst-content h2 .btn.headerlink:hover:before,.rst-content h3 .btn.headerlink:hover:before,.rst-content h4 .btn.headerlink:hover:before,.rst-content h5 .btn.headerlink:hover:before,.rst-content h6 .btn.headerlink:hover:before,.rst-content p .btn.headerlink:hover:before,.rst-content table>caption .btn.headerlink:hover:before,.rst-content tt.download span.btn:first-child:hover:before,.wy-menu-vertical li button.btn.toctree-expand:hover:before{opacity:1}.btn-mini .fa:before,.btn-mini .icon:before,.btn-mini .rst-content .admonition-title:before,.btn-mini .rst-content .code-block-caption .headerlink:before,.btn-mini .rst-content .eqno .headerlink:before,.btn-mini .rst-content code.download span:first-child:before,.btn-mini .rst-content dl dt .headerlink:before,.btn-mini .rst-content h1 .headerlink:before,.btn-mini .rst-content h2 .headerlink:before,.btn-mini .rst-content h3 .headerlink:before,.btn-mini .rst-content h4 .headerlink:before,.btn-mini .rst-content h5 .headerlink:before,.btn-mini .rst-content h6 .headerlink:before,.btn-mini .rst-content p .headerlink:before,.btn-mini .rst-content table>caption .headerlink:before,.btn-mini .rst-content tt.download span:first-child:before,.btn-mini .wy-menu-vertical li button.toctree-expand:before,.rst-content .btn-mini .admonition-title:before,.rst-content .code-block-caption .btn-mini .headerlink:before,.rst-content .eqno .btn-mini .headerlink:before,.rst-content code.download .btn-mini span:first-child:before,.rst-content dl dt .btn-mini .headerlink:before,.rst-content h1 .btn-mini .headerlink:before,.rst-content h2 .btn-mini .headerlink:before,.rst-content h3 .btn-mini .headerlink:before,.rst-content h4 .btn-mini .headerlink:before,.rst-content h5 .btn-mini .headerlink:before,.rst-content h6 .btn-mini .headerlink:before,.rst-content p .btn-mini .headerlink:before,.rst-content table>caption .btn-mini .headerlink:before,.rst-content tt.download .btn-mini span:first-child:before,.wy-menu-vertical li .btn-mini button.toctree-expand:before{font-size:14px;vertical-align:-15%}.rst-content .admonition,.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .danger,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .note,.rst-content .seealso,.rst-content .tip,.rst-content .warning,.wy-alert{padding:12px;line-height:24px;margin-bottom:24px;background:#e7f2fa}.rst-content .admonition-title,.wy-alert-title{font-weight:700;display:block;color:#fff;background:#6ab0de;padding:6px 12px;margin:-12px -12px 12px}.rst-content .danger,.rst-content .error,.rst-content .wy-alert-danger.admonition,.rst-content .wy-alert-danger.admonition-todo,.rst-content .wy-alert-danger.attention,.rst-content .wy-alert-danger.caution,.rst-content .wy-alert-danger.hint,.rst-content .wy-alert-danger.important,.rst-content .wy-alert-danger.note,.rst-content .wy-alert-danger.seealso,.rst-content .wy-alert-danger.tip,.rst-content .wy-alert-danger.warning,.wy-alert.wy-alert-danger{background:#fdf3f2}.rst-content .danger .admonition-title,.rst-content .danger .wy-alert-title,.rst-content .error .admonition-title,.rst-content .error .wy-alert-title,.rst-content .wy-alert-danger.admonition-todo .admonition-title,.rst-content .wy-alert-danger.admonition-todo .wy-alert-title,.rst-content .wy-alert-danger.admonition .admonition-title,.rst-content .wy-alert-danger.admonition .wy-alert-title,.rst-content .wy-alert-danger.attention .admonition-title,.rst-content .wy-alert-danger.attention .wy-alert-title,.rst-content .wy-alert-danger.caution .admonition-title,.rst-content .wy-alert-danger.caution .wy-alert-title,.rst-content .wy-alert-danger.hint .admonition-title,.rst-content .wy-alert-danger.hint .wy-alert-title,.rst-content .wy-alert-danger.important .admonition-title,.rst-content .wy-alert-danger.important .wy-alert-title,.rst-content .wy-alert-danger.note .admonition-title,.rst-content .wy-alert-danger.note .wy-alert-title,.rst-content .wy-alert-danger.seealso .admonition-title,.rst-content .wy-alert-danger.seealso .wy-alert-title,.rst-content .wy-alert-danger.tip .admonition-title,.rst-content .wy-alert-danger.tip .wy-alert-title,.rst-content .wy-alert-danger.warning .admonition-title,.rst-content .wy-alert-danger.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-danger .admonition-title,.wy-alert.wy-alert-danger .rst-content .admonition-title,.wy-alert.wy-alert-danger .wy-alert-title{background:#f29f97}.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .warning,.rst-content .wy-alert-warning.admonition,.rst-content .wy-alert-warning.danger,.rst-content .wy-alert-warning.error,.rst-content .wy-alert-warning.hint,.rst-content .wy-alert-warning.important,.rst-content .wy-alert-warning.note,.rst-content .wy-alert-warning.seealso,.rst-content .wy-alert-warning.tip,.wy-alert.wy-alert-warning{background:#ffedcc}.rst-content .admonition-todo .admonition-title,.rst-content .admonition-todo .wy-alert-title,.rst-content .attention .admonition-title,.rst-content .attention .wy-alert-title,.rst-content .caution .admonition-title,.rst-content .caution .wy-alert-title,.rst-content .warning .admonition-title,.rst-content .warning .wy-alert-title,.rst-content .wy-alert-warning.admonition .admonition-title,.rst-content .wy-alert-warning.admonition .wy-alert-title,.rst-content .wy-alert-warning.danger .admonition-title,.rst-content .wy-alert-warning.danger .wy-alert-title,.rst-content .wy-alert-warning.error .admonition-title,.rst-content .wy-alert-warning.error .wy-alert-title,.rst-content .wy-alert-warning.hint .admonition-title,.rst-content .wy-alert-warning.hint .wy-alert-title,.rst-content .wy-alert-warning.important .admonition-title,.rst-content .wy-alert-warning.important .wy-alert-title,.rst-content .wy-alert-warning.note .admonition-title,.rst-content .wy-alert-warning.note .wy-alert-title,.rst-content .wy-alert-warning.seealso .admonition-title,.rst-content .wy-alert-warning.seealso .wy-alert-title,.rst-content .wy-alert-warning.tip .admonition-title,.rst-content .wy-alert-warning.tip .wy-alert-title,.rst-content .wy-alert.wy-alert-warning .admonition-title,.wy-alert.wy-alert-warning .rst-content .admonition-title,.wy-alert.wy-alert-warning .wy-alert-title{background:#f0b37e}.rst-content .note,.rst-content .seealso,.rst-content .wy-alert-info.admonition,.rst-content .wy-alert-info.admonition-todo,.rst-content .wy-alert-info.attention,.rst-content .wy-alert-info.caution,.rst-content .wy-alert-info.danger,.rst-content .wy-alert-info.error,.rst-content .wy-alert-info.hint,.rst-content .wy-alert-info.important,.rst-content .wy-alert-info.tip,.rst-content .wy-alert-info.warning,.wy-alert.wy-alert-info{background:#e7f2fa}.rst-content .note .admonition-title,.rst-content .note .wy-alert-title,.rst-content .seealso .admonition-title,.rst-content .seealso .wy-alert-title,.rst-content .wy-alert-info.admonition-todo .admonition-title,.rst-content .wy-alert-info.admonition-todo .wy-alert-title,.rst-content .wy-alert-info.admonition .admonition-title,.rst-content .wy-alert-info.admonition .wy-alert-title,.rst-content .wy-alert-info.attention .admonition-title,.rst-content .wy-alert-info.attention .wy-alert-title,.rst-content .wy-alert-info.caution .admonition-title,.rst-content .wy-alert-info.caution .wy-alert-title,.rst-content .wy-alert-info.danger .admonition-title,.rst-content .wy-alert-info.danger .wy-alert-title,.rst-content .wy-alert-info.error .admonition-title,.rst-content .wy-alert-info.error .wy-alert-title,.rst-content .wy-alert-info.hint .admonition-title,.rst-content .wy-alert-info.hint .wy-alert-title,.rst-content .wy-alert-info.important .admonition-title,.rst-content .wy-alert-info.important .wy-alert-title,.rst-content .wy-alert-info.tip .admonition-title,.rst-content .wy-alert-info.tip .wy-alert-title,.rst-content .wy-alert-info.warning .admonition-title,.rst-content .wy-alert-info.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-info .admonition-title,.wy-alert.wy-alert-info .rst-content .admonition-title,.wy-alert.wy-alert-info .wy-alert-title{background:#6ab0de}.rst-content .hint,.rst-content .important,.rst-content .tip,.rst-content .wy-alert-success.admonition,.rst-content .wy-alert-success.admonition-todo,.rst-content .wy-alert-success.attention,.rst-content .wy-alert-success.caution,.rst-content .wy-alert-success.danger,.rst-content .wy-alert-success.error,.rst-content .wy-alert-success.note,.rst-content .wy-alert-success.seealso,.rst-content .wy-alert-success.warning,.wy-alert.wy-alert-success{background:#dbfaf4}.rst-content .hint .admonition-title,.rst-content .hint .wy-alert-title,.rst-content .important .admonition-title,.rst-content .important .wy-alert-title,.rst-content .tip .admonition-title,.rst-content .tip .wy-alert-title,.rst-content .wy-alert-success.admonition-todo .admonition-title,.rst-content .wy-alert-success.admonition-todo .wy-alert-title,.rst-content .wy-alert-success.admonition .admonition-title,.rst-content .wy-alert-success.admonition .wy-alert-title,.rst-content .wy-alert-success.attention .admonition-title,.rst-content .wy-alert-success.attention .wy-alert-title,.rst-content .wy-alert-success.caution .admonition-title,.rst-content .wy-alert-success.caution .wy-alert-title,.rst-content .wy-alert-success.danger .admonition-title,.rst-content .wy-alert-success.danger .wy-alert-title,.rst-content .wy-alert-success.error .admonition-title,.rst-content .wy-alert-success.error .wy-alert-title,.rst-content .wy-alert-success.note .admonition-title,.rst-content .wy-alert-success.note .wy-alert-title,.rst-content .wy-alert-success.seealso .admonition-title,.rst-content .wy-alert-success.seealso .wy-alert-title,.rst-content .wy-alert-success.warning .admonition-title,.rst-content .wy-alert-success.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-success .admonition-title,.wy-alert.wy-alert-success .rst-content .admonition-title,.wy-alert.wy-alert-success .wy-alert-title{background:#1abc9c}.rst-content .wy-alert-neutral.admonition,.rst-content .wy-alert-neutral.admonition-todo,.rst-content .wy-alert-neutral.attention,.rst-content .wy-alert-neutral.caution,.rst-content .wy-alert-neutral.danger,.rst-content .wy-alert-neutral.error,.rst-content .wy-alert-neutral.hint,.rst-content .wy-alert-neutral.important,.rst-content .wy-alert-neutral.note,.rst-content .wy-alert-neutral.seealso,.rst-content .wy-alert-neutral.tip,.rst-content .wy-alert-neutral.warning,.wy-alert.wy-alert-neutral{background:#f3f6f6}.rst-content .wy-alert-neutral.admonition-todo .admonition-title,.rst-content .wy-alert-neutral.admonition-todo .wy-alert-title,.rst-content .wy-alert-neutral.admonition .admonition-title,.rst-content .wy-alert-neutral.admonition .wy-alert-title,.rst-content .wy-alert-neutral.attention .admonition-title,.rst-content .wy-alert-neutral.attention .wy-alert-title,.rst-content .wy-alert-neutral.caution .admonition-title,.rst-content .wy-alert-neutral.caution .wy-alert-title,.rst-content .wy-alert-neutral.danger .admonition-title,.rst-content .wy-alert-neutral.danger .wy-alert-title,.rst-content .wy-alert-neutral.error .admonition-title,.rst-content .wy-alert-neutral.error .wy-alert-title,.rst-content .wy-alert-neutral.hint .admonition-title,.rst-content .wy-alert-neutral.hint .wy-alert-title,.rst-content .wy-alert-neutral.important .admonition-title,.rst-content .wy-alert-neutral.important .wy-alert-title,.rst-content .wy-alert-neutral.note .admonition-title,.rst-content .wy-alert-neutral.note .wy-alert-title,.rst-content .wy-alert-neutral.seealso .admonition-title,.rst-content .wy-alert-neutral.seealso .wy-alert-title,.rst-content .wy-alert-neutral.tip .admonition-title,.rst-content .wy-alert-neutral.tip .wy-alert-title,.rst-content .wy-alert-neutral.warning .admonition-title,.rst-content .wy-alert-neutral.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-neutral .admonition-title,.wy-alert.wy-alert-neutral .rst-content .admonition-title,.wy-alert.wy-alert-neutral .wy-alert-title{color:#404040;background:#e1e4e5}.rst-content .wy-alert-neutral.admonition-todo a,.rst-content .wy-alert-neutral.admonition a,.rst-content .wy-alert-neutral.attention a,.rst-content .wy-alert-neutral.caution a,.rst-content .wy-alert-neutral.danger a,.rst-content .wy-alert-neutral.error a,.rst-content .wy-alert-neutral.hint a,.rst-content .wy-alert-neutral.important a,.rst-content .wy-alert-neutral.note a,.rst-content .wy-alert-neutral.seealso a,.rst-content .wy-alert-neutral.tip a,.rst-content .wy-alert-neutral.warning a,.wy-alert.wy-alert-neutral a{color:#2980b9}.rst-content .admonition-todo p:last-child,.rst-content .admonition p:last-child,.rst-content .attention p:last-child,.rst-content .caution p:last-child,.rst-content .danger p:last-child,.rst-content .error p:last-child,.rst-content .hint p:last-child,.rst-content .important p:last-child,.rst-content .note p:last-child,.rst-content .seealso p:last-child,.rst-content .tip p:last-child,.rst-content .warning p:last-child,.wy-alert p:last-child{margin-bottom:0}.wy-tray-container{position:fixed;bottom:0;left:0;z-index:600}.wy-tray-container li{display:block;width:300px;background:transparent;color:#fff;text-align:center;box-shadow:0 5px 5px 0 rgba(0,0,0,.1);padding:0 24px;min-width:20%;opacity:0;height:0;line-height:56px;overflow:hidden;-webkit-transition:all .3s ease-in;-moz-transition:all .3s ease-in;transition:all .3s ease-in}.wy-tray-container li.wy-tray-item-success{background:#27ae60}.wy-tray-container li.wy-tray-item-info{background:#2980b9}.wy-tray-container li.wy-tray-item-warning{background:#e67e22}.wy-tray-container li.wy-tray-item-danger{background:#e74c3c}.wy-tray-container li.on{opacity:1;height:56px}@media screen and (max-width:768px){.wy-tray-container{bottom:auto;top:0;width:100%}.wy-tray-container li{width:100%}}button{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle;cursor:pointer;line-height:normal;-webkit-appearance:button;*overflow:visible}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}button[disabled]{cursor:default}.btn{display:inline-block;border-radius:2px;line-height:normal;white-space:nowrap;text-align:center;cursor:pointer;font-size:100%;padding:6px 12px 8px;color:#fff;border:1px solid rgba(0,0,0,.1);background-color:#27ae60;text-decoration:none;font-weight:400;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;box-shadow:inset 0 1px 2px -1px hsla(0,0%,100%,.5),inset 0 -2px 0 0 rgba(0,0,0,.1);outline-none:false;vertical-align:middle;*display:inline;zoom:1;-webkit-user-drag:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-transition:all .1s linear;-moz-transition:all .1s linear;transition:all .1s linear}.btn-hover{background:#2e8ece;color:#fff}.btn:hover{background:#2cc36b;color:#fff}.btn:focus{background:#2cc36b;outline:0}.btn:active{box-shadow:inset 0 -1px 0 0 rgba(0,0,0,.05),inset 0 2px 0 0 rgba(0,0,0,.1);padding:8px 12px 6px}.btn:visited{color:#fff}.btn-disabled,.btn-disabled:active,.btn-disabled:focus,.btn-disabled:hover,.btn:disabled{background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);filter:alpha(opacity=40);opacity:.4;cursor:not-allowed;box-shadow:none}.btn::-moz-focus-inner{padding:0;border:0}.btn-small{font-size:80%}.btn-info{background-color:#2980b9!important}.btn-info:hover{background-color:#2e8ece!important}.btn-neutral{background-color:#f3f6f6!important;color:#404040!important}.btn-neutral:hover{background-color:#e5ebeb!important;color:#404040}.btn-neutral:visited{color:#404040!important}.btn-success{background-color:#27ae60!important}.btn-success:hover{background-color:#295!important}.btn-danger{background-color:#e74c3c!important}.btn-danger:hover{background-color:#ea6153!important}.btn-warning{background-color:#e67e22!important}.btn-warning:hover{background-color:#e98b39!important}.btn-invert{background-color:#222}.btn-invert:hover{background-color:#2f2f2f!important}.btn-link{background-color:transparent!important;color:#2980b9;box-shadow:none;border-color:transparent!important}.btn-link:active,.btn-link:hover{background-color:transparent!important;color:#409ad5!important;box-shadow:none}.btn-link:visited{color:#9b59b6}.wy-btn-group .btn,.wy-control .btn{vertical-align:middle}.wy-btn-group{margin-bottom:24px;*zoom:1}.wy-btn-group:after,.wy-btn-group:before{display:table;content:""}.wy-btn-group:after{clear:both}.wy-dropdown{position:relative;display:inline-block}.wy-dropdown-active .wy-dropdown-menu{display:block}.wy-dropdown-menu{position:absolute;left:0;display:none;float:left;top:100%;min-width:100%;background:#fcfcfc;z-index:100;border:1px solid #cfd7dd;box-shadow:0 2px 2px 0 rgba(0,0,0,.1);padding:12px}.wy-dropdown-menu>dd>a{display:block;clear:both;color:#404040;white-space:nowrap;font-size:90%;padding:0 12px;cursor:pointer}.wy-dropdown-menu>dd>a:hover{background:#2980b9;color:#fff}.wy-dropdown-menu>dd.divider{border-top:1px solid #cfd7dd;margin:6px 0}.wy-dropdown-menu>dd.search{padding-bottom:12px}.wy-dropdown-menu>dd.search input[type=search]{width:100%}.wy-dropdown-menu>dd.call-to-action{background:#e3e3e3;text-transform:uppercase;font-weight:500;font-size:80%}.wy-dropdown-menu>dd.call-to-action:hover{background:#e3e3e3}.wy-dropdown-menu>dd.call-to-action .btn{color:#fff}.wy-dropdown.wy-dropdown-up .wy-dropdown-menu{bottom:100%;top:auto;left:auto;right:0}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu{background:#fcfcfc;margin-top:2px}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu a{padding:6px 12px}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu a:hover{background:#2980b9;color:#fff}.wy-dropdown.wy-dropdown-left .wy-dropdown-menu{right:0;left:auto;text-align:right}.wy-dropdown-arrow:before{content:" ";border-bottom:5px solid #f5f5f5;border-left:5px solid transparent;border-right:5px solid transparent;position:absolute;display:block;top:-4px;left:50%;margin-left:-3px}.wy-dropdown-arrow.wy-dropdown-arrow-left:before{left:11px}.wy-form-stacked select{display:block}.wy-form-aligned .wy-help-inline,.wy-form-aligned input,.wy-form-aligned label,.wy-form-aligned select,.wy-form-aligned textarea{display:inline-block;*display:inline;*zoom:1;vertical-align:middle}.wy-form-aligned .wy-control-group>label{display:inline-block;vertical-align:middle;width:10em;margin:6px 12px 0 0;float:left}.wy-form-aligned .wy-control{float:left}.wy-form-aligned .wy-control label{display:block}.wy-form-aligned .wy-control select{margin-top:6px}fieldset{margin:0}fieldset,legend{border:0;padding:0}legend{width:100%;white-space:normal;margin-bottom:24px;font-size:150%;*margin-left:-7px}label,legend{display:block}label{margin:0 0 .3125em;color:#333;font-size:90%}input,select,textarea{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle}.wy-control-group{margin-bottom:24px;max-width:1200px;margin-left:auto;margin-right:auto;*zoom:1}.wy-control-group:after,.wy-control-group:before{display:table;content:""}.wy-control-group:after{clear:both}.wy-control-group.wy-control-group-required>label:after{content:" *";color:#e74c3c}.wy-control-group .wy-form-full,.wy-control-group .wy-form-halves,.wy-control-group .wy-form-thirds{padding-bottom:12px}.wy-control-group .wy-form-full input[type=color],.wy-control-group .wy-form-full input[type=date],.wy-control-group .wy-form-full input[type=datetime-local],.wy-control-group .wy-form-full input[type=datetime],.wy-control-group .wy-form-full input[type=email],.wy-control-group .wy-form-full input[type=month],.wy-control-group .wy-form-full input[type=number],.wy-control-group .wy-form-full input[type=password],.wy-control-group .wy-form-full input[type=search],.wy-control-group .wy-form-full input[type=tel],.wy-control-group .wy-form-full input[type=text],.wy-control-group .wy-form-full input[type=time],.wy-control-group .wy-form-full input[type=url],.wy-control-group .wy-form-full input[type=week],.wy-control-group .wy-form-full select,.wy-control-group .wy-form-halves input[type=color],.wy-control-group .wy-form-halves input[type=date],.wy-control-group .wy-form-halves input[type=datetime-local],.wy-control-group .wy-form-halves input[type=datetime],.wy-control-group .wy-form-halves input[type=email],.wy-control-group .wy-form-halves input[type=month],.wy-control-group .wy-form-halves input[type=number],.wy-control-group .wy-form-halves input[type=password],.wy-control-group .wy-form-halves input[type=search],.wy-control-group .wy-form-halves input[type=tel],.wy-control-group .wy-form-halves input[type=text],.wy-control-group .wy-form-halves input[type=time],.wy-control-group .wy-form-halves input[type=url],.wy-control-group .wy-form-halves input[type=week],.wy-control-group .wy-form-halves select,.wy-control-group .wy-form-thirds input[type=color],.wy-control-group .wy-form-thirds input[type=date],.wy-control-group .wy-form-thirds input[type=datetime-local],.wy-control-group .wy-form-thirds input[type=datetime],.wy-control-group .wy-form-thirds input[type=email],.wy-control-group .wy-form-thirds input[type=month],.wy-control-group .wy-form-thirds input[type=number],.wy-control-group .wy-form-thirds input[type=password],.wy-control-group .wy-form-thirds input[type=search],.wy-control-group .wy-form-thirds input[type=tel],.wy-control-group .wy-form-thirds input[type=text],.wy-control-group .wy-form-thirds input[type=time],.wy-control-group .wy-form-thirds input[type=url],.wy-control-group .wy-form-thirds input[type=week],.wy-control-group .wy-form-thirds select{width:100%}.wy-control-group .wy-form-full{float:left;display:block;width:100%;margin-right:0}.wy-control-group .wy-form-full:last-child{margin-right:0}.wy-control-group .wy-form-halves{float:left;display:block;margin-right:2.35765%;width:48.82117%}.wy-control-group .wy-form-halves:last-child,.wy-control-group .wy-form-halves:nth-of-type(2n){margin-right:0}.wy-control-group .wy-form-halves:nth-of-type(odd){clear:left}.wy-control-group .wy-form-thirds{float:left;display:block;margin-right:2.35765%;width:31.76157%}.wy-control-group .wy-form-thirds:last-child,.wy-control-group .wy-form-thirds:nth-of-type(3n){margin-right:0}.wy-control-group .wy-form-thirds:nth-of-type(3n+1){clear:left}.wy-control-group.wy-control-group-no-input .wy-control,.wy-control-no-input{margin:6px 0 0;font-size:90%}.wy-control-no-input{display:inline-block}.wy-control-group.fluid-input input[type=color],.wy-control-group.fluid-input input[type=date],.wy-control-group.fluid-input input[type=datetime-local],.wy-control-group.fluid-input input[type=datetime],.wy-control-group.fluid-input input[type=email],.wy-control-group.fluid-input input[type=month],.wy-control-group.fluid-input input[type=number],.wy-control-group.fluid-input input[type=password],.wy-control-group.fluid-input input[type=search],.wy-control-group.fluid-input input[type=tel],.wy-control-group.fluid-input input[type=text],.wy-control-group.fluid-input input[type=time],.wy-control-group.fluid-input input[type=url],.wy-control-group.fluid-input input[type=week]{width:100%}.wy-form-message-inline{padding-left:.3em;color:#666;font-size:90%}.wy-form-message{display:block;color:#999;font-size:70%;margin-top:.3125em;font-style:italic}.wy-form-message p{font-size:inherit;font-style:italic;margin-bottom:6px}.wy-form-message p:last-child{margin-bottom:0}input{line-height:normal}input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;*overflow:visible}input[type=color],input[type=date],input[type=datetime-local],input[type=datetime],input[type=email],input[type=month],input[type=number],input[type=password],input[type=search],input[type=tel],input[type=text],input[type=time],input[type=url],input[type=week]{-webkit-appearance:none;padding:6px;display:inline-block;border:1px solid #ccc;font-size:80%;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;box-shadow:inset 0 1px 3px #ddd;border-radius:0;-webkit-transition:border .3s linear;-moz-transition:border .3s linear;transition:border .3s linear}input[type=datetime-local]{padding:.34375em .625em}input[disabled]{cursor:default}input[type=checkbox],input[type=radio]{padding:0;margin-right:.3125em;*height:13px;*width:13px}input[type=checkbox],input[type=radio],input[type=search]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}input[type=color]:focus,input[type=date]:focus,input[type=datetime-local]:focus,input[type=datetime]:focus,input[type=email]:focus,input[type=month]:focus,input[type=number]:focus,input[type=password]:focus,input[type=search]:focus,input[type=tel]:focus,input[type=text]:focus,input[type=time]:focus,input[type=url]:focus,input[type=week]:focus{outline:0;outline:thin dotted\9;border-color:#333}input.no-focus:focus{border-color:#ccc!important}input[type=checkbox]:focus,input[type=file]:focus,input[type=radio]:focus{outline:thin dotted #333;outline:1px auto #129fea}input[type=color][disabled],input[type=date][disabled],input[type=datetime-local][disabled],input[type=datetime][disabled],input[type=email][disabled],input[type=month][disabled],input[type=number][disabled],input[type=password][disabled],input[type=search][disabled],input[type=tel][disabled],input[type=text][disabled],input[type=time][disabled],input[type=url][disabled],input[type=week][disabled]{cursor:not-allowed;background-color:#fafafa}input:focus:invalid,select:focus:invalid,textarea:focus:invalid{color:#e74c3c;border:1px solid #e74c3c}input:focus:invalid:focus,select:focus:invalid:focus,textarea:focus:invalid:focus{border-color:#e74c3c}input[type=checkbox]:focus:invalid:focus,input[type=file]:focus:invalid:focus,input[type=radio]:focus:invalid:focus{outline-color:#e74c3c}input.wy-input-large{padding:12px;font-size:100%}textarea{overflow:auto;vertical-align:top;width:100%;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif}select,textarea{padding:.5em .625em;display:inline-block;border:1px solid #ccc;font-size:80%;box-shadow:inset 0 1px 3px #ddd;-webkit-transition:border .3s linear;-moz-transition:border .3s linear;transition:border .3s linear}select{border:1px solid #ccc;background-color:#fff}select[multiple]{height:auto}select:focus,textarea:focus{outline:0}input[readonly],select[disabled],select[readonly],textarea[disabled],textarea[readonly]{cursor:not-allowed;background-color:#fafafa}input[type=checkbox][disabled],input[type=radio][disabled]{cursor:not-allowed}.wy-checkbox,.wy-radio{margin:6px 0;color:#404040;display:block}.wy-checkbox input,.wy-radio input{vertical-align:baseline}.wy-form-message-inline{display:inline-block;*display:inline;*zoom:1;vertical-align:middle}.wy-input-prefix,.wy-input-suffix{white-space:nowrap;padding:6px}.wy-input-prefix .wy-input-context,.wy-input-suffix .wy-input-context{line-height:27px;padding:0 8px;display:inline-block;font-size:80%;background-color:#f3f6f6;border:1px solid #ccc;color:#999}.wy-input-suffix .wy-input-context{border-left:0}.wy-input-prefix .wy-input-context{border-right:0}.wy-switch{position:relative;display:block;height:24px;margin-top:12px;cursor:pointer}.wy-switch:before{left:0;top:0;width:36px;height:12px;background:#ccc}.wy-switch:after,.wy-switch:before{position:absolute;content:"";display:block;border-radius:4px;-webkit-transition:all .2s ease-in-out;-moz-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.wy-switch:after{width:18px;height:18px;background:#999;left:-3px;top:-3px}.wy-switch span{position:absolute;left:48px;display:block;font-size:12px;color:#ccc;line-height:1}.wy-switch.active:before{background:#1e8449}.wy-switch.active:after{left:24px;background:#27ae60}.wy-switch.disabled{cursor:not-allowed;opacity:.8}.wy-control-group.wy-control-group-error .wy-form-message,.wy-control-group.wy-control-group-error>label{color:#e74c3c}.wy-control-group.wy-control-group-error input[type=color],.wy-control-group.wy-control-group-error input[type=date],.wy-control-group.wy-control-group-error input[type=datetime-local],.wy-control-group.wy-control-group-error input[type=datetime],.wy-control-group.wy-control-group-error input[type=email],.wy-control-group.wy-control-group-error input[type=month],.wy-control-group.wy-control-group-error input[type=number],.wy-control-group.wy-control-group-error input[type=password],.wy-control-group.wy-control-group-error input[type=search],.wy-control-group.wy-control-group-error input[type=tel],.wy-control-group.wy-control-group-error input[type=text],.wy-control-group.wy-control-group-error input[type=time],.wy-control-group.wy-control-group-error input[type=url],.wy-control-group.wy-control-group-error input[type=week],.wy-control-group.wy-control-group-error textarea{border:1px solid #e74c3c}.wy-inline-validate{white-space:nowrap}.wy-inline-validate .wy-input-context{padding:.5em .625em;display:inline-block;font-size:80%}.wy-inline-validate.wy-inline-validate-success .wy-input-context{color:#27ae60}.wy-inline-validate.wy-inline-validate-danger .wy-input-context{color:#e74c3c}.wy-inline-validate.wy-inline-validate-warning .wy-input-context{color:#e67e22}.wy-inline-validate.wy-inline-validate-info .wy-input-context{color:#2980b9}.rotate-90{-webkit-transform:rotate(90deg);-moz-transform:rotate(90deg);-ms-transform:rotate(90deg);-o-transform:rotate(90deg);transform:rotate(90deg)}.rotate-180{-webkit-transform:rotate(180deg);-moz-transform:rotate(180deg);-ms-transform:rotate(180deg);-o-transform:rotate(180deg);transform:rotate(180deg)}.rotate-270{-webkit-transform:rotate(270deg);-moz-transform:rotate(270deg);-ms-transform:rotate(270deg);-o-transform:rotate(270deg);transform:rotate(270deg)}.mirror{-webkit-transform:scaleX(-1);-moz-transform:scaleX(-1);-ms-transform:scaleX(-1);-o-transform:scaleX(-1);transform:scaleX(-1)}.mirror.rotate-90{-webkit-transform:scaleX(-1) rotate(90deg);-moz-transform:scaleX(-1) rotate(90deg);-ms-transform:scaleX(-1) rotate(90deg);-o-transform:scaleX(-1) rotate(90deg);transform:scaleX(-1) rotate(90deg)}.mirror.rotate-180{-webkit-transform:scaleX(-1) rotate(180deg);-moz-transform:scaleX(-1) rotate(180deg);-ms-transform:scaleX(-1) rotate(180deg);-o-transform:scaleX(-1) rotate(180deg);transform:scaleX(-1) rotate(180deg)}.mirror.rotate-270{-webkit-transform:scaleX(-1) rotate(270deg);-moz-transform:scaleX(-1) rotate(270deg);-ms-transform:scaleX(-1) rotate(270deg);-o-transform:scaleX(-1) rotate(270deg);transform:scaleX(-1) rotate(270deg)}@media only screen and (max-width:480px){.wy-form button[type=submit]{margin:.7em 0 0}.wy-form input[type=color],.wy-form input[type=date],.wy-form input[type=datetime-local],.wy-form input[type=datetime],.wy-form input[type=email],.wy-form input[type=month],.wy-form input[type=number],.wy-form input[type=password],.wy-form input[type=search],.wy-form input[type=tel],.wy-form input[type=text],.wy-form input[type=time],.wy-form input[type=url],.wy-form input[type=week],.wy-form label{margin-bottom:.3em;display:block}.wy-form input[type=color],.wy-form input[type=date],.wy-form input[type=datetime-local],.wy-form input[type=datetime],.wy-form input[type=email],.wy-form input[type=month],.wy-form input[type=number],.wy-form input[type=password],.wy-form input[type=search],.wy-form input[type=tel],.wy-form input[type=time],.wy-form input[type=url],.wy-form input[type=week]{margin-bottom:0}.wy-form-aligned .wy-control-group label{margin-bottom:.3em;text-align:left;display:block;width:100%}.wy-form-aligned .wy-control{margin:1.5em 0 0}.wy-form-message,.wy-form-message-inline,.wy-form .wy-help-inline{display:block;font-size:80%;padding:6px 0}}@media screen and (max-width:768px){.tablet-hide{display:none}}@media screen and (max-width:480px){.mobile-hide{display:none}}.float-left{float:left}.float-right{float:right}.full-width{width:100%}.rst-content table.docutils,.rst-content table.field-list,.wy-table{border-collapse:collapse;border-spacing:0;empty-cells:show;margin-bottom:24px}.rst-content table.docutils caption,.rst-content table.field-list caption,.wy-table caption{color:#000;font:italic 85%/1 arial,sans-serif;padding:1em 0;text-align:center}.rst-content table.docutils td,.rst-content table.docutils th,.rst-content table.field-list td,.rst-content table.field-list th,.wy-table td,.wy-table th{font-size:90%;margin:0;overflow:visible;padding:8px 16px}.rst-content table.docutils td:first-child,.rst-content table.docutils th:first-child,.rst-content table.field-list td:first-child,.rst-content table.field-list th:first-child,.wy-table td:first-child,.wy-table th:first-child{border-left-width:0}.rst-content table.docutils thead,.rst-content table.field-list thead,.wy-table thead{color:#000;text-align:left;vertical-align:bottom;white-space:nowrap}.rst-content table.docutils thead th,.rst-content table.field-list thead th,.wy-table thead th{font-weight:700;border-bottom:2px solid #e1e4e5}.rst-content table.docutils td,.rst-content table.field-list td,.wy-table td{background-color:transparent;vertical-align:middle}.rst-content table.docutils td p,.rst-content table.field-list td p,.wy-table td p{line-height:18px}.rst-content table.docutils td p:last-child,.rst-content table.field-list td p:last-child,.wy-table td p:last-child{margin-bottom:0}.rst-content table.docutils .wy-table-cell-min,.rst-content table.field-list .wy-table-cell-min,.wy-table .wy-table-cell-min{width:1%;padding-right:0}.rst-content table.docutils .wy-table-cell-min input[type=checkbox],.rst-content table.field-list .wy-table-cell-min input[type=checkbox],.wy-table .wy-table-cell-min input[type=checkbox]{margin:0}.wy-table-secondary{color:grey;font-size:90%}.wy-table-tertiary{color:grey;font-size:80%}.rst-content table.docutils:not(.field-list) tr:nth-child(2n-1) td,.wy-table-backed,.wy-table-odd td,.wy-table-striped tr:nth-child(2n-1) td{background-color:#f3f6f6}.rst-content table.docutils,.wy-table-bordered-all{border:1px solid #e1e4e5}.rst-content table.docutils td,.wy-table-bordered-all td{border-bottom:1px solid #e1e4e5;border-left:1px solid #e1e4e5}.rst-content table.docutils tbody>tr:last-child td,.wy-table-bordered-all tbody>tr:last-child td{border-bottom-width:0}.wy-table-bordered{border:1px solid #e1e4e5}.wy-table-bordered-rows td{border-bottom:1px solid #e1e4e5}.wy-table-bordered-rows tbody>tr:last-child td{border-bottom-width:0}.wy-table-horizontal td,.wy-table-horizontal th{border-width:0 0 1px;border-bottom:1px solid #e1e4e5}.wy-table-horizontal tbody>tr:last-child td{border-bottom-width:0}.wy-table-responsive{margin-bottom:24px;max-width:100%;overflow:auto}.wy-table-responsive table{margin-bottom:0!important}.wy-table-responsive table td,.wy-table-responsive table th{white-space:nowrap}a{color:#2980b9;text-decoration:none;cursor:pointer}a:hover{color:#3091d1}a:visited{color:#9b59b6}html{height:100%}body,html{overflow-x:hidden}body{font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;font-weight:400;color:#404040;min-height:100%;background:#edf0f2}.wy-text-left{text-align:left}.wy-text-center{text-align:center}.wy-text-right{text-align:right}.wy-text-large{font-size:120%}.wy-text-normal{font-size:100%}.wy-text-small,small{font-size:80%}.wy-text-strike{text-decoration:line-through}.wy-text-warning{color:#e67e22!important}a.wy-text-warning:hover{color:#eb9950!important}.wy-text-info{color:#2980b9!important}a.wy-text-info:hover{color:#409ad5!important}.wy-text-success{color:#27ae60!important}a.wy-text-success:hover{color:#36d278!important}.wy-text-danger{color:#e74c3c!important}a.wy-text-danger:hover{color:#ed7669!important}.wy-text-neutral{color:#404040!important}a.wy-text-neutral:hover{color:#595959!important}.rst-content .toctree-wrapper>p.caption,h1,h2,h3,h4,h5,h6,legend{margin-top:0;font-weight:700;font-family:Roboto Slab,ff-tisa-web-pro,Georgia,Arial,sans-serif}p{line-height:24px;font-size:16px;margin:0 0 24px}h1{font-size:175%}.rst-content .toctree-wrapper>p.caption,h2{font-size:150%}h3{font-size:125%}h4{font-size:115%}h5{font-size:110%}h6{font-size:100%}hr{display:block;height:1px;border:0;border-top:1px solid #e1e4e5;margin:24px 0;padding:0}.rst-content code,.rst-content tt,code{white-space:nowrap;max-width:100%;background:#fff;border:1px solid #e1e4e5;font-size:75%;padding:0 5px;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;color:#e74c3c;overflow-x:auto}.rst-content tt.code-large,code.code-large{font-size:90%}.rst-content .section ul,.rst-content .toctree-wrapper ul,.rst-content section ul,.wy-plain-list-disc,article ul{list-style:disc;line-height:24px;margin-bottom:24px}.rst-content .section ul li,.rst-content .toctree-wrapper ul li,.rst-content section ul li,.wy-plain-list-disc li,article ul li{list-style:disc;margin-left:24px}.rst-content .section ul li p:last-child,.rst-content .section ul li ul,.rst-content .toctree-wrapper ul li p:last-child,.rst-content .toctree-wrapper ul li ul,.rst-content section ul li p:last-child,.rst-content section ul li ul,.wy-plain-list-disc li p:last-child,.wy-plain-list-disc li ul,article ul li p:last-child,article ul li ul{margin-bottom:0}.rst-content .section ul li li,.rst-content .toctree-wrapper ul li li,.rst-content section ul li li,.wy-plain-list-disc li li,article ul li li{list-style:circle}.rst-content .section ul li li li,.rst-content .toctree-wrapper ul li li li,.rst-content section ul li li li,.wy-plain-list-disc li li li,article ul li li li{list-style:square}.rst-content .section ul li ol li,.rst-content .toctree-wrapper ul li ol li,.rst-content section ul li ol li,.wy-plain-list-disc li ol li,article ul li ol li{list-style:decimal}.rst-content .section ol,.rst-content .section ol.arabic,.rst-content .toctree-wrapper ol,.rst-content .toctree-wrapper ol.arabic,.rst-content section ol,.rst-content section ol.arabic,.wy-plain-list-decimal,article ol{list-style:decimal;line-height:24px;margin-bottom:24px}.rst-content .section ol.arabic li,.rst-content .section ol li,.rst-content .toctree-wrapper ol.arabic li,.rst-content .toctree-wrapper ol li,.rst-content section ol.arabic li,.rst-content section ol li,.wy-plain-list-decimal li,article ol li{list-style:decimal;margin-left:24px}.rst-content .section ol.arabic li ul,.rst-content .section ol li p:last-child,.rst-content .section ol li ul,.rst-content .toctree-wrapper ol.arabic li ul,.rst-content .toctree-wrapper ol li p:last-child,.rst-content .toctree-wrapper ol li ul,.rst-content section ol.arabic li ul,.rst-content section ol li p:last-child,.rst-content section ol li ul,.wy-plain-list-decimal li p:last-child,.wy-plain-list-decimal li ul,article ol li p:last-child,article ol li ul{margin-bottom:0}.rst-content .section ol.arabic li ul li,.rst-content .section ol li ul li,.rst-content .toctree-wrapper ol.arabic li ul li,.rst-content .toctree-wrapper ol li ul li,.rst-content section ol.arabic li ul li,.rst-content section ol li ul li,.wy-plain-list-decimal li ul li,article ol li ul li{list-style:disc}.wy-breadcrumbs{*zoom:1}.wy-breadcrumbs:after,.wy-breadcrumbs:before{display:table;content:""}.wy-breadcrumbs:after{clear:both}.wy-breadcrumbs>li{display:inline-block;padding-top:5px}.wy-breadcrumbs>li.wy-breadcrumbs-aside{float:right}.rst-content .wy-breadcrumbs>li code,.rst-content .wy-breadcrumbs>li tt,.wy-breadcrumbs>li .rst-content tt,.wy-breadcrumbs>li code{all:inherit;color:inherit}.breadcrumb-item:before{content:"/";color:#bbb;font-size:13px;padding:0 6px 0 3px}.wy-breadcrumbs-extra{margin-bottom:0;color:#b3b3b3;font-size:80%;display:inline-block}@media screen and (max-width:480px){.wy-breadcrumbs-extra,.wy-breadcrumbs li.wy-breadcrumbs-aside{display:none}}@media print{.wy-breadcrumbs li.wy-breadcrumbs-aside{display:none}}html{font-size:16px}.wy-affix{position:fixed;top:1.618em}.wy-menu a:hover{text-decoration:none}.wy-menu-horiz{*zoom:1}.wy-menu-horiz:after,.wy-menu-horiz:before{display:table;content:""}.wy-menu-horiz:after{clear:both}.wy-menu-horiz li,.wy-menu-horiz ul{display:inline-block}.wy-menu-horiz li:hover{background:hsla(0,0%,100%,.1)}.wy-menu-horiz li.divide-left{border-left:1px solid #404040}.wy-menu-horiz li.divide-right{border-right:1px solid #404040}.wy-menu-horiz a{height:32px;display:inline-block;line-height:32px;padding:0 16px}.wy-menu-vertical{width:300px}.wy-menu-vertical header,.wy-menu-vertical p.caption{color:#55a5d9;height:32px;line-height:32px;padding:0 1.618em;margin:12px 0 0;display:block;font-weight:700;text-transform:uppercase;font-size:85%;white-space:nowrap}.wy-menu-vertical ul{margin-bottom:0}.wy-menu-vertical li.divide-top{border-top:1px solid #404040}.wy-menu-vertical li.divide-bottom{border-bottom:1px solid #404040}.wy-menu-vertical li.current{background:#e3e3e3}.wy-menu-vertical li.current a{color:grey;border-right:1px solid #c9c9c9;padding:.4045em 2.427em}.wy-menu-vertical li.current a:hover{background:#d6d6d6}.rst-content .wy-menu-vertical li tt,.wy-menu-vertical li .rst-content tt,.wy-menu-vertical li code{border:none;background:inherit;color:inherit;padding-left:0;padding-right:0}.wy-menu-vertical li button.toctree-expand{display:block;float:left;margin-left:-1.2em;line-height:18px;color:#4d4d4d;border:none;background:none;padding:0}.wy-menu-vertical li.current>a,.wy-menu-vertical li.on a{color:#404040;font-weight:700;position:relative;background:#fcfcfc;border:none;padding:.4045em 1.618em}.wy-menu-vertical li.current>a:hover,.wy-menu-vertical li.on a:hover{background:#fcfcfc}.wy-menu-vertical li.current>a:hover button.toctree-expand,.wy-menu-vertical li.on a:hover button.toctree-expand{color:grey}.wy-menu-vertical li.current>a button.toctree-expand,.wy-menu-vertical li.on a button.toctree-expand{display:block;line-height:18px;color:#333}.wy-menu-vertical li.toctree-l1.current>a{border-bottom:1px solid #c9c9c9;border-top:1px solid #c9c9c9}.wy-menu-vertical .toctree-l1.current .toctree-l2>ul,.wy-menu-vertical .toctree-l2.current .toctree-l3>ul,.wy-menu-vertical .toctree-l3.current .toctree-l4>ul,.wy-menu-vertical .toctree-l4.current .toctree-l5>ul,.wy-menu-vertical .toctree-l5.current .toctree-l6>ul,.wy-menu-vertical .toctree-l6.current .toctree-l7>ul,.wy-menu-vertical .toctree-l7.current .toctree-l8>ul,.wy-menu-vertical .toctree-l8.current .toctree-l9>ul,.wy-menu-vertical .toctree-l9.current .toctree-l10>ul,.wy-menu-vertical .toctree-l10.current .toctree-l11>ul{display:none}.wy-menu-vertical .toctree-l1.current .current.toctree-l2>ul,.wy-menu-vertical .toctree-l2.current .current.toctree-l3>ul,.wy-menu-vertical .toctree-l3.current .current.toctree-l4>ul,.wy-menu-vertical .toctree-l4.current .current.toctree-l5>ul,.wy-menu-vertical .toctree-l5.current .current.toctree-l6>ul,.wy-menu-vertical .toctree-l6.current .current.toctree-l7>ul,.wy-menu-vertical .toctree-l7.current .current.toctree-l8>ul,.wy-menu-vertical .toctree-l8.current .current.toctree-l9>ul,.wy-menu-vertical .toctree-l9.current .current.toctree-l10>ul,.wy-menu-vertical .toctree-l10.current .current.toctree-l11>ul{display:block}.wy-menu-vertical li.toctree-l3,.wy-menu-vertical li.toctree-l4{font-size:.9em}.wy-menu-vertical li.toctree-l2 a,.wy-menu-vertical li.toctree-l3 a,.wy-menu-vertical li.toctree-l4 a,.wy-menu-vertical li.toctree-l5 a,.wy-menu-vertical li.toctree-l6 a,.wy-menu-vertical li.toctree-l7 a,.wy-menu-vertical li.toctree-l8 a,.wy-menu-vertical li.toctree-l9 a,.wy-menu-vertical li.toctree-l10 a{color:#404040}.wy-menu-vertical li.toctree-l2 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l3 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l4 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l5 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l6 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l7 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l8 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l9 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l10 a:hover button.toctree-expand{color:grey}.wy-menu-vertical li.toctree-l2.current li.toctree-l3>a,.wy-menu-vertical li.toctree-l3.current li.toctree-l4>a,.wy-menu-vertical li.toctree-l4.current li.toctree-l5>a,.wy-menu-vertical li.toctree-l5.current li.toctree-l6>a,.wy-menu-vertical li.toctree-l6.current li.toctree-l7>a,.wy-menu-vertical li.toctree-l7.current li.toctree-l8>a,.wy-menu-vertical li.toctree-l8.current li.toctree-l9>a,.wy-menu-vertical li.toctree-l9.current li.toctree-l10>a,.wy-menu-vertical li.toctree-l10.current li.toctree-l11>a{display:block}.wy-menu-vertical li.toctree-l2.current>a{padding:.4045em 2.427em}.wy-menu-vertical li.toctree-l2.current li.toctree-l3>a{padding:.4045em 1.618em .4045em 4.045em}.wy-menu-vertical li.toctree-l3.current>a{padding:.4045em 4.045em}.wy-menu-vertical li.toctree-l3.current li.toctree-l4>a{padding:.4045em 1.618em .4045em 5.663em}.wy-menu-vertical li.toctree-l4.current>a{padding:.4045em 5.663em}.wy-menu-vertical li.toctree-l4.current li.toctree-l5>a{padding:.4045em 1.618em .4045em 7.281em}.wy-menu-vertical li.toctree-l5.current>a{padding:.4045em 7.281em}.wy-menu-vertical li.toctree-l5.current li.toctree-l6>a{padding:.4045em 1.618em .4045em 8.899em}.wy-menu-vertical li.toctree-l6.current>a{padding:.4045em 8.899em}.wy-menu-vertical li.toctree-l6.current li.toctree-l7>a{padding:.4045em 1.618em .4045em 10.517em}.wy-menu-vertical li.toctree-l7.current>a{padding:.4045em 10.517em}.wy-menu-vertical li.toctree-l7.current li.toctree-l8>a{padding:.4045em 1.618em .4045em 12.135em}.wy-menu-vertical li.toctree-l8.current>a{padding:.4045em 12.135em}.wy-menu-vertical li.toctree-l8.current li.toctree-l9>a{padding:.4045em 1.618em .4045em 13.753em}.wy-menu-vertical li.toctree-l9.current>a{padding:.4045em 13.753em}.wy-menu-vertical li.toctree-l9.current li.toctree-l10>a{padding:.4045em 1.618em .4045em 15.371em}.wy-menu-vertical li.toctree-l10.current>a{padding:.4045em 15.371em}.wy-menu-vertical li.toctree-l10.current li.toctree-l11>a{padding:.4045em 1.618em .4045em 16.989em}.wy-menu-vertical li.toctree-l2.current>a,.wy-menu-vertical li.toctree-l2.current li.toctree-l3>a{background:#c9c9c9}.wy-menu-vertical li.toctree-l2 button.toctree-expand{color:#a3a3a3}.wy-menu-vertical li.toctree-l3.current>a,.wy-menu-vertical li.toctree-l3.current li.toctree-l4>a{background:#bdbdbd}.wy-menu-vertical li.toctree-l3 button.toctree-expand{color:#969696}.wy-menu-vertical li.current ul{display:block}.wy-menu-vertical li ul{margin-bottom:0;display:none}.wy-menu-vertical li ul li a{margin-bottom:0;color:#d9d9d9;font-weight:400}.wy-menu-vertical a{line-height:18px;padding:.4045em 1.618em;display:block;position:relative;font-size:90%;color:#d9d9d9}.wy-menu-vertical a:hover{background-color:#4e4a4a;cursor:pointer}.wy-menu-vertical a:hover button.toctree-expand{color:#d9d9d9}.wy-menu-vertical a:active{background-color:#2980b9;cursor:pointer;color:#fff}.wy-menu-vertical a:active button.toctree-expand{color:#fff}.wy-side-nav-search{display:block;width:300px;padding:.809em;margin-bottom:.809em;z-index:200;background-color:#2980b9;text-align:center;color:#fcfcfc}.wy-side-nav-search input[type=text]{width:100%;border-radius:50px;padding:6px 12px;border-color:#2472a4}.wy-side-nav-search img{display:block;margin:auto auto .809em;height:45px;width:45px;background-color:#2980b9;padding:5px;border-radius:100%}.wy-side-nav-search .wy-dropdown>a,.wy-side-nav-search>a{color:#fcfcfc;font-size:100%;font-weight:700;display:inline-block;padding:4px 6px;margin-bottom:.809em;max-width:100%}.wy-side-nav-search .wy-dropdown>a:hover,.wy-side-nav-search .wy-dropdown>aactive,.wy-side-nav-search .wy-dropdown>afocus,.wy-side-nav-search>a:hover,.wy-side-nav-search>aactive,.wy-side-nav-search>afocus{background:hsla(0,0%,100%,.1)}.wy-side-nav-search .wy-dropdown>a img.logo,.wy-side-nav-search>a img.logo{display:block;margin:0 auto;height:auto;width:auto;border-radius:0;max-width:100%;background:transparent}.wy-side-nav-search .wy-dropdown>a.icon,.wy-side-nav-search>a.icon{display:block}.wy-side-nav-search .wy-dropdown>a.icon img.logo,.wy-side-nav-search>a.icon img.logo{margin-top:.85em}.wy-side-nav-search>div.switch-menus{position:relative;display:block;margin-top:-.4045em;margin-bottom:.809em;font-weight:400;color:hsla(0,0%,100%,.3)}.wy-side-nav-search>div.switch-menus>div.language-switch,.wy-side-nav-search>div.switch-menus>div.version-switch{display:inline-block;padding:.2em}.wy-side-nav-search>div.switch-menus>div.language-switch select,.wy-side-nav-search>div.switch-menus>div.version-switch select{display:inline-block;margin-right:-2rem;padding-right:2rem;max-width:240px;text-align-last:center;background:none;border:none;border-radius:0;box-shadow:none;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;font-size:1em;font-weight:400;color:hsla(0,0%,100%,.3);cursor:pointer;appearance:none;-webkit-appearance:none;-moz-appearance:none}.wy-side-nav-search>div.switch-menus>div.language-switch select:active,.wy-side-nav-search>div.switch-menus>div.language-switch select:focus,.wy-side-nav-search>div.switch-menus>div.language-switch select:hover,.wy-side-nav-search>div.switch-menus>div.version-switch select:active,.wy-side-nav-search>div.switch-menus>div.version-switch select:focus,.wy-side-nav-search>div.switch-menus>div.version-switch select:hover{background:hsla(0,0%,100%,.1);color:hsla(0,0%,100%,.5)}.wy-side-nav-search>div.switch-menus>div.language-switch select option,.wy-side-nav-search>div.switch-menus>div.version-switch select option{color:#000}.wy-side-nav-search>div.switch-menus>div.language-switch:has(>select):after,.wy-side-nav-search>div.switch-menus>div.version-switch:has(>select):after{display:inline-block;width:1.5em;height:100%;padding:.1em;content:"\f0d7";font-size:1em;line-height:1.2em;font-family:FontAwesome;text-align:center;pointer-events:none;box-sizing:border-box}.wy-nav .wy-menu-vertical header{color:#2980b9}.wy-nav .wy-menu-vertical a{color:#b3b3b3}.wy-nav .wy-menu-vertical a:hover{background-color:#2980b9;color:#fff}[data-menu-wrap]{-webkit-transition:all .2s ease-in;-moz-transition:all .2s ease-in;transition:all .2s ease-in;position:absolute;opacity:1;width:100%;opacity:0}[data-menu-wrap].move-center{left:0;right:auto;opacity:1}[data-menu-wrap].move-left{right:auto;left:-100%;opacity:0}[data-menu-wrap].move-right{right:-100%;left:auto;opacity:0}.wy-body-for-nav{background:#fcfcfc}.wy-grid-for-nav{position:absolute;width:100%;height:100%}.wy-nav-side{position:fixed;top:0;bottom:0;left:0;padding-bottom:2em;width:300px;overflow-x:hidden;overflow-y:hidden;min-height:100%;color:#9b9b9b;background:#343131;z-index:200}.wy-side-scroll{width:320px;position:relative;overflow-x:hidden;overflow-y:scroll;height:100%}.wy-nav-top{display:none;background:#2980b9;color:#fff;padding:.4045em .809em;position:relative;line-height:50px;text-align:center;font-size:100%;*zoom:1}.wy-nav-top:after,.wy-nav-top:before{display:table;content:""}.wy-nav-top:after{clear:both}.wy-nav-top a{color:#fff;font-weight:700}.wy-nav-top img{margin-right:12px;height:45px;width:45px;background-color:#2980b9;padding:5px;border-radius:100%}.wy-nav-top i{font-size:30px;float:left;cursor:pointer;padding-top:inherit}.wy-nav-content-wrap{margin-left:300px;background:#fcfcfc;min-height:100%}.wy-nav-content{padding:1.618em 3.236em;height:100%;max-width:800px;margin:auto}.wy-body-mask{position:fixed;width:100%;height:100%;background:rgba(0,0,0,.2);display:none;z-index:499}.wy-body-mask.on{display:block}footer{color:grey}footer p{margin-bottom:12px}.rst-content footer span.commit tt,footer span.commit .rst-content tt,footer span.commit code{padding:0;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;font-size:1em;background:none;border:none;color:grey}.rst-footer-buttons{*zoom:1}.rst-footer-buttons:after,.rst-footer-buttons:before{width:100%;display:table;content:""}.rst-footer-buttons:after{clear:both}.rst-breadcrumbs-buttons{margin-top:12px;*zoom:1}.rst-breadcrumbs-buttons:after,.rst-breadcrumbs-buttons:before{display:table;content:""}.rst-breadcrumbs-buttons:after{clear:both}#search-results .search li{margin-bottom:24px;border-bottom:1px solid #e1e4e5;padding-bottom:24px}#search-results .search li:first-child{border-top:1px solid #e1e4e5;padding-top:24px}#search-results .search li a{font-size:120%;margin-bottom:12px;display:inline-block}#search-results .context{color:grey;font-size:90%}.genindextable li>ul{margin-left:24px}@media screen and (max-width:768px){.wy-body-for-nav{background:#fcfcfc}.wy-nav-top{display:block}.wy-nav-side{left:-300px}.wy-nav-side.shift{width:85%;left:0}.wy-menu.wy-menu-vertical,.wy-side-nav-search,.wy-side-scroll{width:auto}.wy-nav-content-wrap{margin-left:0}.wy-nav-content-wrap .wy-nav-content{padding:1.618em}.wy-nav-content-wrap.shift{position:fixed;min-width:100%;left:85%;top:0;height:100%;overflow:hidden}}@media screen and (min-width:1100px){.wy-nav-content-wrap{background:rgba(0,0,0,.05)}.wy-nav-content{margin:0;background:#fcfcfc}}@media print{.rst-versions,.wy-nav-side,footer{display:none}.wy-nav-content-wrap{margin-left:0}}.rst-versions{position:fixed;bottom:0;left:0;width:300px;color:#fcfcfc;background:#1f1d1d;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;z-index:400}.rst-versions a{color:#2980b9;text-decoration:none}.rst-versions .rst-badge-small{display:none}.rst-versions .rst-current-version{padding:12px;background-color:#272525;display:block;text-align:right;font-size:90%;cursor:pointer;color:#27ae60;*zoom:1}.rst-versions .rst-current-version:after,.rst-versions .rst-current-version:before{display:table;content:""}.rst-versions .rst-current-version:after{clear:both}.rst-content .code-block-caption .rst-versions .rst-current-version .headerlink,.rst-content .eqno .rst-versions .rst-current-version .headerlink,.rst-content .rst-versions .rst-current-version .admonition-title,.rst-content code.download .rst-versions .rst-current-version span:first-child,.rst-content dl dt .rst-versions .rst-current-version .headerlink,.rst-content h1 .rst-versions .rst-current-version .headerlink,.rst-content h2 .rst-versions .rst-current-version .headerlink,.rst-content h3 .rst-versions .rst-current-version .headerlink,.rst-content h4 .rst-versions .rst-current-version .headerlink,.rst-content h5 .rst-versions .rst-current-version .headerlink,.rst-content h6 .rst-versions .rst-current-version .headerlink,.rst-content p .rst-versions .rst-current-version .headerlink,.rst-content table>caption .rst-versions .rst-current-version .headerlink,.rst-content tt.download .rst-versions .rst-current-version span:first-child,.rst-versions .rst-current-version .fa,.rst-versions .rst-current-version .icon,.rst-versions .rst-current-version .rst-content .admonition-title,.rst-versions .rst-current-version .rst-content .code-block-caption .headerlink,.rst-versions .rst-current-version .rst-content .eqno .headerlink,.rst-versions .rst-current-version .rst-content code.download span:first-child,.rst-versions .rst-current-version .rst-content dl dt .headerlink,.rst-versions .rst-current-version .rst-content h1 .headerlink,.rst-versions .rst-current-version .rst-content h2 .headerlink,.rst-versions .rst-current-version .rst-content h3 .headerlink,.rst-versions .rst-current-version .rst-content h4 .headerlink,.rst-versions .rst-current-version .rst-content h5 .headerlink,.rst-versions .rst-current-version .rst-content h6 .headerlink,.rst-versions .rst-current-version .rst-content p .headerlink,.rst-versions .rst-current-version .rst-content table>caption .headerlink,.rst-versions .rst-current-version .rst-content tt.download span:first-child,.rst-versions .rst-current-version .wy-menu-vertical li button.toctree-expand,.wy-menu-vertical li .rst-versions .rst-current-version button.toctree-expand{color:#fcfcfc}.rst-versions .rst-current-version .fa-book,.rst-versions .rst-current-version .icon-book{float:left}.rst-versions .rst-current-version.rst-out-of-date{background-color:#e74c3c;color:#fff}.rst-versions .rst-current-version.rst-active-old-version{background-color:#f1c40f;color:#000}.rst-versions.shift-up{height:auto;max-height:100%;overflow-y:scroll}.rst-versions.shift-up .rst-other-versions{display:block}.rst-versions .rst-other-versions{font-size:90%;padding:12px;color:grey;display:none}.rst-versions .rst-other-versions hr{display:block;height:1px;border:0;margin:20px 0;padding:0;border-top:1px solid #413d3d}.rst-versions .rst-other-versions dd{display:inline-block;margin:0}.rst-versions .rst-other-versions dd a{display:inline-block;padding:6px;color:#fcfcfc}.rst-versions .rst-other-versions .rtd-current-item{font-weight:700}.rst-versions.rst-badge{width:auto;bottom:20px;right:20px;left:auto;border:none;max-width:300px;max-height:90%}.rst-versions.rst-badge .fa-book,.rst-versions.rst-badge .icon-book{float:none;line-height:30px}.rst-versions.rst-badge.shift-up .rst-current-version{text-align:right}.rst-versions.rst-badge.shift-up .rst-current-version .fa-book,.rst-versions.rst-badge.shift-up .rst-current-version .icon-book{float:left}.rst-versions.rst-badge>.rst-current-version{width:auto;height:30px;line-height:30px;padding:0 6px;display:block;text-align:center}@media screen and (max-width:768px){.rst-versions{width:85%;display:none}.rst-versions.shift{display:block}}#flyout-search-form{padding:6px}.rst-content .toctree-wrapper>p.caption,.rst-content h1,.rst-content h2,.rst-content h3,.rst-content h4,.rst-content h5,.rst-content h6{margin-bottom:24px}.rst-content img{max-width:100%;height:auto}.rst-content div.figure,.rst-content figure{margin-bottom:24px}.rst-content div.figure .caption-text,.rst-content figure .caption-text{font-style:italic}.rst-content div.figure p:last-child.caption,.rst-content figure p:last-child.caption{margin-bottom:0}.rst-content div.figure.align-center,.rst-content figure.align-center{text-align:center}.rst-content .section>a>img,.rst-content .section>img,.rst-content section>a>img,.rst-content section>img{margin-bottom:24px}.rst-content abbr[title]{text-decoration:none}.rst-content.style-external-links a.reference.external:after{font-family:FontAwesome;content:"\f08e";color:#b3b3b3;vertical-align:super;font-size:60%;margin:0 .2em}.rst-content blockquote{margin-left:24px;line-height:24px;margin-bottom:24px}.rst-content pre.literal-block{white-space:pre;margin:0;padding:12px;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;display:block;overflow:auto}.rst-content div[class^=highlight],.rst-content pre.literal-block{border:1px solid #e1e4e5;overflow-x:auto;margin:1px 0 24px}.rst-content div[class^=highlight] div[class^=highlight],.rst-content pre.literal-block div[class^=highlight]{padding:0;border:none;margin:0}.rst-content div[class^=highlight] td.code{width:100%}.rst-content .linenodiv pre{border-right:1px solid #e6e9ea;margin:0;padding:12px;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;user-select:none;pointer-events:none}.rst-content div[class^=highlight] pre{white-space:pre;margin:0;padding:12px;display:block;overflow:auto}.rst-content div[class^=highlight] pre .hll{display:block;margin:0 -12px;padding:0 12px}.rst-content .linenodiv pre,.rst-content div[class^=highlight] pre,.rst-content pre.literal-block{font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;font-size:12px;line-height:1.4}.rst-content div.highlight .gp,.rst-content div.highlight span.linenos{user-select:none;pointer-events:none}.rst-content div.highlight span.linenos{display:inline-block;padding-left:0;padding-right:12px;margin-right:12px;border-right:1px solid #e6e9ea}.rst-content .code-block-caption{font-style:italic;font-size:85%;line-height:1;padding:1em 0;text-align:center}@media print{.rst-content .codeblock,.rst-content div[class^=highlight],.rst-content div[class^=highlight] pre{white-space:pre-wrap}}.rst-content .admonition,.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .danger,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .note,.rst-content .seealso,.rst-content .tip,.rst-content .warning{clear:both}.rst-content .admonition-todo .last,.rst-content .admonition-todo>:last-child,.rst-content .admonition .last,.rst-content .admonition>:last-child,.rst-content .attention .last,.rst-content .attention>:last-child,.rst-content .caution .last,.rst-content .caution>:last-child,.rst-content .danger .last,.rst-content .danger>:last-child,.rst-content .error .last,.rst-content .error>:last-child,.rst-content .hint .last,.rst-content .hint>:last-child,.rst-content .important .last,.rst-content .important>:last-child,.rst-content .note .last,.rst-content .note>:last-child,.rst-content .seealso .last,.rst-content .seealso>:last-child,.rst-content .tip .last,.rst-content .tip>:last-child,.rst-content .warning .last,.rst-content .warning>:last-child{margin-bottom:0}.rst-content .admonition-title:before{margin-right:4px}.rst-content .admonition table{border-color:rgba(0,0,0,.1)}.rst-content .admonition table td,.rst-content .admonition table th{background:transparent!important;border-color:rgba(0,0,0,.1)!important}.rst-content .section ol.loweralpha,.rst-content .section ol.loweralpha>li,.rst-content .toctree-wrapper ol.loweralpha,.rst-content .toctree-wrapper ol.loweralpha>li,.rst-content section ol.loweralpha,.rst-content section ol.loweralpha>li{list-style:lower-alpha}.rst-content .section ol.upperalpha,.rst-content .section ol.upperalpha>li,.rst-content .toctree-wrapper ol.upperalpha,.rst-content .toctree-wrapper ol.upperalpha>li,.rst-content section ol.upperalpha,.rst-content section ol.upperalpha>li{list-style:upper-alpha}.rst-content .section ol li>*,.rst-content .section ul li>*,.rst-content .toctree-wrapper ol li>*,.rst-content .toctree-wrapper ul li>*,.rst-content section ol li>*,.rst-content section ul li>*{margin-top:12px;margin-bottom:12px}.rst-content .section ol li>:first-child,.rst-content .section ul li>:first-child,.rst-content .toctree-wrapper ol li>:first-child,.rst-content .toctree-wrapper ul li>:first-child,.rst-content section ol li>:first-child,.rst-content section ul li>:first-child{margin-top:0}.rst-content .section ol li>p,.rst-content .section ol li>p:last-child,.rst-content .section ul li>p,.rst-content .section ul li>p:last-child,.rst-content .toctree-wrapper ol li>p,.rst-content .toctree-wrapper ol li>p:last-child,.rst-content .toctree-wrapper ul li>p,.rst-content .toctree-wrapper ul li>p:last-child,.rst-content section ol li>p,.rst-content section ol li>p:last-child,.rst-content section ul li>p,.rst-content section ul li>p:last-child{margin-bottom:12px}.rst-content .section ol li>p:only-child,.rst-content .section ol li>p:only-child:last-child,.rst-content .section ul li>p:only-child,.rst-content .section ul li>p:only-child:last-child,.rst-content .toctree-wrapper ol li>p:only-child,.rst-content .toctree-wrapper ol li>p:only-child:last-child,.rst-content .toctree-wrapper ul li>p:only-child,.rst-content .toctree-wrapper ul li>p:only-child:last-child,.rst-content section ol li>p:only-child,.rst-content section ol li>p:only-child:last-child,.rst-content section ul li>p:only-child,.rst-content section ul li>p:only-child:last-child{margin-bottom:0}.rst-content .section ol li>ol,.rst-content .section ol li>ul,.rst-content .section ul li>ol,.rst-content .section ul li>ul,.rst-content .toctree-wrapper ol li>ol,.rst-content .toctree-wrapper ol li>ul,.rst-content .toctree-wrapper ul li>ol,.rst-content .toctree-wrapper ul li>ul,.rst-content section ol li>ol,.rst-content section ol li>ul,.rst-content section ul li>ol,.rst-content section ul li>ul{margin-bottom:12px}.rst-content .section ol.simple li>*,.rst-content .section ol.simple li ol,.rst-content .section ol.simple li ul,.rst-content .section ul.simple li>*,.rst-content .section ul.simple li ol,.rst-content .section ul.simple li ul,.rst-content .toctree-wrapper ol.simple li>*,.rst-content .toctree-wrapper ol.simple li ol,.rst-content .toctree-wrapper ol.simple li ul,.rst-content .toctree-wrapper ul.simple li>*,.rst-content .toctree-wrapper ul.simple li ol,.rst-content .toctree-wrapper ul.simple li ul,.rst-content section ol.simple li>*,.rst-content section ol.simple li ol,.rst-content section ol.simple li ul,.rst-content section ul.simple li>*,.rst-content section ul.simple li ol,.rst-content section ul.simple li ul{margin-top:0;margin-bottom:0}.rst-content .line-block{margin-left:0;margin-bottom:24px;line-height:24px}.rst-content .line-block .line-block{margin-left:24px;margin-bottom:0}.rst-content .topic-title{font-weight:700;margin-bottom:12px}.rst-content .toc-backref{color:#404040}.rst-content .align-right{float:right;margin:0 0 24px 24px}.rst-content .align-left{float:left;margin:0 24px 24px 0}.rst-content .align-center{margin:auto}.rst-content .align-center:not(table){display:block}.rst-content .code-block-caption .headerlink,.rst-content .eqno .headerlink,.rst-content .toctree-wrapper>p.caption .headerlink,.rst-content dl dt .headerlink,.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content p.caption .headerlink,.rst-content p .headerlink,.rst-content table>caption .headerlink{opacity:0;font-size:14px;font-family:FontAwesome;margin-left:.5em}.rst-content .code-block-caption .headerlink:focus,.rst-content .code-block-caption:hover .headerlink,.rst-content .eqno .headerlink:focus,.rst-content .eqno:hover .headerlink,.rst-content .toctree-wrapper>p.caption .headerlink:focus,.rst-content .toctree-wrapper>p.caption:hover .headerlink,.rst-content dl dt .headerlink:focus,.rst-content dl dt:hover .headerlink,.rst-content h1 .headerlink:focus,.rst-content h1:hover .headerlink,.rst-content h2 .headerlink:focus,.rst-content h2:hover .headerlink,.rst-content h3 .headerlink:focus,.rst-content h3:hover .headerlink,.rst-content h4 .headerlink:focus,.rst-content h4:hover .headerlink,.rst-content h5 .headerlink:focus,.rst-content h5:hover .headerlink,.rst-content h6 .headerlink:focus,.rst-content h6:hover .headerlink,.rst-content p.caption .headerlink:focus,.rst-content p.caption:hover .headerlink,.rst-content p .headerlink:focus,.rst-content p:hover .headerlink,.rst-content table>caption .headerlink:focus,.rst-content table>caption:hover .headerlink{opacity:1}.rst-content p a{overflow-wrap:anywhere}.rst-content .wy-table td p,.rst-content .wy-table td ul,.rst-content .wy-table th p,.rst-content .wy-table th ul,.rst-content table.docutils td p,.rst-content table.docutils td ul,.rst-content table.docutils th p,.rst-content table.docutils th ul,.rst-content table.field-list td p,.rst-content table.field-list td ul,.rst-content table.field-list th p,.rst-content table.field-list th ul{font-size:inherit}.rst-content .btn:focus{outline:2px solid}.rst-content table>caption .headerlink:after{font-size:12px}.rst-content .centered{text-align:center}.rst-content .sidebar{float:right;width:40%;display:block;margin:0 0 24px 24px;padding:24px;background:#f3f6f6;border:1px solid #e1e4e5}.rst-content .sidebar dl,.rst-content .sidebar p,.rst-content .sidebar ul{font-size:90%}.rst-content .sidebar .last,.rst-content .sidebar>:last-child{margin-bottom:0}.rst-content .sidebar .sidebar-title{display:block;font-family:Roboto Slab,ff-tisa-web-pro,Georgia,Arial,sans-serif;font-weight:700;background:#e1e4e5;padding:6px 12px;margin:-24px -24px 24px;font-size:100%}.rst-content .highlighted{background:#f1c40f;box-shadow:0 0 0 2px #f1c40f;display:inline;font-weight:700}.rst-content .citation-reference,.rst-content .footnote-reference{vertical-align:baseline;position:relative;top:-.4em;line-height:0;font-size:90%}.rst-content .citation-reference>span.fn-bracket,.rst-content .footnote-reference>span.fn-bracket{display:none}.rst-content .hlist{width:100%}.rst-content dl dt span.classifier:before{content:" : "}.rst-content dl dt span.classifier-delimiter{display:none!important}html.writer-html4 .rst-content table.docutils.citation,html.writer-html4 .rst-content table.docutils.footnote{background:none;border:none}html.writer-html4 .rst-content table.docutils.citation td,html.writer-html4 .rst-content table.docutils.citation tr,html.writer-html4 .rst-content table.docutils.footnote td,html.writer-html4 .rst-content table.docutils.footnote tr{border:none;background-color:transparent!important;white-space:normal}html.writer-html4 .rst-content table.docutils.citation td.label,html.writer-html4 .rst-content table.docutils.footnote td.label{padding-left:0;padding-right:0;vertical-align:top}html.writer-html5 .rst-content dl.citation,html.writer-html5 .rst-content dl.field-list,html.writer-html5 .rst-content dl.footnote{display:grid;grid-template-columns:auto minmax(80%,95%)}html.writer-html5 .rst-content dl.citation>dt,html.writer-html5 .rst-content dl.field-list>dt,html.writer-html5 .rst-content dl.footnote>dt{display:inline-grid;grid-template-columns:max-content auto}html.writer-html5 .rst-content aside.citation,html.writer-html5 .rst-content aside.footnote,html.writer-html5 .rst-content div.citation{display:grid;grid-template-columns:auto auto minmax(.65rem,auto) minmax(40%,95%)}html.writer-html5 .rst-content aside.citation>span.label,html.writer-html5 .rst-content aside.footnote>span.label,html.writer-html5 .rst-content div.citation>span.label{grid-column-start:1;grid-column-end:2}html.writer-html5 .rst-content aside.citation>span.backrefs,html.writer-html5 .rst-content aside.footnote>span.backrefs,html.writer-html5 .rst-content div.citation>span.backrefs{grid-column-start:2;grid-column-end:3;grid-row-start:1;grid-row-end:3}html.writer-html5 .rst-content aside.citation>p,html.writer-html5 .rst-content aside.footnote>p,html.writer-html5 .rst-content div.citation>p{grid-column-start:4;grid-column-end:5}html.writer-html5 .rst-content dl.citation,html.writer-html5 .rst-content dl.field-list,html.writer-html5 .rst-content dl.footnote{margin-bottom:24px}html.writer-html5 .rst-content dl.citation>dt,html.writer-html5 .rst-content dl.field-list>dt,html.writer-html5 .rst-content dl.footnote>dt{padding-left:1rem}html.writer-html5 .rst-content dl.citation>dd,html.writer-html5 .rst-content dl.citation>dt,html.writer-html5 .rst-content dl.field-list>dd,html.writer-html5 .rst-content dl.field-list>dt,html.writer-html5 .rst-content dl.footnote>dd,html.writer-html5 .rst-content dl.footnote>dt{margin-bottom:0}html.writer-html5 .rst-content dl.citation,html.writer-html5 .rst-content dl.footnote{font-size:.9rem}html.writer-html5 .rst-content dl.citation>dt,html.writer-html5 .rst-content dl.footnote>dt{margin:0 .5rem .5rem 0;line-height:1.2rem;word-break:break-all;font-weight:400}html.writer-html5 .rst-content dl.citation>dt>span.brackets:before,html.writer-html5 .rst-content dl.footnote>dt>span.brackets:before{content:"["}html.writer-html5 .rst-content dl.citation>dt>span.brackets:after,html.writer-html5 .rst-content dl.footnote>dt>span.brackets:after{content:"]"}html.writer-html5 .rst-content dl.citation>dt>span.fn-backref,html.writer-html5 .rst-content dl.footnote>dt>span.fn-backref{text-align:left;font-style:italic;margin-left:.65rem;word-break:break-word;word-spacing:-.1rem;max-width:5rem}html.writer-html5 .rst-content dl.citation>dt>span.fn-backref>a,html.writer-html5 .rst-content dl.footnote>dt>span.fn-backref>a{word-break:keep-all}html.writer-html5 .rst-content dl.citation>dt>span.fn-backref>a:not(:first-child):before,html.writer-html5 .rst-content dl.footnote>dt>span.fn-backref>a:not(:first-child):before{content:" "}html.writer-html5 .rst-content dl.citation>dd,html.writer-html5 .rst-content dl.footnote>dd{margin:0 0 .5rem;line-height:1.2rem}html.writer-html5 .rst-content dl.citation>dd p,html.writer-html5 .rst-content dl.footnote>dd p{font-size:.9rem}html.writer-html5 .rst-content aside.citation,html.writer-html5 .rst-content aside.footnote,html.writer-html5 .rst-content div.citation{padding-left:1rem;padding-right:1rem;font-size:.9rem;line-height:1.2rem}html.writer-html5 .rst-content aside.citation p,html.writer-html5 .rst-content aside.footnote p,html.writer-html5 .rst-content div.citation p{font-size:.9rem;line-height:1.2rem;margin-bottom:12px}html.writer-html5 .rst-content aside.citation span.backrefs,html.writer-html5 .rst-content aside.footnote span.backrefs,html.writer-html5 .rst-content div.citation span.backrefs{text-align:left;font-style:italic;margin-left:.65rem;word-break:break-word;word-spacing:-.1rem;max-width:5rem}html.writer-html5 .rst-content aside.citation span.backrefs>a,html.writer-html5 .rst-content aside.footnote span.backrefs>a,html.writer-html5 .rst-content div.citation span.backrefs>a{word-break:keep-all}html.writer-html5 .rst-content aside.citation span.backrefs>a:not(:first-child):before,html.writer-html5 .rst-content aside.footnote span.backrefs>a:not(:first-child):before,html.writer-html5 .rst-content div.citation span.backrefs>a:not(:first-child):before{content:" "}html.writer-html5 .rst-content aside.citation span.label,html.writer-html5 .rst-content aside.footnote span.label,html.writer-html5 .rst-content div.citation span.label{line-height:1.2rem}html.writer-html5 .rst-content aside.citation-list,html.writer-html5 .rst-content aside.footnote-list,html.writer-html5 .rst-content div.citation-list{margin-bottom:24px}html.writer-html5 .rst-content dl.option-list kbd{font-size:.9rem}.rst-content table.docutils.footnote,html.writer-html4 .rst-content table.docutils.citation,html.writer-html5 .rst-content aside.footnote,html.writer-html5 .rst-content aside.footnote-list aside.footnote,html.writer-html5 .rst-content div.citation-list>div.citation,html.writer-html5 .rst-content dl.citation,html.writer-html5 .rst-content dl.footnote{color:grey}.rst-content table.docutils.footnote code,.rst-content table.docutils.footnote tt,html.writer-html4 .rst-content table.docutils.citation code,html.writer-html4 .rst-content table.docutils.citation tt,html.writer-html5 .rst-content aside.footnote-list aside.footnote code,html.writer-html5 .rst-content aside.footnote-list aside.footnote tt,html.writer-html5 .rst-content aside.footnote code,html.writer-html5 .rst-content aside.footnote tt,html.writer-html5 .rst-content div.citation-list>div.citation code,html.writer-html5 .rst-content div.citation-list>div.citation tt,html.writer-html5 .rst-content dl.citation code,html.writer-html5 .rst-content dl.citation tt,html.writer-html5 .rst-content dl.footnote code,html.writer-html5 .rst-content dl.footnote tt{color:#555}.rst-content .wy-table-responsive.citation,.rst-content .wy-table-responsive.footnote{margin-bottom:0}.rst-content .wy-table-responsive.citation+:not(.citation),.rst-content .wy-table-responsive.footnote+:not(.footnote){margin-top:24px}.rst-content .wy-table-responsive.citation:last-child,.rst-content .wy-table-responsive.footnote:last-child{margin-bottom:24px}.rst-content table.docutils th{border-color:#e1e4e5}html.writer-html5 .rst-content table.docutils th{border:1px solid #e1e4e5}html.writer-html5 .rst-content table.docutils td>p,html.writer-html5 .rst-content table.docutils th>p{line-height:1rem;margin-bottom:0;font-size:.9rem}.rst-content table.docutils td .last,.rst-content table.docutils td .last>:last-child{margin-bottom:0}.rst-content table.field-list,.rst-content table.field-list td{border:none}.rst-content table.field-list td p{line-height:inherit}.rst-content table.field-list td>strong{display:inline-block}.rst-content table.field-list .field-name{padding-right:10px;text-align:left;white-space:nowrap}.rst-content table.field-list .field-body{text-align:left}.rst-content code,.rst-content tt{color:#000;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;padding:2px 5px}.rst-content code big,.rst-content code em,.rst-content tt big,.rst-content tt em{font-size:100%!important;line-height:normal}.rst-content code.literal,.rst-content tt.literal{color:#e74c3c;white-space:normal}.rst-content code.xref,.rst-content tt.xref,a .rst-content code,a .rst-content tt{font-weight:700;color:#404040;overflow-wrap:normal}.rst-content kbd,.rst-content pre,.rst-content samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace}.rst-content a code,.rst-content a tt{color:#2980b9}.rst-content dl{margin-bottom:24px}.rst-content dl dt{font-weight:700;margin-bottom:12px}.rst-content dl ol,.rst-content dl p,.rst-content dl table,.rst-content dl ul{margin-bottom:12px}.rst-content dl dd{margin:0 0 12px 24px;line-height:24px}.rst-content dl dd>ol:last-child,.rst-content dl dd>p:last-child,.rst-content dl dd>table:last-child,.rst-content dl dd>ul:last-child{margin-bottom:0}html.writer-html4 .rst-content dl:not(.docutils),html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple){margin-bottom:24px}html.writer-html4 .rst-content dl:not(.docutils)>dt,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt{display:table;margin:6px 0;font-size:90%;line-height:normal;background:#e7f2fa;color:#2980b9;border-top:3px solid #6ab0de;padding:6px;position:relative}html.writer-html4 .rst-content dl:not(.docutils)>dt:before,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt:before{color:#6ab0de}html.writer-html4 .rst-content dl:not(.docutils)>dt .headerlink,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt .headerlink{color:#404040;font-size:100%!important}html.writer-html4 .rst-content dl:not(.docutils) dl:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) dl:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt{margin-bottom:6px;border:none;border-left:3px solid #ccc;background:#f0f0f0;color:#555}html.writer-html4 .rst-content dl:not(.docutils) dl:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt .headerlink,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) dl:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt .headerlink{color:#404040;font-size:100%!important}html.writer-html4 .rst-content dl:not(.docutils)>dt:first-child,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt:first-child{margin-top:0}html.writer-html4 .rst-content dl:not(.docutils) code.descclassname,html.writer-html4 .rst-content dl:not(.docutils) code.descname,html.writer-html4 .rst-content dl:not(.docutils) tt.descclassname,html.writer-html4 .rst-content dl:not(.docutils) tt.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) code.descclassname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) code.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) tt.descclassname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) tt.descname{background-color:transparent;border:none;padding:0;font-size:100%!important}html.writer-html4 .rst-content dl:not(.docutils) code.descname,html.writer-html4 .rst-content dl:not(.docutils) tt.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) code.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) tt.descname{font-weight:700}html.writer-html4 .rst-content dl:not(.docutils) .optional,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .optional{display:inline-block;padding:0 4px;color:#000;font-weight:700}html.writer-html4 .rst-content dl:not(.docutils) .property,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .property{display:inline-block;padding-right:8px;max-width:100%}html.writer-html4 .rst-content dl:not(.docutils) .k,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .k{font-style:italic}html.writer-html4 .rst-content dl:not(.docutils) .descclassname,html.writer-html4 .rst-content dl:not(.docutils) .descname,html.writer-html4 .rst-content dl:not(.docutils) .sig-name,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .descclassname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .sig-name{font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;color:#000}.rst-content .viewcode-back,.rst-content .viewcode-link{display:inline-block;color:#27ae60;font-size:80%;padding-left:24px}.rst-content .viewcode-back{display:block;float:right}.rst-content p.rubric{margin-bottom:12px;font-weight:700}.rst-content code.download,.rst-content tt.download{background:inherit;padding:inherit;font-weight:400;font-family:inherit;font-size:inherit;color:inherit;border:inherit;white-space:inherit}.rst-content code.download span:first-child,.rst-content tt.download span:first-child{-webkit-font-smoothing:subpixel-antialiased}.rst-content code.download span:first-child:before,.rst-content tt.download span:first-child:before{margin-right:4px}.rst-content .guilabel,.rst-content .menuselection{font-size:80%;font-weight:700;border-radius:4px;padding:2.4px 6px;margin:auto 2px}.rst-content .guilabel,.rst-content .menuselection{border:1px solid #7fbbe3;background:#e7f2fa}.rst-content :not(dl.option-list)>:not(dt):not(kbd):not(.kbd)>.kbd,.rst-content :not(dl.option-list)>:not(dt):not(kbd):not(.kbd)>kbd{color:inherit;font-size:80%;background-color:#fff;border:1px solid #a6a6a6;border-radius:4px;box-shadow:0 2px grey;padding:2.4px 6px;margin:auto 0}.rst-content .versionmodified{font-style:italic}@media screen and (max-width:480px){.rst-content .sidebar{width:100%}}span[id*=MathJax-Span]{color:#404040}.math{text-align:center}@font-face{font-family:Lato;src:url(fonts/lato-normal.woff2?bd03a2cc277bbbc338d464e679fe9942) format("woff2"),url(fonts/lato-normal.woff?27bd77b9162d388cb8d4c4217c7c5e2a) format("woff");font-weight:400;font-style:normal;font-display:block}@font-face{font-family:Lato;src:url(fonts/lato-bold.woff2?cccb897485813c7c256901dbca54ecf2) format("woff2"),url(fonts/lato-bold.woff?d878b6c29b10beca227e9eef4246111b) format("woff");font-weight:700;font-style:normal;font-display:block}@font-face{font-family:Lato;src:url(fonts/lato-bold-italic.woff2?0b6bb6725576b072c5d0b02ecdd1900d) format("woff2"),url(fonts/lato-bold-italic.woff?9c7e4e9eb485b4a121c760e61bc3707c) format("woff");font-weight:700;font-style:italic;font-display:block}@font-face{font-family:Lato;src:url(fonts/lato-normal-italic.woff2?4eb103b4d12be57cb1d040ed5e162e9d) format("woff2"),url(fonts/lato-normal-italic.woff?f28f2d6482446544ef1ea1ccc6dd5892) format("woff");font-weight:400;font-style:italic;font-display:block}@font-face{font-family:Roboto Slab;font-style:normal;font-weight:400;src:url(fonts/Roboto-Slab-Regular.woff2?7abf5b8d04d26a2cafea937019bca958) format("woff2"),url(fonts/Roboto-Slab-Regular.woff?c1be9284088d487c5e3ff0a10a92e58c) format("woff");font-display:block}@font-face{font-family:Roboto Slab;font-style:normal;font-weight:700;src:url(fonts/Roboto-Slab-Bold.woff2?9984f4a9bda09be08e83f2506954adbe) format("woff2"),url(fonts/Roboto-Slab-Bold.woff?bed5564a116b05148e3b3bea6fb1162a) format("woff");font-display:block} diff --git a/api-doc/_build/html/_static/doctools.js b/api-doc/_build/html/_static/doctools.js new file mode 100644 index 0000000..0398ebb --- /dev/null +++ b/api-doc/_build/html/_static/doctools.js @@ -0,0 +1,149 @@ +/* + * Base JavaScript utilities for all Sphinx HTML documentation. + */ +"use strict"; + +const BLACKLISTED_KEY_CONTROL_ELEMENTS = new Set([ + "TEXTAREA", + "INPUT", + "SELECT", + "BUTTON", +]); + +const _ready = (callback) => { + if (document.readyState !== "loading") { + callback(); + } else { + document.addEventListener("DOMContentLoaded", callback); + } +}; + +/** + * Small JavaScript module for the documentation. + */ +const Documentation = { + init: () => { + Documentation.initDomainIndexTable(); + Documentation.initOnKeyListeners(); + }, + + /** + * i18n support + */ + TRANSLATIONS: {}, + PLURAL_EXPR: (n) => (n === 1 ? 0 : 1), + LOCALE: "unknown", + + // gettext and ngettext don't access this so that the functions + // can safely bound to a different name (_ = Documentation.gettext) + gettext: (string) => { + const translated = Documentation.TRANSLATIONS[string]; + switch (typeof translated) { + case "undefined": + return string; // no translation + case "string": + return translated; // translation exists + default: + return translated[0]; // (singular, plural) translation tuple exists + } + }, + + ngettext: (singular, plural, n) => { + const translated = Documentation.TRANSLATIONS[singular]; + if (typeof translated !== "undefined") + return translated[Documentation.PLURAL_EXPR(n)]; + return n === 1 ? singular : plural; + }, + + addTranslations: (catalog) => { + Object.assign(Documentation.TRANSLATIONS, catalog.messages); + Documentation.PLURAL_EXPR = new Function( + "n", + `return (${catalog.plural_expr})` + ); + Documentation.LOCALE = catalog.locale; + }, + + /** + * helper function to focus on search bar + */ + focusSearchBar: () => { + document.querySelectorAll("input[name=q]")[0]?.focus(); + }, + + /** + * Initialise the domain index toggle buttons + */ + initDomainIndexTable: () => { + const toggler = (el) => { + const idNumber = el.id.substr(7); + const toggledRows = document.querySelectorAll(`tr.cg-${idNumber}`); + if (el.src.substr(-9) === "minus.png") { + el.src = `${el.src.substr(0, el.src.length - 9)}plus.png`; + toggledRows.forEach((el) => (el.style.display = "none")); + } else { + el.src = `${el.src.substr(0, el.src.length - 8)}minus.png`; + toggledRows.forEach((el) => (el.style.display = "")); + } + }; + + const togglerElements = document.querySelectorAll("img.toggler"); + togglerElements.forEach((el) => + el.addEventListener("click", (event) => toggler(event.currentTarget)) + ); + togglerElements.forEach((el) => (el.style.display = "")); + if (DOCUMENTATION_OPTIONS.COLLAPSE_INDEX) togglerElements.forEach(toggler); + }, + + initOnKeyListeners: () => { + // only install a listener if it is really needed + if ( + !DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS && + !DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS + ) + return; + + document.addEventListener("keydown", (event) => { + // bail for input elements + if (BLACKLISTED_KEY_CONTROL_ELEMENTS.has(document.activeElement.tagName)) return; + // bail with special keys + if (event.altKey || event.ctrlKey || event.metaKey) return; + + if (!event.shiftKey) { + switch (event.key) { + case "ArrowLeft": + if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break; + + const prevLink = document.querySelector('link[rel="prev"]'); + if (prevLink && prevLink.href) { + window.location.href = prevLink.href; + event.preventDefault(); + } + break; + case "ArrowRight": + if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break; + + const nextLink = document.querySelector('link[rel="next"]'); + if (nextLink && nextLink.href) { + window.location.href = nextLink.href; + event.preventDefault(); + } + break; + } + } + + // some keyboard layouts may need Shift to get / + switch (event.key) { + case "/": + if (!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS) break; + Documentation.focusSearchBar(); + event.preventDefault(); + } + }); + }, +}; + +// quick alias for translations +const _ = Documentation.gettext; + +_ready(Documentation.init); diff --git a/api-doc/_build/html/_static/documentation_options.js b/api-doc/_build/html/_static/documentation_options.js new file mode 100644 index 0000000..62449dd --- /dev/null +++ b/api-doc/_build/html/_static/documentation_options.js @@ -0,0 +1,13 @@ +const DOCUMENTATION_OPTIONS = { + VERSION: '1.0', + LANGUAGE: 'en', + COLLAPSE_INDEX: false, + BUILDER: 'html', + FILE_SUFFIX: '.html', + LINK_SUFFIX: '.html', + HAS_SOURCE: true, + SOURCELINK_SUFFIX: '.txt', + NAVIGATION_WITH_KEYS: false, + SHOW_SEARCH_SUMMARY: true, + ENABLE_SEARCH_SHORTCUTS: true, +}; diff --git a/api-doc/_build/html/_static/file.png b/api-doc/_build/html/_static/file.png new file mode 100644 index 0000000..89db3f6 Binary files /dev/null and b/api-doc/_build/html/_static/file.png differ diff --git a/api-doc/_build/html/_static/fonts/Lato/lato-bold.eot b/api-doc/_build/html/_static/fonts/Lato/lato-bold.eot new file mode 100644 index 0000000..161f825 Binary files /dev/null and b/api-doc/_build/html/_static/fonts/Lato/lato-bold.eot differ diff --git a/api-doc/_build/html/_static/fonts/Lato/lato-bold.ttf b/api-doc/_build/html/_static/fonts/Lato/lato-bold.ttf new file mode 100644 index 0000000..852d6cf Binary files /dev/null and b/api-doc/_build/html/_static/fonts/Lato/lato-bold.ttf differ diff --git a/api-doc/_build/html/_static/fonts/Lato/lato-bold.woff b/api-doc/_build/html/_static/fonts/Lato/lato-bold.woff new file mode 100644 index 0000000..07bc142 Binary files /dev/null and b/api-doc/_build/html/_static/fonts/Lato/lato-bold.woff differ diff --git a/api-doc/_build/html/_static/fonts/Lato/lato-bold.woff2 b/api-doc/_build/html/_static/fonts/Lato/lato-bold.woff2 new file mode 100644 index 0000000..94eb5c1 Binary files /dev/null and b/api-doc/_build/html/_static/fonts/Lato/lato-bold.woff2 differ diff --git a/api-doc/_build/html/_static/fonts/Lato/lato-bolditalic.eot b/api-doc/_build/html/_static/fonts/Lato/lato-bolditalic.eot new file mode 100644 index 0000000..f96d377 Binary files /dev/null and b/api-doc/_build/html/_static/fonts/Lato/lato-bolditalic.eot differ diff --git a/api-doc/_build/html/_static/fonts/Lato/lato-bolditalic.ttf b/api-doc/_build/html/_static/fonts/Lato/lato-bolditalic.ttf new file mode 100644 index 0000000..ea8063c Binary files /dev/null and b/api-doc/_build/html/_static/fonts/Lato/lato-bolditalic.ttf differ diff --git a/api-doc/_build/html/_static/fonts/Lato/lato-bolditalic.woff b/api-doc/_build/html/_static/fonts/Lato/lato-bolditalic.woff new file mode 100644 index 0000000..dd2d870 Binary files /dev/null and b/api-doc/_build/html/_static/fonts/Lato/lato-bolditalic.woff differ diff --git a/api-doc/_build/html/_static/fonts/Lato/lato-bolditalic.woff2 b/api-doc/_build/html/_static/fonts/Lato/lato-bolditalic.woff2 new file mode 100644 index 0000000..8ae44fa Binary files /dev/null and b/api-doc/_build/html/_static/fonts/Lato/lato-bolditalic.woff2 differ diff --git a/api-doc/_build/html/_static/fonts/Lato/lato-italic.eot b/api-doc/_build/html/_static/fonts/Lato/lato-italic.eot new file mode 100644 index 0000000..3f82642 Binary files /dev/null and b/api-doc/_build/html/_static/fonts/Lato/lato-italic.eot differ diff --git a/api-doc/_build/html/_static/fonts/Lato/lato-italic.ttf b/api-doc/_build/html/_static/fonts/Lato/lato-italic.ttf new file mode 100644 index 0000000..2f4cffb Binary files /dev/null and b/api-doc/_build/html/_static/fonts/Lato/lato-italic.ttf differ diff --git a/api-doc/_build/html/_static/fonts/Lato/lato-italic.woff b/api-doc/_build/html/_static/fonts/Lato/lato-italic.woff new file mode 100644 index 0000000..439cd13 Binary files /dev/null and b/api-doc/_build/html/_static/fonts/Lato/lato-italic.woff differ diff --git a/api-doc/_build/html/_static/fonts/Lato/lato-italic.woff2 b/api-doc/_build/html/_static/fonts/Lato/lato-italic.woff2 new file mode 100644 index 0000000..6285a12 Binary files /dev/null and b/api-doc/_build/html/_static/fonts/Lato/lato-italic.woff2 differ diff --git a/api-doc/_build/html/_static/fonts/Lato/lato-regular.eot b/api-doc/_build/html/_static/fonts/Lato/lato-regular.eot new file mode 100644 index 0000000..5db46c1 Binary files /dev/null and b/api-doc/_build/html/_static/fonts/Lato/lato-regular.eot differ diff --git a/api-doc/_build/html/_static/fonts/Lato/lato-regular.ttf b/api-doc/_build/html/_static/fonts/Lato/lato-regular.ttf new file mode 100644 index 0000000..0615b13 Binary files /dev/null and b/api-doc/_build/html/_static/fonts/Lato/lato-regular.ttf differ diff --git a/api-doc/_build/html/_static/fonts/Lato/lato-regular.woff b/api-doc/_build/html/_static/fonts/Lato/lato-regular.woff new file mode 100644 index 0000000..9519242 Binary files /dev/null and b/api-doc/_build/html/_static/fonts/Lato/lato-regular.woff differ diff --git a/api-doc/_build/html/_static/fonts/Lato/lato-regular.woff2 b/api-doc/_build/html/_static/fonts/Lato/lato-regular.woff2 new file mode 100644 index 0000000..cd49ad9 Binary files /dev/null and b/api-doc/_build/html/_static/fonts/Lato/lato-regular.woff2 differ diff --git a/api-doc/_build/html/_static/fonts/RobotoSlab/roboto-slab-v7-bold.eot b/api-doc/_build/html/_static/fonts/RobotoSlab/roboto-slab-v7-bold.eot new file mode 100644 index 0000000..47cf377 Binary files /dev/null and b/api-doc/_build/html/_static/fonts/RobotoSlab/roboto-slab-v7-bold.eot differ diff --git a/api-doc/_build/html/_static/fonts/RobotoSlab/roboto-slab-v7-bold.ttf b/api-doc/_build/html/_static/fonts/RobotoSlab/roboto-slab-v7-bold.ttf new file mode 100644 index 0000000..e68f4af Binary files /dev/null and b/api-doc/_build/html/_static/fonts/RobotoSlab/roboto-slab-v7-bold.ttf differ diff --git a/api-doc/_build/html/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff b/api-doc/_build/html/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff new file mode 100644 index 0000000..6cb6000 Binary files /dev/null and b/api-doc/_build/html/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff differ diff --git a/api-doc/_build/html/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff2 b/api-doc/_build/html/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff2 new file mode 100644 index 0000000..4e12420 Binary files /dev/null and b/api-doc/_build/html/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff2 differ diff --git a/api-doc/_build/html/_static/fonts/RobotoSlab/roboto-slab-v7-regular.eot b/api-doc/_build/html/_static/fonts/RobotoSlab/roboto-slab-v7-regular.eot new file mode 100644 index 0000000..9a4685a Binary files /dev/null and b/api-doc/_build/html/_static/fonts/RobotoSlab/roboto-slab-v7-regular.eot differ diff --git a/api-doc/_build/html/_static/fonts/RobotoSlab/roboto-slab-v7-regular.ttf b/api-doc/_build/html/_static/fonts/RobotoSlab/roboto-slab-v7-regular.ttf new file mode 100644 index 0000000..8ff0226 Binary files /dev/null and b/api-doc/_build/html/_static/fonts/RobotoSlab/roboto-slab-v7-regular.ttf differ diff --git a/api-doc/_build/html/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff b/api-doc/_build/html/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff new file mode 100644 index 0000000..125af8f Binary files /dev/null and b/api-doc/_build/html/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff differ diff --git a/api-doc/_build/html/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff2 b/api-doc/_build/html/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff2 new file mode 100644 index 0000000..f2c76e5 Binary files /dev/null and b/api-doc/_build/html/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff2 differ diff --git a/api-doc/_build/html/_static/jquery.js b/api-doc/_build/html/_static/jquery.js new file mode 100644 index 0000000..c4c6022 --- /dev/null +++ b/api-doc/_build/html/_static/jquery.js @@ -0,0 +1,2 @@ +/*! jQuery v3.6.0 | (c) OpenJS Foundation and other contributors | jquery.org/license */ +!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(C,e){"use strict";var t=[],r=Object.getPrototypeOf,s=t.slice,g=t.flat?function(e){return t.flat.call(e)}:function(e){return t.concat.apply([],e)},u=t.push,i=t.indexOf,n={},o=n.toString,v=n.hasOwnProperty,a=v.toString,l=a.call(Object),y={},m=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType&&"function"!=typeof e.item},x=function(e){return null!=e&&e===e.window},E=C.document,c={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||E).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function w(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[o.call(e)]||"object":typeof e}var f="3.6.0",S=function(e,t){return new S.fn.init(e,t)};function p(e){var t=!!e&&"length"in e&&e.length,n=w(e);return!m(e)&&!x(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp(F),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+F),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\[\\da-fA-F]{1,6}"+M+"?|\\\\([^\\r\\n\\f])","g"),ne=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(p.childNodes),p.childNodes),t[p.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&y(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!N[t+" "]&&(!v||!v.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&(U.test(t)||z.test(t))){(f=ee.test(t)&&ye(e.parentNode)||e)===e&&d.scope||((s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=S)),o=(l=h(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+xe(l[o]);c=l.join(",")}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){N(t,!0)}finally{s===S&&e.removeAttribute("id")}}}return g(t.replace($,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[S]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ve(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ye(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e&&e.namespaceURI,n=e&&(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:p;return r!=C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),p!=C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.scope=ce(function(e){return a.appendChild(e).appendChild(C.createElement("div")),"undefined"!=typeof e.querySelectorAll&&!e.querySelectorAll(":scope fieldset div").length}),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=S,!C.getElementsByName||!C.getElementsByName(S).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],v=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){var t;a.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&v.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||v.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+S+"-]").length||v.push("~="),(t=C.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||v.push("\\["+M+"*name"+M+"*="+M+"*(?:''|\"\")"),e.querySelectorAll(":checked").length||v.push(":checked"),e.querySelectorAll("a#"+S+"+*").length||v.push(".#.+[+~]"),e.querySelectorAll("\\\f"),v.push("[\\r\\n\\f]")}),ce(function(e){e.innerHTML="";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&v.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&v.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&v.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),v.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",F)}),v=v.length&&new RegExp(v.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),y=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},j=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e==C||e.ownerDocument==p&&y(p,e)?-1:t==C||t.ownerDocument==p&&y(p,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e==C?-1:t==C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]==p?-1:s[r]==p?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if(T(e),d.matchesSelector&&E&&!N[t+" "]&&(!s||!s.test(t))&&(!v||!v.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){N(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=m[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&m(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,n,r){return m(n)?S.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?S.grep(e,function(e){return e===n!==r}):"string"!=typeof n?S.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(S.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||D,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:q.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof S?t[0]:t,S.merge(this,S.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),N.test(r[1])&&S.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(S):S.makeArray(e,this)}).prototype=S.fn,D=S(E);var L=/^(?:parents|prev(?:Until|All))/,H={children:!0,contents:!0,next:!0,prev:!0};function O(e,t){while((e=e[t])&&1!==e.nodeType);return e}S.fn.extend({has:function(e){var t=S(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i;ce=E.createDocumentFragment().appendChild(E.createElement("div")),(fe=E.createElement("input")).setAttribute("type","radio"),fe.setAttribute("checked","checked"),fe.setAttribute("name","t"),ce.appendChild(fe),y.checkClone=ce.cloneNode(!0).cloneNode(!0).lastChild.checked,ce.innerHTML="",y.noCloneChecked=!!ce.cloneNode(!0).lastChild.defaultValue,ce.innerHTML="",y.option=!!ce.lastChild;var ge={thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function ve(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?S.merge([e],n):n}function ye(e,t){for(var n=0,r=e.length;n",""]);var me=/<|&#?\w+;/;function xe(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d\s*$/g;function je(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&S(e).children("tbody")[0]||e}function De(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function qe(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Le(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(Y.hasData(e)&&(s=Y.get(e).events))for(i in Y.remove(t,"handle events"),s)for(n=0,r=s[i].length;n").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),E.head.appendChild(r[0])},abort:function(){i&&i()}}});var _t,zt=[],Ut=/(=)\?(?=&|$)|\?\?/;S.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=zt.pop()||S.expando+"_"+wt.guid++;return this[e]=!0,e}}),S.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Ut.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Ut.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Ut,"$1"+r):!1!==e.jsonp&&(e.url+=(Tt.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||S.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?S(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,zt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),y.createHTMLDocument=((_t=E.implementation.createHTMLDocument("").body).innerHTML="
",2===_t.childNodes.length),S.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(y.createHTMLDocument?((r=(t=E.implementation.createHTMLDocument("")).createElement("base")).href=E.location.href,t.head.appendChild(r)):t=E),o=!n&&[],(i=N.exec(e))?[t.createElement(i[1])]:(i=xe([e],t,o),o&&o.length&&S(o).remove(),S.merge([],i.childNodes)));var r,i,o},S.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(S.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},S.expr.pseudos.animated=function(t){return S.grep(S.timers,function(e){return t===e.elem}).length},S.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=S.css(e,"position"),c=S(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=S.css(e,"top"),u=S.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,S.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},S.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){S.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===S.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===S.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=S(e).offset()).top+=S.css(e,"borderTopWidth",!0),i.left+=S.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-S.css(r,"marginTop",!0),left:t.left-i.left-S.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===S.css(e,"position"))e=e.offsetParent;return e||re})}}),S.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;S.fn[t]=function(e){return $(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),S.each(["top","left"],function(e,n){S.cssHooks[n]=Fe(y.pixelPosition,function(e,t){if(t)return t=We(e,n),Pe.test(t)?S(e).position()[n]+"px":t})}),S.each({Height:"height",Width:"width"},function(a,s){S.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){S.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return $(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?S.css(e,t,i):S.style(e,t,n,i)},s,n?e:void 0,n)}})}),S.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){S.fn[t]=function(e){return this.on(t,e)}}),S.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),S.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){S.fn[n]=function(e,t){return 0"),n("table.docutils.footnote").wrap("
"),n("table.docutils.citation").wrap("
"),n(".wy-menu-vertical ul").not(".simple").siblings("a").each((function(){var t=n(this);expand=n(''),expand.on("click",(function(n){return e.toggleCurrent(t),n.stopPropagation(),!1})),t.prepend(expand)}))},reset:function(){var n=encodeURI(window.location.hash)||"#";try{var e=$(".wy-menu-vertical"),t=e.find('[href="'+n+'"]');if(0===t.length){var i=$('.document [id="'+n.substring(1)+'"]').closest("div.section");0===(t=e.find('[href="#'+i.attr("id")+'"]')).length&&(t=e.find('[href="#"]'))}if(t.length>0){$(".wy-menu-vertical .current").removeClass("current").attr("aria-expanded","false"),t.addClass("current").attr("aria-expanded","true"),t.closest("li.toctree-l1").parent().addClass("current").attr("aria-expanded","true");for(let n=1;n<=10;n++)t.closest("li.toctree-l"+n).addClass("current").attr("aria-expanded","true");t[0].scrollIntoView()}}catch(n){console.log("Error expanding nav for anchor",n)}},onScroll:function(){this.winScroll=!1;var n=this.win.scrollTop(),e=n+this.winHeight,t=this.navBar.scrollTop()+(n-this.winPosition);n<0||e>this.docHeight||(this.navBar.scrollTop(t),this.winPosition=n)},onResize:function(){this.winResize=!1,this.winHeight=this.win.height(),this.docHeight=$(document).height()},hashChange:function(){this.linkScroll=!0,this.win.one("hashchange",(function(){this.linkScroll=!1}))},toggleCurrent:function(n){var e=n.closest("li");e.siblings("li.current").removeClass("current").attr("aria-expanded","false"),e.siblings().find("li.current").removeClass("current").attr("aria-expanded","false");var t=e.find("> ul li");t.length&&(t.removeClass("current").attr("aria-expanded","false"),e.toggleClass("current").attr("aria-expanded",(function(n,e){return"true"==e?"false":"true"})))}},"undefined"!=typeof window&&(window.SphinxRtdTheme={Navigation:n.exports.ThemeNav,StickyNav:n.exports.ThemeNav}),function(){for(var n=0,e=["ms","moz","webkit","o"],t=0;t a.language.name.localeCompare(b.language.name)); + + const languagesHTML = ` +
+
Languages
+ ${languages + .map( + (translation) => ` +
+ ${translation.language.code} +
+ `, + ) + .join("\n")} +
+ `; + return languagesHTML; + } + + function renderVersions(config) { + if (!config.versions.active.length) { + return ""; + } + const versionsHTML = ` +
+
Versions
+ ${config.versions.active + .map( + (version) => ` +
+ ${version.slug} +
+ `, + ) + .join("\n")} +
+ `; + return versionsHTML; + } + + function renderDownloads(config) { + if (!Object.keys(config.versions.current.downloads).length) { + return ""; + } + const downloadsNameDisplay = { + pdf: "PDF", + epub: "Epub", + htmlzip: "HTML", + }; + + const downloadsHTML = ` +
+
Downloads
+ ${Object.entries(config.versions.current.downloads) + .map( + ([name, url]) => ` +
+ ${downloadsNameDisplay[name]} +
+ `, + ) + .join("\n")} +
+ `; + return downloadsHTML; + } + + document.addEventListener("readthedocs-addons-data-ready", function (event) { + const config = event.detail.data(); + + const flyout = ` +
+ + Read the Docs + v: ${config.versions.current.slug} + + +
+
+ ${renderLanguages(config)} + ${renderVersions(config)} + ${renderDownloads(config)} +
+
On Read the Docs
+
+ Project Home +
+
+ Builds +
+
+ Downloads +
+
+
+
Search
+
+
+ +
+
+
+
+ + Hosted by Read the Docs + +
+
+ `; + + // Inject the generated flyout into the body HTML element. + document.body.insertAdjacentHTML("beforeend", flyout); + + // Trigger the Read the Docs Addons Search modal when clicking on the "Search docs" input from inside the flyout. + document + .querySelector("#flyout-search-form") + .addEventListener("focusin", () => { + const event = new CustomEvent("readthedocs-search-show"); + document.dispatchEvent(event); + }); + }) +} + +if (themeLanguageSelector || themeVersionSelector) { + function onSelectorSwitch(event) { + const option = event.target.selectedIndex; + const item = event.target.options[option]; + window.location.href = item.dataset.url; + } + + document.addEventListener("readthedocs-addons-data-ready", function (event) { + const config = event.detail.data(); + + const versionSwitch = document.querySelector( + "div.switch-menus > div.version-switch", + ); + if (themeVersionSelector) { + let versions = config.versions.active; + if (config.versions.current.hidden || config.versions.current.type === "external") { + versions.unshift(config.versions.current); + } + const versionSelect = ` + + `; + + versionSwitch.innerHTML = versionSelect; + versionSwitch.firstElementChild.addEventListener("change", onSelectorSwitch); + } + + const languageSwitch = document.querySelector( + "div.switch-menus > div.language-switch", + ); + + if (themeLanguageSelector) { + if (config.projects.translations.length) { + // Add the current language to the options on the selector + let languages = config.projects.translations.concat( + config.projects.current, + ); + languages = languages.sort((a, b) => + a.language.name.localeCompare(b.language.name), + ); + + const languageSelect = ` + + `; + + languageSwitch.innerHTML = languageSelect; + languageSwitch.firstElementChild.addEventListener("change", onSelectorSwitch); + } + else { + languageSwitch.remove(); + } + } + }); +} + +document.addEventListener("readthedocs-addons-data-ready", function (event) { + // Trigger the Read the Docs Addons Search modal when clicking on "Search docs" input from the topnav. + document + .querySelector("[role='search'] input") + .addEventListener("focusin", () => { + const event = new CustomEvent("readthedocs-search-show"); + document.dispatchEvent(event); + }); +}); diff --git a/api-doc/_build/html/_static/language_data.js b/api-doc/_build/html/_static/language_data.js new file mode 100644 index 0000000..a5ea78e --- /dev/null +++ b/api-doc/_build/html/_static/language_data.js @@ -0,0 +1,191 @@ +/* + * This script contains the language-specific data used by searchtools.js, + * namely the list of stopwords, stemmer, scorer and splitter. + */ + +var stopwords = ["a", "and", "are", "as", "at", "be", "but", "by", "for", "if", "in", "into", "is", "it", "near", "no", "not", "of", "on", "or", "such", "that", "the", "their", "then", "there", "these", "they", "this", "to", "was", "will", "with"]; + + +/* Non-minified version is copied as a separate JS file, if available */ + +/** + * Porter Stemmer + */ +var Stemmer = function() { + + var step2list = { + ational: 'ate', + tional: 'tion', + enci: 'ence', + anci: 'ance', + izer: 'ize', + bli: 'ble', + alli: 'al', + entli: 'ent', + eli: 'e', + ousli: 'ous', + ization: 'ize', + ation: 'ate', + ator: 'ate', + alism: 'al', + iveness: 'ive', + fulness: 'ful', + ousness: 'ous', + aliti: 'al', + iviti: 'ive', + biliti: 'ble', + logi: 'log' + }; + + var step3list = { + icate: 'ic', + ative: '', + alize: 'al', + iciti: 'ic', + ical: 'ic', + ful: '', + ness: '' + }; + + var c = "[^aeiou]"; // consonant + var v = "[aeiouy]"; // vowel + var C = c + "[^aeiouy]*"; // consonant sequence + var V = v + "[aeiou]*"; // vowel sequence + + var mgr0 = "^(" + C + ")?" + V + C; // [C]VC... is m>0 + var meq1 = "^(" + C + ")?" + V + C + "(" + V + ")?$"; // [C]VC[V] is m=1 + var mgr1 = "^(" + C + ")?" + V + C + V + C; // [C]VCVC... is m>1 + var s_v = "^(" + C + ")?" + v; // vowel in stem + + this.stemWord = function (w) { + var stem; + var suffix; + var firstch; + var origword = w; + + if (w.length < 3) + return w; + + var re; + var re2; + var re3; + var re4; + + firstch = w.substr(0,1); + if (firstch == "y") + w = firstch.toUpperCase() + w.substr(1); + + // Step 1a + re = /^(.+?)(ss|i)es$/; + re2 = /^(.+?)([^s])s$/; + + if (re.test(w)) + w = w.replace(re,"$1$2"); + else if (re2.test(w)) + w = w.replace(re2,"$1$2"); + + // Step 1b + re = /^(.+?)eed$/; + re2 = /^(.+?)(ed|ing)$/; + if (re.test(w)) { + var fp = re.exec(w); + re = new RegExp(mgr0); + if (re.test(fp[1])) { + re = /.$/; + w = w.replace(re,""); + } + } + else if (re2.test(w)) { + var fp = re2.exec(w); + stem = fp[1]; + re2 = new RegExp(s_v); + if (re2.test(stem)) { + w = stem; + re2 = /(at|bl|iz)$/; + re3 = new RegExp("([^aeiouylsz])\\1$"); + re4 = new RegExp("^" + C + v + "[^aeiouwxy]$"); + if (re2.test(w)) + w = w + "e"; + else if (re3.test(w)) { + re = /.$/; + w = w.replace(re,""); + } + else if (re4.test(w)) + w = w + "e"; + } + } + + // Step 1c + re = /^(.+?)y$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = new RegExp(s_v); + if (re.test(stem)) + w = stem + "i"; + } + + // Step 2 + re = /^(.+?)(ational|tional|enci|anci|izer|bli|alli|entli|eli|ousli|ization|ation|ator|alism|iveness|fulness|ousness|aliti|iviti|biliti|logi)$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + suffix = fp[2]; + re = new RegExp(mgr0); + if (re.test(stem)) + w = stem + step2list[suffix]; + } + + // Step 3 + re = /^(.+?)(icate|ative|alize|iciti|ical|ful|ness)$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + suffix = fp[2]; + re = new RegExp(mgr0); + if (re.test(stem)) + w = stem + step3list[suffix]; + } + + // Step 4 + re = /^(.+?)(al|ance|ence|er|ic|able|ible|ant|ement|ment|ent|ou|ism|ate|iti|ous|ive|ize)$/; + re2 = /^(.+?)(s|t)(ion)$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = new RegExp(mgr1); + if (re.test(stem)) + w = stem; + } + else if (re2.test(w)) { + var fp = re2.exec(w); + stem = fp[1] + fp[2]; + re2 = new RegExp(mgr1); + if (re2.test(stem)) + w = stem; + } + + // Step 5 + re = /^(.+?)e$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = new RegExp(mgr1); + re2 = new RegExp(meq1); + re3 = new RegExp("^" + C + v + "[^aeiouwxy]$"); + if (re.test(stem) || (re2.test(stem) && !(re3.test(stem)))) + w = stem; + } + re = /ll$/; + re2 = new RegExp(mgr1); + if (re.test(w) && re2.test(w)) { + re = /.$/; + w = w.replace(re,""); + } + + // and turn initial Y back to y + if (firstch == "y") + w = firstch.toLowerCase() + w.substr(1); + return w; + } +} diff --git a/api-doc/_build/html/_static/minus.png b/api-doc/_build/html/_static/minus.png new file mode 100644 index 0000000..a68c398 Binary files /dev/null and b/api-doc/_build/html/_static/minus.png differ diff --git a/api-doc/_build/html/_static/plus.png b/api-doc/_build/html/_static/plus.png new file mode 100644 index 0000000..83d1bd6 Binary files /dev/null and b/api-doc/_build/html/_static/plus.png differ diff --git a/api-doc/_build/html/_static/pygments.css b/api-doc/_build/html/_static/pygments.css new file mode 100644 index 0000000..110e5a5 --- /dev/null +++ b/api-doc/_build/html/_static/pygments.css @@ -0,0 +1,75 @@ +pre { line-height: 125%; } +td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +.highlight .hll { background-color: #ffffcc } +.highlight { background: #eeffcc; } +.highlight .c { color: #408090; font-style: italic } /* Comment */ +.highlight .err { border: 1px solid #F00 } /* Error */ +.highlight .k { color: #007020; font-weight: bold } /* Keyword */ +.highlight .o { color: #666 } /* Operator */ +.highlight .ch { color: #408090; font-style: italic } /* Comment.Hashbang */ +.highlight .cm { color: #408090; font-style: italic } /* Comment.Multiline */ +.highlight .cp { color: #007020 } /* Comment.Preproc */ +.highlight .cpf { color: #408090; font-style: italic } /* Comment.PreprocFile */ +.highlight .c1 { color: #408090; font-style: italic } /* Comment.Single */ +.highlight .cs { color: #408090; background-color: #FFF0F0 } /* Comment.Special */ +.highlight .gd { color: #A00000 } /* Generic.Deleted */ +.highlight .ge { font-style: italic } /* Generic.Emph */ +.highlight .ges { font-weight: bold; font-style: italic } /* Generic.EmphStrong */ +.highlight .gr { color: #F00 } /* Generic.Error */ +.highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */ +.highlight .gi { color: #00A000 } /* Generic.Inserted */ +.highlight .go { color: #333 } /* Generic.Output */ +.highlight .gp { color: #C65D09; font-weight: bold } /* Generic.Prompt */ +.highlight .gs { font-weight: bold } /* Generic.Strong */ +.highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ +.highlight .gt { color: #04D } /* Generic.Traceback */ +.highlight .kc { color: #007020; font-weight: bold } /* Keyword.Constant */ +.highlight .kd { color: #007020; font-weight: bold } /* Keyword.Declaration */ +.highlight .kn { color: #007020; font-weight: bold } /* Keyword.Namespace */ +.highlight .kp { color: #007020 } /* Keyword.Pseudo */ +.highlight .kr { color: #007020; font-weight: bold } /* Keyword.Reserved */ +.highlight .kt { color: #902000 } /* Keyword.Type */ +.highlight .m { color: #208050 } /* Literal.Number */ +.highlight .s { color: #4070A0 } /* Literal.String */ +.highlight .na { color: #4070A0 } /* Name.Attribute */ +.highlight .nb { color: #007020 } /* Name.Builtin */ +.highlight .nc { color: #0E84B5; font-weight: bold } /* Name.Class */ +.highlight .no { color: #60ADD5 } /* Name.Constant */ +.highlight .nd { color: #555; font-weight: bold } /* Name.Decorator */ +.highlight .ni { color: #D55537; font-weight: bold } /* Name.Entity */ +.highlight .ne { color: #007020 } /* Name.Exception */ +.highlight .nf { color: #06287E } /* Name.Function */ +.highlight .nl { color: #002070; font-weight: bold } /* Name.Label */ +.highlight .nn { color: #0E84B5; font-weight: bold } /* Name.Namespace */ +.highlight .nt { color: #062873; font-weight: bold } /* Name.Tag */ +.highlight .nv { color: #BB60D5 } /* Name.Variable */ +.highlight .ow { color: #007020; font-weight: bold } /* Operator.Word */ +.highlight .w { color: #BBB } /* Text.Whitespace */ +.highlight .mb { color: #208050 } /* Literal.Number.Bin */ +.highlight .mf { color: #208050 } /* Literal.Number.Float */ +.highlight .mh { color: #208050 } /* Literal.Number.Hex */ +.highlight .mi { color: #208050 } /* Literal.Number.Integer */ +.highlight .mo { color: #208050 } /* Literal.Number.Oct */ +.highlight .sa { color: #4070A0 } /* Literal.String.Affix */ +.highlight .sb { color: #4070A0 } /* Literal.String.Backtick */ +.highlight .sc { color: #4070A0 } /* Literal.String.Char */ +.highlight .dl { color: #4070A0 } /* Literal.String.Delimiter */ +.highlight .sd { color: #4070A0; font-style: italic } /* Literal.String.Doc */ +.highlight .s2 { color: #4070A0 } /* Literal.String.Double */ +.highlight .se { color: #4070A0; font-weight: bold } /* Literal.String.Escape */ +.highlight .sh { color: #4070A0 } /* Literal.String.Heredoc */ +.highlight .si { color: #70A0D0; font-style: italic } /* Literal.String.Interpol */ +.highlight .sx { color: #C65D09 } /* Literal.String.Other */ +.highlight .sr { color: #235388 } /* Literal.String.Regex */ +.highlight .s1 { color: #4070A0 } /* Literal.String.Single */ +.highlight .ss { color: #517918 } /* Literal.String.Symbol */ +.highlight .bp { color: #007020 } /* Name.Builtin.Pseudo */ +.highlight .fm { color: #06287E } /* Name.Function.Magic */ +.highlight .vc { color: #BB60D5 } /* Name.Variable.Class */ +.highlight .vg { color: #BB60D5 } /* Name.Variable.Global */ +.highlight .vi { color: #BB60D5 } /* Name.Variable.Instance */ +.highlight .vm { color: #BB60D5 } /* Name.Variable.Magic */ +.highlight .il { color: #208050 } /* Literal.Number.Integer.Long */ diff --git a/api-doc/_build/html/_static/searchtools.js b/api-doc/_build/html/_static/searchtools.js new file mode 100644 index 0000000..91f4be5 --- /dev/null +++ b/api-doc/_build/html/_static/searchtools.js @@ -0,0 +1,635 @@ +/* + * Sphinx JavaScript utilities for the full-text search. + */ +"use strict"; + +/** + * Simple result scoring code. + */ +if (typeof Scorer === "undefined") { + var Scorer = { + // Implement the following function to further tweak the score for each result + // The function takes a result array [docname, title, anchor, descr, score, filename] + // and returns the new score. + /* + score: result => { + const [docname, title, anchor, descr, score, filename, kind] = result + return score + }, + */ + + // query matches the full name of an object + objNameMatch: 11, + // or matches in the last dotted part of the object name + objPartialMatch: 6, + // Additive scores depending on the priority of the object + objPrio: { + 0: 15, // used to be importantResults + 1: 5, // used to be objectResults + 2: -5, // used to be unimportantResults + }, + // Used when the priority is not in the mapping. + objPrioDefault: 0, + + // query found in title + title: 15, + partialTitle: 7, + // query found in terms + term: 5, + partialTerm: 2, + }; +} + +// Global search result kind enum, used by themes to style search results. +class SearchResultKind { + static get index() { return "index"; } + static get object() { return "object"; } + static get text() { return "text"; } + static get title() { return "title"; } +} + +const _removeChildren = (element) => { + while (element && element.lastChild) element.removeChild(element.lastChild); +}; + +/** + * See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping + */ +const _escapeRegExp = (string) => + string.replace(/[.*+\-?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string + +const _displayItem = (item, searchTerms, highlightTerms) => { + const docBuilder = DOCUMENTATION_OPTIONS.BUILDER; + const docFileSuffix = DOCUMENTATION_OPTIONS.FILE_SUFFIX; + const docLinkSuffix = DOCUMENTATION_OPTIONS.LINK_SUFFIX; + const showSearchSummary = DOCUMENTATION_OPTIONS.SHOW_SEARCH_SUMMARY; + const contentRoot = document.documentElement.dataset.content_root; + + const [docName, title, anchor, descr, score, _filename, kind] = item; + + let listItem = document.createElement("li"); + // Add a class representing the item's type: + // can be used by a theme's CSS selector for styling + // See SearchResultKind for the class names. + listItem.classList.add(`kind-${kind}`); + let requestUrl; + let linkUrl; + if (docBuilder === "dirhtml") { + // dirhtml builder + let dirname = docName + "/"; + if (dirname.match(/\/index\/$/)) + dirname = dirname.substring(0, dirname.length - 6); + else if (dirname === "index/") dirname = ""; + requestUrl = contentRoot + dirname; + linkUrl = requestUrl; + } else { + // normal html builders + requestUrl = contentRoot + docName + docFileSuffix; + linkUrl = docName + docLinkSuffix; + } + let linkEl = listItem.appendChild(document.createElement("a")); + linkEl.href = linkUrl + anchor; + linkEl.dataset.score = score; + linkEl.innerHTML = title; + if (descr) { + listItem.appendChild(document.createElement("span")).innerHTML = + " (" + descr + ")"; + // highlight search terms in the description + if (SPHINX_HIGHLIGHT_ENABLED) // set in sphinx_highlight.js + highlightTerms.forEach((term) => _highlightText(listItem, term, "highlighted")); + } + else if (showSearchSummary) + fetch(requestUrl) + .then((responseData) => responseData.text()) + .then((data) => { + if (data) + listItem.appendChild( + Search.makeSearchSummary(data, searchTerms, anchor) + ); + // highlight search terms in the summary + if (SPHINX_HIGHLIGHT_ENABLED) // set in sphinx_highlight.js + highlightTerms.forEach((term) => _highlightText(listItem, term, "highlighted")); + }); + Search.output.appendChild(listItem); +}; +const _finishSearch = (resultCount) => { + Search.stopPulse(); + Search.title.innerText = _("Search Results"); + if (!resultCount) + Search.status.innerText = Documentation.gettext( + "Your search did not match any documents. Please make sure that all words are spelled correctly and that you've selected enough categories." + ); + else + Search.status.innerText = Documentation.ngettext( + "Search finished, found one page matching the search query.", + "Search finished, found ${resultCount} pages matching the search query.", + resultCount, + ).replace('${resultCount}', resultCount); +}; +const _displayNextItem = ( + results, + resultCount, + searchTerms, + highlightTerms, +) => { + // results left, load the summary and display it + // this is intended to be dynamic (don't sub resultsCount) + if (results.length) { + _displayItem(results.pop(), searchTerms, highlightTerms); + setTimeout( + () => _displayNextItem(results, resultCount, searchTerms, highlightTerms), + 5 + ); + } + // search finished, update title and status message + else _finishSearch(resultCount); +}; +// Helper function used by query() to order search results. +// Each input is an array of [docname, title, anchor, descr, score, filename, kind]. +// Order the results by score (in opposite order of appearance, since the +// `_displayNextItem` function uses pop() to retrieve items) and then alphabetically. +const _orderResultsByScoreThenName = (a, b) => { + const leftScore = a[4]; + const rightScore = b[4]; + if (leftScore === rightScore) { + // same score: sort alphabetically + const leftTitle = a[1].toLowerCase(); + const rightTitle = b[1].toLowerCase(); + if (leftTitle === rightTitle) return 0; + return leftTitle > rightTitle ? -1 : 1; // inverted is intentional + } + return leftScore > rightScore ? 1 : -1; +}; + +/** + * Default splitQuery function. Can be overridden in ``sphinx.search`` with a + * custom function per language. + * + * The regular expression works by splitting the string on consecutive characters + * that are not Unicode letters, numbers, underscores, or emoji characters. + * This is the same as ``\W+`` in Python, preserving the surrogate pair area. + */ +if (typeof splitQuery === "undefined") { + var splitQuery = (query) => query + .split(/[^\p{Letter}\p{Number}_\p{Emoji_Presentation}]+/gu) + .filter(term => term) // remove remaining empty strings +} + +/** + * Search Module + */ +const Search = { + _index: null, + _queued_query: null, + _pulse_status: -1, + + htmlToText: (htmlString, anchor) => { + const htmlElement = new DOMParser().parseFromString(htmlString, 'text/html'); + for (const removalQuery of [".headerlink", "script", "style"]) { + htmlElement.querySelectorAll(removalQuery).forEach((el) => { el.remove() }); + } + if (anchor) { + const anchorContent = htmlElement.querySelector(`[role="main"] ${anchor}`); + if (anchorContent) return anchorContent.textContent; + + console.warn( + `Anchored content block not found. Sphinx search tries to obtain it via DOM query '[role=main] ${anchor}'. Check your theme or template.` + ); + } + + // if anchor not specified or not found, fall back to main content + const docContent = htmlElement.querySelector('[role="main"]'); + if (docContent) return docContent.textContent; + + console.warn( + "Content block not found. Sphinx search tries to obtain it via DOM query '[role=main]'. Check your theme or template." + ); + return ""; + }, + + init: () => { + const query = new URLSearchParams(window.location.search).get("q"); + document + .querySelectorAll('input[name="q"]') + .forEach((el) => (el.value = query)); + if (query) Search.performSearch(query); + }, + + loadIndex: (url) => + (document.body.appendChild(document.createElement("script")).src = url), + + setIndex: (index) => { + Search._index = index; + if (Search._queued_query !== null) { + const query = Search._queued_query; + Search._queued_query = null; + Search.query(query); + } + }, + + hasIndex: () => Search._index !== null, + + deferQuery: (query) => (Search._queued_query = query), + + stopPulse: () => (Search._pulse_status = -1), + + startPulse: () => { + if (Search._pulse_status >= 0) return; + + const pulse = () => { + Search._pulse_status = (Search._pulse_status + 1) % 4; + Search.dots.innerText = ".".repeat(Search._pulse_status); + if (Search._pulse_status >= 0) window.setTimeout(pulse, 500); + }; + pulse(); + }, + + /** + * perform a search for something (or wait until index is loaded) + */ + performSearch: (query) => { + // create the required interface elements + const searchText = document.createElement("h2"); + searchText.textContent = _("Searching"); + const searchSummary = document.createElement("p"); + searchSummary.classList.add("search-summary"); + searchSummary.innerText = ""; + const searchList = document.createElement("ul"); + searchList.setAttribute("role", "list"); + searchList.classList.add("search"); + + const out = document.getElementById("search-results"); + Search.title = out.appendChild(searchText); + Search.dots = Search.title.appendChild(document.createElement("span")); + Search.status = out.appendChild(searchSummary); + Search.output = out.appendChild(searchList); + + const searchProgress = document.getElementById("search-progress"); + // Some themes don't use the search progress node + if (searchProgress) { + searchProgress.innerText = _("Preparing search..."); + } + Search.startPulse(); + + // index already loaded, the browser was quick! + if (Search.hasIndex()) Search.query(query); + else Search.deferQuery(query); + }, + + _parseQuery: (query) => { + // stem the search terms and add them to the correct list + const stemmer = new Stemmer(); + const searchTerms = new Set(); + const excludedTerms = new Set(); + const highlightTerms = new Set(); + const objectTerms = new Set(splitQuery(query.toLowerCase().trim())); + splitQuery(query.trim()).forEach((queryTerm) => { + const queryTermLower = queryTerm.toLowerCase(); + + // maybe skip this "word" + // stopwords array is from language_data.js + if ( + stopwords.indexOf(queryTermLower) !== -1 || + queryTerm.match(/^\d+$/) + ) + return; + + // stem the word + let word = stemmer.stemWord(queryTermLower); + // select the correct list + if (word[0] === "-") excludedTerms.add(word.substr(1)); + else { + searchTerms.add(word); + highlightTerms.add(queryTermLower); + } + }); + + if (SPHINX_HIGHLIGHT_ENABLED) { // set in sphinx_highlight.js + localStorage.setItem("sphinx_highlight_terms", [...highlightTerms].join(" ")) + } + + // console.debug("SEARCH: searching for:"); + // console.info("required: ", [...searchTerms]); + // console.info("excluded: ", [...excludedTerms]); + + return [query, searchTerms, excludedTerms, highlightTerms, objectTerms]; + }, + + /** + * execute search (requires search index to be loaded) + */ + _performSearch: (query, searchTerms, excludedTerms, highlightTerms, objectTerms) => { + const filenames = Search._index.filenames; + const docNames = Search._index.docnames; + const titles = Search._index.titles; + const allTitles = Search._index.alltitles; + const indexEntries = Search._index.indexentries; + + // Collect multiple result groups to be sorted separately and then ordered. + // Each is an array of [docname, title, anchor, descr, score, filename, kind]. + const normalResults = []; + const nonMainIndexResults = []; + + _removeChildren(document.getElementById("search-progress")); + + const queryLower = query.toLowerCase().trim(); + for (const [title, foundTitles] of Object.entries(allTitles)) { + if (title.toLowerCase().trim().includes(queryLower) && (queryLower.length >= title.length/2)) { + for (const [file, id] of foundTitles) { + const score = Math.round(Scorer.title * queryLower.length / title.length); + const boost = titles[file] === title ? 1 : 0; // add a boost for document titles + normalResults.push([ + docNames[file], + titles[file] !== title ? `${titles[file]} > ${title}` : title, + id !== null ? "#" + id : "", + null, + score + boost, + filenames[file], + SearchResultKind.title, + ]); + } + } + } + + // search for explicit entries in index directives + for (const [entry, foundEntries] of Object.entries(indexEntries)) { + if (entry.includes(queryLower) && (queryLower.length >= entry.length/2)) { + for (const [file, id, isMain] of foundEntries) { + const score = Math.round(100 * queryLower.length / entry.length); + const result = [ + docNames[file], + titles[file], + id ? "#" + id : "", + null, + score, + filenames[file], + SearchResultKind.index, + ]; + if (isMain) { + normalResults.push(result); + } else { + nonMainIndexResults.push(result); + } + } + } + } + + // lookup as object + objectTerms.forEach((term) => + normalResults.push(...Search.performObjectSearch(term, objectTerms)) + ); + + // lookup as search terms in fulltext + normalResults.push(...Search.performTermsSearch(searchTerms, excludedTerms)); + + // let the scorer override scores with a custom scoring function + if (Scorer.score) { + normalResults.forEach((item) => (item[4] = Scorer.score(item))); + nonMainIndexResults.forEach((item) => (item[4] = Scorer.score(item))); + } + + // Sort each group of results by score and then alphabetically by name. + normalResults.sort(_orderResultsByScoreThenName); + nonMainIndexResults.sort(_orderResultsByScoreThenName); + + // Combine the result groups in (reverse) order. + // Non-main index entries are typically arbitrary cross-references, + // so display them after other results. + let results = [...nonMainIndexResults, ...normalResults]; + + // remove duplicate search results + // note the reversing of results, so that in the case of duplicates, the highest-scoring entry is kept + let seen = new Set(); + results = results.reverse().reduce((acc, result) => { + let resultStr = result.slice(0, 4).concat([result[5]]).map(v => String(v)).join(','); + if (!seen.has(resultStr)) { + acc.push(result); + seen.add(resultStr); + } + return acc; + }, []); + + return results.reverse(); + }, + + query: (query) => { + const [searchQuery, searchTerms, excludedTerms, highlightTerms, objectTerms] = Search._parseQuery(query); + const results = Search._performSearch(searchQuery, searchTerms, excludedTerms, highlightTerms, objectTerms); + + // for debugging + //Search.lastresults = results.slice(); // a copy + // console.info("search results:", Search.lastresults); + + // print the results + _displayNextItem(results, results.length, searchTerms, highlightTerms); + }, + + /** + * search for object names + */ + performObjectSearch: (object, objectTerms) => { + const filenames = Search._index.filenames; + const docNames = Search._index.docnames; + const objects = Search._index.objects; + const objNames = Search._index.objnames; + const titles = Search._index.titles; + + const results = []; + + const objectSearchCallback = (prefix, match) => { + const name = match[4] + const fullname = (prefix ? prefix + "." : "") + name; + const fullnameLower = fullname.toLowerCase(); + if (fullnameLower.indexOf(object) < 0) return; + + let score = 0; + const parts = fullnameLower.split("."); + + // check for different match types: exact matches of full name or + // "last name" (i.e. last dotted part) + if (fullnameLower === object || parts.slice(-1)[0] === object) + score += Scorer.objNameMatch; + else if (parts.slice(-1)[0].indexOf(object) > -1) + score += Scorer.objPartialMatch; // matches in last name + + const objName = objNames[match[1]][2]; + const title = titles[match[0]]; + + // If more than one term searched for, we require other words to be + // found in the name/title/description + const otherTerms = new Set(objectTerms); + otherTerms.delete(object); + if (otherTerms.size > 0) { + const haystack = `${prefix} ${name} ${objName} ${title}`.toLowerCase(); + if ( + [...otherTerms].some((otherTerm) => haystack.indexOf(otherTerm) < 0) + ) + return; + } + + let anchor = match[3]; + if (anchor === "") anchor = fullname; + else if (anchor === "-") anchor = objNames[match[1]][1] + "-" + fullname; + + const descr = objName + _(", in ") + title; + + // add custom score for some objects according to scorer + if (Scorer.objPrio.hasOwnProperty(match[2])) + score += Scorer.objPrio[match[2]]; + else score += Scorer.objPrioDefault; + + results.push([ + docNames[match[0]], + fullname, + "#" + anchor, + descr, + score, + filenames[match[0]], + SearchResultKind.object, + ]); + }; + Object.keys(objects).forEach((prefix) => + objects[prefix].forEach((array) => + objectSearchCallback(prefix, array) + ) + ); + return results; + }, + + /** + * search for full-text terms in the index + */ + performTermsSearch: (searchTerms, excludedTerms) => { + // prepare search + const terms = Search._index.terms; + const titleTerms = Search._index.titleterms; + const filenames = Search._index.filenames; + const docNames = Search._index.docnames; + const titles = Search._index.titles; + + const scoreMap = new Map(); + const fileMap = new Map(); + + // perform the search on the required terms + searchTerms.forEach((word) => { + const files = []; + // find documents, if any, containing the query word in their text/title term indices + // use Object.hasOwnProperty to avoid mismatching against prototype properties + const arr = [ + { files: terms.hasOwnProperty(word) ? terms[word] : undefined, score: Scorer.term }, + { files: titleTerms.hasOwnProperty(word) ? titleTerms[word] : undefined, score: Scorer.title }, + ]; + // add support for partial matches + if (word.length > 2) { + const escapedWord = _escapeRegExp(word); + if (!terms.hasOwnProperty(word)) { + Object.keys(terms).forEach((term) => { + if (term.match(escapedWord)) + arr.push({ files: terms[term], score: Scorer.partialTerm }); + }); + } + if (!titleTerms.hasOwnProperty(word)) { + Object.keys(titleTerms).forEach((term) => { + if (term.match(escapedWord)) + arr.push({ files: titleTerms[term], score: Scorer.partialTitle }); + }); + } + } + + // no match but word was a required one + if (arr.every((record) => record.files === undefined)) return; + + // found search word in contents + arr.forEach((record) => { + if (record.files === undefined) return; + + let recordFiles = record.files; + if (recordFiles.length === undefined) recordFiles = [recordFiles]; + files.push(...recordFiles); + + // set score for the word in each file + recordFiles.forEach((file) => { + if (!scoreMap.has(file)) scoreMap.set(file, new Map()); + const fileScores = scoreMap.get(file); + fileScores.set(word, record.score); + }); + }); + + // create the mapping + files.forEach((file) => { + if (!fileMap.has(file)) fileMap.set(file, [word]); + else if (fileMap.get(file).indexOf(word) === -1) fileMap.get(file).push(word); + }); + }); + + // now check if the files don't contain excluded terms + const results = []; + for (const [file, wordList] of fileMap) { + // check if all requirements are matched + + // as search terms with length < 3 are discarded + const filteredTermCount = [...searchTerms].filter( + (term) => term.length > 2 + ).length; + if ( + wordList.length !== searchTerms.size && + wordList.length !== filteredTermCount + ) + continue; + + // ensure that none of the excluded terms is in the search result + if ( + [...excludedTerms].some( + (term) => + terms[term] === file || + titleTerms[term] === file || + (terms[term] || []).includes(file) || + (titleTerms[term] || []).includes(file) + ) + ) + break; + + // select one (max) score for the file. + const score = Math.max(...wordList.map((w) => scoreMap.get(file).get(w))); + // add result to the result list + results.push([ + docNames[file], + titles[file], + "", + null, + score, + filenames[file], + SearchResultKind.text, + ]); + } + return results; + }, + + /** + * helper function to return a node containing the + * search summary for a given text. keywords is a list + * of stemmed words. + */ + makeSearchSummary: (htmlText, keywords, anchor) => { + const text = Search.htmlToText(htmlText, anchor); + if (text === "") return null; + + const textLower = text.toLowerCase(); + const actualStartPosition = [...keywords] + .map((k) => textLower.indexOf(k.toLowerCase())) + .filter((i) => i > -1) + .slice(-1)[0]; + const startWithContext = Math.max(actualStartPosition - 120, 0); + + const top = startWithContext === 0 ? "" : "..."; + const tail = startWithContext + 240 < text.length ? "..." : ""; + + let summary = document.createElement("p"); + summary.classList.add("context"); + summary.textContent = top + text.substr(startWithContext, 240).trim() + tail; + + return summary; + }, +}; + +_ready(Search.init); diff --git a/api-doc/_build/html/_static/sphinx_highlight.js b/api-doc/_build/html/_static/sphinx_highlight.js new file mode 100644 index 0000000..8a96c69 --- /dev/null +++ b/api-doc/_build/html/_static/sphinx_highlight.js @@ -0,0 +1,154 @@ +/* Highlighting utilities for Sphinx HTML documentation. */ +"use strict"; + +const SPHINX_HIGHLIGHT_ENABLED = true + +/** + * highlight a given string on a node by wrapping it in + * span elements with the given class name. + */ +const _highlight = (node, addItems, text, className) => { + if (node.nodeType === Node.TEXT_NODE) { + const val = node.nodeValue; + const parent = node.parentNode; + const pos = val.toLowerCase().indexOf(text); + if ( + pos >= 0 && + !parent.classList.contains(className) && + !parent.classList.contains("nohighlight") + ) { + let span; + + const closestNode = parent.closest("body, svg, foreignObject"); + const isInSVG = closestNode && closestNode.matches("svg"); + if (isInSVG) { + span = document.createElementNS("http://www.w3.org/2000/svg", "tspan"); + } else { + span = document.createElement("span"); + span.classList.add(className); + } + + span.appendChild(document.createTextNode(val.substr(pos, text.length))); + const rest = document.createTextNode(val.substr(pos + text.length)); + parent.insertBefore( + span, + parent.insertBefore( + rest, + node.nextSibling + ) + ); + node.nodeValue = val.substr(0, pos); + /* There may be more occurrences of search term in this node. So call this + * function recursively on the remaining fragment. + */ + _highlight(rest, addItems, text, className); + + if (isInSVG) { + const rect = document.createElementNS( + "http://www.w3.org/2000/svg", + "rect" + ); + const bbox = parent.getBBox(); + rect.x.baseVal.value = bbox.x; + rect.y.baseVal.value = bbox.y; + rect.width.baseVal.value = bbox.width; + rect.height.baseVal.value = bbox.height; + rect.setAttribute("class", className); + addItems.push({ parent: parent, target: rect }); + } + } + } else if (node.matches && !node.matches("button, select, textarea")) { + node.childNodes.forEach((el) => _highlight(el, addItems, text, className)); + } +}; +const _highlightText = (thisNode, text, className) => { + let addItems = []; + _highlight(thisNode, addItems, text, className); + addItems.forEach((obj) => + obj.parent.insertAdjacentElement("beforebegin", obj.target) + ); +}; + +/** + * Small JavaScript module for the documentation. + */ +const SphinxHighlight = { + + /** + * highlight the search words provided in localstorage in the text + */ + highlightSearchWords: () => { + if (!SPHINX_HIGHLIGHT_ENABLED) return; // bail if no highlight + + // get and clear terms from localstorage + const url = new URL(window.location); + const highlight = + localStorage.getItem("sphinx_highlight_terms") + || url.searchParams.get("highlight") + || ""; + localStorage.removeItem("sphinx_highlight_terms") + url.searchParams.delete("highlight"); + window.history.replaceState({}, "", url); + + // get individual terms from highlight string + const terms = highlight.toLowerCase().split(/\s+/).filter(x => x); + if (terms.length === 0) return; // nothing to do + + // There should never be more than one element matching "div.body" + const divBody = document.querySelectorAll("div.body"); + const body = divBody.length ? divBody[0] : document.querySelector("body"); + window.setTimeout(() => { + terms.forEach((term) => _highlightText(body, term, "highlighted")); + }, 10); + + const searchBox = document.getElementById("searchbox"); + if (searchBox === null) return; + searchBox.appendChild( + document + .createRange() + .createContextualFragment( + '" + ) + ); + }, + + /** + * helper function to hide the search marks again + */ + hideSearchWords: () => { + document + .querySelectorAll("#searchbox .highlight-link") + .forEach((el) => el.remove()); + document + .querySelectorAll("span.highlighted") + .forEach((el) => el.classList.remove("highlighted")); + localStorage.removeItem("sphinx_highlight_terms") + }, + + initEscapeListener: () => { + // only install a listener if it is really needed + if (!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS) return; + + document.addEventListener("keydown", (event) => { + // bail for input elements + if (BLACKLISTED_KEY_CONTROL_ELEMENTS.has(document.activeElement.tagName)) return; + // bail with special keys + if (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey) return; + if (DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS && (event.key === "Escape")) { + SphinxHighlight.hideSearchWords(); + event.preventDefault(); + } + }); + }, +}; + +_ready(() => { + /* Do not call highlightSearchWords() when we are on the search page. + * It will highlight words from the *previous* search query. + */ + if (typeof Search === "undefined") SphinxHighlight.highlightSearchWords(); + SphinxHighlight.initEscapeListener(); +}); diff --git a/api-doc/_build/html/genindex.html b/api-doc/_build/html/genindex.html new file mode 100644 index 0000000..a39ae0d --- /dev/null +++ b/api-doc/_build/html/genindex.html @@ -0,0 +1,1389 @@ + + + + + + + + Index — AMQ Adapter Python 1.0 documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • + +
  • +
  • +
+
+
+
+
+ + +

Index

+ +
+ _ + | A + | B + | C + | D + | E + | F + | G + | H + | I + | K + | L + | M + | O + | P + | Q + | R + | S + | T + | U + | V + | W + +
+

_

+ + +
+ +

A

+ + + +
+ +

B

+ + + +
+ +

C

+ + + +
+ +

D

+ + + +
+ +

E

+ + + +
+ +

F

+ + + +
+ +

G

+ + + +
+ +

H

+ + + +
+ +

I

+ + + +
+ +

K

+ + +
+ +

L

+ + + +
+ +

M

+ + + +
+ +

O

+ + + +
+ +

P

+ + + +
+ +

Q

+ + + +
+ +

R

+ + + +
+ +

S

+ + + +
+ +

T

+ + + +
+ +

U

+ + + +
+ +

V

+ + + +
+ +

W

+ + + +
+ + + +
+
+ +
+
+
+
+ + + + diff --git a/api-doc/_build/html/index.html b/api-doc/_build/html/index.html new file mode 100644 index 0000000..c302a73 --- /dev/null +++ b/api-doc/_build/html/index.html @@ -0,0 +1,230 @@ + + + + + + + + + AMQ Adapter Python API Documentation — AMQ Adapter Python 1.0 documentation + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

AMQ Adapter Python API Documentation

+

Welcome to the AMQ Adapter Python API documentation. This library provides a Python implementation of the AMQ adapter for routing traffic over RabbitMQ to intended services with automated service discovery.

+
+

Contents:

+ +
+
+
+

Indices and tables

+ +
+ + +
+
+ +
+
+
+
+ + + + diff --git a/api-doc/_build/html/modules/amqp.adapter.html b/api-doc/_build/html/modules/amqp.adapter.html new file mode 100644 index 0000000..0035264 --- /dev/null +++ b/api-doc/_build/html/modules/amqp.adapter.html @@ -0,0 +1,2119 @@ + + + + + + + + + Adapter Module — AMQ Adapter Python 1.0 documentation + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

Adapter Module

+

The layer that interfaces with the CleverThis service that this Adapter is integrating with. +Methods here will invoke the actual CleverThis Service code, which are passed as Callable object.

+
+
+amqp.adapter.cleverthis_service_adapter.is_likely_json(text)[source]
+

Quick test to see if should convert the response string to JSON format.

+
+
Parameters:
+

text (str)

+
+
Return type:
+

bool

+
+
+
+ +
+
+class amqp.adapter.cleverthis_service_adapter.CleverThisServiceAdapter(amq_configuration, routes, session=None, authenticated_user_arg_name='authenticated_user', extra_init_data=None)[source]
+

Bases: object

+

A base class for CleverThis service adapter that handles the incoming AMQP messages and routes +them to the appropriate CleverThisService API endpoint. +IMPORTANT: This class is not intended to be used directly. It should be subclassed for +each CleverThis service and override the methods to handle the specific API calls.

+
+
Parameters:
+
    +
  • amq_configuration (AMQConfiguration)

  • +
  • routes (List[APIRoute])

  • +
  • session (ClientSession)

  • +
  • authenticated_user_arg_name (str)

  • +
+
+
+
+
+__init__(amq_configuration, routes, session=None, authenticated_user_arg_name='authenticated_user', extra_init_data=None)[source]
+

Initializes the CleverThisServiceAdapter with the given AMQ configuration and optional HTTP +session. The HTTP session is for the case when the adapter uses loose coupling with +the CleverThis service via HTTP REST API.

+
+
Parameters:
+
    +
  • amq_configuration (AMQConfiguration)

  • +
  • routes (List[APIRoute])

  • +
  • session (ClientSession)

  • +
  • authenticated_user_arg_name (str)

  • +
+
+
+
+ +
+
+get_endpoint(endpoint)[source]
+

Optionally, Service can override this method to alter the endpoint mapping. +This is useful for example when the CleverThis service has a JSON wrapper +around the endpoint, like CleverSwarm, which might be the preferred endpoint to call.

+
+
Parameters:
+

endpoint (Any) – The original endpoint to be called.

+
+
Returns:
+

The actual endpoint to be called.

+
+
Return type:
+

Any

+
+
+
+ +
+
+clone_and_adapt_route(original_route)[source]
+

Creates a clone of APIRoute. Checks if there’s preferred endpoint with json serialization. +It also adjusts the path regex to ensure it matches the path in different API versions.

+
+
Parameters:
+

original_route (APIRoute)

+
+
Return type:
+

APIRoute

+
+
+
+ +
+
+group_routes_by_method(api_routes)[source]
+

Groups API routes by their HTTP method. This is important to ensure that URLPath is matched +correctly, as different methods may share the same URLPath (e.g. GET, POST PUT, DELETE).

+
+
Parameters:
+

api_routes (List[APIRoute])

+
+
Return type:
+

Dict[str, List[APIRoute]]

+
+
+
+ +
+
+get_content_type(message_headers)[source]
+

Pull the value of the Content-Type header from the message headers. +:param message_headers: The headers of the message. +return: The content type of the message.

+
+
Parameters:
+

message_headers (Dict[str, List[str]])

+
+
Return type:
+

str

+
+
+
+ +
+
+async get_current_user(token)[source]
+

To Be Overriden in subclass for given CleverThis Service, to return the current_availability user based +on the token, in format appropriate for the service.

+
+
Parameters:
+

token (str) – The token to be used for authentication.

+
+
Returns:
+

A user object or None if the user is not authenticated.

+
+
Return type:
+

object

+
+
+
+ +
+
+async on_message(message)[source]
+

Called when a message is received from the AMQP. +:param message: +:return: AMQP response class with operation status code and optional error details.

+
+
Parameters:
+

message (AMQMessage)

+
+
Return type:
+

DataResponse

+
+
+
+ +
+
+async get(message, auth_user)[source]
+

Handles the GET requests for the CleverThis API. In tight coupling (self.session is None), +use path, path params and query string to determine the endpoint method to invoke. +In loose coupling (self.session is not None), forward the request as REST request using +the session, i.e. attempt to call the REST endpoint directly using HTTP protocol.

+
+
Parameters:
+
    +
  • message (AMQMessage) – message from the AMQP that contains the GET request

  • +
  • auth_user (Any) – user authenticated by the API Gateway, as UserSchem

  • +
+
+
Returns:
+

DataResponse to be sent back to the API Gateway via AMQP

+
+
Return type:
+

DataResponse

+
+
+
+ +
+
+async put(message, auth_user)[source]
+

Handles the PUT requests for the CleverThis API. Uses path, path params, query string and +body payload to determine the endpoint method to invoke and its input arguments +In loose coupling (self.session is not None), attempt to call the REST endpoint directly +using HTTP protocol.

+
+
Parameters:
+
    +
  • message (AMQMessage) – message from the AMQP that contains the PUT request

  • +
  • auth_user (Any) – user authenticated by the API Gateway, as UserSchem

  • +
+
+
Returns:
+

DataResponse to be sent back to the API Gateway via AMQP

+
+
Return type:
+

DataResponse

+
+
+
+ +
+
+async post(message, auth_user)[source]
+

Handles the POST requests for the CleverThis API. Uses path, path params, query string and +body payload to determine the endpoint method to invoke and its input arguments +In loose coupling (self.session is not None), attempt to call the REST endpoint directly +using HTTP protocol.

+
+
Parameters:
+
    +
  • message (AMQMessage) – message from AMQP that contains the POST request, including body and headers

  • +
  • auth_user (Any) – user authenticated by the API Gateway, as UserSchem

  • +
+
+
Returns:
+

DataResponse to be sent back to the API Gateway via AMQP

+
+
Return type:
+

DataResponse

+
+
+
+ +
+
+async head(message, auth_user)[source]
+

Handles the HEAD requests for the CleverThis API. In tight coupling (self.session is None), +use path, path params and query string to determine the endpoint method to invoke. +In loose coupling (self.session is not None), HEAD call is not supported.

+
+
Parameters:
+
    +
  • message (AMQMessage) – message from AMQP containing the HEAD request, including body and headers

  • +
  • auth_user (Any) – user authenticated by the API Gateway, as UserSchema

  • +
+
+
Returns:
+

DataResponse to be sent back to the API Gateway via AMQP

+
+
Return type:
+

DataResponse

+
+
+
+ +
+
+async delete(message, auth_user)[source]
+

Handles the DELETE requests for the CleverThis API.

+
+
Parameters:
+
    +
  • message (AMQMessage) – message from AMQP that contains the DELETE request, including body & headers

  • +
  • auth_user (Any) – user authenticated by the API Gateway, as UserSchema

  • +
+
+
Returns:
+

DataResponse to be sent back to the API Gateway via AMQP

+
+
Return type:
+

DataResponse

+
+
+
+ +
+
+async options(message, auth_user)[source]
+

Handles the OPTIONS requests. In tight coupling (self.session is None), +use path, path params and query string to determine the endpoint method to invoke. +In loose coupling (self.session is not None), forward the request as REST request using +the session, i.e. attempt to call the REST endpoint directly using HTTP protocol.

+
+
Parameters:
+
    +
  • message (AMQMessage) – message from AMQP that contains the OPTIONS request, including headers

  • +
  • auth_user (Any) – user authenticated by the API Gateway, as UserSchema

  • +
+
+
Returns:
+

DataResponse to be sent back to the API Gateway via AMQP

+
+
Return type:
+

DataResponse

+
+
+
+ +
+
+async patch(message, auth_user)[source]
+

Handles the PATCH requests for the CleverBRAG API.

+
+
Parameters:
+
    +
  • message (AMQMessage) – message from AMQP that contains the PATCH request, including body & headers

  • +
  • auth_user (Any) – user authenticated by the API Gateway, as UserSchema

  • +
+
+
Returns:
+

DataResponse to be sent back to the API Gateway via AMQP

+
+
Return type:
+

DataResponse

+
+
+
+ +
+
+async handle_no_body_payload(message, auth_user, method)[source]
+

Handles the requests that do not have a body payload. +:param message: message from the AMQP that contains the request +:param auth_user: user authenticated by the API Gateway, as UserSchem +:return: DataResponse to be sent back to the API Gateway via AMQP

+
+
Parameters:
+
    +
  • message (AMQMessage)

  • +
  • auth_user (Any)

  • +
  • method (str)

  • +
+
+
Return type:
+

DataResponse

+
+
+
+ +
+
+async handle_body_payload(message, auth_user, method)[source]
+

Handles the requests that have a body payload, including Multipart data. +:param message: message from the AMQP that contains the request +:param auth_user: user authenticated by the API Gateway, as UserSchem +:param method: HTTP method (POST, PUT, etc.) +:return: DataResponse to be sent back to the API Gateway via AMQP

+
+
Parameters:
+
    +
  • message (AMQMessage)

  • +
  • auth_user (Any)

  • +
  • method (str)

  • +
+
+
Return type:
+

DataResponse

+
+
+
+ +
+
+async call_cleverthis_api(message, args, api_method, success_code)[source]
+

Handles POST and PUT requests, which typically contain possibly complex payload data, +passed as a dictionary. Invokes the provided API method with the given arguments and +authenticated user, and returns the response as DataResponse.

+
+
Parameters:
+
    +
  • message (AMQMessage) – DataMessage containing the details of the call and the payload

  • +
  • args (Any) – map of the arguments to pass to the API method

  • +
  • api_method (Callable[[Any, Any], Coroutine]) – Callable - the method to invoke. Last parameter is the authenticated user

  • +
  • success_code (int) – HTTP code to be returned when operation suceeds

  • +
+
+
Returns:
+

DataResponse class

+
+
Return type:
+

DataResponse

+
+
+
+ +
+ +
+
+class amqp.adapter.amq_route_factory.AMQRouteFactory[source]
+

Bases: object

+

Define the methods to create and validate AMQRoute objects.

+
+
+static from_string(data)[source]
+

Builds the AMQRoute from its string form. +:param data: route string +:return: AMQRoute object

+
+ +
+
+static validate(route_str)[source]
+

Validates the route string. +:param route_str: route string +:return: boolean validity indicator

+
+ +
+ +
+
+class amqp.adapter.backpressure_handler.ScaleRequestV1(serviceId, taskId, max_availability, current_availability, requestType)[source]
+

Bases: object

+
+
Parameters:
+
    +
  • serviceId (str)

  • +
  • taskId (str)

  • +
  • max_availability (int)

  • +
  • current_availability (int)

  • +
  • requestType (ScalingRequestAlert)

  • +
+
+
+
+
+__init__(serviceId, taskId, max_availability, current_availability, requestType)[source]
+
+
Parameters:
+
    +
  • serviceId (str)

  • +
  • taskId (str)

  • +
  • max_availability (int)

  • +
  • current_availability (int)

  • +
  • requestType (ScalingRequestAlert)

  • +
+
+
+
+ +
+
+to_json()[source]
+

Converts the object to a JSON string matching the Java structure

+
+
Return type:
+

str

+
+
+
+ +
+ +
+
+class amqp.adapter.backpressure_handler.BackpressureHandler(channel, loop, config)[source]
+

Bases: object

+
+
Parameters:
+
    +
  • channel (AbstractRobustChannel)

  • +
  • loop (AbstractEventLoop)

  • +
  • config (AMQConfiguration)

  • +
+
+
+
+
+last_backpressure_event_time = 0
+
+ +
+
+last_backpressure_event = 'UPDATE'
+
+ +
+
+__init__(channel, loop, config)[source]
+
+
Parameters:
+
    +
  • channel (AbstractRobustChannel)

  • +
  • loop (AbstractEventLoop)

  • +
  • config (AMQConfiguration)

  • +
+
+
+
+ +
+
+current_availability = 0
+
+ +
+
+increase_current_load()[source]
+

Increase the number of parallel executions, or current_availability load

+
+ +
+
+decrease_current_load()[source]
+

Decrease the number of parallel executions, or current load

+
+ +
+
+update_current_availability(count)[source]
+

Update the number of parallel executions, or current load

+
+
Parameters:
+

count (int)

+
+
+
+ +
+
+update_last_backpressure_event_time()[source]
+

Update the last data message time

+
+ +
+
+async update_backpressure_value(current_availability, maximum)[source]
+

Update the current backpressure value and check for overload conditions. +This method is called by the AMQService.backpressure() method to update +the current parallel executions count and check for overload conditions.

+
+
Parameters:
+
    +
  • current_availability (int) – Current value of the backpressure metric

  • +
  • maximum (int) – Maximum value of the backpressure metric

  • +
+
+
+
+ +
+
+start_backpressure_monitor()[source]
+
+
Return type:
+

Thread | None

+
+
+
+ +
+
+async check_overload_condition()[source]
+

Check if the current_availability availability is too low (OVERLOAD) +or if the service has been idle for too long with high availability (IDLE).

+

Note: current_availability represents available capacity, not used capacity. +- Low availability (close to 0) means OVERLOAD +- High availability (close to maximum) with no activity means IDLE

+
+ +
+
+backpressure_monitor_loop()[source]
+
+ +
+
+async handle_backpressure_overload_event()[source]
+
+ +
+
+async publish_backpressure_request(scaling_request)[source]
+
+
Parameters:
+

scaling_request (ScaleRequestV1)

+
+
+
+ +
+ +
+
+amqp.adapter.consul_global_id_generator.get_config_values(config)[source]
+

Get configuration values from AMQConfiguration or environment variables.

+
+
Returns:
+

(consul_host, consul_port, consul_counter_key, max_retries, retry_delay_seconds)

+
+
Return type:
+

tuple

+
+
Parameters:
+

config (AMQAdapter)

+
+
+
+ +
+
+amqp.adapter.consul_global_id_generator.generate_fallback_id()[source]
+

Generate a fallback ID when Consul is not available. +Uses a combination of timestamp, hostname hash, and random number.

+
+
Returns:
+

A reasonably unique integer ID

+
+
Return type:
+

int

+
+
+
+ +
+
+amqp.adapter.consul_global_id_generator.get_unique_instance_id(config)[source]
+

Get a globally unique ID from Consul. +Uses Consul’s atomic Compare-And-Set operations to safely increment a counter. +Falls back to a local generation method if Consul is unavailable.

+
+
Returns:
+

A globally unique integer ID

+
+
Return type:
+

int

+
+
Parameters:
+

config (AMQAdapter)

+
+
+
+ +

methods to create, serialize and deserialize DataMessage objects.

+
+
+class amqp.adapter.data_message_factory.DataMessageFactory(machine_id)[source]
+

Bases: object

+
+
Parameters:
+

machine_id (int)

+
+
+
+
+SNOWFLAKE_ID_BITS = 80
+
+ +
+
+SEQUENCE_BITS = 12
+
+ +
+
+MACHINE_ID_BITS = 24
+
+ +
+
+TIMESTAMP_BITS = 44
+
+ +
+
+MACHINE_ID_MASK = 16777215
+
+ +
+
+SEQUENCE_MASK = 4095
+
+ +
+
+snowflake_epoch = 1727740800.0
+
+ +
+
+__init__(machine_id)[source]
+
+
Parameters:
+

machine_id (int)

+
+
+
+ +
+
+classmethod get_instance(machine_id)[source]
+

return reusable factory instance +:param machine_id: a generator ID, should be unique for each component to prevent duplicate IDs +:return: factory instance

+
+
Parameters:
+

machine_id (int)

+
+
+
+ +
+
+generate_snowflake_id()[source]
+

Generate an unique ID following the SnowflakeID structure, see https://en.wikipedia.org/wiki/Snowflake_ID +:return: 80-bit wide ID

+
+
Return type:
+

SnowflakeId

+
+
+
+ +
+
+create_request_message(method, domain, path, headers, message)[source]
+

Encapsulate / hide the system level inputs, create the AMQ message from business relevant input +:param method: a method requested (e.g. HTTP method like GET, POST in case of REST request) +:param domain: service host name +:param path: path at the service +:param headers: HTTP headers +:param message: payload +:return: DataMessage object

+
+
Parameters:
+
    +
  • method (str)

  • +
  • domain (str)

  • +
  • path (str)

  • +
  • headers (Dict[str, List[str]])

  • +
+
+
Return type:
+

DataMessage

+
+
+
+ +
+
+safe_rsplit(fq_method_name, default='_')[source]
+

Safely split a fully qualified method name into package, class, and method

+
+
Parameters:
+
    +
  • fq_method_name (str) – Fully qualified method name

  • +
  • default (str, optional) – Default value for missing tokens. Defaults to ‘’.

  • +
+
+
Returns:
+

(package, class_name, method)

+
+
Return type:
+

tuple

+
+
+
+ +
+
+create_rpc_message(fq_method_name, swarm_id, args=None)[source]
+

Create a RPC message with the given parameters. +:param fq_method_name: fully qualified method name (e.g. ‘com.example.ServiceClass.method_name’) +:param args: arguments whose serialized form will form the request body +:return: DataMessage object

+
+
Parameters:
+
    +
  • fq_method_name (str)

  • +
  • swarm_id (str)

  • +
  • args (Dict[str, Any])

  • +
+
+
Return type:
+

DataMessage

+
+
+
+ +
+
+static amq_message_routing_key(message)[source]
+

Constructs the message specific routing key. +:param message: DataMessage input +:return: routing key (as string) compliant to RabbitMQ allowed character set.

+
+
Parameters:
+

message (DataMessage)

+
+
Return type:
+

str

+
+
+
+ +
+
+static get_timestamp_from_id(_id)[source]
+

Helper method - retrieve the timestamp from the ID +:param _id: SnowflakeID (includes encoded timestamp) +:return: the message timestamp

+
+
Parameters:
+

_id (SnowflakeId)

+
+
Return type:
+

int

+
+
+
+ +
+
+static to_string(message)[source]
+

Human-readable representation. +:param message: DataMessage input +:return: as string

+
+
Parameters:
+

message (DataMessage)

+
+
Return type:
+

str

+
+
+
+ +
+
+static serialize(message)[source]
+

Converts DataMessage to byte array +:param message: message to serialize +:return: messages as bytes

+
+
Parameters:
+

message (DataMessage)

+
+
Return type:
+

bytes

+
+
+
+ +
+
+static from_bytes(input_bytes)[source]
+

Builds the DataMessage from its serialized (byte array) form. +:param input_bytes: serialized message +:return: DataMessage instance

+
+
Parameters:
+

input_bytes (bytes)

+
+
Return type:
+

DataMessage

+
+
+
+ +
+
+async static from_stream(stream, rabbitmq_url, loop, file_handler=<amqp.adapter.file_handler.StreamingFileHandler object>)[source]
+

Builds the DataMessage from its serialized (byte array) form. +:param stream: serialized message +:param rabbitmq_url: RabbitMQ connection URL +:param loop: event loop +:param file_handler: file handler for downloading the message +:return: DataMessage instance

+
+
Parameters:
+
    +
  • stream (bytes)

  • +
  • rabbitmq_url (str)

  • +
  • file_handler (FileHandler)

  • +
+
+
Return type:
+

DataMessage | None

+
+
+
+ +
+ +
+
+class amqp.adapter.data_parser.MultipartFormDataParser(message)[source]
+

Bases: object

+

Parses multipart/form-data content and extracts form fields (key/value pairs) and files. +Note: multipart message that contains file(s) to upload are converted to CleverMicro JSON-based streaming format, +where files are transmitted separately via stream using dedicated queue, so the message content is no longer +the HTTP multipart format, but the Content-Type is being preserved for compatibility with the original message. +This class supports both formats (multipart and JSON) depending on the message content, because if no file is +included, the message is transmitted in the unchanged multipart/form-data format.

+
+
Parameters:
+

message (AMQMessage)

+
+
+
+
+__init__(message)[source]
+
+
Parameters:
+

message (AMQMessage)

+
+
+
+ +
+
+on_field(field)[source]
+
+
Parameters:
+

field (Field)

+
+
+
+ +
+
+on_file(file)[source]
+
+
Parameters:
+

file (File)

+
+
+
+ +
+ +
+
+class amqp.adapter.data_parser.AMQDataParser[source]
+

Bases: object

+

A class for parsing AMQ data messages. It provides methods to parse form data,

+
+
+static process_form_data(json_input)[source]
+

Processes the input JSON to extract form data. The JSON has two top entries: ‘files’ and ‘form’. +This is part of the file stream implementation in AMQ adapter to stream large files in chunks. +‘files’ contains information about the AMQ queue from which read the file, while ‘form’ contains +additional non-file form values. +This function combines the file and values at the top level in the returned dictionary. +:param json_input: JSON string or dict containing the form and files data

+
+
Returns:
+

Processed form data with files data overriding non-empty values

+
+
Return type:
+

dict

+
+
+
+ +
+
+static parse_get_url(url)[source]
+

Parses an HTTP GET URL and returns (context_path, query_params_dict)

+
+
Parameters:
+

url (str) – The URL string to parse (e.g., “http://example.com/path?foo=1&bar=2&foo=3”)

+
+
Returns:
+

(context_path, query_params_dict) +- context_path: The path part of the URL (e.g., “/path”) +- query_params_dict: Query parameters where values are str for single value or list of str for multivalue

+
+
Return type:
+

tuple

+
+
+
+ +
+
+static parse_request_body(message)[source]
+

Parses the HTTP POST request body based on the MIME type. +This is typically called fom the concrete CleverThis Service to extract parameters from the message body. +:param message: Contains raw request and MIME headers. +:type message: DataMessage

+
+
Returns:
+

Parsed data as a dictionary.

+
+
Return type:
+

dict

+
+
Parameters:
+

message (AMQMessage)

+
+
+
+ +
+ +

method to create, serialize and deserialize DataResponse message

+
+
+class amqp.adapter.data_response_factory.DataResponseFactory[source]
+

Bases: object

+
+
+static create_async_response_message(_id)[source]
+

Helper method to create standard OK response. +:param _id: ID +:return: Response message

+
+
Parameters:
+

_id (SnowflakeId)

+
+
Return type:
+

DataResponse

+
+
+
+ +
+
+static of(id, response_code, content_type, body, trace_info, error=None, error_cause=None)[source]
+

Create the DataResponse message supplying all key values +:param id: SnowflakeID +:param response_code: HTTP response code +:param content_type: MIME content type +:param body: payload +:param trace_info: Jaeger trace info (id) +:return: DataResponseMessage object

+
+
Parameters:
+
    +
  • id (SnowflakeId)

  • +
  • response_code (int)

  • +
  • content_type (str)

  • +
  • body (bytes)

  • +
  • trace_info (dict[str, str])

  • +
  • error (str | None)

  • +
  • error_cause (str | None)

  • +
+
+
Return type:
+

DataResponse

+
+
+
+ +
+
+static of_error_message(id, error_message, trace_info)[source]
+
+
Parameters:
+
+
+
Return type:
+

DataResponse

+
+
+
+ +
+
+static serialize(response)[source]
+

Serialize the message to byte array (to be sent over RabbitMQ) +:param response: Object to serialize +:return: byte array as serialized message

+
+
Parameters:
+

response (DataResponse)

+
+
Return type:
+

bytes

+
+
+
+ +
+
+static from_bytes(input_bytes)[source]
+

Buil;ds the DataResponse object from its serialized form +:param input_bytes: byte array - serialized response +:return: DataResponse object

+
+
Parameters:
+

input_bytes (bytes)

+
+
Return type:
+

DataResponse

+
+
+
+ +
+ +
+
+amqp.adapter.data_response_factory.AMQResponseFactory
+

alias of DataResponseFactory

+
+ +

implements the file downloader for the amqp adapter, where file is sent in chunks over RabbitMQ

+
+
+class amqp.adapter.file_handler.FileHandler[source]
+

Bases: object

+

The abstract base class for file handling. This is mainly to make the code testable, as there are only 2 +options for pushing (uploading) a file(s) to a service: as HTTP multipart message delivered via RabbitMQ +or via stream in RabbitMQ. Downloading a file is always done via RabbitMQ stream. Thus the method signiture +is tighly coupled to the RabbitMQ API in v1. Only alternative implementation is a mock object in unittests.

+
+
+__init__()[source]
+
+ +
+
+ensure_directory_exists(filepath)[source]
+

Ensures that the directory for the given filepath exists, and handles file versioning.

+
+
Parameters:
+

filepath – The path to the file.

+
+
Returns:
+

The original filepath if the file does not exist, or a modified filepath +pointing to the next available version of the file if it does.

+
+
Return type:
+

str

+
+
+
+ +
+
+async download_buffer(loop, rabbitmq_url, queue_name)[source]
+

Asynchronously downloads a bytearray from RabbitMQ based on the provided file information. +It assumes entire array is in the first (and only) chunk +:param loop: The asyncio event loop. +:param rabbitmq_url: The RabbitMQ connection URL. +:param queue_name: The directory where the file should be saved. +:type queue_name: str

+
+
Return type:
+

(<class ‘bytearray’>, <class ‘int’>)

+
+
+
+ +
+
+async download_file(loop, rabbitmq_url, file_info, message_data, output_dir)[source]
+

Asynchronously downloads a file from RabbitMQ based on the provided file information. +takes the file_info and message_data and output_dir, creates queue consumer and waits for the file +to be downloaded. File may consist of multiple chunks, so the function will wait until all chunks are received. +:param loop: The asyncio event loop. +:param rabbitmq_url: The RabbitMQ connection URL. +:param file_info: A dictionary containing file metadata +:type file_info: dict +:param message_data: The entire message +:type message_data: dict +:param output_dir: The directory where the file should be saved. +:type output_dir: str

+
+
Return type:
+

(<class ‘int’>, <class ‘int’>)

+
+
+
+ +
+
+async on_message(message, json_data, rabbitmq_url, loop, output_dir='downloaded_files')[source]
+

Asynchronous callback function for handling messages from the ‘amq-cleverswarm’ queue.

+
+
Parameters:
+
    +
  • message (AbstractIncomingMessage) – The incoming RabbitMQ message.

  • +
  • rabbitmq_url (str) – The RabbitMQ connection URL.

  • +
  • json_data (bytes)

  • +
+
+
Return type:
+

defaultdict

+
+
+
+ +
+
+async publish_file(exchange, file_path, routing_key, loop, max_chunk_size=1048576, content_type='application/octet-stream', delivery_mode=DeliveryMode.PERSISTENT)[source]
+

Reads a file from the file system and publishes its content to a RabbitMQ exchange +in chunks. It declares a queue with a random name, binds it to the specified +exchange, and uses the queue name as the routing key.

+
+
Parameters:
+
    +
  • exchange (AbstractExchange) – The aio_pika RobustChannel to use for communication with RabbitMQ.

  • +
  • file_path (Path) – The path to the file to publish.

  • +
  • max_chunk_size (int) – The maximum size of each chunk (in bytes).

  • +
  • routing_key (str) – The name of the RabbitMQ exchange routing key to publish to correct queue.

  • +
  • loop (AbstractEventLoop) – correct event loop

  • +
  • max_chunk_size – chunk size to send

  • +
  • content_type (str) – The content type of the file data.

  • +
  • delivery_mode (DeliveryMode) – The delivery mode (e.g., PERSISTENT or TRANSIENT).

  • +
+
+
Returns:
+

The name of the randomly generated queue where the file content was published.

+
+
Return type:
+

tuple[str, int]

+
+
+
+ +
+ +
+
+class amqp.adapter.file_handler.StreamingFileHandler[source]
+

Bases: FileHandler

+

Handles file downloads from RabbitMQ streams.

+
+
+__init__()[source]
+
+ +
+
+async download_file(loop, rabbitmq_url, file_info, message_data, output_dir)[source]
+

Asynchronously downloads a file from RabbitMQ based on the provided file information. +takes the file_info and message_data and output_dir, creates queue consumer and waits for the file to be downloaded +:param loop: The asyncio event loop. +:param rabbitmq_url: The RabbitMQ connection URL. +:param file_info: A dictionary containing file metadata +:type file_info: dict +:param message_data: The entire message +:type message_data: dict +:param output_dir: The directory where the file should be saved. +:type output_dir: str

+
+
Return type:
+

(<class ‘int’>, <class ‘int’>)

+
+
+
+ +
+
+async download_buffer(loop, rabbitmq_url, queue_name)[source]
+

Asynchronously downloads a bytearay from RabbitMQ based on the provided file information. +It assumes entire array is in the first (and only) chunk +:param loop: The asyncio event loop. +:param rabbitmq_url: The RabbitMQ connection URL. +:param queue_name: The directory where the file should be saved. +:type queue_name: str

+
+
Parameters:
+

queue_name (str | None)

+
+
Return type:
+

(<class ‘bytearray’>, <class ‘int’>)

+
+
+
+ +
+
+async on_message(message, json_data, rabbitmq_url, loop, output_dir='downloaded_files')[source]
+

Asynchronous callback function for handling messages from the ‘amq-cleverswarm’ queue.

+
+
Parameters:
+
    +
  • message (AbstractIncomingMessage) – The incoming RabbitMQ message.

  • +
  • json_data (bytes) – The JSON data from the message.

  • +
  • rabbitmq_url (str) – The RabbitMQ connection URL.

  • +
  • loop – The asyncio event loop of the RabbitMQ API

  • +
  • output_dir

  • +
+
+
Return type:
+

defaultdict

+
+
+
+ +
+
+async publish_file(exchange, file_path, routing_key, loop, max_chunk_size=1048576, content_type='application/octet-stream', delivery_mode=DeliveryMode.PERSISTENT)[source]
+

Reads a file from the file system and publishes its content to a RabbitMQ exchange +in chunks. It declares a queue with a random name, binds it to the specified +exchange, and uses the queue name as the routing key.

+
+
Parameters:
+
    +
  • exchange (AbstractExchange) – The aio_pika RobustChannel to use for communication with RabbitMQ.

  • +
  • file_path (Path) – The path to the file to publish.

  • +
  • max_chunk_size (int) – The maximum size of each chunk (in bytes).

  • +
  • routing_key (str) – The name of the RabbitMQ exchange routing key to publish to correct queue.

  • +
  • loop (AbstractEventLoop) – correct event loop

  • +
  • max_chunk_size – chunk size to send

  • +
  • content_type (str) – The content type of the file data.

  • +
  • delivery_mode (DeliveryMode) – The delivery mode (e.g., PERSISTENT or TRANSIENT).

  • +
+
+
Returns:
+

The name of the randomly generated queue where the file content was published.

+
+
Return type:
+

tuple[str, int]

+
+
+
+ +
+ +
+
+amqp.adapter.logging_utils.get_context_info()[source]
+

Retrieves the current_availability module, line number, and function name.

+
+
Returns:
+

A tuple containing (module name, line number, function name). +Returns (None, None, None) if it fails to retrieve the information.

+
+
+
+ +
+
+amqp.adapter.logging_utils.logging_error(msg, *args)[source]
+

Log an error message according to current_availability logging for ‘ERROR’ level. +Add module name, line and function name where the error occurred, on single line. +:param msg: The error message to log. +:type msg: str

+
+
Parameters:
+

msg (str)

+
+
Return type:
+

None

+
+
+
+ +
+
+amqp.adapter.logging_utils.logging_warning(msg, *args)[source]
+

Log a warning message according to current_availability logging for ‘WARN’ level. +Add module name, line and function name where the print occurred, on single line. +:param msg: The warning message to log. +:type msg: str

+
+
Parameters:
+

msg (str)

+
+
Return type:
+

None

+
+
+
+ +
+
+amqp.adapter.logging_utils.logging_info(msg, *args)[source]
+

Log an info message according to current_availability logging for ‘INFO’ level. +Add module name, line and function name where the print occurred, on single line. +:param msg: The info message to log. +:type msg: str

+
+
Parameters:
+

msg (str)

+
+
Return type:
+

None

+
+
+
+ +
+
+amqp.adapter.logging_utils.logging_debug(msg, *args)[source]
+

Log a debug message according to current_availability logging for ‘DEBUG’ level. +Add module name, line and function name where the print occurred, on single line. +:param msg: The debug message to log. +:type msg: str

+
+
Parameters:
+

msg (str)

+
+
Return type:
+

None

+
+
+
+ +

This module provides serialization and deserialization functions for Pydantic models that extend BaseModel

+
+
+amqp.adapter.pydantic_serializer.serialize_object(obj)[source]
+

Serializes an R2RSerializable object to a string in the format +“<ClassName>:{key1:value1, key2:value2,…}”. Handles UUIDs and datetimes, +and nested Pydantic models.

+
+
Parameters:
+

obj (BaseModel) – The BaseModel object to serialize.

+
+
Returns:
+

A string representation of the object.

+
+
Return type:
+

str

+
+
+
+ +
+
+amqp.adapter.pydantic_serializer.python_type_to_json(data)[source]
+

Convert Python objects (dict, list, tuple, set, datetime, etc.) to a JSON string. +Handles common non-JSON-serializable types like datetime, date, and sets.

+
+
Parameters:
+

data – Python object to be converted (dict, list, tuple, set, etc.)

+
+
Returns:
+

JSON string representation of the input data

+
+
Return type:
+

str

+
+
Raises:
+

TypeError – If the object contains non-serializable types (e.g., custom classes)

+
+
+
+ +
+
+amqp.adapter.pydantic_serializer.try_deserialize_object(data)[source]
+

Try deserialize object, if failed, return the input string as-is.

+
+
Parameters:
+

data (str)

+
+
Return type:
+

BaseModel | str

+
+
+
+ +
+
+amqp.adapter.pydantic_serializer.deserialize_object(data)[source]
+

Deserializes a string in the format “<ClassName>:{key1:value1, key2:value2,…}” +back into an BaseModel object. Handles UUIDs, datetimes, and +nested Pydantic models.

+
+
Parameters:
+

data (str) – The string to deserialize.

+
+
Returns:
+

An R2RSerializable object.

+
+
Return type:
+

BaseModel

+
+
+
+ +
+
+amqp.adapter.pydantic_serializer.parse_url_query(url)[source]
+

Parses a HTTP GET URL and returns (context_path, query_params_dict)

+
+
Parameters:
+

url – The URL string to parse (e.g., “http://example.com/path?foo=1&bar=2&foo=3”)

+
+
Returns:
+

(context_path, query_params_dict) +- context_path: The path part of the URL (e.g., “/path”) +- query_params_dict: Dictionary of query parameters where values are always lists

+
+

(e.g., {“foo”: [“1”, “3”], “bar”: [“2”]})

+
+

+
+
Return type:
+

tuple

+
+
+
+ +

Helper functions used to aid with (de)serialization of messages sent over AMQP.

+
+
+amqp.adapter.serializer.long_to_bytes(x)[source]
+
+
Parameters:
+

x (int)

+
+
Return type:
+

bytes

+
+
+
+ +
+
+amqp.adapter.serializer.bytes_to_long(b)[source]
+
+
Parameters:
+

b (bytes)

+
+
Return type:
+

int

+
+
+
+ +
+
+amqp.adapter.serializer.read_long(bis)[source]
+
+
Parameters:
+

bis (BytesIO)

+
+
Return type:
+

int

+
+
+
+ +
+
+amqp.adapter.serializer.object_or_list_as_string(value)[source]
+
+
Parameters:
+

value (str | List[str])

+
+
Return type:
+

str

+
+
+
+ +
+
+amqp.adapter.serializer.map_as_string(map_)[source]
+
+
Parameters:
+

map_ (Dict[str, str])

+
+
Return type:
+

str

+
+
+
+ +
+
+amqp.adapter.serializer.map_with_list_as_string(map_)[source]
+
+
Parameters:
+

map_ (Dict[str, List[str]])

+
+
Return type:
+

str

+
+
+
+ +
+
+amqp.adapter.serializer.parse_map_string(input_str)[source]
+
+
Parameters:
+

input_str (str)

+
+
Return type:
+

Dict[str, str]

+
+
+
+ +
+
+amqp.adapter.serializer.csv_as_list(input_str)[source]
+
+
Parameters:
+

input_str (str)

+
+
Return type:
+

List[str]

+
+
+
+ +
+
+amqp.adapter.serializer.add_to_map(map_, token)[source]
+
+
Parameters:
+
    +
  • map_ (Dict[str, List[str]])

  • +
  • token (str)

  • +
+
+
Return type:
+

None

+
+
+
+ +
+
+amqp.adapter.serializer.parse_map_list_string(input_str)[source]
+
+
Parameters:
+

input_str (str)

+
+
Return type:
+

Dict[str, List[str]]

+
+
+
+ +
+
+amqp.adapter.serializer.str_to_bool(value)[source]
+
+
Parameters:
+

value (str)

+
+
Return type:
+

bool

+
+
+
+ +
+
+class amqp.adapter.service_message_factory.ServiceMessageFactory(name)[source]
+

Bases: object

+

build/serialize/deserialize CleverMicro Service Message class

+
+
Parameters:
+

name (str)

+
+
+
+
+__init__(name)[source]
+
+
Parameters:
+

name (str)

+
+
+
+ +
+
+to_string(message)[source]
+

Convert ServiceMessage to human-readable format +:param message: ServiceMessage object +:return: as string

+
+
Parameters:
+

message (ServiceMessage)

+
+
Return type:
+

str

+
+
+
+ +
+
+of(message_type, payload, recipient_name)[source]
+

Builds ServiceMessage object from relevant values. +:param message_type: enum Message type +:param payload: Message content, stored as-is +:param recipient_name: Recipient name (from the routing expression) +:return: ServiceMessage object

+
+
Parameters:
+
+
+
Return type:
+

ServiceMessage

+
+
+
+ +
+
+as_base64(message_type, payload, recipient_name)[source]
+

Builds ServiceMessage object from relevant values, stores content Base64 encoded. +:param message_type: enum Message type +:param payload: Message content, stored as Base64 string +:param recipient_name: Recipient name (from the routing expression) +:return: ServiceMessage object

+
+
Parameters:
+
+
+
Return type:
+

ServiceMessage

+
+
+
+ +
+
+from_bytes(input_bytes)[source]
+

Deserialize ServiceMessage from byte array serialized form. +:param input_bytes: serialized ServiceMessage +:return: ServiceMessage object represented by the input.

+
+
Parameters:
+

input_bytes (bytes)

+
+
Return type:
+

ServiceMessage

+
+
+
+ +
+
+serialize(message)[source]
+

Serialize ServiceMessage to byte array. +:param message: ServiceMessage to serialize. +:return: serialized object.

+
+
Parameters:
+

message (ServiceMessage)

+
+
Return type:
+

bytes

+
+
+
+ +
+
+format_message_type(message)[source]
+

Formats the Enum message type and decorates it with base64 flag at highest bit. +For compatibility with 0-based enum ordinals in Java subtract 1 from ordinal value. +:param message: Service message to send +:return: formatted message type

+
+
Parameters:
+

message (ServiceMessage)

+
+
Return type:
+

str

+
+
+
+ +
+ +
+
+class amqp.adapter.trace_info_adapter.TraceInfoAdapter[source]
+

Bases: object

+

TODO This class should set up the Open Telemetry tracing stack. To be done later.

+
+
+
+
+static create_produce_span(message_id)[source]
+
+
Parameters:
+

message_id (str)

+
+
Return type:
+

Dict[str, str]

+
+
+
+ +
+
+__init__()
+
+
Return type:
+

None

+
+
+
+ +
+ +
+
+class amqp.adapter.type_descriptor.TypeDescriptor(type_string, extra_init_data)[source]
+

Bases: object

+
+
Parameters:
+
    +
  • type_string (str)

  • +
  • extra_init_data (dict)

  • +
+
+
+
+
+__init__(type_string, extra_init_data)[source]
+
+
Parameters:
+
    +
  • type_string (str)

  • +
  • extra_init_data (dict)

  • +
+
+
+
+ +
+
+instantiate(payload, is_json=False)[source]
+
+
Parameters:
+
    +
  • payload (Any)

  • +
  • is_json (bool)

  • +
+
+
Return type:
+

Any

+
+
+
+ +
+ +
+
+amqp.adapter.type_descriptor.merge_extra_init_data(cls, json_payload, extra_init_data)[source]
+

Merges extra_init_data into json_payload only when: +1. cls has an attribute named like the key +2. json_payload doesn’t already have the key

+
+
Parameters:
+

extra_init_data (dict)

+
+
+
+ +
+ + +
+
+ +
+
+
+
+ + + + diff --git a/api-doc/_build/html/modules/amqp.config.html b/api-doc/_build/html/modules/amqp.config.html new file mode 100644 index 0000000..7477c87 --- /dev/null +++ b/api-doc/_build/html/modules/amqp.config.html @@ -0,0 +1,276 @@ + + + + + + + + + Configuration Module — AMQ Adapter Python 1.0 documentation + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

Configuration Module

+
+
+class amqp.config.amq_configuration.AMQAdapter(config)[source]
+

Bases: object

+

configuration with prefix ‘cm.amq-adapter’

+
+
Parameters:
+

config (ConfigParser)

+
+
+
+
+PREFIX: str = 'cm.amq-adapter.'
+
+ +
+
+__init__(config)[source]
+

cm.amq-adapter.service-name=amq-adapter +cm.amq-adapter.generator-id=164 +cm.amq-adapter.route-mapping=

+
+
Parameters:
+

config (ConfigParser) – config parser to read configuration values

+
+
+
+ +
+
+adapter_prefix()[source]
+
+
Return type:
+

str

+
+
+
+ +
+ +
+
+class amqp.config.amq_configuration.Dispatch(config)[source]
+

Bases: object

+

configuration with prefix ‘cm.dispatch’

+
+
Parameters:
+

config (ConfigParser)

+
+
+
+
+PREFIX: str = 'cm.dispatch.'
+
+ +
+
+__init__(config)[source]
+

cm.dispatch.use-dlq=true +cm.dispatch.amq-host=rabbitmq +cm.dispatch.amq-port=5672 +cm.dispatch.amq-port-tls=5671 +cm.dispatch.service-host= +cm.dispatch.service-port=8080

+
+
Parameters:
+

config (ConfigParser) – config parser to read configuration values

+
+
+
+ +
+ +
+
+class amqp.config.amq_configuration.Backpressure(config)[source]
+

Bases: object

+

configuration with prefix ‘cm.backpressure’

+
+
Parameters:
+

config (ConfigParser)

+
+
+
+
+PREFIX: str = 'cm.backpressure.'
+
+ +
+
+__init__(config)[source]
+

cm.backpressure.threshold=20 +cm.backpressure.time-window=3000

+
+
Parameters:
+

config (ConfigParser) – config parser to read configuration values

+
+
+
+ +
+ +
+
+class amqp.config.amq_configuration.AMQConfiguration(config_file_name=None)[source]
+

Bases: object

+

Top level configuration context holder / object

+
+
Parameters:
+

config_file_name (str)

+
+
+
+
+__init__(config_file_name=None)[source]
+
+
Parameters:
+

config_file_name (str)

+
+
+
+ +
+
+get_unique_instance_id()[source]
+

Get a unique instance ID for this AMQ service.

+
+
Returns:
+

A unique instance ID

+
+
Return type:
+

int

+
+
+
+ +
+ +
+ + +
+
+ +
+
+
+
+ + + + diff --git a/api-doc/_build/html/modules/amqp.html b/api-doc/_build/html/modules/amqp.html new file mode 100644 index 0000000..9bb8cc1 --- /dev/null +++ b/api-doc/_build/html/modules/amqp.html @@ -0,0 +1,115 @@ + + + + + + + + + AMQP Package — AMQ Adapter Python 1.0 documentation + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

AMQP Package

+
+ + +
+
+ +
+
+
+
+ + + + diff --git a/api-doc/_build/html/modules/amqp.model.html b/api-doc/_build/html/modules/amqp.model.html new file mode 100644 index 0000000..47c4fb3 --- /dev/null +++ b/api-doc/_build/html/modules/amqp.model.html @@ -0,0 +1,1055 @@ + + + + + + + + + Model Module — AMQ Adapter Python 1.0 documentation + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

Model Module

+
+
+amqp.model.model.RS = ':'
+

Define the structure of messages used in Clever Micro

+
+ +
+
+class amqp.model.model.DataMessageMagicByte(*values)[source]
+

Bases: Enum

+
+
+HTTP_REQUEST = 'A'
+
+ +
+
+RPC_REQUEST = 'R'
+
+ +
+ +
+
+class amqp.model.model.CleverMicroMessage(magic: str, id: amqp.model.snowflake_id.SnowflakeId, m_field: str, d_field: str, p_field: str, headers: Dict[str, List[str]], trace_info: Dict[str, str], base64body: bytes)[source]
+

Bases: object

+
+
Parameters:
+
    +
  • magic (str)

  • +
  • id (SnowflakeId)

  • +
  • m_field (str)

  • +
  • d_field (str)

  • +
  • p_field (str)

  • +
  • headers (Dict[str, List[str]])

  • +
  • trace_info (Dict[str, str])

  • +
  • base64body (bytes)

  • +
+
+
+
+
+DEFAULT_CONTENT_TYPE = ['application/octet-stream']
+

The common structure of a message that transfers the user request to the Service and back. All data related messages +conform to this base structure:

+

magic - identifies the message type (REST, RPC, DataResponse) +id - unique message ID +m_field - method name (for REST style calls) or service name (for RPC style calls) or response code for DataResponse +d_field - domain name (for REST style calls) or module name (for RPC style calls) or error code for DataResponse +p_field - path (for REST style calls) or method name (for RPC style calls) or error cause for DataResponse +headers - headers of the message (e.g. Content-Type, Accept, etc.) +trace_info - trace information (e.g. trace ID, span ID, etc. for Jaeger trace monitor) +base64body - the body of the message encoded in base64

+
+ +
+
+__init__(magic, id, m_field, d_field, p_field, headers, trace_info, base64body)[source]
+
+
Parameters:
+
    +
  • magic (str)

  • +
  • id (SnowflakeId)

  • +
  • m_field (str)

  • +
  • d_field (str)

  • +
  • p_field (str)

  • +
  • headers (Dict[str, List[str]])

  • +
  • trace_info (Dict[str, str])

  • +
  • base64body (bytes)

  • +
+
+
+
+ +
+
+magic()[source]
+
+ +
+
+id()[source]
+
+ +
+
+m_field()[source]
+
+
Return type:
+

str

+
+
+
+ +
+
+d_field()[source]
+
+
Return type:
+

str

+
+
+
+ +
+
+p_field()[source]
+
+
Return type:
+

str

+
+
+
+ +
+
+headers()[source]
+
+
Return type:
+

Dict[str, List[str]]

+
+
+
+ +
+
+trace_info()[source]
+
+
Return type:
+

Dict[str, str]

+
+
+
+ +
+
+base64body()[source]
+
+
Return type:
+

bytes

+
+
+
+ +
+
+body()[source]
+
+
Return type:
+

bytes

+
+
+
+ +
+
+content_type()[source]
+
+
Return type:
+

str

+
+
+
+ +
+
+return_address()[source]
+
+
Return type:
+

str

+
+
+
+ +
+ +
+
+class amqp.model.model.DataMessage(magic, id, method, domain, path, headers, trace_info, base64body)[source]
+

Bases: CleverMicroMessage

+

The class represents a structure of a message that transfers the user request to the Service. +It may also be used to make a call/request between services. +The response to this call is in format of DataResponse class.

+

See DataMessageFactory for code to instantiate, serialize and deserialize the class

+
+
Parameters:
+
    +
  • magic (str)

  • +
  • id (SnowflakeId)

  • +
  • method (str)

  • +
  • domain (str)

  • +
  • path (str)

  • +
  • headers (Dict[str, List[str]])

  • +
  • trace_info (Dict[str, str])

  • +
  • base64body (bytes)

  • +
+
+
+
+
+__init__(magic, id, method, domain, path, headers, trace_info, base64body)[source]
+
+
Parameters:
+
    +
  • magic (str)

  • +
  • id (SnowflakeId)

  • +
  • method (str)

  • +
  • domain (str)

  • +
  • path (str)

  • +
  • headers (Dict[str, List[str]])

  • +
  • trace_info (Dict[str, str])

  • +
  • base64body (bytes)

  • +
+
+
+
+ +
+
+method()[source]
+
+
Return type:
+

str

+
+
+
+ +
+
+domain()[source]
+
+
Return type:
+

str

+
+
+
+ +
+
+path()[source]
+
+
Return type:
+

str

+
+
+
+ +
+
+routing_key()[source]
+
+
Return type:
+

str

+
+
+
+ +
+ +
+
+class amqp.model.model.MultipartDataMessage(message, extra_data)[source]
+

Bases: DataMessage

+
+
Parameters:
+
+
+
+
+
+__init__(message, extra_data)[source]
+
+
Parameters:
+
+
+
+
+ +
+ +
+
+class amqp.model.model.AMQMessage(data_message, connection)[source]
+

Bases: DataMessage

+

Expand the core DataMessage type to include the channel, which is required to create a dedicated queue +in case the response should initiate file download.

+
+
Parameters:
+
    +
  • data_message (DataMessage)

  • +
  • connection (AbstractRobustConnection)

  • +
+
+
+
+
+__init__(data_message, connection)[source]
+
+
Parameters:
+
    +
  • data_message (DataMessage)

  • +
  • connection (AbstractRobustConnection)

  • +
+
+
+
+ +
+
+property connection: AbstractRobustConnection
+

Get the AMQP connection associated with this message

+
+ +
+ +
+
+class amqp.model.model.DataResponse(magic, id, response_code, content_type, error, error_cause, headers, trace_info, base64body)[source]
+

Bases: CleverMicroMessage

+

The response message to REST & RPC style calls (note: the request is made with DataMessage)

+

See DataResponseFactory for code to instantiate, serialize and deserialize the class

+
+
Parameters:
+
    +
  • magic (str)

  • +
  • id (SnowflakeId)

  • +
  • response_code (str)

  • +
  • content_type (str)

  • +
  • error (str)

  • +
  • error_cause (str)

  • +
  • headers (Dict[str, List[str]])

  • +
  • trace_info (Dict[str, str])

  • +
  • base64body (bytes)

  • +
+
+
+
+
+__init__(magic, id, response_code, content_type, error, error_cause, headers, trace_info, base64body)[source]
+
+
Parameters:
+
    +
  • magic (str)

  • +
  • id (SnowflakeId)

  • +
  • response_code (str)

  • +
  • content_type (str)

  • +
  • error (str)

  • +
  • error_cause (str)

  • +
  • headers (Dict[str, List[str]])

  • +
  • trace_info (Dict[str, str])

  • +
  • base64body (bytes)

  • +
+
+
+
+ +
+
+response_code()[source]
+
+
Return type:
+

str

+
+
+
+ +
+
+error()[source]
+
+
Return type:
+

str

+
+
+
+ +
+
+error_cause()[source]
+
+
Return type:
+

str

+
+
+
+ +
+ +
+
+class amqp.model.model.AMQRoute(component_name, exchange, queue, key, timeout, valid_until)[source]
+

Bases: object

+

Configuration item representing one AMQRoute. Adapter uses this to declare the Exchange +and/or queue and bind the key as routing expression.

+
+
Parameters:
+
    +
  • component_name (str) – a service/component name that this route points to (owner of the route)

  • +
  • exchange (str) – RabbitMQ exchange name

  • +
  • queue (str) – RabbitMQ queue name

  • +
  • key (str) – RabbitMQ TOPIC Exchange routing key

  • +
  • timeout (int) – Timeout in milliseconds, how long to wait for reply, or not waiting if less then 1

  • +
  • valid_until (int) – optional timestamp (millis from epoch) after which the route is ignored

  • +
+
+
+
+
+component_name: str
+
+ +
+
+exchange: str
+
+ +
+
+queue: str
+
+ +
+
+key: str
+
+ +
+
+timeout: int
+
+ +
+
+valid_until: int
+
+ +
+
+FORMAT = 'Component-Name:Exchange-Name:Queue-Name:Routing-Key:Timeout:Valid-Until'
+
+ +
+
+property primary_key
+

Build the unique identifier of the route +:return primary key

+
+ +
+
+as_string()[source]
+
+
Return type:
+

str

+
+
+
+ +
+
+__init__(component_name, exchange, queue, key, timeout, valid_until)
+
+
Parameters:
+
    +
  • component_name (str)

  • +
  • exchange (str)

  • +
  • queue (str)

  • +
  • key (str)

  • +
  • timeout (int)

  • +
  • valid_until (int)

  • +
+
+
Return type:
+

None

+
+
+
+ +
+ +
+
+amqp.model.model.AMQResponse
+

alias of DataResponse

+
+ +
+
+class amqp.model.model.ServiceMessage(id=None, payload_type=0, message_type=ServiceMessageType.UNKNOWN, recipient_name=None, reply_to=None, trace_info=<factory>, message=None)[source]
+

Bases: object

+

The CleverMicro service message that is used to communicate the internal state of the AMQ channel. +In v0.2 it supports service discovery / route registration and Backpressure status notification.

+
+
Parameters:
+
    +
  • id (SnowflakeId)

  • +
  • payload_type (int)

  • +
  • message_type (ServiceMessageType)

  • +
  • recipient_name (str)

  • +
  • reply_to (str)

  • +
  • trace_info (Dict[str, str])

  • +
  • message (str)

  • +
+
+
+
+
+id: SnowflakeId = None
+
+ +
+
+payload_type: int = 0
+
+ +
+
+message_type: ServiceMessageType = 6
+
+ +
+
+recipient_name: str = None
+
+ +
+
+reply_to: str = None
+
+ +
+
+trace_info: Dict[str, str]
+
+ +
+
+message: str = None
+
+ +
+
+is_valid()[source]
+
+
Return type:
+

bool

+
+
+
+ +
+
+__init__(id=None, payload_type=0, message_type=ServiceMessageType.UNKNOWN, recipient_name=None, reply_to=None, trace_info=<factory>, message=None)
+
+
Parameters:
+
    +
  • id (SnowflakeId)

  • +
  • payload_type (int)

  • +
  • message_type (ServiceMessageType)

  • +
  • recipient_name (str)

  • +
  • reply_to (str)

  • +
  • trace_info (Dict[str, str])

  • +
  • message (str)

  • +
+
+
Return type:
+

None

+
+
+
+ +
+ +
+
+class amqp.model.model.ScalingRequestAlert(*values)[source]
+

Bases: Enum

+
+
+OVERLOAD = 'OVERLOAD'
+
+ +
+
+IDLE = 'IDLE'
+
+ +
+
+UPDATE = 'UPDATE'
+
+ +
+
+SCALE_UP = 'SCALE_UP'
+
+ +
+
+SCALE_DOWN = 'SCALE_DOWN'
+
+ +
+ +
+
+class amqp.model.model.ScalingRequest(service_id: str, task_id: str, max_availability: int, current_availability: int, request_type: amqp.model.model.ScalingRequestAlert, version: int = 1)[source]
+

Bases: object

+
+
Parameters:
+
    +
  • service_id (str)

  • +
  • task_id (str)

  • +
  • max_availability (int)

  • +
  • current_availability (int)

  • +
  • request_type (ScalingRequestAlert)

  • +
  • version (int)

  • +
+
+
+
+
+service_id: str
+
+ +
+
+task_id: str
+
+ +
+
+max_availability: int
+
+ +
+
+current_availability: int
+
+ +
+
+request_type: ScalingRequestAlert
+
+ +
+
+version: int = 1
+
+ +
+
+to_json()[source]
+

Converts the object to a JSON string matching the Java structure

+
+
Return type:
+

str

+
+
+
+ +
+
+classmethod from_json(json_str)[source]
+

Creates a ScalingRequest from a JSON string

+
+
Parameters:
+

json_str (str)

+
+
Return type:
+

ScalingRequest

+
+
+
+ +
+
+__init__(service_id, task_id, max_availability, current_availability, request_type, version=1)
+
+
Parameters:
+
    +
  • service_id (str)

  • +
  • task_id (str)

  • +
  • max_availability (int)

  • +
  • current_availability (int)

  • +
  • request_type (ScalingRequestAlert)

  • +
  • version (int)

  • +
+
+
Return type:
+

None

+
+
+
+ +
+ +
+
+class amqp.model.model.AMQErrorMessage(response_code, error, detail, content_type='application/json')[source]
+

Bases: object

+

The error response message to RPC style call (note: the request is made with DataMessage)

+
+
Parameters:
+
    +
  • response_code (int)

  • +
  • error (str)

  • +
  • detail (str)

  • +
  • content_type (str)

  • +
+
+
+
+
+__init__(response_code, error, detail, content_type='application/json')[source]
+
+
Parameters:
+
    +
  • response_code (int)

  • +
  • error (str)

  • +
  • detail (str)

  • +
  • content_type (str)

  • +
+
+
+
+ +
+
+serialize()[source]
+
+
Return type:
+

bytes

+
+
+
+ +
+ +
+
+class amqp.model.service_message_type.ServiceMessageType(*values)[source]
+

Bases: Enum

+
+
+ROUTING_DATA_REQUEST = 1
+
+ +
+
+ROUTING_DATA = 2
+
+ +
+
+ROUTING_DATA_REMOVE = 3
+
+ +
+
+ROUTING_DATA_REPLACE = 4
+
+ +
+
+BACKPRESSURE = 5
+
+ +
+
+UNKNOWN = 6
+
+ +
+ +
+
+class amqp.model.snowflake_id.SnowflakeId(hi_bits_or_bytes=None, lo_bits=None)[source]
+

Bases: object

+

see https://en.wikipedia.org/wiki/Snowflake_ID

+
+
+SNOWFLAKE_ID_WIDTH = 10
+
+ +
+
+__init__(hi_bits_or_bytes=None, lo_bits=None)[source]
+
+ +
+
+static from_hex(_input)[source]
+
+ +
+
+get_bytes()[source]
+
+ +
+
+get_bits()[source]
+
+ +
+
+as_string()[source]
+
+
Return type:
+

str

+
+
+
+ +
+ +
+ + +
+
+ +
+
+
+
+ + + + diff --git a/api-doc/_build/html/modules/amqp.rabbitmq.html b/api-doc/_build/html/modules/amqp.rabbitmq.html new file mode 100644 index 0000000..075f15d --- /dev/null +++ b/api-doc/_build/html/modules/amqp.rabbitmq.html @@ -0,0 +1,443 @@ + + + + + + + + + RabbitMQ Module — AMQ Adapter Python 1.0 documentation + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

RabbitMQ Module

+
+
+class amqp.rabbitmq.rabbit_mq_client.RabbitMQClient(configuration, router)[source]
+

Bases: object

+
+
+PREFETCH_COUNT = 1
+
+ +
+
+DLQ_ARGS = {'x-dead-letter-exchange': 'DLX'}
+
+ +
+
+__init__(configuration, router)[source]
+
+ +
+
+async init(loop)[source]
+
+
Return type:
+

AbstractRobustExchange

+
+
+
+ +
+
+cleanup()[source]
+
+ +
+
+async register_inbound_routes(route_mapping, callback)[source]
+

Register inbound routes for the AMQ adapter. The route mapping is a comma-separated string that contains +list of routes. A route is the mapping of domain and path to the exchange and queue name. +This method declare (and thus create) the queue and exchange, bind them together using the +regex-like routing expression given in the route. +After this the Service is listening on (consuming from) the queue for incoming messages.

+
+
Parameters:
+

route_mapping (str)

+
+
+
+ +
+
+async register_data_reply_callback(rpc_callback)[source]
+
+ +
+
+async setup_amq_channel(loop)[source]
+

Set up the RabbitMQ connection +:return: nothing, it applies changes to instance variables

+
+ +
+
+async send_message(message)[source]
+

The API routine that sends a datagram message (DataMessage) to the destination. The Destination +is determined from the DataMessage metadata section, using domain and path as key. +:param message: message to send +:return: nothing (void) as submitting message to RabbitMQ exchange does not return anything

+
+

meaningful. There will be asynch ACK later that will confirm the message was sent to RMQ.

+
+
+
Parameters:
+

message (DataMessage)

+
+
+
+ +
+
+async request_routes()[source]
+
+ +
+
+find_route(domain, path)[source]
+
+
Parameters:
+
    +
  • domain (str)

  • +
  • path (str)

  • +
+
+
Return type:
+

AMQRoute

+
+
+
+ +
+ +
+
+exception amqp.rabbitmq.user_management_service_client.UserManagementServiceException(exception_type, message, additional_info=None)[source]
+

Bases: Exception

+

Exception raised for errors in the User Management Service.

+
+
Parameters:
+
    +
  • exception_type (str)

  • +
  • message (str)

  • +
  • additional_info (Dict[str, Any])

  • +
+
+
+
+
+__init__(exception_type, message, additional_info=None)[source]
+
+
Parameters:
+
    +
  • exception_type (str)

  • +
  • message (str)

  • +
  • additional_info (Dict[str, Any])

  • +
+
+
+
+ +
+ +
+
+class amqp.rabbitmq.user_management_service_client.UserManagementServiceClient(configuration, loop=None)[source]
+

Bases: object

+

Client for the User Management Service that communicates over RabbitMQ. +Supports operations for token, user, group, tenant, and permission services.

+
+
Parameters:
+
+
+
+
+
+EXCHANGE_NAME = 'cleverthis.clevermicro.auth.endpoints'
+
+ +
+
+TOKEN_SERVICE = 'token-service-v1'
+
+ +
+
+USER_SERVICE = 'user-service-v1'
+
+ +
+
+GROUP_SERVICE = 'group-service-v1'
+
+ +
+
+TENANT_SERVICE = 'tenant-service-v1'
+
+ +
+
+PERMISSION_SERVICE = 'permission-service-v1'
+
+ +
+
+RESPONSE_QUEUE_NAME = 'user-management-service-cleverswarm'
+
+ +
+
+__init__(configuration, loop=None)[source]
+

Initialize the User Management Service client.

+
+
Parameters:
+
    +
  • configuration (AMQConfiguration) – AMQ configuration

  • +
  • loop (AbstractEventLoop) – Event loop to use (optional)

  • +
+
+
+
+ +
+
+async initialize()[source]
+

Initialize the RabbitMQ connection and set up the exchange and response queue.

+
+
Return type:
+

None

+
+
+
+ +
+
+async close()[source]
+

Close the RabbitMQ connection.

+
+
Return type:
+

None

+
+
+
+ +
+
+async call_service(service, method, args=None)[source]
+

Call a method on a User Management Service.

+
+
Parameters:
+
    +
  • service (str) – Service routing key (e.g., ‘token-service-v1’)

  • +
  • method (str) – Method name to call

  • +
  • args (Dict[str, Any]) – Method arguments

  • +
+
+
Returns:
+

Response data

+
+
Raises:
+

UserManagementServiceException – If the service returns an error

+
+
Return type:
+

Dict[str, Any]

+
+
+
+ +
+
+async validate_token(token)[source]
+

Validate a JWT token.

+
+
Parameters:
+

token (str) – JWT token to validate

+
+
Returns:
+

Token information

+
+
Return type:
+

Dict[str, Any]

+
+
+
+ +
+
+async query_user_by_id(user_id)[source]
+

Query user information by user ID.

+
+
Parameters:
+

user_id (str) – User ID

+
+
Returns:
+

User information

+
+
Return type:
+

Dict[str, Any]

+
+
+
+ +
+
+async list_users_in_group(group_id)[source]
+

List users in a group.

+
+
Parameters:
+

group_id (str) – Group ID

+
+
Returns:
+

List of users

+
+
Return type:
+

List[Dict[str, Any]]

+
+
+
+ +
+ +
+ + +
+
+ +
+
+
+
+ + + + diff --git a/api-doc/_build/html/modules/amqp.router.html b/api-doc/_build/html/modules/amqp.router.html new file mode 100644 index 0000000..c04d43a --- /dev/null +++ b/api-doc/_build/html/modules/amqp.router.html @@ -0,0 +1,810 @@ + + + + + + + + + Router Module — AMQ Adapter Python 1.0 documentation + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

Router Module

+
+
+class amqp.router.router_base.RouterBase[source]
+

Bases: object

+
+
+__init__()[source]
+
+ +
+
+get_routing_key(message)[source]
+
+
Parameters:
+

message (DataMessage)

+
+
Return type:
+

str

+
+
+
+ +
+
+wrap_routes()[source]
+
+
Return type:
+

str

+
+
+
+ +
+
+wrap_own_routes()[source]
+
+
Return type:
+

str

+
+
+
+ +
+
+unwrap_route_list(route_string)[source]
+
+
Parameters:
+

route_string (str)

+
+
Return type:
+

List[AMQRoute]

+
+
+
+ +
+
+get_routes()[source]
+
+
Return type:
+

Set[AMQRoute]

+
+
+
+ +
+
+find_route(domain, path)[source]
+
+
Parameters:
+
    +
  • domain (str)

  • +
  • path (str)

  • +
+
+
Return type:
+

AMQRoute | None

+
+
+
+ +
+
+find_route_by_message(message)[source]
+
+
Parameters:
+

message (DataMessage)

+
+
Return type:
+

AMQRoute | None

+
+
+
+ +
+
+find_route_by_service(service_name)[source]
+
+
Parameters:
+

service_name (str)

+
+
Return type:
+

AMQRoute | None

+
+
+
+ +
+
+add_routes(message)[source]
+
+
Parameters:
+

message (str)

+
+
Return type:
+

None

+
+
+
+ +
+
+set_route_added_notifier(callback)[source]
+
+
Parameters:
+

callback (Callable[[AMQRoute], None])

+
+
+
+ +
+
+remove_routes(message)[source]
+
+
Parameters:
+

message (str)

+
+
Return type:
+

None

+
+
+
+ +
+
+remove_route(route)[source]
+
+
Parameters:
+

route (AMQRoute)

+
+
Return type:
+

bool

+
+
+
+ +
+
+add_route(route)[source]
+
+
Parameters:
+

route (AMQRoute)

+
+
Return type:
+

None

+
+
+
+ +
+
+add_own_route(route)[source]
+
+
Parameters:
+

route (AMQRoute)

+
+
Return type:
+

None

+
+
+
+ +
+ +
+
+class amqp.router.router_consumer.RouterConsumer(configuration)[source]
+

Bases: RouterProducer

+
    +
  • Contains Consumer logic. The logic here:

  • +
  • Sets a listener on CM_C_REQ_QUEUE queue to receive routing data requests

  • +
  • Upon receipt of routing data request, responds on CM_RESPONSE_EXCHANGE with local routing data.

  • +
  • It also proactively pushes the local routing data to CM_RESPONSE_EXCHANGE on startup to

  • +
  • notify the other (excluding self) Producers about its presence.

  • +
  • +
  • Consumer is also Producer (Consumer sits alongside the CM Service, and anytime the Service needs

  • +
  • to communicate with other Service, it can do so via the Adapter). Therefore, the Producer

  • +
  • capability is also required.

  • +
+
+
Parameters:
+

configuration (AMQConfiguration)

+
+
+
+
+__init__(configuration)[source]
+
    +
  • Initialize router - need to supply the config values and RabbitMQ channel.

  • +
  • +
    param configuration:
    +

    adapter’s configuration reference.

    +
    +
    +
  • +
+
+
Parameters:
+

configuration (AMQConfiguration)

+
+
+
+ +
+
+async init_routing_paths_consumer(_channel)[source]
+
    +
  • Initialize router according to Consumer mode.

  • +
+
+
Parameters:
+

_channel (AbstractRobustChannel)

+
+
+
+ +
+
+async handle_routing_data_request_message(message)[source]
+
+
Parameters:
+

message (AbstractIncomingMessage)

+
+
+
+ +
+
+async register_route(route)[source]
+
    +
  • Called in Consumer (Service Adapter) context. Registers the routing data for self Service

  • +
  • with the Master router by sending own routing data to Master via CleverMicroResponse queue.

  • +
  • +
    param route:
    +

    route to register with Master

    +
    +
    +
  • +
+
+
Parameters:
+

route (AMQRoute)

+
+
+
+ +
+
+async remove_route_on_cleanup(route)[source]
+
    +
  • Unregister / remove route from the list

  • +
  • +
    param route:
    +

    route to remove

    +
    +
    +
  • +
  • :return result of remove operation

  • +
+
+
Parameters:
+

route (AMQRoute)

+
+
Return type:
+

bool

+
+
+
+ +
+
+cleanup()[source]
+
    +
  • Cleanup - de-register routes with master.

  • +
+
+ +
+ +
    +
  • Maps the request to RabbitMQ exchange. Handles the service discovery logic.

  • +
  • +
  • Router can be either in ‘Master’ or ‘Service (Consumer)’ modes. ‘Master’ mode is the outward

  • +
  • facing side that receives the requests and routes it to queue associated with requested service.

  • +
  • ‘Service’ mode is consumer side for given service, which receives the message from AMQ and

  • +
  • forwards it to the application.

  • +
+
+
+class amqp.router.router_producer.RouterProducer(configuration)[source]
+

Bases: RouterBase

+
+
Parameters:
+

configuration (AMQConfiguration)

+
+
+
+
+__init__(configuration)[source]
+
    +
  • Initialize router - need to supply the config values and RabbitMQ channel.

  • +
  • +
    param configuration:
    +

    adapter’s configuration reference.

    +
    +
    +
  • +
+
+
Parameters:
+

configuration (AMQConfiguration)

+
+
+
+ +
+
+async init_internal_routing_paths(channel)[source]
+
    +
  • Initialize internal routes for ServiceMessages.

  • +
  • The logic here sets a producer and listener on CleverMicroRequest/CleverMicroResponse queues

  • +
  • created to enable the service discovery.

  • +
  • The Producer mode sends out routing data request on CleverMicroRequest and listens

  • +
  • on CleverMicroResponse for any service responding with its routing data.

  • +
+
+
Parameters:
+

channel (AbstractRobustChannel | None)

+
+
+
+ +
+
+async handle_routing_data_message(message)[source]
+
    +
  • Handler for CLEVER_MICRO_RESPONSE queue messages. Used in Master mode to handle receipt of

  • +
  • routing data, invokes addRoute or removeRoute depending on the type of the message.

  • +
+
+
Parameters:
+

message (AbstractIncomingMessage)

+
+
+
+ +
+
+async request_routes_from_adapters()[source]
+
    +
  • Send a broadcast message prompting all listening adapters to reply with own routing data.

  • +
+
+ +
+
+async publish_service_message(message, exchange)[source]
+
    +
  • Publishes a service message to the service queue and logs success log entry.

  • +
  • +
  • +
    param message:
    +

    Service Message to be sent

    +
    +
    +
  • +
  • +
    param exchange:
    +

    target exchange name where to publish the message

    +
    +
    +
  • +
  • :return True if channel is open, False otherwise

  • +
  • @throws IOException if something else goes wrong

  • +
+
+
Parameters:
+
+
+
Return type:
+

bool

+
+
+
+ +
+
+get_reply_to_queue_name()[source]
+
    +
  • self should create a unique name for self process and also when there’s several instances.

  • +
  • @return unique reply-to queue name.

  • +
+
+
Return type:
+

str

+
+
+
+ +
+
+get_response_queue_name()[source]
+
    +
  • self should create a unique name for self process and also when there’s several instances.

  • +
  • @return unique reply-to queue name.

  • +
+
+
Return type:
+

str

+
+
+
+ +
+
+get_consume_queue_name()[source]
+
    +
  • self should create a unique name for self process and also when there’s several instances.

  • +
  • @return unique reply-to queue name.

  • +
+
+
Return type:
+

str

+
+
+
+ +
+
+is_open(_channel)[source]
+
    +
  • Test if the channel is usable (opened).

  • +
  • +
    param _channel:
    +

    the channel

    +
    +
    +
  • +
  • :return True if the channel is opened.

  • +
+
+
Parameters:
+

_channel (AbstractRobustChannel)

+
+
Return type:
+

bool

+
+
+
+ +
+ +
+
+class amqp.router.route_database.RouteDatabase[source]
+

Bases: object

+
+
+__init__()[source]
+
+ +
+
+get_routes()[source]
+
+
Return type:
+

Set[AMQRoute]

+
+
+
+ +
+
+pattern(routing_key)[source]
+
+
Parameters:
+

routing_key (str)

+
+
Return type:
+

Pattern

+
+
+
+ +
+
+matches(routing_key_from_route, routing_key_from_message)[source]
+
+
Parameters:
+
    +
  • routing_key_from_route (str)

  • +
  • routing_key_from_message (str)

  • +
+
+
Return type:
+

bool

+
+
+
+ +
+
+wrap_routes()[source]
+
+
Return type:
+

str

+
+
+
+ +
+
+find_route(domain, path)[source]
+
+
Parameters:
+
    +
  • domain (str)

  • +
  • path (str)

  • +
+
+
Return type:
+

AMQRoute | None

+
+
+
+ +
+
+find_route_by_service(service_name)[source]
+
+
Parameters:
+

service_name (str)

+
+
Return type:
+

AMQRoute | None

+
+
+
+ +
+
+add_route(route)[source]
+
+
Parameters:
+

route (AMQRoute)

+
+
Return type:
+

bool

+
+
+
+ +
+
+remove_route(route)[source]
+
+
Parameters:
+

route (AMQRoute)

+
+
Return type:
+

bool

+
+
+
+ +
+ +
+
+amqp.router.utils.sanitize_as_rabbitmq_name(input_str)[source]
+
+
Parameters:
+

input_str (str)

+
+
Return type:
+

str

+
+
+
+ +
+
+amqp.router.utils.sanitize_as_rabbitmq_domain_name(token)[source]
+
+
Parameters:
+

token (str)

+
+
Return type:
+

str

+
+
+
+ +
+
+async amqp.router.utils.await_future(future, sleep_time=0.05)[source]
+

Wait for a future to complete.

+
+
Parameters:
+

future (Future)

+
+
+
+ +
+
+async amqp.router.utils.await_result(future, sleep_time=0.05)[source]
+

Wait for a future to complete and return its result.

+
+
Parameters:
+

future (Future)

+
+
+
+ +
+ + +
+
+ +
+
+
+
+ + + + diff --git a/api-doc/_build/html/modules/amqp.service.html b/api-doc/_build/html/modules/amqp.service.html new file mode 100644 index 0000000..fd5105f --- /dev/null +++ b/api-doc/_build/html/modules/amqp.service.html @@ -0,0 +1,905 @@ + + + + + + + + + Service Module — AMQ Adapter Python 1.0 documentation + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

Service Module

+
+
+class amqp.service.amq_service.AMQService(amq_configuration, service_adapter=None)[source]
+

Bases: object

+
+
Parameters:
+

amq_configuration (AMQConfiguration)

+
+
+
+
+CLEVER_AMQP_CLIENT_VERSION = None
+
+ +
+
+MAX_WAIT = 300
+
+ +
+
+__init__(amq_configuration, service_adapter=None)[source]
+
+
Parameters:
+

amq_configuration (AMQConfiguration)

+
+
+
+ +
+
+run()[source]
+

Start the AMQ service, initializing RabbitMQ connection and message handlers.

+
+ +
+
+backpressure(current_availability, maximum=-1)[source]
+

Send Backpressure event to the Management Service. The Backpressure logic determines the type of the event +based on the current_availability and maximum values. +- OVERLOAD event is triggered immediately when the current_availability value is below threshold +typically set to 10% of the maximum value, or 0 if no maximum is provided. +- IDLE event is triggered when the current_availability value exeeds 80% of the maximum value or is greater +than 0 if no maximum is provided. +- UPDATE event is sent if neither OVERLOAD nor IDLE conditions are met, indicating that the backpressure is +within acceptable limits. +:param current_availability: Currently available capacity. +:param maximum: Maximum value of the backpressure metric. If undefined or set to lt 0 (e.g. -1), +the maximum is than determined from max value of current_availability seen over the time.

+
+
Parameters:
+
    +
  • current_availability (int)

  • +
  • maximum (int)

  • +
+
+
+
+ +
+
+rpc(service_name, fq_method_name, **kwargs)[source]
+

Send a Remote Procedure Call (RPC) message to the AMQ service. +:param service_name: The service name to receive the RPC message. +:param fq_method_name: The fully qualified method name to execute the RPC message. +:param kwargs: Additional keyword arguments to be passed to the RPC method. +:return: A Future that resolves when the RPC response is received.

+
+
Parameters:
+
    +
  • service_name (str)

  • +
  • fq_method_name (str)

  • +
+
+
Return type:
+

Future

+
+
+
+ +
+
+command(service_name, fq_method_name, **kwargs)[source]
+

Send a unidirectional Command message to the AMQ service. +:param service_name: The service name to receive the RPC message. +:param fq_method_name: The fully qualified method name to execute the RPC message. +:param kwargs: Additional keyword arguments to be passed to the RPC method. +:return: A Future that resolves when the RPC response is received.

+
+
Parameters:
+
    +
  • service_name (str)

  • +
  • fq_method_name (str)

  • +
+
+
Return type:
+

bool

+
+
+
+ +
+
+async data_message_generator(route)[source]
+

An async iterator that blocks until messages arrive, processes them, +and continues in a loop.

+
+
Parameters:
+
    +
  • queue_name – The name of the queue to consume messages from

  • +
  • route (AMQRoute)

  • +
+
+
Yield:
+

The processed DataResponse for each message

+
+
Return type:
+

AsyncIterator[DataMessage]

+
+
+
+ +
+
+get_unique_instance_id()[source]
+

Provides cluster-wide unique instance ID for this AMQ service instance. +This ID is used to identify the service instance in the cluster and is unique across all instances. +IDs are managed by shared Consul KV store, ensuring that even if multiple instances of the service are running, +they will each have unique ID.

+
+
Return type:
+

int

+
+
+
+ +
+
+async init(loop, service_adapter)[source]
+
+ +
+
+cleanup()[source]
+
+ +
+
+async reinitialize_if_disconnected()[source]
+
+ +
+
+async register_routes()[source]
+
+
Return type:
+

AbstractRobustExchange | None

+
+
+
+ +
+
+set_outstanding_future(message, future)[source]
+
+
Parameters:
+
+
+
+
+ +
+
+remove_outstanding_future(message_id)[source]
+
+
Parameters:
+

message_id (str)

+
+
+
+ +
+ +
+
+amqp.service.amq_message_handler.collect_trace_info()[source]
+
+ +
+
+amqp.service.amq_message_handler.get_default_trace_state()[source]
+
+ +
+
+amqp.service.amq_message_handler.set_text(carrier, key, value)[source]
+
+ +
+
+async amqp.service.amq_message_handler.invoke_command(package_name, class_name, function_name, msg_id='', kwargs=None)[source]
+
+
Parameters:
+
    +
  • package_name (str)

  • +
  • class_name (str)

  • +
  • function_name (str)

  • +
  • msg_id (str)

  • +
+
+
Return type:
+

Any

+
+
+
+ +
+
+class amqp.service.amq_message_handler.DataMessageHandler(service_adapter, tracer, message_factory, service_message_factory, reply_to_exchange, rabbit_mq_client, amq_configuration, loop, backpressure_handler, file_handler=<amqp.adapter.file_handler.StreamingFileHandler object>)[source]
+

Bases: object

+
+
Parameters:
+
+
+
+
+
+__init__(service_adapter, tracer, message_factory, service_message_factory, reply_to_exchange, rabbit_mq_client, amq_configuration, loop, backpressure_handler, file_handler=<amqp.adapter.file_handler.StreamingFileHandler object>)[source]
+
+
Parameters:
+
+
+
+
+ +
+
+async inbound_data_message_callback_as_thread(message)[source]
+

The callback method that is called when a DataMessage is received from RabbitMQ, starts new Thread.

+
+
Parameters:
+

message (AbstractIncomingMessage)

+
+
+
+ +
+
+async inbound_data_message_callback_no_thread(message)[source]
+

Receives the DataMessage as bytes from RabbitMQ queue and deserializes it to the DataMessage object +Ensures Jaeger trace-id propagation. +Invokes custom handler/mapper/adapter method that corresponds to this message +:param message: +:return:

+
+
Parameters:
+

message (AbstractIncomingMessage)

+
+
+
+ +
+
+async run_http_request_magic(message, amq_message, loop)[source]
+

Synchronous version of on_message method. This is a wrapper around the async on_message method. +It also counts parallel executions and handles backpressure. +:param message: RabbitMQ raw message +:param amq_message: DataMessage +:param loop: Event loop +:return: DataResponse

+
+
Parameters:
+
    +
  • message (AbstractIncomingMessage)

  • +
  • amq_message (DataMessage)

  • +
  • loop (AbstractEventLoop)

  • +
+
+
Return type:
+

None

+
+
+
+ +
+
+async run_rpc_request(message, amq_message, loop)[source]
+

Synchronous version of on_message method. This is a wrapper around the async on_message method. +It also counts parallel executions and handles backpressure. +:param message: RabbitMQ raw message +:param amq_message: DataMessage +:param loop: Event loop +:return: DataResponse

+
+
Parameters:
+
    +
  • message (AbstractIncomingMessage)

  • +
  • amq_message (DataMessage)

  • +
  • loop (AbstractEventLoop)

  • +
+
+
Return type:
+

None

+
+
+
+ +
+
+async on_rpc(a_message)[source]
+

Dynamically invokes a function or a class method from a specified package.

+
+
Parameters:
+

a_message (AMQMessage) – The message containing the package, class, and function details.

+
+
Return type:
+

DataResponse

+
+
+
+
Args encoded in the AMQMessage:

package_name (str): The name of the Python package to import. +class_name (str): The name of the class within the package.

+
+

Use ‘__global__’ if the function_name refers to a global function +in the package.

+
+

function_name (str): The name of the function or method to invoke. +kwargs (Dict[str, Any]): A dictionary of keyword arguments to pass to the function/method (in the body).

+
+
+
+
Returns:
+

The return value of the invoked function or method.

+
+
Return type:
+

Any

+
+
Raises:
+
    +
  • ModuleNotFoundError – If the specified package cannot be imported.

  • +
  • AttributeError – If the specified class or function/method does not exist.

  • +
  • Exception – For any other errors during invocation.

  • +
+
+
Parameters:
+

a_message (AMQMessage)

+
+
+
+ +
+
+async reconstitute_data_message(message)[source]
+

DataMessage may be serialized as string in separate queue (addressing == 1) or be a MultiPart message with +files streamed over individual queues, one per file. +This function uses ‘addressing’ mode from message headers to determine how to deserialize (reconstitute) +the original message.

+
+
Parameters:
+

message (AbstractIncomingMessage)

+
+
Return type:
+

DataMessage | None

+
+
+
+ +
+
+ensure_trace_info(amq_message)[source]
+
+
Parameters:
+

amq_message (DataMessage)

+
+
+
+ +
+
+record_duration(start, delivery_tag)[source]
+
+ +
+
+async send_reply(reply_to, response)[source]
+

The Service side code. When the CleverXXX service has output ready, in DataResponse object, +call this method to return the reply back to the caller +:param reply_to:connection.channel +:param response: +:return:

+
+
Parameters:
+
+
+
+
+ +
+
+async reply_received_callback(message)[source]
+

The handler for reply for DataMessage that we sent out earlier - +the code matches the received response with the original request and passes the response to the caller. +:param message: +:return:

+
+
Parameters:
+

message (AbstractIncomingMessage)

+
+
+
+ +
+
+async send_rpc(route, message)[source]
+

Sends an RPC message to the specified route and returns a Future that will be completed +when the response is received.

+
+
Parameters:
+
    +
  • route – AMQRoute object containing exchange and routing information

  • +
  • message (DataMessage) – DataMessage to be sent

  • +
+
+
Returns:
+

Future that will be completed with the response payload

+
+
Return type:
+

Future

+
+
+
+ +
+
+async send_command(route, message)[source]
+

Sends an RPC message to the specified route and returns a Future that will be completed +when the response is received.

+
+
Parameters:
+
    +
  • route – AMQRoute object containing exchange and routing information

  • +
  • message (DataMessage) – DataMessage to be sent

  • +
+
+
Returns:
+

Future that will be completed with the response payload

+
+
Return type:
+

bool

+
+
+
+ +
+ +

OpenTelemetry configuration for tracing and metrics.

+

This module provides functionality to initialize and configure OpenTelemetry +for both tracing (Jaeger via OTLP) and metrics (Prometheus) in the AMQ adapter.

+
+
+class amqp.service.tracing.TracingConfig(service_name, service_version, otlp_endpoint='http://localhost:4317', insecure=True, debug=False, additional_attributes=None)[source]
+

Bases: object

+

Configuration for OpenTelemetry tracing.

+
+
Parameters:
+
    +
  • service_name (str)

  • +
  • service_version (str)

  • +
  • otlp_endpoint (str)

  • +
  • insecure (bool)

  • +
  • debug (bool)

  • +
  • additional_attributes (Dict[str, Any] | None)

  • +
+
+
+
+
+service_name: str
+
+ +
+
+service_version: str
+
+ +
+
+otlp_endpoint: str = 'http://localhost:4317'
+
+ +
+
+insecure: bool = True
+
+ +
+
+debug: bool = False
+
+ +
+
+additional_attributes: Dict[str, Any] | None = None
+
+ +
+
+__init__(service_name, service_version, otlp_endpoint='http://localhost:4317', insecure=True, debug=False, additional_attributes=None)
+
+
Parameters:
+
    +
  • service_name (str)

  • +
  • service_version (str)

  • +
  • otlp_endpoint (str)

  • +
  • insecure (bool)

  • +
  • debug (bool)

  • +
  • additional_attributes (Dict[str, Any] | None)

  • +
+
+
Return type:
+

None

+
+
+
+ +
+ +
+
+class amqp.service.tracing.MetricsConfig(prometheus_port=9464, prometheus_host='localhost', export_interval_millis=30000, additional_attributes=None)[source]
+

Bases: object

+

Configuration for OpenTelemetry metrics.

+
+
Parameters:
+
    +
  • prometheus_port (int)

  • +
  • prometheus_host (str)

  • +
  • export_interval_millis (int)

  • +
  • additional_attributes (Dict[str, Any] | None)

  • +
+
+
+
+
+prometheus_port: int = 9464
+
+ +
+
+prometheus_host: str = 'localhost'
+
+ +
+
+export_interval_millis: int = 30000
+
+ +
+
+additional_attributes: Dict[str, Any] | None = None
+
+ +
+
+__init__(prometheus_port=9464, prometheus_host='localhost', export_interval_millis=30000, additional_attributes=None)
+
+
Parameters:
+
    +
  • prometheus_port (int)

  • +
  • prometheus_host (str)

  • +
  • export_interval_millis (int)

  • +
  • additional_attributes (Dict[str, Any] | None)

  • +
+
+
Return type:
+

None

+
+
+
+ +
+ +
+
+amqp.service.tracing.create_resource(config)[source]
+

Create an OpenTelemetry resource with service information.

+
+
Parameters:
+

config (TracingConfig)

+
+
Return type:
+

Resource

+
+
+
+ +
+
+amqp.service.tracing.setup_otlp_exporter(config)[source]
+

Configure and create an OTLP exporter for Jaeger.

+
+
Parameters:
+

config (TracingConfig)

+
+
Return type:
+

OTLPSpanExporter

+
+
+
+ +
+
+amqp.service.tracing.setup_trace_provider(config, resource)[source]
+

Set up the TracerProvider with OTLP exporter.

+
+
Parameters:
+
+
+
Return type:
+

TracerProvider

+
+
+
+ +
+
+amqp.service.tracing.setup_metrics_provider(config, resource)[source]
+

Set up the MeterProvider with Prometheus export.

+
+
Parameters:
+
+
+
Return type:
+

MeterProvider

+
+
+
+ +
+
+amqp.service.tracing.initialize_telemetry(tracing_config=None, metrics_config=None)[source]
+

Initialize OpenTelemetry with tracing and metrics.

+
+
Parameters:
+
    +
  • tracing_config (TracingConfig | None) – Configuration for tracing

  • +
  • metrics_config (MetricsConfig | None) – Configuration for metrics

  • +
+
+
Returns:
+

Configured OpenTelemetry tracer

+
+
Return type:
+

Tracer

+
+
+

Example

+

```python +config = TracingConfig(

+
+

service_name=”amq-adapter”, +service_version=”1.0.0”, +otlp_endpoint=”http://jaeger:4317”, +debug=True

+
+

)

+
+
metrics_config = MetricsConfig(

prometheus_port=9464, +prometheus_host=”0.0.0.0”

+
+
+

)

+

tracer = initialize_telemetry(config, metrics_config) +```

+
+ +
+
+amqp.service.tracing.create_span(tracer, name, attributes=None)[source]
+

Create a new span with the given name and attributes.

+
+
Parameters:
+
    +
  • tracer (Tracer) – The OpenTelemetry tracer

  • +
  • name (str) – Name of the span

  • +
  • attributes (Dict[str, Any] | None) – Optional attributes to add to the span

  • +
+
+
Returns:
+

A context manager that creates and manages a span

+
+
+
+ +
+ + +
+
+ +
+
+
+
+ + + + diff --git a/api-doc/_build/html/objects.inv b/api-doc/_build/html/objects.inv new file mode 100644 index 0000000..81ed451 Binary files /dev/null and b/api-doc/_build/html/objects.inv differ diff --git a/api-doc/_build/html/py-modindex.html b/api-doc/_build/html/py-modindex.html new file mode 100644 index 0000000..8b842eb --- /dev/null +++ b/api-doc/_build/html/py-modindex.html @@ -0,0 +1,296 @@ + + + + + + + + Python Module Index — AMQ Adapter Python 1.0 documentation + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • + +
  • +
  • +
+
+
+
+
+ + +

Python Module Index

+ +
+ a +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
 
+ a
+ amqp +
    + amqp.adapter +
    + amqp.adapter.amq_route_factory +
    + amqp.adapter.backpressure_handler +
    + amqp.adapter.cleverthis_service_adapter +
    + amqp.adapter.consul_global_id_generator +
    + amqp.adapter.data_message_factory +
    + amqp.adapter.data_parser +
    + amqp.adapter.data_response_factory +
    + amqp.adapter.file_handler +
    + amqp.adapter.logging_utils +
    + amqp.adapter.pydantic_serializer +
    + amqp.adapter.serializer +
    + amqp.adapter.service_message_factory +
    + amqp.adapter.trace_info_adapter +
    + amqp.adapter.type_descriptor +
    + amqp.config +
    + amqp.config.amq_configuration +
    + amqp.model +
    + amqp.model.model +
    + amqp.model.service_message_type +
    + amqp.model.snowflake_id +
    + amqp.rabbitmq +
    + amqp.rabbitmq.rabbit_mq_client +
    + amqp.rabbitmq.user_management_service_client +
    + amqp.router +
    + amqp.router.route_database +
    + amqp.router.router_base +
    + amqp.router.router_consumer +
    + amqp.router.router_producer +
    + amqp.router.utils +
    + amqp.service +
    + amqp.service.amq_message_handler +
    + amqp.service.amq_service +
    + amqp.service.tracing +
+ + +
+
+ +
+
+
+
+ + + + diff --git a/api-doc/_build/html/search.html b/api-doc/_build/html/search.html new file mode 100644 index 0000000..1886559 --- /dev/null +++ b/api-doc/_build/html/search.html @@ -0,0 +1,126 @@ + + + + + + + + Search — AMQ Adapter Python 1.0 documentation + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • + +
  • +
  • +
+
+
+
+
+ + + + +
+ +
+ +
+
+ +
+
+
+
+ + + + + + + + + diff --git a/api-doc/_build/html/searchindex.js b/api-doc/_build/html/searchindex.js new file mode 100644 index 0000000..b05da9f --- /dev/null +++ b/api-doc/_build/html/searchindex.js @@ -0,0 +1 @@ +Search.setIndex({"alltitles":{"AMQ Adapter Python API Documentation":[[0,null]],"AMQP Package":[[1,null]],"Adapter Module":[[2,null]],"Configuration Module":[[3,null]],"Contents:":[[0,null]],"Indices and tables":[[0,"indices-and-tables"]],"Model Module":[[4,null]],"RabbitMQ Module":[[5,null]],"Router Module":[[6,null]],"Service Module":[[7,null]]},"docnames":["index","modules/amqp","modules/amqp.adapter","modules/amqp.config","modules/amqp.model","modules/amqp.rabbitmq","modules/amqp.router","modules/amqp.service"],"envversion":{"sphinx":65,"sphinx.domains.c":3,"sphinx.domains.changeset":1,"sphinx.domains.citation":1,"sphinx.domains.cpp":9,"sphinx.domains.index":1,"sphinx.domains.javascript":3,"sphinx.domains.math":2,"sphinx.domains.python":4,"sphinx.domains.rst":2,"sphinx.domains.std":2,"sphinx.ext.intersphinx":1,"sphinx.ext.todo":2,"sphinx.ext.viewcode":1},"filenames":["index.rst","modules/amqp.rst","modules/amqp.adapter.rst","modules/amqp.config.rst","modules/amqp.model.rst","modules/amqp.rabbitmq.rst","modules/amqp.router.rst","modules/amqp.service.rst"],"indexentries":{"__init__() (amqp.adapter.backpressure_handler.backpressurehandler method)":[[2,"amqp.adapter.backpressure_handler.BackpressureHandler.__init__",false]],"__init__() (amqp.adapter.backpressure_handler.scalerequestv1 method)":[[2,"amqp.adapter.backpressure_handler.ScaleRequestV1.__init__",false]],"__init__() (amqp.adapter.cleverthis_service_adapter.cleverthisserviceadapter method)":[[2,"amqp.adapter.cleverthis_service_adapter.CleverThisServiceAdapter.__init__",false]],"__init__() (amqp.adapter.data_message_factory.datamessagefactory method)":[[2,"amqp.adapter.data_message_factory.DataMessageFactory.__init__",false]],"__init__() (amqp.adapter.data_parser.multipartformdataparser method)":[[2,"amqp.adapter.data_parser.MultipartFormDataParser.__init__",false]],"__init__() (amqp.adapter.file_handler.filehandler method)":[[2,"amqp.adapter.file_handler.FileHandler.__init__",false]],"__init__() (amqp.adapter.file_handler.streamingfilehandler method)":[[2,"amqp.adapter.file_handler.StreamingFileHandler.__init__",false]],"__init__() (amqp.adapter.service_message_factory.servicemessagefactory method)":[[2,"amqp.adapter.service_message_factory.ServiceMessageFactory.__init__",false]],"__init__() (amqp.adapter.trace_info_adapter.traceinfoadapter method)":[[2,"amqp.adapter.trace_info_adapter.TraceInfoAdapter.__init__",false]],"__init__() (amqp.adapter.type_descriptor.typedescriptor method)":[[2,"amqp.adapter.type_descriptor.TypeDescriptor.__init__",false]],"__init__() (amqp.config.amq_configuration.amqadapter method)":[[3,"amqp.config.amq_configuration.AMQAdapter.__init__",false]],"__init__() (amqp.config.amq_configuration.amqconfiguration method)":[[3,"amqp.config.amq_configuration.AMQConfiguration.__init__",false]],"__init__() (amqp.config.amq_configuration.backpressure method)":[[3,"amqp.config.amq_configuration.Backpressure.__init__",false]],"__init__() (amqp.config.amq_configuration.dispatch method)":[[3,"amqp.config.amq_configuration.Dispatch.__init__",false]],"__init__() (amqp.model.model.amqerrormessage method)":[[4,"amqp.model.model.AMQErrorMessage.__init__",false]],"__init__() (amqp.model.model.amqmessage method)":[[4,"amqp.model.model.AMQMessage.__init__",false]],"__init__() (amqp.model.model.amqroute method)":[[4,"amqp.model.model.AMQRoute.__init__",false]],"__init__() (amqp.model.model.clevermicromessage method)":[[4,"amqp.model.model.CleverMicroMessage.__init__",false]],"__init__() (amqp.model.model.datamessage method)":[[4,"amqp.model.model.DataMessage.__init__",false]],"__init__() (amqp.model.model.dataresponse method)":[[4,"amqp.model.model.DataResponse.__init__",false]],"__init__() (amqp.model.model.multipartdatamessage method)":[[4,"amqp.model.model.MultipartDataMessage.__init__",false]],"__init__() (amqp.model.model.scalingrequest method)":[[4,"amqp.model.model.ScalingRequest.__init__",false]],"__init__() (amqp.model.model.servicemessage method)":[[4,"amqp.model.model.ServiceMessage.__init__",false]],"__init__() (amqp.model.snowflake_id.snowflakeid method)":[[4,"amqp.model.snowflake_id.SnowflakeId.__init__",false]],"__init__() (amqp.rabbitmq.rabbit_mq_client.rabbitmqclient method)":[[5,"amqp.rabbitmq.rabbit_mq_client.RabbitMQClient.__init__",false]],"__init__() (amqp.rabbitmq.user_management_service_client.usermanagementserviceclient method)":[[5,"amqp.rabbitmq.user_management_service_client.UserManagementServiceClient.__init__",false]],"__init__() (amqp.rabbitmq.user_management_service_client.usermanagementserviceexception method)":[[5,"amqp.rabbitmq.user_management_service_client.UserManagementServiceException.__init__",false]],"__init__() (amqp.router.route_database.routedatabase method)":[[6,"amqp.router.route_database.RouteDatabase.__init__",false]],"__init__() (amqp.router.router_base.routerbase method)":[[6,"amqp.router.router_base.RouterBase.__init__",false]],"__init__() (amqp.router.router_consumer.routerconsumer method)":[[6,"amqp.router.router_consumer.RouterConsumer.__init__",false]],"__init__() (amqp.router.router_producer.routerproducer method)":[[6,"amqp.router.router_producer.RouterProducer.__init__",false]],"__init__() (amqp.service.amq_message_handler.datamessagehandler method)":[[7,"amqp.service.amq_message_handler.DataMessageHandler.__init__",false]],"__init__() (amqp.service.amq_service.amqservice method)":[[7,"amqp.service.amq_service.AMQService.__init__",false]],"__init__() (amqp.service.tracing.metricsconfig method)":[[7,"amqp.service.tracing.MetricsConfig.__init__",false]],"__init__() (amqp.service.tracing.tracingconfig method)":[[7,"amqp.service.tracing.TracingConfig.__init__",false]],"adapter_prefix() (amqp.config.amq_configuration.amqadapter method)":[[3,"amqp.config.amq_configuration.AMQAdapter.adapter_prefix",false]],"add_own_route() (amqp.router.router_base.routerbase method)":[[6,"amqp.router.router_base.RouterBase.add_own_route",false]],"add_route() (amqp.router.route_database.routedatabase method)":[[6,"amqp.router.route_database.RouteDatabase.add_route",false]],"add_route() (amqp.router.router_base.routerbase method)":[[6,"amqp.router.router_base.RouterBase.add_route",false]],"add_routes() (amqp.router.router_base.routerbase method)":[[6,"amqp.router.router_base.RouterBase.add_routes",false]],"add_to_map() (in module amqp.adapter.serializer)":[[2,"amqp.adapter.serializer.add_to_map",false]],"additional_attributes (amqp.service.tracing.metricsconfig attribute)":[[7,"amqp.service.tracing.MetricsConfig.additional_attributes",false]],"additional_attributes (amqp.service.tracing.tracingconfig attribute)":[[7,"amqp.service.tracing.TracingConfig.additional_attributes",false]],"amq_message_routing_key() (amqp.adapter.data_message_factory.datamessagefactory static method)":[[2,"amqp.adapter.data_message_factory.DataMessageFactory.amq_message_routing_key",false]],"amqadapter (class in amqp.config.amq_configuration)":[[3,"amqp.config.amq_configuration.AMQAdapter",false]],"amqconfiguration (class in amqp.config.amq_configuration)":[[3,"amqp.config.amq_configuration.AMQConfiguration",false]],"amqdataparser (class in amqp.adapter.data_parser)":[[2,"amqp.adapter.data_parser.AMQDataParser",false]],"amqerrormessage (class in amqp.model.model)":[[4,"amqp.model.model.AMQErrorMessage",false]],"amqmessage (class in amqp.model.model)":[[4,"amqp.model.model.AMQMessage",false]],"amqp":[[1,"module-amqp",false]],"amqp.adapter":[[2,"module-amqp.adapter",false]],"amqp.adapter.amq_route_factory":[[2,"module-amqp.adapter.amq_route_factory",false]],"amqp.adapter.backpressure_handler":[[2,"module-amqp.adapter.backpressure_handler",false]],"amqp.adapter.cleverthis_service_adapter":[[2,"module-amqp.adapter.cleverthis_service_adapter",false]],"amqp.adapter.consul_global_id_generator":[[2,"module-amqp.adapter.consul_global_id_generator",false]],"amqp.adapter.data_message_factory":[[2,"module-amqp.adapter.data_message_factory",false]],"amqp.adapter.data_parser":[[2,"module-amqp.adapter.data_parser",false]],"amqp.adapter.data_response_factory":[[2,"module-amqp.adapter.data_response_factory",false]],"amqp.adapter.file_handler":[[2,"module-amqp.adapter.file_handler",false]],"amqp.adapter.logging_utils":[[2,"module-amqp.adapter.logging_utils",false]],"amqp.adapter.pydantic_serializer":[[2,"module-amqp.adapter.pydantic_serializer",false]],"amqp.adapter.serializer":[[2,"module-amqp.adapter.serializer",false]],"amqp.adapter.service_message_factory":[[2,"module-amqp.adapter.service_message_factory",false]],"amqp.adapter.trace_info_adapter":[[2,"module-amqp.adapter.trace_info_adapter",false]],"amqp.adapter.type_descriptor":[[2,"module-amqp.adapter.type_descriptor",false]],"amqp.config":[[3,"module-amqp.config",false]],"amqp.config.amq_configuration":[[3,"module-amqp.config.amq_configuration",false]],"amqp.model":[[4,"module-amqp.model",false]],"amqp.model.model":[[4,"module-amqp.model.model",false]],"amqp.model.service_message_type":[[4,"module-amqp.model.service_message_type",false]],"amqp.model.snowflake_id":[[4,"module-amqp.model.snowflake_id",false]],"amqp.rabbitmq":[[5,"module-amqp.rabbitmq",false]],"amqp.rabbitmq.rabbit_mq_client":[[5,"module-amqp.rabbitmq.rabbit_mq_client",false]],"amqp.rabbitmq.user_management_service_client":[[5,"module-amqp.rabbitmq.user_management_service_client",false]],"amqp.router":[[6,"module-amqp.router",false]],"amqp.router.route_database":[[6,"module-amqp.router.route_database",false]],"amqp.router.router_base":[[6,"module-amqp.router.router_base",false]],"amqp.router.router_consumer":[[6,"module-amqp.router.router_consumer",false]],"amqp.router.router_producer":[[6,"module-amqp.router.router_producer",false]],"amqp.router.utils":[[6,"module-amqp.router.utils",false]],"amqp.service":[[7,"module-amqp.service",false]],"amqp.service.amq_message_handler":[[7,"module-amqp.service.amq_message_handler",false]],"amqp.service.amq_service":[[7,"module-amqp.service.amq_service",false]],"amqp.service.tracing":[[7,"module-amqp.service.tracing",false]],"amqresponse (in module amqp.model.model)":[[4,"amqp.model.model.AMQResponse",false]],"amqresponsefactory (in module amqp.adapter.data_response_factory)":[[2,"amqp.adapter.data_response_factory.AMQResponseFactory",false]],"amqroute (class in amqp.model.model)":[[4,"amqp.model.model.AMQRoute",false]],"amqroutefactory (class in amqp.adapter.amq_route_factory)":[[2,"amqp.adapter.amq_route_factory.AMQRouteFactory",false]],"amqservice (class in amqp.service.amq_service)":[[7,"amqp.service.amq_service.AMQService",false]],"as_base64() (amqp.adapter.service_message_factory.servicemessagefactory method)":[[2,"amqp.adapter.service_message_factory.ServiceMessageFactory.as_base64",false]],"as_string() (amqp.model.model.amqroute method)":[[4,"amqp.model.model.AMQRoute.as_string",false]],"as_string() (amqp.model.snowflake_id.snowflakeid method)":[[4,"amqp.model.snowflake_id.SnowflakeId.as_string",false]],"await_future() (in module amqp.router.utils)":[[6,"amqp.router.utils.await_future",false]],"await_result() (in module amqp.router.utils)":[[6,"amqp.router.utils.await_result",false]],"backpressure (amqp.model.service_message_type.servicemessagetype attribute)":[[4,"amqp.model.service_message_type.ServiceMessageType.BACKPRESSURE",false]],"backpressure (class in amqp.config.amq_configuration)":[[3,"amqp.config.amq_configuration.Backpressure",false]],"backpressure() (amqp.service.amq_service.amqservice method)":[[7,"amqp.service.amq_service.AMQService.backpressure",false]],"backpressure_monitor_loop() (amqp.adapter.backpressure_handler.backpressurehandler method)":[[2,"amqp.adapter.backpressure_handler.BackpressureHandler.backpressure_monitor_loop",false]],"backpressurehandler (class in amqp.adapter.backpressure_handler)":[[2,"amqp.adapter.backpressure_handler.BackpressureHandler",false]],"base64body() (amqp.model.model.clevermicromessage method)":[[4,"amqp.model.model.CleverMicroMessage.base64body",false]],"body() (amqp.model.model.clevermicromessage method)":[[4,"amqp.model.model.CleverMicroMessage.body",false]],"bytes_to_long() (in module amqp.adapter.serializer)":[[2,"amqp.adapter.serializer.bytes_to_long",false]],"call_cleverthis_api() (amqp.adapter.cleverthis_service_adapter.cleverthisserviceadapter method)":[[2,"amqp.adapter.cleverthis_service_adapter.CleverThisServiceAdapter.call_cleverthis_api",false]],"call_service() (amqp.rabbitmq.user_management_service_client.usermanagementserviceclient method)":[[5,"amqp.rabbitmq.user_management_service_client.UserManagementServiceClient.call_service",false]],"check_overload_condition() (amqp.adapter.backpressure_handler.backpressurehandler method)":[[2,"amqp.adapter.backpressure_handler.BackpressureHandler.check_overload_condition",false]],"cleanup() (amqp.rabbitmq.rabbit_mq_client.rabbitmqclient method)":[[5,"amqp.rabbitmq.rabbit_mq_client.RabbitMQClient.cleanup",false]],"cleanup() (amqp.router.router_consumer.routerconsumer method)":[[6,"amqp.router.router_consumer.RouterConsumer.cleanup",false]],"cleanup() (amqp.service.amq_service.amqservice method)":[[7,"amqp.service.amq_service.AMQService.cleanup",false]],"clever_amqp_client_version (amqp.service.amq_service.amqservice attribute)":[[7,"amqp.service.amq_service.AMQService.CLEVER_AMQP_CLIENT_VERSION",false]],"clevermicromessage (class in amqp.model.model)":[[4,"amqp.model.model.CleverMicroMessage",false]],"cleverthisserviceadapter (class in amqp.adapter.cleverthis_service_adapter)":[[2,"amqp.adapter.cleverthis_service_adapter.CleverThisServiceAdapter",false]],"clone_and_adapt_route() (amqp.adapter.cleverthis_service_adapter.cleverthisserviceadapter method)":[[2,"amqp.adapter.cleverthis_service_adapter.CleverThisServiceAdapter.clone_and_adapt_route",false]],"close() (amqp.rabbitmq.user_management_service_client.usermanagementserviceclient method)":[[5,"amqp.rabbitmq.user_management_service_client.UserManagementServiceClient.close",false]],"collect_trace_info() (in module amqp.service.amq_message_handler)":[[7,"amqp.service.amq_message_handler.collect_trace_info",false]],"command() (amqp.service.amq_service.amqservice method)":[[7,"amqp.service.amq_service.AMQService.command",false]],"component_name (amqp.model.model.amqroute attribute)":[[4,"amqp.model.model.AMQRoute.component_name",false]],"connection (amqp.model.model.amqmessage property)":[[4,"amqp.model.model.AMQMessage.connection",false]],"content_type() (amqp.model.model.clevermicromessage method)":[[4,"amqp.model.model.CleverMicroMessage.content_type",false]],"create_async_response_message() (amqp.adapter.data_response_factory.dataresponsefactory static method)":[[2,"amqp.adapter.data_response_factory.DataResponseFactory.create_async_response_message",false]],"create_produce_span() (amqp.adapter.trace_info_adapter.traceinfoadapter static method)":[[2,"amqp.adapter.trace_info_adapter.TraceInfoAdapter.create_produce_span",false]],"create_request_message() (amqp.adapter.data_message_factory.datamessagefactory method)":[[2,"amqp.adapter.data_message_factory.DataMessageFactory.create_request_message",false]],"create_resource() (in module amqp.service.tracing)":[[7,"amqp.service.tracing.create_resource",false]],"create_rpc_message() (amqp.adapter.data_message_factory.datamessagefactory method)":[[2,"amqp.adapter.data_message_factory.DataMessageFactory.create_rpc_message",false]],"create_span() (in module amqp.service.tracing)":[[7,"amqp.service.tracing.create_span",false]],"csv_as_list() (in module amqp.adapter.serializer)":[[2,"amqp.adapter.serializer.csv_as_list",false]],"current_availability (amqp.adapter.backpressure_handler.backpressurehandler attribute)":[[2,"amqp.adapter.backpressure_handler.BackpressureHandler.current_availability",false]],"current_availability (amqp.model.model.scalingrequest attribute)":[[4,"amqp.model.model.ScalingRequest.current_availability",false]],"d_field() (amqp.model.model.clevermicromessage method)":[[4,"amqp.model.model.CleverMicroMessage.d_field",false]],"data_message_generator() (amqp.service.amq_service.amqservice method)":[[7,"amqp.service.amq_service.AMQService.data_message_generator",false]],"datamessage (class in amqp.model.model)":[[4,"amqp.model.model.DataMessage",false]],"datamessagefactory (class in amqp.adapter.data_message_factory)":[[2,"amqp.adapter.data_message_factory.DataMessageFactory",false]],"datamessagehandler (class in amqp.service.amq_message_handler)":[[7,"amqp.service.amq_message_handler.DataMessageHandler",false]],"datamessagemagicbyte (class in amqp.model.model)":[[4,"amqp.model.model.DataMessageMagicByte",false]],"dataresponse (class in amqp.model.model)":[[4,"amqp.model.model.DataResponse",false]],"dataresponsefactory (class in amqp.adapter.data_response_factory)":[[2,"amqp.adapter.data_response_factory.DataResponseFactory",false]],"debug (amqp.service.tracing.tracingconfig attribute)":[[7,"amqp.service.tracing.TracingConfig.debug",false]],"decrease_current_load() (amqp.adapter.backpressure_handler.backpressurehandler method)":[[2,"amqp.adapter.backpressure_handler.BackpressureHandler.decrease_current_load",false]],"default_content_type (amqp.model.model.clevermicromessage attribute)":[[4,"amqp.model.model.CleverMicroMessage.DEFAULT_CONTENT_TYPE",false]],"delete() (amqp.adapter.cleverthis_service_adapter.cleverthisserviceadapter method)":[[2,"amqp.adapter.cleverthis_service_adapter.CleverThisServiceAdapter.delete",false]],"deserialize_object() (in module amqp.adapter.pydantic_serializer)":[[2,"amqp.adapter.pydantic_serializer.deserialize_object",false]],"dispatch (class in amqp.config.amq_configuration)":[[3,"amqp.config.amq_configuration.Dispatch",false]],"dlq_args (amqp.rabbitmq.rabbit_mq_client.rabbitmqclient attribute)":[[5,"amqp.rabbitmq.rabbit_mq_client.RabbitMQClient.DLQ_ARGS",false]],"domain() (amqp.model.model.datamessage method)":[[4,"amqp.model.model.DataMessage.domain",false]],"download_buffer() (amqp.adapter.file_handler.filehandler method)":[[2,"amqp.adapter.file_handler.FileHandler.download_buffer",false]],"download_buffer() (amqp.adapter.file_handler.streamingfilehandler method)":[[2,"amqp.adapter.file_handler.StreamingFileHandler.download_buffer",false]],"download_file() (amqp.adapter.file_handler.filehandler method)":[[2,"amqp.adapter.file_handler.FileHandler.download_file",false]],"download_file() (amqp.adapter.file_handler.streamingfilehandler method)":[[2,"amqp.adapter.file_handler.StreamingFileHandler.download_file",false]],"ensure_directory_exists() (amqp.adapter.file_handler.filehandler method)":[[2,"amqp.adapter.file_handler.FileHandler.ensure_directory_exists",false]],"ensure_trace_info() (amqp.service.amq_message_handler.datamessagehandler method)":[[7,"amqp.service.amq_message_handler.DataMessageHandler.ensure_trace_info",false]],"error() (amqp.model.model.dataresponse method)":[[4,"amqp.model.model.DataResponse.error",false]],"error_cause() (amqp.model.model.dataresponse method)":[[4,"amqp.model.model.DataResponse.error_cause",false]],"exchange (amqp.model.model.amqroute attribute)":[[4,"amqp.model.model.AMQRoute.exchange",false]],"exchange_name (amqp.rabbitmq.user_management_service_client.usermanagementserviceclient attribute)":[[5,"amqp.rabbitmq.user_management_service_client.UserManagementServiceClient.EXCHANGE_NAME",false]],"export_interval_millis (amqp.service.tracing.metricsconfig attribute)":[[7,"amqp.service.tracing.MetricsConfig.export_interval_millis",false]],"filehandler (class in amqp.adapter.file_handler)":[[2,"amqp.adapter.file_handler.FileHandler",false]],"find_route() (amqp.rabbitmq.rabbit_mq_client.rabbitmqclient method)":[[5,"amqp.rabbitmq.rabbit_mq_client.RabbitMQClient.find_route",false]],"find_route() (amqp.router.route_database.routedatabase method)":[[6,"amqp.router.route_database.RouteDatabase.find_route",false]],"find_route() (amqp.router.router_base.routerbase method)":[[6,"amqp.router.router_base.RouterBase.find_route",false]],"find_route_by_message() (amqp.router.router_base.routerbase method)":[[6,"amqp.router.router_base.RouterBase.find_route_by_message",false]],"find_route_by_service() (amqp.router.route_database.routedatabase method)":[[6,"amqp.router.route_database.RouteDatabase.find_route_by_service",false]],"find_route_by_service() (amqp.router.router_base.routerbase method)":[[6,"amqp.router.router_base.RouterBase.find_route_by_service",false]],"format (amqp.model.model.amqroute attribute)":[[4,"amqp.model.model.AMQRoute.FORMAT",false]],"format_message_type() (amqp.adapter.service_message_factory.servicemessagefactory method)":[[2,"amqp.adapter.service_message_factory.ServiceMessageFactory.format_message_type",false]],"from_bytes() (amqp.adapter.data_message_factory.datamessagefactory static method)":[[2,"amqp.adapter.data_message_factory.DataMessageFactory.from_bytes",false]],"from_bytes() (amqp.adapter.data_response_factory.dataresponsefactory static method)":[[2,"amqp.adapter.data_response_factory.DataResponseFactory.from_bytes",false]],"from_bytes() (amqp.adapter.service_message_factory.servicemessagefactory method)":[[2,"amqp.adapter.service_message_factory.ServiceMessageFactory.from_bytes",false]],"from_hex() (amqp.model.snowflake_id.snowflakeid static method)":[[4,"amqp.model.snowflake_id.SnowflakeId.from_hex",false]],"from_json() (amqp.model.model.scalingrequest class method)":[[4,"amqp.model.model.ScalingRequest.from_json",false]],"from_stream() (amqp.adapter.data_message_factory.datamessagefactory static method)":[[2,"amqp.adapter.data_message_factory.DataMessageFactory.from_stream",false]],"from_string() (amqp.adapter.amq_route_factory.amqroutefactory static method)":[[2,"amqp.adapter.amq_route_factory.AMQRouteFactory.from_string",false]],"generate_fallback_id() (in module amqp.adapter.consul_global_id_generator)":[[2,"amqp.adapter.consul_global_id_generator.generate_fallback_id",false]],"generate_snowflake_id() (amqp.adapter.data_message_factory.datamessagefactory method)":[[2,"amqp.adapter.data_message_factory.DataMessageFactory.generate_snowflake_id",false]],"get() (amqp.adapter.cleverthis_service_adapter.cleverthisserviceadapter method)":[[2,"amqp.adapter.cleverthis_service_adapter.CleverThisServiceAdapter.get",false]],"get_bits() (amqp.model.snowflake_id.snowflakeid method)":[[4,"amqp.model.snowflake_id.SnowflakeId.get_bits",false]],"get_bytes() (amqp.model.snowflake_id.snowflakeid method)":[[4,"amqp.model.snowflake_id.SnowflakeId.get_bytes",false]],"get_config_values() (in module amqp.adapter.consul_global_id_generator)":[[2,"amqp.adapter.consul_global_id_generator.get_config_values",false]],"get_consume_queue_name() (amqp.router.router_producer.routerproducer method)":[[6,"amqp.router.router_producer.RouterProducer.get_consume_queue_name",false]],"get_content_type() (amqp.adapter.cleverthis_service_adapter.cleverthisserviceadapter method)":[[2,"amqp.adapter.cleverthis_service_adapter.CleverThisServiceAdapter.get_content_type",false]],"get_context_info() (in module amqp.adapter.logging_utils)":[[2,"amqp.adapter.logging_utils.get_context_info",false]],"get_current_user() (amqp.adapter.cleverthis_service_adapter.cleverthisserviceadapter method)":[[2,"amqp.adapter.cleverthis_service_adapter.CleverThisServiceAdapter.get_current_user",false]],"get_default_trace_state() (in module amqp.service.amq_message_handler)":[[7,"amqp.service.amq_message_handler.get_default_trace_state",false]],"get_endpoint() (amqp.adapter.cleverthis_service_adapter.cleverthisserviceadapter method)":[[2,"amqp.adapter.cleverthis_service_adapter.CleverThisServiceAdapter.get_endpoint",false]],"get_instance() (amqp.adapter.data_message_factory.datamessagefactory class method)":[[2,"amqp.adapter.data_message_factory.DataMessageFactory.get_instance",false]],"get_reply_to_queue_name() (amqp.router.router_producer.routerproducer method)":[[6,"amqp.router.router_producer.RouterProducer.get_reply_to_queue_name",false]],"get_response_queue_name() (amqp.router.router_producer.routerproducer method)":[[6,"amqp.router.router_producer.RouterProducer.get_response_queue_name",false]],"get_routes() (amqp.router.route_database.routedatabase method)":[[6,"amqp.router.route_database.RouteDatabase.get_routes",false]],"get_routes() (amqp.router.router_base.routerbase method)":[[6,"amqp.router.router_base.RouterBase.get_routes",false]],"get_routing_key() (amqp.router.router_base.routerbase method)":[[6,"amqp.router.router_base.RouterBase.get_routing_key",false]],"get_timestamp_from_id() (amqp.adapter.data_message_factory.datamessagefactory static method)":[[2,"amqp.adapter.data_message_factory.DataMessageFactory.get_timestamp_from_id",false]],"get_unique_instance_id() (amqp.config.amq_configuration.amqconfiguration method)":[[3,"amqp.config.amq_configuration.AMQConfiguration.get_unique_instance_id",false]],"get_unique_instance_id() (amqp.service.amq_service.amqservice method)":[[7,"amqp.service.amq_service.AMQService.get_unique_instance_id",false]],"get_unique_instance_id() (in module amqp.adapter.consul_global_id_generator)":[[2,"amqp.adapter.consul_global_id_generator.get_unique_instance_id",false]],"group_routes_by_method() (amqp.adapter.cleverthis_service_adapter.cleverthisserviceadapter method)":[[2,"amqp.adapter.cleverthis_service_adapter.CleverThisServiceAdapter.group_routes_by_method",false]],"group_service (amqp.rabbitmq.user_management_service_client.usermanagementserviceclient attribute)":[[5,"amqp.rabbitmq.user_management_service_client.UserManagementServiceClient.GROUP_SERVICE",false]],"handle_backpressure_overload_event() (amqp.adapter.backpressure_handler.backpressurehandler method)":[[2,"amqp.adapter.backpressure_handler.BackpressureHandler.handle_backpressure_overload_event",false]],"handle_body_payload() (amqp.adapter.cleverthis_service_adapter.cleverthisserviceadapter method)":[[2,"amqp.adapter.cleverthis_service_adapter.CleverThisServiceAdapter.handle_body_payload",false]],"handle_no_body_payload() (amqp.adapter.cleverthis_service_adapter.cleverthisserviceadapter method)":[[2,"amqp.adapter.cleverthis_service_adapter.CleverThisServiceAdapter.handle_no_body_payload",false]],"handle_routing_data_message() (amqp.router.router_producer.routerproducer method)":[[6,"amqp.router.router_producer.RouterProducer.handle_routing_data_message",false]],"handle_routing_data_request_message() (amqp.router.router_consumer.routerconsumer method)":[[6,"amqp.router.router_consumer.RouterConsumer.handle_routing_data_request_message",false]],"head() (amqp.adapter.cleverthis_service_adapter.cleverthisserviceadapter method)":[[2,"amqp.adapter.cleverthis_service_adapter.CleverThisServiceAdapter.head",false]],"headers() (amqp.model.model.clevermicromessage method)":[[4,"amqp.model.model.CleverMicroMessage.headers",false]],"http_request (amqp.model.model.datamessagemagicbyte attribute)":[[4,"amqp.model.model.DataMessageMagicByte.HTTP_REQUEST",false]],"id (amqp.model.model.servicemessage attribute)":[[4,"amqp.model.model.ServiceMessage.id",false]],"id() (amqp.model.model.clevermicromessage method)":[[4,"amqp.model.model.CleverMicroMessage.id",false]],"idle (amqp.model.model.scalingrequestalert attribute)":[[4,"amqp.model.model.ScalingRequestAlert.IDLE",false]],"inbound_data_message_callback_as_thread() (amqp.service.amq_message_handler.datamessagehandler method)":[[7,"amqp.service.amq_message_handler.DataMessageHandler.inbound_data_message_callback_as_thread",false]],"inbound_data_message_callback_no_thread() (amqp.service.amq_message_handler.datamessagehandler method)":[[7,"amqp.service.amq_message_handler.DataMessageHandler.inbound_data_message_callback_no_thread",false]],"increase_current_load() (amqp.adapter.backpressure_handler.backpressurehandler method)":[[2,"amqp.adapter.backpressure_handler.BackpressureHandler.increase_current_load",false]],"init() (amqp.rabbitmq.rabbit_mq_client.rabbitmqclient method)":[[5,"amqp.rabbitmq.rabbit_mq_client.RabbitMQClient.init",false]],"init() (amqp.service.amq_service.amqservice method)":[[7,"amqp.service.amq_service.AMQService.init",false]],"init_internal_routing_paths() (amqp.router.router_producer.routerproducer method)":[[6,"amqp.router.router_producer.RouterProducer.init_internal_routing_paths",false]],"init_routing_paths_consumer() (amqp.router.router_consumer.routerconsumer method)":[[6,"amqp.router.router_consumer.RouterConsumer.init_routing_paths_consumer",false]],"initialize() (amqp.rabbitmq.user_management_service_client.usermanagementserviceclient method)":[[5,"amqp.rabbitmq.user_management_service_client.UserManagementServiceClient.initialize",false]],"initialize_telemetry() (in module amqp.service.tracing)":[[7,"amqp.service.tracing.initialize_telemetry",false]],"insecure (amqp.service.tracing.tracingconfig attribute)":[[7,"amqp.service.tracing.TracingConfig.insecure",false]],"instantiate() (amqp.adapter.type_descriptor.typedescriptor method)":[[2,"amqp.adapter.type_descriptor.TypeDescriptor.instantiate",false]],"invoke_command() (in module amqp.service.amq_message_handler)":[[7,"amqp.service.amq_message_handler.invoke_command",false]],"is_likely_json() (in module amqp.adapter.cleverthis_service_adapter)":[[2,"amqp.adapter.cleverthis_service_adapter.is_likely_json",false]],"is_open() (amqp.router.router_producer.routerproducer method)":[[6,"amqp.router.router_producer.RouterProducer.is_open",false]],"is_valid() (amqp.model.model.servicemessage method)":[[4,"amqp.model.model.ServiceMessage.is_valid",false]],"key (amqp.model.model.amqroute attribute)":[[4,"amqp.model.model.AMQRoute.key",false]],"last_backpressure_event (amqp.adapter.backpressure_handler.backpressurehandler attribute)":[[2,"amqp.adapter.backpressure_handler.BackpressureHandler.last_backpressure_event",false]],"last_backpressure_event_time (amqp.adapter.backpressure_handler.backpressurehandler attribute)":[[2,"amqp.adapter.backpressure_handler.BackpressureHandler.last_backpressure_event_time",false]],"list_users_in_group() (amqp.rabbitmq.user_management_service_client.usermanagementserviceclient method)":[[5,"amqp.rabbitmq.user_management_service_client.UserManagementServiceClient.list_users_in_group",false]],"logging_debug() (in module amqp.adapter.logging_utils)":[[2,"amqp.adapter.logging_utils.logging_debug",false]],"logging_error() (in module amqp.adapter.logging_utils)":[[2,"amqp.adapter.logging_utils.logging_error",false]],"logging_info() (in module amqp.adapter.logging_utils)":[[2,"amqp.adapter.logging_utils.logging_info",false]],"logging_warning() (in module amqp.adapter.logging_utils)":[[2,"amqp.adapter.logging_utils.logging_warning",false]],"long_to_bytes() (in module amqp.adapter.serializer)":[[2,"amqp.adapter.serializer.long_to_bytes",false]],"m_field() (amqp.model.model.clevermicromessage method)":[[4,"amqp.model.model.CleverMicroMessage.m_field",false]],"machine_id_bits (amqp.adapter.data_message_factory.datamessagefactory attribute)":[[2,"amqp.adapter.data_message_factory.DataMessageFactory.MACHINE_ID_BITS",false]],"machine_id_mask (amqp.adapter.data_message_factory.datamessagefactory attribute)":[[2,"amqp.adapter.data_message_factory.DataMessageFactory.MACHINE_ID_MASK",false]],"magic() (amqp.model.model.clevermicromessage method)":[[4,"amqp.model.model.CleverMicroMessage.magic",false]],"map_as_string() (in module amqp.adapter.serializer)":[[2,"amqp.adapter.serializer.map_as_string",false]],"map_with_list_as_string() (in module amqp.adapter.serializer)":[[2,"amqp.adapter.serializer.map_with_list_as_string",false]],"matches() (amqp.router.route_database.routedatabase method)":[[6,"amqp.router.route_database.RouteDatabase.matches",false]],"max_availability (amqp.model.model.scalingrequest attribute)":[[4,"amqp.model.model.ScalingRequest.max_availability",false]],"max_wait (amqp.service.amq_service.amqservice attribute)":[[7,"amqp.service.amq_service.AMQService.MAX_WAIT",false]],"merge_extra_init_data() (in module amqp.adapter.type_descriptor)":[[2,"amqp.adapter.type_descriptor.merge_extra_init_data",false]],"message (amqp.model.model.servicemessage attribute)":[[4,"amqp.model.model.ServiceMessage.message",false]],"message_type (amqp.model.model.servicemessage attribute)":[[4,"amqp.model.model.ServiceMessage.message_type",false]],"method() (amqp.model.model.datamessage method)":[[4,"amqp.model.model.DataMessage.method",false]],"metricsconfig (class in amqp.service.tracing)":[[7,"amqp.service.tracing.MetricsConfig",false]],"module":[[1,"module-amqp",false],[2,"module-amqp.adapter",false],[2,"module-amqp.adapter.amq_route_factory",false],[2,"module-amqp.adapter.backpressure_handler",false],[2,"module-amqp.adapter.cleverthis_service_adapter",false],[2,"module-amqp.adapter.consul_global_id_generator",false],[2,"module-amqp.adapter.data_message_factory",false],[2,"module-amqp.adapter.data_parser",false],[2,"module-amqp.adapter.data_response_factory",false],[2,"module-amqp.adapter.file_handler",false],[2,"module-amqp.adapter.logging_utils",false],[2,"module-amqp.adapter.pydantic_serializer",false],[2,"module-amqp.adapter.serializer",false],[2,"module-amqp.adapter.service_message_factory",false],[2,"module-amqp.adapter.trace_info_adapter",false],[2,"module-amqp.adapter.type_descriptor",false],[3,"module-amqp.config",false],[3,"module-amqp.config.amq_configuration",false],[4,"module-amqp.model",false],[4,"module-amqp.model.model",false],[4,"module-amqp.model.service_message_type",false],[4,"module-amqp.model.snowflake_id",false],[5,"module-amqp.rabbitmq",false],[5,"module-amqp.rabbitmq.rabbit_mq_client",false],[5,"module-amqp.rabbitmq.user_management_service_client",false],[6,"module-amqp.router",false],[6,"module-amqp.router.route_database",false],[6,"module-amqp.router.router_base",false],[6,"module-amqp.router.router_consumer",false],[6,"module-amqp.router.router_producer",false],[6,"module-amqp.router.utils",false],[7,"module-amqp.service",false],[7,"module-amqp.service.amq_message_handler",false],[7,"module-amqp.service.amq_service",false],[7,"module-amqp.service.tracing",false]],"multipartdatamessage (class in amqp.model.model)":[[4,"amqp.model.model.MultipartDataMessage",false]],"multipartformdataparser (class in amqp.adapter.data_parser)":[[2,"amqp.adapter.data_parser.MultipartFormDataParser",false]],"object_or_list_as_string() (in module amqp.adapter.serializer)":[[2,"amqp.adapter.serializer.object_or_list_as_string",false]],"of() (amqp.adapter.data_response_factory.dataresponsefactory static method)":[[2,"amqp.adapter.data_response_factory.DataResponseFactory.of",false]],"of() (amqp.adapter.service_message_factory.servicemessagefactory method)":[[2,"amqp.adapter.service_message_factory.ServiceMessageFactory.of",false]],"of_error_message() (amqp.adapter.data_response_factory.dataresponsefactory static method)":[[2,"amqp.adapter.data_response_factory.DataResponseFactory.of_error_message",false]],"on_field() (amqp.adapter.data_parser.multipartformdataparser method)":[[2,"amqp.adapter.data_parser.MultipartFormDataParser.on_field",false]],"on_file() (amqp.adapter.data_parser.multipartformdataparser method)":[[2,"amqp.adapter.data_parser.MultipartFormDataParser.on_file",false]],"on_message() (amqp.adapter.cleverthis_service_adapter.cleverthisserviceadapter method)":[[2,"amqp.adapter.cleverthis_service_adapter.CleverThisServiceAdapter.on_message",false]],"on_message() (amqp.adapter.file_handler.filehandler method)":[[2,"amqp.adapter.file_handler.FileHandler.on_message",false]],"on_message() (amqp.adapter.file_handler.streamingfilehandler method)":[[2,"amqp.adapter.file_handler.StreamingFileHandler.on_message",false]],"on_rpc() (amqp.service.amq_message_handler.datamessagehandler method)":[[7,"amqp.service.amq_message_handler.DataMessageHandler.on_rpc",false]],"options() (amqp.adapter.cleverthis_service_adapter.cleverthisserviceadapter method)":[[2,"amqp.adapter.cleverthis_service_adapter.CleverThisServiceAdapter.options",false]],"otlp_endpoint (amqp.service.tracing.tracingconfig attribute)":[[7,"amqp.service.tracing.TracingConfig.otlp_endpoint",false]],"overload (amqp.model.model.scalingrequestalert attribute)":[[4,"amqp.model.model.ScalingRequestAlert.OVERLOAD",false]],"p_field() (amqp.model.model.clevermicromessage method)":[[4,"amqp.model.model.CleverMicroMessage.p_field",false]],"parse_get_url() (amqp.adapter.data_parser.amqdataparser static method)":[[2,"amqp.adapter.data_parser.AMQDataParser.parse_get_url",false]],"parse_map_list_string() (in module amqp.adapter.serializer)":[[2,"amqp.adapter.serializer.parse_map_list_string",false]],"parse_map_string() (in module amqp.adapter.serializer)":[[2,"amqp.adapter.serializer.parse_map_string",false]],"parse_request_body() (amqp.adapter.data_parser.amqdataparser static method)":[[2,"amqp.adapter.data_parser.AMQDataParser.parse_request_body",false]],"parse_url_query() (in module amqp.adapter.pydantic_serializer)":[[2,"amqp.adapter.pydantic_serializer.parse_url_query",false]],"patch() (amqp.adapter.cleverthis_service_adapter.cleverthisserviceadapter method)":[[2,"amqp.adapter.cleverthis_service_adapter.CleverThisServiceAdapter.patch",false]],"path() (amqp.model.model.datamessage method)":[[4,"amqp.model.model.DataMessage.path",false]],"pattern() (amqp.router.route_database.routedatabase method)":[[6,"amqp.router.route_database.RouteDatabase.pattern",false]],"payload_type (amqp.model.model.servicemessage attribute)":[[4,"amqp.model.model.ServiceMessage.payload_type",false]],"permission_service (amqp.rabbitmq.user_management_service_client.usermanagementserviceclient attribute)":[[5,"amqp.rabbitmq.user_management_service_client.UserManagementServiceClient.PERMISSION_SERVICE",false]],"post() (amqp.adapter.cleverthis_service_adapter.cleverthisserviceadapter method)":[[2,"amqp.adapter.cleverthis_service_adapter.CleverThisServiceAdapter.post",false]],"prefetch_count (amqp.rabbitmq.rabbit_mq_client.rabbitmqclient attribute)":[[5,"amqp.rabbitmq.rabbit_mq_client.RabbitMQClient.PREFETCH_COUNT",false]],"prefix (amqp.config.amq_configuration.amqadapter attribute)":[[3,"amqp.config.amq_configuration.AMQAdapter.PREFIX",false]],"prefix (amqp.config.amq_configuration.backpressure attribute)":[[3,"amqp.config.amq_configuration.Backpressure.PREFIX",false]],"prefix (amqp.config.amq_configuration.dispatch attribute)":[[3,"amqp.config.amq_configuration.Dispatch.PREFIX",false]],"primary_key (amqp.model.model.amqroute property)":[[4,"amqp.model.model.AMQRoute.primary_key",false]],"process_form_data() (amqp.adapter.data_parser.amqdataparser static method)":[[2,"amqp.adapter.data_parser.AMQDataParser.process_form_data",false]],"prometheus_host (amqp.service.tracing.metricsconfig attribute)":[[7,"amqp.service.tracing.MetricsConfig.prometheus_host",false]],"prometheus_port (amqp.service.tracing.metricsconfig attribute)":[[7,"amqp.service.tracing.MetricsConfig.prometheus_port",false]],"publish_backpressure_request() (amqp.adapter.backpressure_handler.backpressurehandler method)":[[2,"amqp.adapter.backpressure_handler.BackpressureHandler.publish_backpressure_request",false]],"publish_file() (amqp.adapter.file_handler.filehandler method)":[[2,"amqp.adapter.file_handler.FileHandler.publish_file",false]],"publish_file() (amqp.adapter.file_handler.streamingfilehandler method)":[[2,"amqp.adapter.file_handler.StreamingFileHandler.publish_file",false]],"publish_service_message() (amqp.router.router_producer.routerproducer method)":[[6,"amqp.router.router_producer.RouterProducer.publish_service_message",false]],"put() (amqp.adapter.cleverthis_service_adapter.cleverthisserviceadapter method)":[[2,"amqp.adapter.cleverthis_service_adapter.CleverThisServiceAdapter.put",false]],"python_type_to_json() (in module amqp.adapter.pydantic_serializer)":[[2,"amqp.adapter.pydantic_serializer.python_type_to_json",false]],"query_user_by_id() (amqp.rabbitmq.user_management_service_client.usermanagementserviceclient method)":[[5,"amqp.rabbitmq.user_management_service_client.UserManagementServiceClient.query_user_by_id",false]],"queue (amqp.model.model.amqroute attribute)":[[4,"amqp.model.model.AMQRoute.queue",false]],"rabbitmqclient (class in amqp.rabbitmq.rabbit_mq_client)":[[5,"amqp.rabbitmq.rabbit_mq_client.RabbitMQClient",false]],"read_long() (in module amqp.adapter.serializer)":[[2,"amqp.adapter.serializer.read_long",false]],"recipient_name (amqp.model.model.servicemessage attribute)":[[4,"amqp.model.model.ServiceMessage.recipient_name",false]],"reconstitute_data_message() (amqp.service.amq_message_handler.datamessagehandler method)":[[7,"amqp.service.amq_message_handler.DataMessageHandler.reconstitute_data_message",false]],"record_duration() (amqp.service.amq_message_handler.datamessagehandler method)":[[7,"amqp.service.amq_message_handler.DataMessageHandler.record_duration",false]],"register_data_reply_callback() (amqp.rabbitmq.rabbit_mq_client.rabbitmqclient method)":[[5,"amqp.rabbitmq.rabbit_mq_client.RabbitMQClient.register_data_reply_callback",false]],"register_inbound_routes() (amqp.rabbitmq.rabbit_mq_client.rabbitmqclient method)":[[5,"amqp.rabbitmq.rabbit_mq_client.RabbitMQClient.register_inbound_routes",false]],"register_route() (amqp.router.router_consumer.routerconsumer method)":[[6,"amqp.router.router_consumer.RouterConsumer.register_route",false]],"register_routes() (amqp.service.amq_service.amqservice method)":[[7,"amqp.service.amq_service.AMQService.register_routes",false]],"reinitialize_if_disconnected() (amqp.service.amq_service.amqservice method)":[[7,"amqp.service.amq_service.AMQService.reinitialize_if_disconnected",false]],"remove_outstanding_future() (amqp.service.amq_service.amqservice method)":[[7,"amqp.service.amq_service.AMQService.remove_outstanding_future",false]],"remove_route() (amqp.router.route_database.routedatabase method)":[[6,"amqp.router.route_database.RouteDatabase.remove_route",false]],"remove_route() (amqp.router.router_base.routerbase method)":[[6,"amqp.router.router_base.RouterBase.remove_route",false]],"remove_route_on_cleanup() (amqp.router.router_consumer.routerconsumer method)":[[6,"amqp.router.router_consumer.RouterConsumer.remove_route_on_cleanup",false]],"remove_routes() (amqp.router.router_base.routerbase method)":[[6,"amqp.router.router_base.RouterBase.remove_routes",false]],"reply_received_callback() (amqp.service.amq_message_handler.datamessagehandler method)":[[7,"amqp.service.amq_message_handler.DataMessageHandler.reply_received_callback",false]],"reply_to (amqp.model.model.servicemessage attribute)":[[4,"amqp.model.model.ServiceMessage.reply_to",false]],"request_routes() (amqp.rabbitmq.rabbit_mq_client.rabbitmqclient method)":[[5,"amqp.rabbitmq.rabbit_mq_client.RabbitMQClient.request_routes",false]],"request_routes_from_adapters() (amqp.router.router_producer.routerproducer method)":[[6,"amqp.router.router_producer.RouterProducer.request_routes_from_adapters",false]],"request_type (amqp.model.model.scalingrequest attribute)":[[4,"amqp.model.model.ScalingRequest.request_type",false]],"response_code() (amqp.model.model.dataresponse method)":[[4,"amqp.model.model.DataResponse.response_code",false]],"response_queue_name (amqp.rabbitmq.user_management_service_client.usermanagementserviceclient attribute)":[[5,"amqp.rabbitmq.user_management_service_client.UserManagementServiceClient.RESPONSE_QUEUE_NAME",false]],"return_address() (amqp.model.model.clevermicromessage method)":[[4,"amqp.model.model.CleverMicroMessage.return_address",false]],"routedatabase (class in amqp.router.route_database)":[[6,"amqp.router.route_database.RouteDatabase",false]],"routerbase (class in amqp.router.router_base)":[[6,"amqp.router.router_base.RouterBase",false]],"routerconsumer (class in amqp.router.router_consumer)":[[6,"amqp.router.router_consumer.RouterConsumer",false]],"routerproducer (class in amqp.router.router_producer)":[[6,"amqp.router.router_producer.RouterProducer",false]],"routing_data (amqp.model.service_message_type.servicemessagetype attribute)":[[4,"amqp.model.service_message_type.ServiceMessageType.ROUTING_DATA",false]],"routing_data_remove (amqp.model.service_message_type.servicemessagetype attribute)":[[4,"amqp.model.service_message_type.ServiceMessageType.ROUTING_DATA_REMOVE",false]],"routing_data_replace (amqp.model.service_message_type.servicemessagetype attribute)":[[4,"amqp.model.service_message_type.ServiceMessageType.ROUTING_DATA_REPLACE",false]],"routing_data_request (amqp.model.service_message_type.servicemessagetype attribute)":[[4,"amqp.model.service_message_type.ServiceMessageType.ROUTING_DATA_REQUEST",false]],"routing_key() (amqp.model.model.datamessage method)":[[4,"amqp.model.model.DataMessage.routing_key",false]],"rpc() (amqp.service.amq_service.amqservice method)":[[7,"amqp.service.amq_service.AMQService.rpc",false]],"rpc_request (amqp.model.model.datamessagemagicbyte attribute)":[[4,"amqp.model.model.DataMessageMagicByte.RPC_REQUEST",false]],"rs (in module amqp.model.model)":[[4,"amqp.model.model.RS",false]],"run() (amqp.service.amq_service.amqservice method)":[[7,"amqp.service.amq_service.AMQService.run",false]],"run_http_request_magic() (amqp.service.amq_message_handler.datamessagehandler method)":[[7,"amqp.service.amq_message_handler.DataMessageHandler.run_http_request_magic",false]],"run_rpc_request() (amqp.service.amq_message_handler.datamessagehandler method)":[[7,"amqp.service.amq_message_handler.DataMessageHandler.run_rpc_request",false]],"safe_rsplit() (amqp.adapter.data_message_factory.datamessagefactory method)":[[2,"amqp.adapter.data_message_factory.DataMessageFactory.safe_rsplit",false]],"sanitize_as_rabbitmq_domain_name() (in module amqp.router.utils)":[[6,"amqp.router.utils.sanitize_as_rabbitmq_domain_name",false]],"sanitize_as_rabbitmq_name() (in module amqp.router.utils)":[[6,"amqp.router.utils.sanitize_as_rabbitmq_name",false]],"scale_down (amqp.model.model.scalingrequestalert attribute)":[[4,"amqp.model.model.ScalingRequestAlert.SCALE_DOWN",false]],"scale_up (amqp.model.model.scalingrequestalert attribute)":[[4,"amqp.model.model.ScalingRequestAlert.SCALE_UP",false]],"scalerequestv1 (class in amqp.adapter.backpressure_handler)":[[2,"amqp.adapter.backpressure_handler.ScaleRequestV1",false]],"scalingrequest (class in amqp.model.model)":[[4,"amqp.model.model.ScalingRequest",false]],"scalingrequestalert (class in amqp.model.model)":[[4,"amqp.model.model.ScalingRequestAlert",false]],"send_command() (amqp.service.amq_message_handler.datamessagehandler method)":[[7,"amqp.service.amq_message_handler.DataMessageHandler.send_command",false]],"send_message() (amqp.rabbitmq.rabbit_mq_client.rabbitmqclient method)":[[5,"amqp.rabbitmq.rabbit_mq_client.RabbitMQClient.send_message",false]],"send_reply() (amqp.service.amq_message_handler.datamessagehandler method)":[[7,"amqp.service.amq_message_handler.DataMessageHandler.send_reply",false]],"send_rpc() (amqp.service.amq_message_handler.datamessagehandler method)":[[7,"amqp.service.amq_message_handler.DataMessageHandler.send_rpc",false]],"sequence_bits (amqp.adapter.data_message_factory.datamessagefactory attribute)":[[2,"amqp.adapter.data_message_factory.DataMessageFactory.SEQUENCE_BITS",false]],"sequence_mask (amqp.adapter.data_message_factory.datamessagefactory attribute)":[[2,"amqp.adapter.data_message_factory.DataMessageFactory.SEQUENCE_MASK",false]],"serialize() (amqp.adapter.data_message_factory.datamessagefactory static method)":[[2,"amqp.adapter.data_message_factory.DataMessageFactory.serialize",false]],"serialize() (amqp.adapter.data_response_factory.dataresponsefactory static method)":[[2,"amqp.adapter.data_response_factory.DataResponseFactory.serialize",false]],"serialize() (amqp.adapter.service_message_factory.servicemessagefactory method)":[[2,"amqp.adapter.service_message_factory.ServiceMessageFactory.serialize",false]],"serialize() (amqp.model.model.amqerrormessage method)":[[4,"amqp.model.model.AMQErrorMessage.serialize",false]],"serialize_object() (in module amqp.adapter.pydantic_serializer)":[[2,"amqp.adapter.pydantic_serializer.serialize_object",false]],"service_id (amqp.model.model.scalingrequest attribute)":[[4,"amqp.model.model.ScalingRequest.service_id",false]],"service_name (amqp.service.tracing.tracingconfig attribute)":[[7,"amqp.service.tracing.TracingConfig.service_name",false]],"service_version (amqp.service.tracing.tracingconfig attribute)":[[7,"amqp.service.tracing.TracingConfig.service_version",false]],"servicemessage (class in amqp.model.model)":[[4,"amqp.model.model.ServiceMessage",false]],"servicemessagefactory (class in amqp.adapter.service_message_factory)":[[2,"amqp.adapter.service_message_factory.ServiceMessageFactory",false]],"servicemessagetype (class in amqp.model.service_message_type)":[[4,"amqp.model.service_message_type.ServiceMessageType",false]],"set_outstanding_future() (amqp.service.amq_service.amqservice method)":[[7,"amqp.service.amq_service.AMQService.set_outstanding_future",false]],"set_route_added_notifier() (amqp.router.router_base.routerbase method)":[[6,"amqp.router.router_base.RouterBase.set_route_added_notifier",false]],"set_text() (in module amqp.service.amq_message_handler)":[[7,"amqp.service.amq_message_handler.set_text",false]],"setup_amq_channel() (amqp.rabbitmq.rabbit_mq_client.rabbitmqclient method)":[[5,"amqp.rabbitmq.rabbit_mq_client.RabbitMQClient.setup_amq_channel",false]],"setup_metrics_provider() (in module amqp.service.tracing)":[[7,"amqp.service.tracing.setup_metrics_provider",false]],"setup_otlp_exporter() (in module amqp.service.tracing)":[[7,"amqp.service.tracing.setup_otlp_exporter",false]],"setup_trace_provider() (in module amqp.service.tracing)":[[7,"amqp.service.tracing.setup_trace_provider",false]],"snowflake_epoch (amqp.adapter.data_message_factory.datamessagefactory attribute)":[[2,"amqp.adapter.data_message_factory.DataMessageFactory.snowflake_epoch",false]],"snowflake_id_bits (amqp.adapter.data_message_factory.datamessagefactory attribute)":[[2,"amqp.adapter.data_message_factory.DataMessageFactory.SNOWFLAKE_ID_BITS",false]],"snowflake_id_width (amqp.model.snowflake_id.snowflakeid attribute)":[[4,"amqp.model.snowflake_id.SnowflakeId.SNOWFLAKE_ID_WIDTH",false]],"snowflakeid (class in amqp.model.snowflake_id)":[[4,"amqp.model.snowflake_id.SnowflakeId",false]],"start_backpressure_monitor() (amqp.adapter.backpressure_handler.backpressurehandler method)":[[2,"amqp.adapter.backpressure_handler.BackpressureHandler.start_backpressure_monitor",false]],"str_to_bool() (in module amqp.adapter.serializer)":[[2,"amqp.adapter.serializer.str_to_bool",false]],"streamingfilehandler (class in amqp.adapter.file_handler)":[[2,"amqp.adapter.file_handler.StreamingFileHandler",false]],"task_id (amqp.model.model.scalingrequest attribute)":[[4,"amqp.model.model.ScalingRequest.task_id",false]],"tenant_service (amqp.rabbitmq.user_management_service_client.usermanagementserviceclient attribute)":[[5,"amqp.rabbitmq.user_management_service_client.UserManagementServiceClient.TENANT_SERVICE",false]],"timeout (amqp.model.model.amqroute attribute)":[[4,"amqp.model.model.AMQRoute.timeout",false]],"timestamp_bits (amqp.adapter.data_message_factory.datamessagefactory attribute)":[[2,"amqp.adapter.data_message_factory.DataMessageFactory.TIMESTAMP_BITS",false]],"to_json() (amqp.adapter.backpressure_handler.scalerequestv1 method)":[[2,"amqp.adapter.backpressure_handler.ScaleRequestV1.to_json",false]],"to_json() (amqp.model.model.scalingrequest method)":[[4,"amqp.model.model.ScalingRequest.to_json",false]],"to_string() (amqp.adapter.data_message_factory.datamessagefactory static method)":[[2,"amqp.adapter.data_message_factory.DataMessageFactory.to_string",false]],"to_string() (amqp.adapter.service_message_factory.servicemessagefactory method)":[[2,"amqp.adapter.service_message_factory.ServiceMessageFactory.to_string",false]],"token_service (amqp.rabbitmq.user_management_service_client.usermanagementserviceclient attribute)":[[5,"amqp.rabbitmq.user_management_service_client.UserManagementServiceClient.TOKEN_SERVICE",false]],"trace_info (amqp.model.model.servicemessage attribute)":[[4,"amqp.model.model.ServiceMessage.trace_info",false]],"trace_info() (amqp.model.model.clevermicromessage method)":[[4,"amqp.model.model.CleverMicroMessage.trace_info",false]],"traceinfoadapter (class in amqp.adapter.trace_info_adapter)":[[2,"amqp.adapter.trace_info_adapter.TraceInfoAdapter",false]],"tracingconfig (class in amqp.service.tracing)":[[7,"amqp.service.tracing.TracingConfig",false]],"try_deserialize_object() (in module amqp.adapter.pydantic_serializer)":[[2,"amqp.adapter.pydantic_serializer.try_deserialize_object",false]],"typedescriptor (class in amqp.adapter.type_descriptor)":[[2,"amqp.adapter.type_descriptor.TypeDescriptor",false]],"unknown (amqp.model.service_message_type.servicemessagetype attribute)":[[4,"amqp.model.service_message_type.ServiceMessageType.UNKNOWN",false]],"unwrap_route_list() (amqp.router.router_base.routerbase method)":[[6,"amqp.router.router_base.RouterBase.unwrap_route_list",false]],"update (amqp.model.model.scalingrequestalert attribute)":[[4,"amqp.model.model.ScalingRequestAlert.UPDATE",false]],"update_backpressure_value() (amqp.adapter.backpressure_handler.backpressurehandler method)":[[2,"amqp.adapter.backpressure_handler.BackpressureHandler.update_backpressure_value",false]],"update_current_availability() (amqp.adapter.backpressure_handler.backpressurehandler method)":[[2,"amqp.adapter.backpressure_handler.BackpressureHandler.update_current_availability",false]],"update_last_backpressure_event_time() (amqp.adapter.backpressure_handler.backpressurehandler method)":[[2,"amqp.adapter.backpressure_handler.BackpressureHandler.update_last_backpressure_event_time",false]],"user_service (amqp.rabbitmq.user_management_service_client.usermanagementserviceclient attribute)":[[5,"amqp.rabbitmq.user_management_service_client.UserManagementServiceClient.USER_SERVICE",false]],"usermanagementserviceclient (class in amqp.rabbitmq.user_management_service_client)":[[5,"amqp.rabbitmq.user_management_service_client.UserManagementServiceClient",false]],"usermanagementserviceexception":[[5,"amqp.rabbitmq.user_management_service_client.UserManagementServiceException",false]],"valid_until (amqp.model.model.amqroute attribute)":[[4,"amqp.model.model.AMQRoute.valid_until",false]],"validate() (amqp.adapter.amq_route_factory.amqroutefactory static method)":[[2,"amqp.adapter.amq_route_factory.AMQRouteFactory.validate",false]],"validate_token() (amqp.rabbitmq.user_management_service_client.usermanagementserviceclient method)":[[5,"amqp.rabbitmq.user_management_service_client.UserManagementServiceClient.validate_token",false]],"version (amqp.model.model.scalingrequest attribute)":[[4,"amqp.model.model.ScalingRequest.version",false]],"wrap_own_routes() (amqp.router.router_base.routerbase method)":[[6,"amqp.router.router_base.RouterBase.wrap_own_routes",false]],"wrap_routes() (amqp.router.route_database.routedatabase method)":[[6,"amqp.router.route_database.RouteDatabase.wrap_routes",false]],"wrap_routes() (amqp.router.router_base.routerbase method)":[[6,"amqp.router.router_base.RouterBase.wrap_routes",false]]},"objects":{"":[[1,0,0,"-","amqp"]],"amqp":[[2,0,0,"-","adapter"],[3,0,0,"-","config"],[4,0,0,"-","model"],[5,0,0,"-","rabbitmq"],[6,0,0,"-","router"],[7,0,0,"-","service"]],"amqp.adapter":[[2,0,0,"-","amq_route_factory"],[2,0,0,"-","backpressure_handler"],[2,0,0,"-","cleverthis_service_adapter"],[2,0,0,"-","consul_global_id_generator"],[2,0,0,"-","data_message_factory"],[2,0,0,"-","data_parser"],[2,0,0,"-","data_response_factory"],[2,0,0,"-","file_handler"],[2,0,0,"-","logging_utils"],[2,0,0,"-","pydantic_serializer"],[2,0,0,"-","serializer"],[2,0,0,"-","service_message_factory"],[2,0,0,"-","trace_info_adapter"],[2,0,0,"-","type_descriptor"]],"amqp.adapter.amq_route_factory":[[2,1,1,"","AMQRouteFactory"]],"amqp.adapter.amq_route_factory.AMQRouteFactory":[[2,2,1,"","from_string"],[2,2,1,"","validate"]],"amqp.adapter.backpressure_handler":[[2,1,1,"","BackpressureHandler"],[2,1,1,"","ScaleRequestV1"]],"amqp.adapter.backpressure_handler.BackpressureHandler":[[2,2,1,"","__init__"],[2,2,1,"","backpressure_monitor_loop"],[2,2,1,"","check_overload_condition"],[2,3,1,"","current_availability"],[2,2,1,"","decrease_current_load"],[2,2,1,"","handle_backpressure_overload_event"],[2,2,1,"","increase_current_load"],[2,3,1,"","last_backpressure_event"],[2,3,1,"","last_backpressure_event_time"],[2,2,1,"","publish_backpressure_request"],[2,2,1,"","start_backpressure_monitor"],[2,2,1,"","update_backpressure_value"],[2,2,1,"","update_current_availability"],[2,2,1,"","update_last_backpressure_event_time"]],"amqp.adapter.backpressure_handler.ScaleRequestV1":[[2,2,1,"","__init__"],[2,2,1,"","to_json"]],"amqp.adapter.cleverthis_service_adapter":[[2,1,1,"","CleverThisServiceAdapter"],[2,4,1,"","is_likely_json"]],"amqp.adapter.cleverthis_service_adapter.CleverThisServiceAdapter":[[2,2,1,"","__init__"],[2,2,1,"","call_cleverthis_api"],[2,2,1,"","clone_and_adapt_route"],[2,2,1,"","delete"],[2,2,1,"","get"],[2,2,1,"","get_content_type"],[2,2,1,"","get_current_user"],[2,2,1,"","get_endpoint"],[2,2,1,"","group_routes_by_method"],[2,2,1,"","handle_body_payload"],[2,2,1,"","handle_no_body_payload"],[2,2,1,"","head"],[2,2,1,"","on_message"],[2,2,1,"","options"],[2,2,1,"","patch"],[2,2,1,"","post"],[2,2,1,"","put"]],"amqp.adapter.consul_global_id_generator":[[2,4,1,"","generate_fallback_id"],[2,4,1,"","get_config_values"],[2,4,1,"","get_unique_instance_id"]],"amqp.adapter.data_message_factory":[[2,1,1,"","DataMessageFactory"]],"amqp.adapter.data_message_factory.DataMessageFactory":[[2,3,1,"","MACHINE_ID_BITS"],[2,3,1,"","MACHINE_ID_MASK"],[2,3,1,"","SEQUENCE_BITS"],[2,3,1,"","SEQUENCE_MASK"],[2,3,1,"","SNOWFLAKE_ID_BITS"],[2,3,1,"","TIMESTAMP_BITS"],[2,2,1,"","__init__"],[2,2,1,"","amq_message_routing_key"],[2,2,1,"","create_request_message"],[2,2,1,"","create_rpc_message"],[2,2,1,"","from_bytes"],[2,2,1,"","from_stream"],[2,2,1,"","generate_snowflake_id"],[2,2,1,"","get_instance"],[2,2,1,"","get_timestamp_from_id"],[2,2,1,"","safe_rsplit"],[2,2,1,"","serialize"],[2,3,1,"","snowflake_epoch"],[2,2,1,"","to_string"]],"amqp.adapter.data_parser":[[2,1,1,"","AMQDataParser"],[2,1,1,"","MultipartFormDataParser"]],"amqp.adapter.data_parser.AMQDataParser":[[2,2,1,"","parse_get_url"],[2,2,1,"","parse_request_body"],[2,2,1,"","process_form_data"]],"amqp.adapter.data_parser.MultipartFormDataParser":[[2,2,1,"","__init__"],[2,2,1,"","on_field"],[2,2,1,"","on_file"]],"amqp.adapter.data_response_factory":[[2,3,1,"","AMQResponseFactory"],[2,1,1,"","DataResponseFactory"]],"amqp.adapter.data_response_factory.DataResponseFactory":[[2,2,1,"","create_async_response_message"],[2,2,1,"","from_bytes"],[2,2,1,"","of"],[2,2,1,"","of_error_message"],[2,2,1,"","serialize"]],"amqp.adapter.file_handler":[[2,1,1,"","FileHandler"],[2,1,1,"","StreamingFileHandler"]],"amqp.adapter.file_handler.FileHandler":[[2,2,1,"","__init__"],[2,2,1,"","download_buffer"],[2,2,1,"","download_file"],[2,2,1,"","ensure_directory_exists"],[2,2,1,"","on_message"],[2,2,1,"","publish_file"]],"amqp.adapter.file_handler.StreamingFileHandler":[[2,2,1,"","__init__"],[2,2,1,"","download_buffer"],[2,2,1,"","download_file"],[2,2,1,"","on_message"],[2,2,1,"","publish_file"]],"amqp.adapter.logging_utils":[[2,4,1,"","get_context_info"],[2,4,1,"","logging_debug"],[2,4,1,"","logging_error"],[2,4,1,"","logging_info"],[2,4,1,"","logging_warning"]],"amqp.adapter.pydantic_serializer":[[2,4,1,"","deserialize_object"],[2,4,1,"","parse_url_query"],[2,4,1,"","python_type_to_json"],[2,4,1,"","serialize_object"],[2,4,1,"","try_deserialize_object"]],"amqp.adapter.serializer":[[2,4,1,"","add_to_map"],[2,4,1,"","bytes_to_long"],[2,4,1,"","csv_as_list"],[2,4,1,"","long_to_bytes"],[2,4,1,"","map_as_string"],[2,4,1,"","map_with_list_as_string"],[2,4,1,"","object_or_list_as_string"],[2,4,1,"","parse_map_list_string"],[2,4,1,"","parse_map_string"],[2,4,1,"","read_long"],[2,4,1,"","str_to_bool"]],"amqp.adapter.service_message_factory":[[2,1,1,"","ServiceMessageFactory"]],"amqp.adapter.service_message_factory.ServiceMessageFactory":[[2,2,1,"","__init__"],[2,2,1,"","as_base64"],[2,2,1,"","format_message_type"],[2,2,1,"","from_bytes"],[2,2,1,"","of"],[2,2,1,"","serialize"],[2,2,1,"","to_string"]],"amqp.adapter.trace_info_adapter":[[2,1,1,"","TraceInfoAdapter"]],"amqp.adapter.trace_info_adapter.TraceInfoAdapter":[[2,2,1,"","__init__"],[2,2,1,"","create_produce_span"]],"amqp.adapter.type_descriptor":[[2,1,1,"","TypeDescriptor"],[2,4,1,"","merge_extra_init_data"]],"amqp.adapter.type_descriptor.TypeDescriptor":[[2,2,1,"","__init__"],[2,2,1,"","instantiate"]],"amqp.config":[[3,0,0,"-","amq_configuration"]],"amqp.config.amq_configuration":[[3,1,1,"","AMQAdapter"],[3,1,1,"","AMQConfiguration"],[3,1,1,"","Backpressure"],[3,1,1,"","Dispatch"]],"amqp.config.amq_configuration.AMQAdapter":[[3,3,1,"","PREFIX"],[3,2,1,"","__init__"],[3,2,1,"","adapter_prefix"]],"amqp.config.amq_configuration.AMQConfiguration":[[3,2,1,"","__init__"],[3,2,1,"","get_unique_instance_id"]],"amqp.config.amq_configuration.Backpressure":[[3,3,1,"","PREFIX"],[3,2,1,"","__init__"]],"amqp.config.amq_configuration.Dispatch":[[3,3,1,"","PREFIX"],[3,2,1,"","__init__"]],"amqp.model":[[4,0,0,"-","model"],[4,0,0,"-","service_message_type"],[4,0,0,"-","snowflake_id"]],"amqp.model.model":[[4,1,1,"","AMQErrorMessage"],[4,1,1,"","AMQMessage"],[4,3,1,"","AMQResponse"],[4,1,1,"","AMQRoute"],[4,1,1,"","CleverMicroMessage"],[4,1,1,"","DataMessage"],[4,1,1,"","DataMessageMagicByte"],[4,1,1,"","DataResponse"],[4,1,1,"","MultipartDataMessage"],[4,6,1,"","RS"],[4,1,1,"","ScalingRequest"],[4,1,1,"","ScalingRequestAlert"],[4,1,1,"","ServiceMessage"]],"amqp.model.model.AMQErrorMessage":[[4,2,1,"","__init__"],[4,2,1,"","serialize"]],"amqp.model.model.AMQMessage":[[4,2,1,"","__init__"],[4,5,1,"","connection"]],"amqp.model.model.AMQRoute":[[4,3,1,"","FORMAT"],[4,2,1,"","__init__"],[4,2,1,"","as_string"],[4,3,1,"","component_name"],[4,3,1,"","exchange"],[4,3,1,"","key"],[4,5,1,"","primary_key"],[4,3,1,"","queue"],[4,3,1,"","timeout"],[4,3,1,"","valid_until"]],"amqp.model.model.CleverMicroMessage":[[4,3,1,"","DEFAULT_CONTENT_TYPE"],[4,2,1,"","__init__"],[4,2,1,"","base64body"],[4,2,1,"","body"],[4,2,1,"","content_type"],[4,2,1,"","d_field"],[4,2,1,"","headers"],[4,2,1,"","id"],[4,2,1,"","m_field"],[4,2,1,"","magic"],[4,2,1,"","p_field"],[4,2,1,"","return_address"],[4,2,1,"","trace_info"]],"amqp.model.model.DataMessage":[[4,2,1,"","__init__"],[4,2,1,"","domain"],[4,2,1,"","method"],[4,2,1,"","path"],[4,2,1,"","routing_key"]],"amqp.model.model.DataMessageMagicByte":[[4,3,1,"","HTTP_REQUEST"],[4,3,1,"","RPC_REQUEST"]],"amqp.model.model.DataResponse":[[4,2,1,"","__init__"],[4,2,1,"","error"],[4,2,1,"","error_cause"],[4,2,1,"","response_code"]],"amqp.model.model.MultipartDataMessage":[[4,2,1,"","__init__"]],"amqp.model.model.ScalingRequest":[[4,2,1,"","__init__"],[4,3,1,"","current_availability"],[4,2,1,"","from_json"],[4,3,1,"","max_availability"],[4,3,1,"","request_type"],[4,3,1,"","service_id"],[4,3,1,"","task_id"],[4,2,1,"","to_json"],[4,3,1,"","version"]],"amqp.model.model.ScalingRequestAlert":[[4,3,1,"","IDLE"],[4,3,1,"","OVERLOAD"],[4,3,1,"","SCALE_DOWN"],[4,3,1,"","SCALE_UP"],[4,3,1,"","UPDATE"]],"amqp.model.model.ServiceMessage":[[4,2,1,"","__init__"],[4,3,1,"","id"],[4,2,1,"","is_valid"],[4,3,1,"","message"],[4,3,1,"","message_type"],[4,3,1,"","payload_type"],[4,3,1,"","recipient_name"],[4,3,1,"","reply_to"],[4,3,1,"","trace_info"]],"amqp.model.service_message_type":[[4,1,1,"","ServiceMessageType"]],"amqp.model.service_message_type.ServiceMessageType":[[4,3,1,"","BACKPRESSURE"],[4,3,1,"","ROUTING_DATA"],[4,3,1,"","ROUTING_DATA_REMOVE"],[4,3,1,"","ROUTING_DATA_REPLACE"],[4,3,1,"","ROUTING_DATA_REQUEST"],[4,3,1,"","UNKNOWN"]],"amqp.model.snowflake_id":[[4,1,1,"","SnowflakeId"]],"amqp.model.snowflake_id.SnowflakeId":[[4,3,1,"","SNOWFLAKE_ID_WIDTH"],[4,2,1,"","__init__"],[4,2,1,"","as_string"],[4,2,1,"","from_hex"],[4,2,1,"","get_bits"],[4,2,1,"","get_bytes"]],"amqp.rabbitmq":[[5,0,0,"-","rabbit_mq_client"],[5,0,0,"-","user_management_service_client"]],"amqp.rabbitmq.rabbit_mq_client":[[5,1,1,"","RabbitMQClient"]],"amqp.rabbitmq.rabbit_mq_client.RabbitMQClient":[[5,3,1,"","DLQ_ARGS"],[5,3,1,"","PREFETCH_COUNT"],[5,2,1,"","__init__"],[5,2,1,"","cleanup"],[5,2,1,"","find_route"],[5,2,1,"","init"],[5,2,1,"","register_data_reply_callback"],[5,2,1,"","register_inbound_routes"],[5,2,1,"","request_routes"],[5,2,1,"","send_message"],[5,2,1,"","setup_amq_channel"]],"amqp.rabbitmq.user_management_service_client":[[5,1,1,"","UserManagementServiceClient"],[5,7,1,"","UserManagementServiceException"]],"amqp.rabbitmq.user_management_service_client.UserManagementServiceClient":[[5,3,1,"","EXCHANGE_NAME"],[5,3,1,"","GROUP_SERVICE"],[5,3,1,"","PERMISSION_SERVICE"],[5,3,1,"","RESPONSE_QUEUE_NAME"],[5,3,1,"","TENANT_SERVICE"],[5,3,1,"","TOKEN_SERVICE"],[5,3,1,"","USER_SERVICE"],[5,2,1,"","__init__"],[5,2,1,"","call_service"],[5,2,1,"","close"],[5,2,1,"","initialize"],[5,2,1,"","list_users_in_group"],[5,2,1,"","query_user_by_id"],[5,2,1,"","validate_token"]],"amqp.rabbitmq.user_management_service_client.UserManagementServiceException":[[5,2,1,"","__init__"]],"amqp.router":[[6,0,0,"-","route_database"],[6,0,0,"-","router_base"],[6,0,0,"-","router_consumer"],[6,0,0,"-","router_producer"],[6,0,0,"-","utils"]],"amqp.router.route_database":[[6,1,1,"","RouteDatabase"]],"amqp.router.route_database.RouteDatabase":[[6,2,1,"","__init__"],[6,2,1,"","add_route"],[6,2,1,"","find_route"],[6,2,1,"","find_route_by_service"],[6,2,1,"","get_routes"],[6,2,1,"","matches"],[6,2,1,"","pattern"],[6,2,1,"","remove_route"],[6,2,1,"","wrap_routes"]],"amqp.router.router_base":[[6,1,1,"","RouterBase"]],"amqp.router.router_base.RouterBase":[[6,2,1,"","__init__"],[6,2,1,"","add_own_route"],[6,2,1,"","add_route"],[6,2,1,"","add_routes"],[6,2,1,"","find_route"],[6,2,1,"","find_route_by_message"],[6,2,1,"","find_route_by_service"],[6,2,1,"","get_routes"],[6,2,1,"","get_routing_key"],[6,2,1,"","remove_route"],[6,2,1,"","remove_routes"],[6,2,1,"","set_route_added_notifier"],[6,2,1,"","unwrap_route_list"],[6,2,1,"","wrap_own_routes"],[6,2,1,"","wrap_routes"]],"amqp.router.router_consumer":[[6,1,1,"","RouterConsumer"]],"amqp.router.router_consumer.RouterConsumer":[[6,2,1,"","__init__"],[6,2,1,"","cleanup"],[6,2,1,"","handle_routing_data_request_message"],[6,2,1,"","init_routing_paths_consumer"],[6,2,1,"","register_route"],[6,2,1,"","remove_route_on_cleanup"]],"amqp.router.router_producer":[[6,1,1,"","RouterProducer"]],"amqp.router.router_producer.RouterProducer":[[6,2,1,"","__init__"],[6,2,1,"","get_consume_queue_name"],[6,2,1,"","get_reply_to_queue_name"],[6,2,1,"","get_response_queue_name"],[6,2,1,"","handle_routing_data_message"],[6,2,1,"","init_internal_routing_paths"],[6,2,1,"","is_open"],[6,2,1,"","publish_service_message"],[6,2,1,"","request_routes_from_adapters"]],"amqp.router.utils":[[6,4,1,"","await_future"],[6,4,1,"","await_result"],[6,4,1,"","sanitize_as_rabbitmq_domain_name"],[6,4,1,"","sanitize_as_rabbitmq_name"]],"amqp.service":[[7,0,0,"-","amq_message_handler"],[7,0,0,"-","amq_service"],[7,0,0,"-","tracing"]],"amqp.service.amq_message_handler":[[7,1,1,"","DataMessageHandler"],[7,4,1,"","collect_trace_info"],[7,4,1,"","get_default_trace_state"],[7,4,1,"","invoke_command"],[7,4,1,"","set_text"]],"amqp.service.amq_message_handler.DataMessageHandler":[[7,2,1,"","__init__"],[7,2,1,"","ensure_trace_info"],[7,2,1,"","inbound_data_message_callback_as_thread"],[7,2,1,"","inbound_data_message_callback_no_thread"],[7,2,1,"","on_rpc"],[7,2,1,"","reconstitute_data_message"],[7,2,1,"","record_duration"],[7,2,1,"","reply_received_callback"],[7,2,1,"","run_http_request_magic"],[7,2,1,"","run_rpc_request"],[7,2,1,"","send_command"],[7,2,1,"","send_reply"],[7,2,1,"","send_rpc"]],"amqp.service.amq_service":[[7,1,1,"","AMQService"]],"amqp.service.amq_service.AMQService":[[7,3,1,"","CLEVER_AMQP_CLIENT_VERSION"],[7,3,1,"","MAX_WAIT"],[7,2,1,"","__init__"],[7,2,1,"","backpressure"],[7,2,1,"","cleanup"],[7,2,1,"","command"],[7,2,1,"","data_message_generator"],[7,2,1,"","get_unique_instance_id"],[7,2,1,"","init"],[7,2,1,"","register_routes"],[7,2,1,"","reinitialize_if_disconnected"],[7,2,1,"","remove_outstanding_future"],[7,2,1,"","rpc"],[7,2,1,"","run"],[7,2,1,"","set_outstanding_future"]],"amqp.service.tracing":[[7,1,1,"","MetricsConfig"],[7,1,1,"","TracingConfig"],[7,4,1,"","create_resource"],[7,4,1,"","create_span"],[7,4,1,"","initialize_telemetry"],[7,4,1,"","setup_metrics_provider"],[7,4,1,"","setup_otlp_exporter"],[7,4,1,"","setup_trace_provider"]],"amqp.service.tracing.MetricsConfig":[[7,2,1,"","__init__"],[7,3,1,"","additional_attributes"],[7,3,1,"","export_interval_millis"],[7,3,1,"","prometheus_host"],[7,3,1,"","prometheus_port"]],"amqp.service.tracing.TracingConfig":[[7,2,1,"","__init__"],[7,3,1,"","additional_attributes"],[7,3,1,"","debug"],[7,3,1,"","insecure"],[7,3,1,"","otlp_endpoint"],[7,3,1,"","service_name"],[7,3,1,"","service_version"]]},"objnames":{"0":["py","module","Python module"],"1":["py","class","Python class"],"2":["py","method","Python method"],"3":["py","attribute","Python attribute"],"4":["py","function","Python function"],"5":["py","property","Python property"],"6":["py","data","Python data"],"7":["py","exception","Python exception"]},"objtypes":{"0":"py:module","1":"py:class","2":"py:method","3":"py:attribute","4":"py:function","5":"py:property","6":"py:data","7":"py:exception"},"terms":{"":[2,6],"0":[2,4,6,7],"05":6,"1":[2,4,5,7],"10":[4,7],"1048576":2,"12":2,"164":3,"16777215":2,"1727740800":2,"2":[2,4],"20":3,"24":2,"3":[2,4],"300":7,"3000":3,"30000":7,"4":4,"4095":2,"4317":7,"44":2,"5":4,"5671":3,"5672":3,"6":4,"80":[2,7],"8080":3,"9464":7,"A":[2,3,4,5,7],"And":2,"Be":2,"For":[2,7],"If":[2,5,7],"In":[2,4],"It":[2,4,6,7],"The":[2,4,5,6,7],"There":5,"To":2,"_":2,"__global__":7,"__init__":[2,3,4,5,6,7],"_channel":6,"_id":2,"_input":4,"a_messag":7,"about":[2,6],"abstract":2,"abstracteventloop":[2,5,7],"abstractexchang":2,"abstractincomingmessag":[2,6,7],"abstractrobustchannel":[2,6],"abstractrobustconnect":4,"abstractrobustexchang":[5,6,7],"accept":[4,7],"accord":[2,6],"ack":5,"across":7,"activ":2,"actual":2,"adapt":[3,4,5,6,7],"adapter_prefix":3,"add":[2,7],"add_own_rout":6,"add_rout":6,"add_to_map":[0,2],"addit":[2,7],"additional_attribut":7,"additional_info":5,"address":7,"addrout":6,"adjust":2,"after":[4,5],"aid":2,"aio_pika":2,"alia":[2,4],"all":[2,4,6,7],"allow":2,"alongsid":6,"alreadi":2,"also":[2,4,6,7],"alter":2,"altern":2,"alwai":2,"amq":[2,3,4,5,6,7],"amq_configur":[2,3,7],"amq_messag":7,"amq_message_handl":7,"amq_message_routing_kei":2,"amq_route_factori":2,"amq_servic":7,"amqadapt":[0,2,3],"amqconfigur":[0,2,3,5,6,7],"amqdatapars":[0,2],"amqerrormessag":[0,2,4],"amqmessag":[0,2,4,7],"amqp":[0,2,3,4,5,6,7],"amqrespons":[0,4],"amqresponsefactori":[0,2],"amqrout":[0,2,4,5,6,7],"amqroutefactori":[0,2],"amqservic":[0,2,7],"an":[2,5,7],"ani":[2,5,6,7],"anyth":5,"anytim":6,"api":[2,5],"api_method":2,"api_rout":2,"apirout":2,"appli":5,"applic":[2,4,6],"appropri":2,"ar":[2,7],"arg":[2,5,7],"argument":[2,5,7],"around":[2,7],"arrai":2,"arriv":7,"as_base64":2,"as_str":4,"associ":[4,6],"assum":2,"async":[2,5,6,7],"asynch":5,"asynchron":2,"asyncio":2,"asynciter":7,"atom":2,"attempt":2,"attribut":[2,7],"attributeerror":7,"auth":5,"auth_us":2,"authent":2,"authenticated_us":2,"authenticated_user_arg_nam":2,"autom":0,"avail":[2,7],"await_futur":[0,6],"await_result":[0,6],"b":2,"back":[2,4,7],"backpressur":[0,2,3,4,7],"backpressure_handl":[2,7],"backpressure_monitor_loop":2,"backpressurehandl":[0,2,7],"bar":2,"base":[2,3,4,5,6,7],"base64":[2,4],"base64bodi":4,"basemodel":2,"becaus":2,"been":2,"being":2,"below":7,"between":4,"bi":2,"bind":[2,4,5],"bit":2,"block":7,"bodi":[2,4,7],"bool":[2,4,6,7],"boolean":2,"both":[2,7],"broadcast":6,"buil":2,"build":[2,4],"busi":2,"byte":[2,4,7],"bytearai":2,"bytearrai":2,"bytes_to_long":[0,2],"bytesio":2,"call":[2,4,5,6,7],"call_cleverthis_api":2,"call_servic":5,"callabl":[2,6],"callback":[2,5,6,7],"caller":7,"can":[2,6],"cannot":7,"capabl":6,"capac":[2,7],"carrier":7,"case":[2,4],"caus":4,"chang":5,"channel":[2,4,6,7],"charact":2,"check":2,"check_overload_condit":2,"chunk":2,"cl":2,"class":[2,3,4,5,6,7],"class_nam":[2,7],"classmethod":[2,4],"classnam":2,"cleanup":[5,6,7],"clever":4,"clever_amqp_client_vers":7,"clever_micro_respons":6,"cleverbrag":2,"clevermicro":[2,4,5],"clevermicromessag":[0,4],"clevermicrorequest":6,"clevermicrorespons":6,"cleverswarm":[2,5],"cleverthi":[2,5],"cleverthis_service_adapt":2,"cleverthisservic":2,"cleverthisserviceadapt":[0,2,7],"cleverxxx":7,"client":5,"clientsess":2,"clone":2,"clone_and_adapt_rout":2,"close":[2,5],"cluster":7,"cm":[3,6],"cm_c_req_queu":6,"cm_response_exchang":6,"code":[2,4,7],"collect_trace_info":[0,7],"com":2,"combin":2,"comma":5,"command":7,"common":[2,4],"commun":[2,4,5,6],"compar":2,"compat":2,"complet":[6,7],"complex":2,"compliant":2,"compon":[2,4],"component_nam":4,"concret":2,"condit":[2,7],"config":[2,3,6,7],"config_file_nam":3,"configpars":3,"configur":[0,2,4,5,6,7],"confirm":5,"conform":4,"connect":[2,4,5,7],"consist":2,"construct":2,"consul":[2,7],"consul_counter_kei":2,"consul_global_id_gener":2,"consul_host":2,"consul_port":2,"consum":[2,5,6,7],"contain":[2,5,6,7],"content":[2,4],"content_typ":[2,4],"context":[3,6,7],"context_path":2,"continu":7,"convert":[2,4],"core":4,"coroutin":2,"correct":2,"correctli":2,"correspond":7,"count":[2,7],"counter":2,"coupl":2,"creat":[2,4,5,6,7],"create_async_response_messag":2,"create_produce_span":2,"create_request_messag":2,"create_resourc":[0,7],"create_rpc_messag":2,"create_span":[0,7],"csv_as_list":[0,2],"current":[2,7],"current_avail":[2,4,7],"custom":[2,7],"d":2,"d_field":4,"data":[2,4,5,6],"data_messag":4,"data_message_factori":2,"data_message_gener":7,"data_pars":2,"data_response_factori":2,"datagram":5,"datamessag":[0,2,4,5,6,7],"datamessagefactori":[0,2,4,7],"datamessagehandl":[0,7],"datamessagemagicbyt":[0,4],"datarespons":[0,2,4,7],"dataresponsefactori":[0,2,4],"dataresponsemessag":2,"date":2,"datetim":2,"de":[2,6],"dead":5,"debug":[2,7],"declar":[2,4,5],"decor":2,"decreas":2,"decrease_current_load":2,"dedic":[2,4],"default":2,"default_content_typ":4,"defaultdict":2,"defin":[2,4],"delet":2,"deliv":2,"deliveri":2,"delivery_mod":2,"delivery_tag":7,"deliverymod":2,"depend":[2,6],"deseri":[2,4,7],"deserialize_object":[0,2],"destin":5,"detail":[2,4,7],"determin":[2,5,7],"dict":[2,4,5,7],"dictionari":[2,7],"differ":2,"directli":2,"directori":2,"discoveri":[0,4,6],"dispatch":[0,3],"dlq":3,"dlq_arg":5,"dlx":5,"do":[2,6],"doe":[2,5,7],"doesn":2,"domain":[2,4,5,6],"done":2,"download":[2,4],"download_buff":2,"download_fil":2,"downloaded_fil":2,"duplic":2,"dure":7,"dynam":7,"e":[2,4,5,7],"each":[2,7],"earlier":7,"either":6,"els":6,"empti":2,"en":[2,4],"enabl":6,"encapsul":2,"encod":[2,4,7],"endpoint":[2,5],"ensur":[2,7],"ensure_directory_exist":2,"ensure_trace_info":7,"entir":2,"entri":[2,6],"enum":[2,4],"environ":2,"epoch":4,"error":[2,4,5,7],"error_caus":[2,4],"error_messag":2,"etc":[2,4],"even":7,"event":[2,5,7],"exampl":[2,7],"except":[5,7],"exception_typ":5,"exchang":[2,4,5,6,7],"exchange_nam":5,"exclud":6,"exe":7,"execut":[2,7],"exist":[2,7],"expand":4,"export":7,"export_interval_milli":7,"express":[2,4,5],"extend":2,"extra_data":4,"extra_init_data":2,"extract":2,"face":6,"factori":[2,4],"fail":2,"fall":2,"fallback":2,"fals":[2,6,7],"field":2,"file":[2,4,7],"file_handl":[2,7],"file_info":2,"file_path":2,"filehandl":[0,2,7],"filepath":2,"find_rout":[5,6],"find_route_by_messag":6,"find_route_by_servic":6,"first":2,"flag":2,"follow":2,"fom":2,"foo":2,"form":2,"format":[2,4],"format_message_typ":2,"forward":[2,6],"fq_method_nam":[2,7],"from":[2,4,5,6,7],"from_byt":2,"from_hex":4,"from_json":4,"from_str":2,"from_stream":2,"fulli":[2,7],"function":[2,7],"function_nam":7,"futur":[6,7],"g":[2,4,5,7],"gatewai":2,"gener":[2,3],"generate_fallback_id":[0,2],"generate_snowflake_id":2,"get":[2,3,4],"get_bit":4,"get_byt":4,"get_config_valu":[0,2],"get_consume_queue_nam":6,"get_content_typ":2,"get_context_info":[0,2],"get_current_us":2,"get_default_trace_st":[0,7],"get_endpoint":2,"get_inst":2,"get_reply_to_queue_nam":6,"get_response_queue_nam":6,"get_rout":6,"get_routing_kei":6,"get_timestamp_from_id":2,"get_unique_instance_id":[0,2,3,7],"given":[2,5,6,7],"global":[2,7],"goe":6,"greater":7,"group":[2,5],"group_id":5,"group_routes_by_method":2,"group_servic":5,"ha":[2,7],"handl":[2,6,7],"handle_backpressure_overload_ev":2,"handle_body_payload":2,"handle_no_body_payload":2,"handle_routing_data_messag":6,"handle_routing_data_request_messag":6,"handler":[2,6,7],"hash":2,"have":[2,7],"head":2,"header":[2,4,7],"helper":2,"here":[2,6],"hi_bits_or_byt":4,"hide":2,"high":2,"highest":2,"holder":3,"host":[2,3],"hostnam":2,"how":[4,7],"http":[2,4,7],"http_request":4,"human":2,"i":[2,4,5,6,7],"id":[2,3,4,5,7],"identifi":[4,7],"idl":[2,4,7],"ignor":4,"immedi":7,"implement":[0,2],"import":[2,7],"inbound":5,"inbound_data_message_callback_as_thread":7,"inbound_data_message_callback_no_thread":7,"includ":[2,4],"incom":[2,5],"increas":2,"increase_current_load":2,"increment":2,"index":0,"indic":[2,7],"individu":7,"info":2,"inform":[2,4,5,7],"init":[5,7],"init_internal_routing_path":6,"init_routing_paths_consum":6,"initi":[2,4,5,6,7],"initialize_telemetri":[0,7],"input":2,"input_byt":2,"input_str":[2,6],"insecur":7,"instanc":[2,3,5,6,7],"instanti":[2,4],"int":[2,3,4,7],"integ":2,"integr":2,"intend":[0,2],"interfac":2,"intern":[4,6],"invoc":7,"invok":[2,6,7],"invoke_command":[0,7],"ioexcept":6,"is_json":2,"is_likely_json":[0,2],"is_open":6,"is_valid":4,"item":4,"iter":7,"its":[2,6],"jaeger":[2,4,7],"java":[2,4],"json":[2,4],"json_data":2,"json_input":2,"json_payload":2,"json_str":4,"jwt":5,"kei":[2,4,5,7],"key1":2,"key2":2,"keyword":7,"kv":7,"kwarg":7,"larg":2,"last":2,"last_backpressure_ev":2,"last_backpressure_event_tim":2,"later":[2,5],"layer":2,"less":4,"letter":5,"level":[2,3],"librari":0,"like":[2,5],"limit":7,"line":2,"list":[2,4,5,6],"list_users_in_group":5,"listen":[5,6],"lo_bit":4,"load":2,"local":[2,6],"localhost":7,"log":[2,6],"logging_debug":[0,2],"logging_error":[0,2],"logging_info":[0,2],"logging_util":2,"logging_warn":[0,2],"logic":[6,7],"long":[2,4],"long_to_byt":[0,2],"longer":2,"loop":[2,5,7],"loos":2,"low":2,"lt":7,"m_field":4,"machine_id":2,"machine_id_bit":2,"machine_id_mask":2,"made":4,"magic":4,"mai":[2,4,7],"mainli":2,"make":[2,4],"manag":[5,7],"map":[2,3,5,6],"map_":2,"map_as_str":[0,2],"map_with_list_as_str":[0,2],"mapper":7,"master":6,"match":[2,4,6,7],"max":7,"max_avail":[2,4],"max_chunk_s":2,"max_retri":2,"max_wait":7,"maximum":[2,7],"mean":2,"meaning":5,"merg":2,"merge_extra_init_data":[0,2],"messag":[2,4,5,6,7],"message_data":2,"message_factori":7,"message_head":2,"message_id":[2,7],"message_typ":[2,4],"met":7,"metadata":[2,5],"meterprovid":7,"method":[2,4,5,7],"method_nam":2,"metric":[2,7],"metrics_config":7,"metricsconfig":[0,7],"micro":4,"might":2,"milli":4,"millisecond":4,"mime":2,"miss":2,"mock":2,"mode":[2,6,7],"model":[0,2],"modifi":2,"modul":0,"modulenotfounderror":7,"monitor":4,"msg":2,"msg_id":7,"multipart":[2,7],"multipartdatamessag":[0,4],"multipartformdatapars":[0,2],"multipl":[2,7],"multivalu":2,"name":[2,3,4,5,6,7],"need":6,"neither":7,"nest":2,"new":7,"next":2,"non":2,"none":[2,3,4,5,6,7],"nor":7,"note":[2,4],"noth":5,"notif":4,"notifi":6,"number":2,"obj":2,"object":[2,3,4,5,6,7],"object_or_list_as_str":[0,2],"occur":2,"octet":[2,4],"of_error_messag":2,"ok":2,"on_field":2,"on_fil":2,"on_messag":[2,7],"on_rpc":7,"one":[4,7],"onli":2,"open":[2,6],"opentelemetri":7,"oper":[2,5,6],"option":[2,4,5,7],"ordin":2,"org":[2,4],"origin":[2,7],"original_rout":2,"other":[6,7],"otherwis":6,"otlp":7,"otlp_endpoint":7,"otlpspanexport":7,"out":[6,7],"output":7,"output_dir":2,"outward":6,"over":[0,2,5,7],"overload":[2,4,7],"overrid":2,"overriden":2,"own":6,"owner":4,"p_field":4,"packag":[0,2,7],"package_nam":7,"page":0,"pair":2,"parallel":[2,7],"param":[2,5,6,7],"paramet":[2,3,4,5,6,7],"pars":2,"parse_get_url":2,"parse_map_list_str":[0,2],"parse_map_str":[0,2],"parse_request_bodi":2,"parse_url_queri":[0,2],"parser":3,"part":2,"pass":[2,7],"patch":2,"path":[2,4,5,6],"pattern":6,"payload":[2,7],"payload_typ":4,"per":7,"permiss":5,"permission_servic":5,"persist":2,"point":[2,4],"port":3,"possibli":2,"post":2,"prefer":2,"prefetch_count":5,"prefix":3,"presenc":6,"preserv":2,"prevent":2,"primari":4,"primary_kei":4,"print":2,"proactiv":6,"procedur":7,"process":[2,6,7],"process_form_data":2,"produc":6,"prometheu":7,"prometheus_host":7,"prometheus_port":7,"prompt":6,"propag":7,"properti":4,"protocol":2,"provid":[0,2,7],"publish":[2,6],"publish_backpressure_request":2,"publish_fil":2,"publish_service_messag":6,"pull":2,"push":[2,6],"put":2,"pydant":2,"pydantic_seri":2,"python":[2,7],"python_type_to_json":[0,2],"qualifi":[2,7],"queri":[2,5],"query_params_dict":2,"query_user_by_id":5,"queue":[2,4,5,6,7],"queue_nam":[2,7],"quick":2,"r":[0,4],"r2rserializ":2,"rabbit_mq_cli":[5,7],"rabbitmq":[0,2,3,4,6,7],"rabbitmq_url":2,"rabbitmqcli":[0,5,7],"rais":[2,5,7],"random":2,"randomli":2,"raw":[2,7],"read":[2,3],"read_long":[0,2],"readabl":2,"readi":7,"reason":2,"receipt":6,"receiv":[2,6,7],"recipi":2,"recipient_nam":[2,4],"reconstitut":7,"reconstitute_data_messag":7,"record_dur":7,"refer":[6,7],"regex":[2,5],"regist":[5,6],"register_data_reply_callback":5,"register_inbound_rout":5,"register_rout":[6,7],"registr":4,"reinitialize_if_disconnect":7,"relat":4,"relev":2,"remot":7,"remov":6,"remove_outstanding_futur":7,"remove_rout":6,"remove_route_on_cleanup":6,"removerout":6,"repli":[4,6,7],"reply_received_callback":7,"reply_to":[4,7],"reply_to_exchang":7,"repres":[2,4],"represent":2,"request":[2,4,6,7],"request_rout":5,"request_routes_from_adapt":6,"request_typ":4,"requesttyp":2,"requir":[4,6],"resolv":7,"resourc":7,"respond":6,"respons":[2,4,5,7],"response_cod":[2,4],"response_queue_nam":5,"rest":[2,4],"result":6,"retriev":2,"retry_delay_second":2,"return":[2,3,4,5,6,7],"return_address":4,"reusabl":2,"rmq":5,"robustchannel":2,"rout":[0,2,3,4,5,6,7],"route_databas":6,"route_map":5,"route_str":[2,6],"routedatabas":[0,6],"router":[0,5],"router_bas":6,"router_consum":6,"router_produc":6,"routerbas":[0,6],"routerconsum":[0,6],"routerproduc":[0,6],"routin":5,"routing_data":4,"routing_data_remov":4,"routing_data_replac":4,"routing_data_request":4,"routing_kei":[2,4,6],"routing_key_from_messag":6,"routing_key_from_rout":6,"rpc":[2,4,7],"rpc_callback":5,"rpc_request":4,"run":7,"run_http_request_mag":7,"run_rpc_request":7,"safe":2,"safe_rsplit":2,"same":2,"sanitize_as_rabbitmq_domain_nam":[0,6],"sanitize_as_rabbitmq_nam":[0,6],"save":2,"scale_down":4,"scale_up":4,"scalerequestv1":[0,2],"scaling_request":2,"scalingrequest":[0,4],"scalingrequestalert":[0,2,4],"search":0,"section":5,"see":[2,4],"seen":7,"self":[2,6],"send":[2,5,6,7],"send_command":7,"send_messag":5,"send_repli":7,"send_rpc":7,"sent":[2,5,6,7],"separ":[2,5,7],"sequence_bit":2,"sequence_mask":2,"serial":[2,4,7],"serializ":2,"serialize_object":[0,2],"servic":[0,2,3,4,5,6],"service_adapt":7,"service_id":4,"service_message_factori":[2,7],"service_message_typ":4,"service_nam":[6,7],"service_vers":7,"serviceclass":2,"serviceid":2,"servicemessag":[0,2,4,6],"servicemessagefactori":[0,2,7],"servicemessagetyp":[0,2,4],"session":2,"set":[2,5,6,7],"set_outstanding_futur":7,"set_route_added_notifi":6,"set_text":[0,7],"setup_amq_channel":5,"setup_metrics_provid":[0,7],"setup_otlp_export":[0,7],"setup_trace_provid":[0,7],"sever":6,"share":[2,7],"should":[2,4,6],"side":[6,7],"signitur":2,"singl":2,"sit":6,"size":2,"sleep_tim":6,"snowflake_epoch":2,"snowflake_id":[2,4],"snowflake_id_bit":2,"snowflake_id_width":4,"snowflakeid":[0,2,4],"so":[2,6],"someth":6,"sourc":[2,3,4,5,6,7],"span":[4,7],"specif":2,"specifi":[2,7],"split":2,"stack":2,"standard":2,"start":7,"start_backpressure_monitor":2,"startup":6,"state":4,"static":[2,4],"statu":[2,4],"store":[2,7],"str":[2,3,4,5,6,7],"str_to_bool":[0,2],"stream":[2,4,7],"streamingfilehandl":[0,2,7],"string":[2,4,5,7],"structur":[2,4],"style":4,"subclass":2,"submit":5,"subtract":2,"success":6,"success_cod":2,"suce":2,"suppli":[2,6],"support":[2,4,5],"swarm_id":2,"synchron":7,"system":2,"t":2,"take":2,"target":6,"task_id":4,"taskid":2,"telemetri":2,"tenant":5,"tenant_servic":5,"test":[2,6],"testabl":2,"text":2,"than":7,"thei":7,"them":[2,5,7],"therefor":6,"thi":[0,2,3,4,5,7],"thread":[2,7],"threshold":[3,7],"throw":6,"thu":[2,5],"tighli":2,"tight":2,"time":[2,3,7],"timeout":4,"timestamp":[2,4],"timestamp_bit":2,"tl":3,"to_json":[2,4],"to_str":2,"todo":2,"togeth":5,"token":[2,5,6],"token_servic":5,"too":2,"top":[2,3],"topic":4,"trace":[2,4,7],"trace_info":[2,4],"trace_info_adapt":2,"traceinfoadapt":[0,2],"tracer":7,"tracerprovid":7,"tracing_config":7,"tracingconfig":[0,7],"traffic":0,"transfer":4,"transient":2,"transmit":2,"trigger":7,"true":[3,6,7],"try":2,"try_deserialize_object":[0,2],"tupl":2,"two":2,"type":[2,3,4,5,6,7],"type_descriptor":2,"type_str":2,"typedescriptor":[0,2],"typeerror":2,"typic":[2,7],"unavail":2,"unchang":2,"undefin":7,"unidirect":7,"uniqu":[2,3,4,6,7],"unittest":2,"unknown":4,"unregist":6,"until":[2,4,7],"unwrap_route_list":6,"up":[2,5,7],"updat":[2,4,7],"update_backpressure_valu":2,"update_current_avail":2,"update_last_backpressure_event_tim":2,"upload":2,"upon":6,"url":2,"urlpath":2,"us":[2,3,4,5,6,7],"usabl":6,"user":[2,4,5],"user_id":5,"user_management_service_cli":5,"user_servic":5,"usermanagementservicecli":[0,5],"usermanagementserviceexcept":[0,5],"userschem":2,"userschema":2,"util":6,"uuid":2,"v0":4,"v1":[2,5],"valid":[2,4,5],"valid_until":4,"validate_token":5,"valu":[2,3,4,6,7],"value1":2,"value2":2,"variabl":[2,5],"version":[2,4,7],"via":[2,6,7],"void":5,"wa":[2,5],"wait":[2,4,6],"warn":2,"we":7,"welcom":0,"when":[2,6,7],"where":[2,6],"which":[2,4,6],"while":2,"whose":2,"wide":[2,7],"wiki":[2,4],"wikipedia":[2,4],"window":3,"within":7,"wrap_own_rout":6,"wrap_rout":6,"wrapper":[2,7],"wrong":6,"x":[2,5],"yield":7},"titles":["AMQ Adapter Python API Documentation","AMQP Package","Adapter Module","Configuration Module","Model Module","RabbitMQ Module","Router Module","Service Module"],"titleterms":{"adapt":[0,2],"amq":0,"amqp":1,"api":0,"configur":3,"content":0,"document":0,"indic":0,"model":4,"modul":[2,3,4,5,6,7],"packag":1,"python":0,"rabbitmq":5,"router":6,"servic":7,"tabl":0}}) diff --git a/api-doc/conf.py b/api-doc/conf.py new file mode 100644 index 0000000..b85bd03 --- /dev/null +++ b/api-doc/conf.py @@ -0,0 +1,82 @@ +import os +import sys + +# Configuration file for the Sphinx documentation builder. +# +# For the full list of built-in configuration values, see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Project information ----------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information + +project = "AMQ Adapter Python" +copyright = "2024, CleverThis" +author = "CleverThis" +release = "1.0" + +# -- General configuration --------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration + +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.viewcode", + "sphinx.ext.napoleon", + "sphinx.ext.intersphinx", + "sphinx.ext.todo", +] + +templates_path = ["_templates"] +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] + +# -- Options for HTML output ------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output + +html_theme = "sphinx_rtd_theme" +html_static_path = ["_static"] + +# -- Extension configuration ------------------------------------------------- + +# Add any paths that contain templates here, relative to this directory. +templates_path = ["_templates"] + +# The suffix of source filenames. +source_suffix = ".rst" + +# The master toctree document. +master_doc = "index" + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +language = "en" + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = ["_build"] + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = "sphinx" + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = True + +# -- Autodoc configuration -------------------------------------------------- + +# Automatically extract typehints when specified and place them in +# descriptions of the relevant function/method. +autodoc_typehints = "description" + +# Don't show type hints in the signature +autodoc_typehints_format = "short" + +# Include both class docstring and __init__ docstring +autodoc_default_options = { + "members": True, + "member-order": "bysource", + "special-members": "__init__", + "undoc-members": True, + "exclude-members": "__weakref__", +} + +# Add the parent directory to the Python path so we can import amqp + +sys.path.insert(0, os.path.abspath("..")) diff --git a/api-doc/index.html b/api-doc/index.html new file mode 100644 index 0000000..7737d43 --- /dev/null +++ b/api-doc/index.html @@ -0,0 +1,77 @@ + + + + + + AMQ Adapter Python API Documentation + + + +
+

AMQ Adapter Python API Documentation

+

Welcome to the API documentation for the AMQ Adapter Python library.

+ +
+

Quick Navigation

+

Click the button below to view the complete API documentation:

+ View API Documentation +
+ +
+

Documentation Features

+
    +
  • Search - Find classes, methods, and functions quickly
  • +
  • Module Navigation - Browse by package structure
  • +
  • Class Inheritance - View class hierarchies
  • +
  • Source Code - See the actual Python implementation
  • +
  • Cross-references - Navigate between related components
  • +
+
+ +
+

Key Modules

+
    +
  • amqp.config - Configuration management
  • +
  • amqp.adapter - Core adapter functionality
  • +
  • amqp.service - Service layer and message handling
  • +
  • amqp.router - Routing and service discovery
  • +
  • amqp.model - Data models and message structures
  • +
  • amqp.rabbitmq - RabbitMQ client implementations
  • +
+
+ +

Generated using Sphinx with Read the Docs theme

+
+ + diff --git a/api-doc/index.rst b/api-doc/index.rst new file mode 100644 index 0000000..4fc59f2 --- /dev/null +++ b/api-doc/index.rst @@ -0,0 +1,23 @@ +AMQ Adapter Python API Documentation +==================================== + +Welcome to the AMQ Adapter Python API documentation. This library provides a Python implementation of the AMQ adapter for routing traffic over RabbitMQ to intended services with automated service discovery. + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + modules/amqp + modules/amqp.config + modules/amqp.adapter + modules/amqp.service + modules/amqp.router + modules/amqp.model + modules/amqp.rabbitmq + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/api-doc/make.bat b/api-doc/make.bat new file mode 100644 index 0000000..954237b --- /dev/null +++ b/api-doc/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.https://www.sphinx-doc.org/ + exit /b 1 +) + +if "%1" == "" goto help + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/api-doc/modules/amqp.adapter.rst b/api-doc/modules/amqp.adapter.rst new file mode 100644 index 0000000..d7802b5 --- /dev/null +++ b/api-doc/modules/amqp.adapter.rst @@ -0,0 +1,77 @@ +Adapter Module +============= + +.. automodule:: amqp.adapter + :members: + :undoc-members: + :show-inheritance: + +.. automodule:: amqp.adapter.cleverthis_service_adapter + :members: + :undoc-members: + :show-inheritance: + +.. automodule:: amqp.adapter.amq_route_factory + :members: + :undoc-members: + :show-inheritance: + +.. automodule:: amqp.adapter.backpressure_handler + :members: + :undoc-members: + :show-inheritance: + +.. automodule:: amqp.adapter.consul_global_id_generator + :members: + :undoc-members: + :show-inheritance: + +.. automodule:: amqp.adapter.data_message_factory + :members: + :undoc-members: + :show-inheritance: + +.. automodule:: amqp.adapter.data_parser + :members: + :undoc-members: + :show-inheritance: + +.. automodule:: amqp.adapter.data_response_factory + :members: + :undoc-members: + :show-inheritance: + +.. automodule:: amqp.adapter.file_handler + :members: + :undoc-members: + :show-inheritance: + +.. automodule:: amqp.adapter.logging_utils + :members: + :undoc-members: + :show-inheritance: + +.. automodule:: amqp.adapter.pydantic_serializer + :members: + :undoc-members: + :show-inheritance: + +.. automodule:: amqp.adapter.serializer + :members: + :undoc-members: + :show-inheritance: + +.. automodule:: amqp.adapter.service_message_factory + :members: + :undoc-members: + :show-inheritance: + +.. automodule:: amqp.adapter.trace_info_adapter + :members: + :undoc-members: + :show-inheritance: + +.. automodule:: amqp.adapter.type_descriptor + :members: + :undoc-members: + :show-inheritance: diff --git a/api-doc/modules/amqp.config.rst b/api-doc/modules/amqp.config.rst new file mode 100644 index 0000000..986f01b --- /dev/null +++ b/api-doc/modules/amqp.config.rst @@ -0,0 +1,12 @@ +Configuration Module +=================== + +.. automodule:: amqp.config + :members: + :undoc-members: + :show-inheritance: + +.. automodule:: amqp.config.amq_configuration + :members: + :undoc-members: + :show-inheritance: diff --git a/api-doc/modules/amqp.model.rst b/api-doc/modules/amqp.model.rst new file mode 100644 index 0000000..93a83a0 --- /dev/null +++ b/api-doc/modules/amqp.model.rst @@ -0,0 +1,22 @@ +Model Module +=========== + +.. automodule:: amqp.model + :members: + :undoc-members: + :show-inheritance: + +.. automodule:: amqp.model.model + :members: + :undoc-members: + :show-inheritance: + +.. automodule:: amqp.model.service_message_type + :members: + :undoc-members: + :show-inheritance: + +.. automodule:: amqp.model.snowflake_id + :members: + :undoc-members: + :show-inheritance: diff --git a/api-doc/modules/amqp.rabbitmq.rst b/api-doc/modules/amqp.rabbitmq.rst new file mode 100644 index 0000000..a7acf76 --- /dev/null +++ b/api-doc/modules/amqp.rabbitmq.rst @@ -0,0 +1,17 @@ +RabbitMQ Module +============== + +.. automodule:: amqp.rabbitmq + :members: + :undoc-members: + :show-inheritance: + +.. automodule:: amqp.rabbitmq.rabbit_mq_client + :members: + :undoc-members: + :show-inheritance: + +.. automodule:: amqp.rabbitmq.user_management_service_client + :members: + :undoc-members: + :show-inheritance: diff --git a/api-doc/modules/amqp.router.rst b/api-doc/modules/amqp.router.rst new file mode 100644 index 0000000..d107b4a --- /dev/null +++ b/api-doc/modules/amqp.router.rst @@ -0,0 +1,32 @@ +Router Module +============ + +.. automodule:: amqp.router + :members: + :undoc-members: + :show-inheritance: + +.. automodule:: amqp.router.router_base + :members: + :undoc-members: + :show-inheritance: + +.. automodule:: amqp.router.router_consumer + :members: + :undoc-members: + :show-inheritance: + +.. automodule:: amqp.router.router_producer + :members: + :undoc-members: + :show-inheritance: + +.. automodule:: amqp.router.route_database + :members: + :undoc-members: + :show-inheritance: + +.. automodule:: amqp.router.utils + :members: + :undoc-members: + :show-inheritance: diff --git a/api-doc/modules/amqp.rst b/api-doc/modules/amqp.rst new file mode 100644 index 0000000..df1b2b0 --- /dev/null +++ b/api-doc/modules/amqp.rst @@ -0,0 +1,7 @@ +AMQP Package +=========== + +.. automodule:: amqp + :members: + :undoc-members: + :show-inheritance: diff --git a/api-doc/modules/amqp.service.rst b/api-doc/modules/amqp.service.rst new file mode 100644 index 0000000..a81f753 --- /dev/null +++ b/api-doc/modules/amqp.service.rst @@ -0,0 +1,22 @@ +Service Module +============= + +.. automodule:: amqp.service + :members: + :undoc-members: + :show-inheritance: + +.. automodule:: amqp.service.amq_service + :members: + :undoc-members: + :show-inheritance: + +.. automodule:: amqp.service.amq_message_handler + :members: + :undoc-members: + :show-inheritance: + +.. automodule:: amqp.service.tracing + :members: + :undoc-members: + :show-inheritance: diff --git a/pydoc-markdown.yml b/pydoc-markdown.yml new file mode 100644 index 0000000..3ff6ffc --- /dev/null +++ b/pydoc-markdown.yml @@ -0,0 +1,32 @@ +pydoc_markdown: + modules: + - amqp + renderer: + type: html + output_directory: api-doc + template: default + mkdocs_config: + site_name: AMQ Adapter Python API Documentation + site_description: API documentation for the AMQ Adapter Python library + theme: + name: material + features: + - navigation.tabs + - navigation.sections + - navigation.expand + - search.highlight + - search.share + nav: + - Home: index.md + - Configuration: amqp.config.md + - Adapter: amqp.adapter.md + - Service: amqp.service.md + - Router: amqp.router.md + - Model: amqp.model.md + - RabbitMQ: amqp.rabbitmq.md + processors: + - type: filter + documented_only: true + - type: smart + docstring_style: google + docstring_parser: google