{%- import "util.include.jinja" as util -%}
{%- set messagegroups = root.messagegroups %}
{%- filter wordwrap(120) %}
# {{ project_name | capitalize }} - AMQP 1.0 Producer

Auto-generated Python producer for sending messages via AMQP 1.0 protocol.

## Overview

This module provides a type-safe AMQP 1.0 producer for sending events with optional CloudEvents envelope format. Built on the `python-qpid-proton` library for Python.

## What is AMQP 1.0?

**AMQP (Advanced Message Queuing Protocol) 1.0** is an open standard for business messaging that supports:
- **Protocol-level interoperability** between different platforms and vendors
- **Reliable message delivery** with settlement modes and flow control
- **Multiple messaging patterns** including queues, topics, and request-reply
- **Built-in security** with SASL authentication and TLS encryption

Use cases: Enterprise integration, IoT messaging, financial services, cloud-native applications.

**Supported AMQP 1.0 Brokers:**
- Apache ActiveMQ Artemis (native AMQP 1.0)
- Apache Qpid (native AMQP 1.0)
- Azure Service Bus (native AMQP 1.0)
- RabbitMQ (with AMQP 1.0 plugin - [setup guide](https://github.com/clemensv/xregistry-cli/blob/main/docs/rabbitmq_amqp_setup.md))

**Note for RabbitMQ users:** RabbitMQ requires the AMQP 1.0 plugin to be enabled. See the [RabbitMQ AMQP 1.0 Setup Guide](https://github.com/clemensv/xregistry-cli/blob/main/docs/rabbitmq_amqp_setup.md) for detailed instructions.

## Installation

```bash
pip install python-qpid-proton cloudevents
```

## Generated Producers

{% for messagegroupid, messagegroup in messagegroups.items() %}
{%- set uses_cloudevents_message = (messagegroup | exists("envelope","CloudEvents/1.0")) %}
{%- set uses_plain_amqp_message = not uses_cloudevents_message %}

### {{ (messagegroupid | pascal | strip_dots) + "Producer" }}

`{{ (messagegroupid | pascal | strip_dots) + "Producer" }}` sends messages for the {{ messagegroupid }} message group.

#### Quick Start

```python
from {{ project_name | snake }} import {{ (messagegroupid | pascal | strip_dots) + "Producer" }}

# Create producer
producer = {{ (messagegroupid | pascal | strip_dots) + "Producer" }}(
    host="localhost",
    address="my-queue",
    port=5672,
    username="guest",
    password="guest"
)

# Send a message
{% for messageid, message in messagegroup.messages.items() %}
{%- set messagename = messageid | pascal | strip_namespace %}
{%- set isCloudEvent = (message | exists("envelope","CloudEvents/1.0")) %}
{%- set type_name = util.DeclareDataType( data_project_name, root, message ) | strip_namespace %}
producer.send_{{ messagename | snake }}(
    data={{ type_name }}(...),
    {%- if isCloudEvent and message.metadata %}
    {%- for attrname, attribute in message.metadata.items() if attribute.value is not defined and attrname not in ["time", "id", "datacontenttype", "dataschema"] and attribute.required %}
    _{{ attrname }}="value",
    {%- endfor %}
    {%- endif %}
    content_type="application/json"
)
{% endfor %}

# Close producer
producer.close()
```

#### Configuration Options

The producer constructor accepts:

- `host` (str): AMQP broker hostname
- `address` (str): AMQP address (queue or topic name)
- `port` (int): AMQP broker port (default: 5672, for TLS typically 5671)
- `username` (Optional[str]): Username for SASL authentication
- `password` (Optional[str]): Password for SASL authentication
{%- if uses_cloudevents_message %}
- `content_mode` (Literal['structured', 'binary']): CloudEvents encoding mode (default: 'structured')
  - **structured**: Entire CloudEvent as JSON in message body
  - **binary**: Event data in body, CloudEvents attributes in AMQP application properties
- `format_type` (str): Content type for structured mode (default: 'application/json')
{%- endif %}

#### Available Methods

{% for messageid, message in messagegroup.messages.items() -%}
{%- set messagename = messageid | pascal | strip_namespace %}
{%- set type_name = util.DeclareDataType( data_project_name, root, message ) | strip_namespace %}
{%- set isCloudEvent = (message | exists("envelope","CloudEvents/1.0")) %}

##### `send_{{ messagename | snake }}()`

{%- if message.description %}
{{ message.description }}
{%- endif %}

**Parameters:**
- `data` ({{ type_name }}): The message data object
{%- if isCloudEvent %}
{%- for attrname, attribute in message.envelopemetadata.items() if attribute.type == "uritemplate" %}
{%- for placeholder in attribute.value | regex_search('\\{([A-Za-z0-9_]+)\\}') %}
- `_{{ placeholder | snake }}` (str): Value for placeholder {{ placeholder }} in attribute {{ attrname }}
{%- endfor %}
{%- endfor %}
{%- for attrname, attribute in message.envelopemetadata.items() if attribute.value is not defined and attrname not in ["time", "id", "datacontenttype", "dataschema"] %}
- `_{{ attrname }}` ({% if not attribute.required %}Optional[str]{% else %}str{% endif %}): CloudEvent {{ attrname }} attribute
{%- endfor %}
{%- endif %}
- `content_type` (str): Content type of the message data (default: 'application/json')

##### `send_{{ messagename | snake }}_batch()`

Send multiple {{ messagename }} messages in sequence.

**Parameters:**
- `data_array` (List[{{ type_name }}]): Array of message data objects
{%- if isCloudEvent %}
{%- for attrname, attribute in message.envelopemetadata.items() if attribute.type == "uritemplate" %}
{%- for placeholder in attribute.value | regex_search('\\{([A-Za-z0-9_]+)\\}') %}
- `_{{ placeholder | snake }}` (str): Value for placeholder {{ placeholder }} in attribute {{ attrname }}
{%- endfor %}
{%- endfor %}
{%- for attrname, attribute in message.envelopemetadata.items() if attribute.value is not defined and attrname not in ["time", "id", "datacontenttype", "dataschema"] %}
- `_{{ attrname }}` ({% if not attribute.required %}Optional[str]{% else %}str{% endif %}): CloudEvent {{ attrname }} attribute
{%- endfor %}
{%- endif %}
- `content_type` (str): Content type of the message data

{% endfor %}
{% endfor %}

## AMQP Broker Compatibility

This library has been tested with:
- **Azure Service Bus** (AMQP 1.0 over TLS on port 5671)
- **Apache ActiveMQ Artemis** (AMQP 1.0 on port 5672)
- **Apache Qpid Broker-J** (AMQP 1.0 on port 5672)

## Security Considerations

- Always use TLS/SSL encryption in production (port 5671)
- Store credentials securely (environment variables, key vaults)
- Use SASL authentication mechanisms appropriate for your broker
- Consider using token-based authentication (OAuth, JWT) where supported

## Advanced Usage

### Batch Sending with Concurrency Control

Send multiple messages efficiently with rate limiting:

```python
import asyncio
from typing import List
{% for messagegroupid, messagegroup in messagegroups.items() %}
{%- set producer_class = (messagegroupid | pascal | strip_dots) + "Producer" %}
{%- set first_message = messagegroup.messages.items() | list | first %}
{%- set type_name = util.DeclareDataType( data_project_name, root, first_message[1] ) | strip_namespace %}

class BatchProducer:
    def __init__(self, producer: {{ producer_class }}, max_concurrency: int = 10):
        self.producer = producer
        self.semaphore = asyncio.Semaphore(max_concurrency)
    
    async def send_batch_async(self, data_array: List[{{ type_name }}]):
        """Send multiple messages with concurrency control."""
        async def send_one(data):
            async with self.semaphore:
                await asyncio.to_thread(
                    self.producer.send_{{ first_message[0] | pascal | strip_namespace | snake }},
                    data
                )
        
        tasks = [send_one(data) for data in data_array]
        await asyncio.gather(*tasks, return_exceptions=True)

# Usage
producer = {{ producer_class }}(host="localhost", address="my-queue")
batch_producer = BatchProducer(producer, max_concurrency=20)

data_list = [{{ type_name }}(...) for _ in range(100)]
asyncio.run(batch_producer.send_batch_async(data_list))
```
{% endfor %}

### Connection Pooling

Reuse producer connections across your application:

```python
from threading import Lock
{% for messagegroupid, messagegroup in messagegroups.items() %}
{%- set producer_class = (messagegroupid | pascal | strip_dots) + "Producer" %}

class ProducerPool:
    _instance = None
    _lock = Lock()
    
    def __new__(cls):
        if cls._instance is None:
            with cls._lock:
                if cls._instance is None:
                    cls._instance = super().__new__(cls)
                    cls._instance.producer = {{ producer_class }}(
                        host="localhost",
                        address="my-queue",
                        username="guest",
                        password="guest"
                    )
        return cls._instance
    
    def get_producer(self) -> {{ producer_class }}:
        return self.producer
    
    def close(self):
        if self.producer:
            self.producer.close()

# Use from anywhere
pool = ProducerPool()
producer = pool.get_producer()
{% endfor %}
```

### Connection Resilience

Automatically reconnect on connection failures:

```python
import time
from typing import Optional
{% for messagegroupid, messagegroup in messagegroups.items() %}
{%- set producer_class = (messagegroupid | pascal | strip_dots) + "Producer" %}
{%- set first_message = messagegroup.messages.items() | list | first %}
{%- set type_name = util.DeclareDataType( data_project_name, root, first_message[1] ) | strip_namespace %}

class ResilientProducer:
    def __init__(self, host: str, address: str, **kwargs):
        self.host = host
        self.address = address
        self.kwargs = kwargs
        self.producer: Optional[{{ producer_class }}] = None
        self._connect()
    
    def _connect(self):
        """Establish connection to AMQP broker."""
        if self.producer:
            try:
                self.producer.close()
            except:
                pass
        
        self.producer = {{ producer_class }}(
            host=self.host,
            address=self.address,
            **self.kwargs
        )
    
    def send_with_retry(self, data: {{ type_name }}, max_retries: int = 3):
        """Send message with automatic retry on connection failure."""
        for attempt in range(max_retries):
            try:
                self.producer.send_{{ first_message[0] | pascal | strip_namespace | snake }}(data)
                return
            except Exception as e:
                if attempt < max_retries - 1:
                    time.sleep(2 ** attempt)  # Exponential backoff
                    self._connect()  # Reconnect
                else:
                    raise Exception(f"Failed after {max_retries} attempts") from e
    
    def close(self):
        if self.producer:
            self.producer.close()
{% endfor %}
```

### Custom CloudEvents Attributes

Add extension attributes to CloudEvents:

```python
{% for messagegroupid, messagegroup in messagegroups.items() %}
{%- if (messagegroup | exists("envelope","CloudEvents/1.0")) %}
{%- set producer_class = (messagegroupid | pascal | strip_dots) + "Producer" %}
{%- set first_message = messagegroup.messages.items() | list | first %}
{%- set messagename = first_message[0] | pascal | strip_namespace %}

producer = {{ producer_class }}(host="localhost", address="my-queue")

# CloudEvents extension attributes can be added via metadata
producer.send_{{ messagename | snake }}(
    data=data,
    _tenant="contoso",           # Custom extension attribute
    _deviceid="device-001",      # Custom extension attribute
    _region="us-west-2",         # Custom extension attribute
    _priority="high",            # Custom extension attribute
    content_type="application/json"
)
{%- endif %}
{% endfor %}
```

## Error Handling

### Basic Error Handling

```python
from uamqp import errors

try:
    producer.send_message(data)
except errors.AMQPConnectionError as e:
    print(f"Connection error: {e}")
except errors.MessageException as e:
    print(f"Message error: {e}")
finally:
    producer.close()
```

### Retry with Exponential Backoff

```python
import time
from typing import Any
{% for messagegroupid, messagegroup in messagegroups.items() %}
{%- set producer_class = (messagegroupid | pascal | strip_dots) + "Producer" %}
{%- set first_message = messagegroup.messages.items() | list | first %}
{%- set messagename = first_message[0] | pascal | strip_namespace %}
{%- set type_name = util.DeclareDataType( data_project_name, root, first_message[1] ) | strip_namespace %}

def send_with_retry(
    producer: {{ producer_class }},
    data: {{ type_name }},
    max_retries: int = 5,
    initial_delay: float = 1.0,
    max_delay: float = 60.0
) -> None:
    """
    Send message with exponential backoff retry.
    
    Args:
        producer: The producer instance
        data: Message data to send
        max_retries: Maximum number of retry attempts
        initial_delay: Initial delay in seconds
        max_delay: Maximum delay between retries
    
    Raises:
        Exception: If all retries are exhausted
    """
    for attempt in range(max_retries):
        try:
            producer.send_{{ messagename | snake }}(data)
            return  # Success
        except errors.AMQPConnectionError as e:
            if attempt < max_retries - 1:
                delay = min(initial_delay * (2 ** attempt), max_delay)
                print(f"Attempt {attempt + 1}/{max_retries} failed. Retrying in {delay}s...")
                time.sleep(delay)
            else:
                raise Exception(f"Failed after {max_retries} attempts") from e
{% endfor %}
```

### Circuit Breaker Pattern

```python
from datetime import datetime, timedelta
{% for messagegroupid, messagegroup in messagegroups.items() %}
{%- set producer_class = (messagegroupid | pascal | strip_dots) + "Producer" %}
{%- set first_message = messagegroup.messages.items() | list | first %}
{%- set type_name = util.DeclareDataType( data_project_name, root, first_message[1] ) | strip_namespace %}

class CircuitBreakerProducer:
    """Producer with circuit breaker to prevent cascading failures."""
    
    def __init__(self, producer: {{ producer_class }}, threshold: int = 5, timeout: int = 60):
        self.producer = producer
        self.threshold = threshold
        self.timeout = timedelta(seconds=timeout)
        self.failure_count = 0
        self.last_failure_time = None
        self.is_open = False
    
    def send(self, data: {{ type_name }}):
        """Send message with circuit breaker protection."""
        # Check if circuit breaker is open
        if self.is_open:
            if datetime.now() - self.last_failure_time < self.timeout:
                raise Exception("Circuit breaker is open. Too many consecutive failures.")
            else:
                # Try to close the circuit
                self.is_open = False
                self.failure_count = 0
        
        try:
            self.producer.send_{{ first_message[0] | pascal | strip_namespace | snake }}(data)
            self.failure_count = 0  # Reset on success
        except Exception as e:
            self.failure_count += 1
            self.last_failure_time = datetime.now()
            
            if self.failure_count >= self.threshold:
                self.is_open = True
            raise
{% endfor %}
```

## Building and Testing

```bash
# Install dependencies
poetry install

# Run tests
poetry run pytest

# Build package
poetry build
```

## Dependencies

- `uamqp>=1.6.10` - AMQP 1.0 client library
- `cloudevents>=1.10.1` - CloudEvents SDK
- Python 3.10+

## License

This generated code is provided as-is for use in your projects.
{%- endfilter %}
