Flask-RMQ 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3 @@
1
+ from .app import create_app
2
+
3
+ __all__ = ['create_app']
@@ -0,0 +1,35 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+
5
+ from flask import Flask
6
+
7
+ from flask_rmq import FlaskRMQ
8
+
9
+ from .messaging import register_messaging
10
+ from .views import api
11
+
12
+ rmq = FlaskRMQ()
13
+
14
+
15
+ def create_app() -> Flask:
16
+ app = Flask(__name__)
17
+ app.config['RABBITMQ_CONNECTIONS'] = {
18
+ 'default': {
19
+ 'HOST': os.getenv('RABBITMQ_HOST', 'localhost'),
20
+ 'PORT': int(os.getenv('RABBITMQ_PORT', '5672')),
21
+ 'VIRTUAL_HOST': os.getenv('RABBITMQ_VHOST', '/'),
22
+ 'USER': os.getenv('RABBITMQ_USER', 'guest'),
23
+ 'PASSWORD': os.getenv('RABBITMQ_PASSWORD', 'guest'),
24
+ 'HEARTBEAT': 60,
25
+ 'BLOCKED_CONNECTION_TIMEOUT': 30,
26
+ 'RECONNECT_INITIAL_BACKOFF': 1,
27
+ 'RECONNECT_MAX_BACKOFF': 15,
28
+ }
29
+ }
30
+ app.config['CONSUMED_FILE'] = os.getenv('CONSUMED_FILE', 'consumed.jsonl')
31
+ rmq.init_app(app)
32
+ app.register_blueprint(api)
33
+ with app.app_context():
34
+ register_messaging()
35
+ return app
@@ -0,0 +1,48 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ from flask import current_app
6
+ from pika.adapters.blocking_connection import BlockingChannel
7
+
8
+ from flask_rmq import Consumer, Producer, QueueConfig, get_consumers_registry, get_setup_registry
9
+
10
+ EVENTS_QUEUE = QueueConfig(
11
+ name='flask_rmq.events',
12
+ durable=True,
13
+ dead_letter_exchange='flask_rmq.dlx',
14
+ dead_letter_routing_key='flask_rmq.events.failed',
15
+ )
16
+ producer = Producer(exchange='flask_rmq.events', queue=EVENTS_QUEUE)
17
+ consumer = Consumer(queue=EVENTS_QUEUE)
18
+
19
+
20
+ def setup_topology(channel: BlockingChannel) -> None:
21
+ channel.exchange_declare(exchange='flask_rmq.events', exchange_type='direct', durable=True)
22
+ channel.exchange_declare(exchange='flask_rmq.dlx', exchange_type='direct', durable=True)
23
+ channel.queue_declare(queue=EVENTS_QUEUE.name, durable=True, arguments=EVENTS_QUEUE.arguments)
24
+ channel.queue_bind(
25
+ exchange='flask_rmq.events',
26
+ queue=EVENTS_QUEUE.name,
27
+ routing_key=EVENTS_QUEUE.name,
28
+ )
29
+ channel.queue_declare(queue='flask_rmq.events.failed', durable=True)
30
+ channel.queue_bind(
31
+ exchange='flask_rmq.dlx',
32
+ queue='flask_rmq.events.failed',
33
+ routing_key='flask_rmq.events.failed',
34
+ )
35
+
36
+
37
+ @consumer
38
+ def save_event(channel, method, properties, body: bytes) -> None:
39
+ output = Path(current_app.config['CONSUMED_FILE'])
40
+ with output.open('ab') as stream:
41
+ stream.write(body + b'\n')
42
+ current_app.logger.info('Consumed: %s', body.decode('utf-8'))
43
+ channel.basic_ack(delivery_tag=method.delivery_tag)
44
+
45
+
46
+ def register_messaging() -> None:
47
+ get_setup_registry().register(setup_topology)
48
+ get_consumers_registry().register(consumer)
@@ -0,0 +1,28 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from datetime import UTC, datetime
5
+ from uuid import uuid4
6
+
7
+ from flask import Blueprint, jsonify, request
8
+
9
+ from .messaging import producer
10
+
11
+ api = Blueprint('api', __name__)
12
+
13
+
14
+ @api.get('/health')
15
+ def health():
16
+ return {'status': 'ok'}
17
+
18
+
19
+ @api.post('/events')
20
+ def publish_event():
21
+ incoming = request.get_json(silent=True) or {}
22
+ event = {
23
+ 'id': str(uuid4()),
24
+ 'created_at': datetime.now(UTC).isoformat(),
25
+ 'payload': incoming,
26
+ }
27
+ producer.publish(json.dumps(event))
28
+ return jsonify(event), 202
flask_rmq/__init__.py ADDED
@@ -0,0 +1,28 @@
1
+ from flask_rmq.consumer import Consumer, MessageCallback
2
+ from flask_rmq.exceptions import ConfigurationError, FlaskRMQError, NotInitializedError
3
+ from flask_rmq.extension import FlaskRMQ
4
+ from flask_rmq.producer import Producer
5
+ from flask_rmq.queues import QueueConfig
6
+ from flask_rmq.registries import (
7
+ ConsumersRegistry,
8
+ SetupRegistry,
9
+ get_consumers_registry,
10
+ get_setup_registry,
11
+ )
12
+
13
+ __all__ = [
14
+ 'ConfigurationError',
15
+ 'Consumer',
16
+ 'ConsumersRegistry',
17
+ 'FlaskRMQ',
18
+ 'FlaskRMQError',
19
+ 'MessageCallback',
20
+ 'NotInitializedError',
21
+ 'Producer',
22
+ 'QueueConfig',
23
+ 'SetupRegistry',
24
+ 'get_consumers_registry',
25
+ 'get_setup_registry',
26
+ ]
27
+
28
+ __version__ = '0.1.0'
flask_rmq/cli.py ADDED
@@ -0,0 +1,111 @@
1
+ from __future__ import annotations
2
+
3
+ import signal
4
+ import threading
5
+ from types import FrameType
6
+
7
+ import click
8
+ from flask import Flask, current_app
9
+ from flask.cli import with_appcontext
10
+
11
+ from flask_rmq.consumer import Consumer
12
+ from flask_rmq.state import get_state
13
+
14
+
15
+ def _aliases(using: str | None) -> list[str]:
16
+ state = get_state()
17
+ if using is not None:
18
+ if using not in state.connection_managers:
19
+ raise click.ClickException(f'Unknown RabbitMQ alias {using!r}')
20
+ return [using]
21
+ return list(state.connection_managers)
22
+
23
+
24
+ @click.group('rmq')
25
+ def rmq_cli() -> None:
26
+ """Set up RabbitMQ topology and run registered consumers."""
27
+
28
+
29
+ @rmq_cli.command('check')
30
+ @with_appcontext
31
+ def check_command() -> None:
32
+ """Validate configuration and show registered objects without connecting."""
33
+
34
+ state = get_state()
35
+ for alias in state.connection_managers:
36
+ setups = len(state.setup_registries[alias].all())
37
+ consumers = len(state.consumers_registries[alias].all())
38
+ click.echo(f'{alias}: configuration OK, {setups} setup callback(s), {consumers} consumer(s)')
39
+
40
+
41
+ @rmq_cli.command('setup')
42
+ @click.option('--using', help='Configure only this RABBITMQ_CONNECTIONS alias.')
43
+ @with_appcontext
44
+ def setup_command(using: str | None) -> None:
45
+ """Run all registered idempotent topology callbacks."""
46
+
47
+ state = get_state()
48
+ for alias in _aliases(using):
49
+ callbacks = state.setup_registries[alias].all()
50
+ click.echo(f'[{alias}] applying {len(callbacks)} topology callback(s)')
51
+ channel = state.connection_managers[alias].get_producer_channel()
52
+ for callback in callbacks:
53
+ callback(channel)
54
+ click.echo(f' ✓ {callback.__module__}.{callback.__name__}')
55
+
56
+
57
+ def _run_consumer(app: Flask, consumer: Consumer, stop_event: threading.Event) -> None:
58
+ with app.app_context():
59
+ consumer.consume(stop_event=stop_event)
60
+
61
+
62
+ @rmq_cli.command('consume')
63
+ @click.option('--using', help='Start only consumers registered for this alias.')
64
+ @with_appcontext
65
+ def consume_command(using: str | None) -> None:
66
+ """Run every registered consumer in its own thread until SIGINT/SIGTERM."""
67
+
68
+ state = get_state()
69
+ app = current_app
70
+ pairs = [(alias, consumer) for alias in _aliases(using) for consumer in state.consumers_registries[alias].all()]
71
+ if not pairs:
72
+ click.echo('No consumers registered.')
73
+ return
74
+
75
+ stop_event = threading.Event()
76
+
77
+ def request_stop(_signum: int, _frame: FrameType | None) -> None:
78
+ stop_event.set()
79
+
80
+ previous_sigint = signal.signal(signal.SIGINT, request_stop)
81
+ previous_sigterm = signal.signal(signal.SIGTERM, request_stop)
82
+
83
+ threads = []
84
+ try:
85
+ for alias, consumer in pairs:
86
+ thread = threading.Thread(
87
+ target=_run_consumer,
88
+ args=(app, consumer, stop_event),
89
+ name=f'flask-rmq-{alias}-{consumer.queue}',
90
+ daemon=False,
91
+ )
92
+ thread.start()
93
+ threads.append(thread)
94
+ click.echo(f'[{alias}] started {consumer.queue} → {consumer.handler_name}')
95
+ for thread in threads:
96
+ while thread.is_alive():
97
+ thread.join(timeout=0.5)
98
+ except KeyboardInterrupt:
99
+ stop_event.set()
100
+ finally:
101
+ stop_event.set()
102
+ for thread in threads:
103
+ thread.join(timeout=5)
104
+ signal.signal(signal.SIGINT, previous_sigint)
105
+ signal.signal(signal.SIGTERM, previous_sigterm)
106
+
107
+
108
+ def main() -> None:
109
+ """Explain the app-bound entry point when invoked as ``flask-rmq``."""
110
+
111
+ raise click.ClickException('Flask-RMQ commands are app-bound. Run: flask --app your_module rmq --help')
flask_rmq/config.py ADDED
@@ -0,0 +1,55 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Mapping
4
+ from dataclasses import dataclass
5
+ from typing import Any
6
+
7
+ from flask_rmq.exceptions import ConfigurationError
8
+
9
+
10
+ @dataclass(frozen=True, slots=True)
11
+ class RabbitMQConfig:
12
+ """Resolved settings for one independent RabbitMQ broker."""
13
+
14
+ host: str
15
+ port: int = 5672
16
+ virtual_host: str = '/'
17
+ user: str = 'guest'
18
+ password: str = 'guest'
19
+ heartbeat: int = 600
20
+ blocked_connection_timeout: float = 300.0
21
+ reconnect_initial_backoff: float = 1.0
22
+ reconnect_max_backoff: float = 30.0
23
+
24
+ @classmethod
25
+ def from_mapping(cls, alias: str, values: Mapping[str, Any]) -> RabbitMQConfig:
26
+ """Build and validate a config, accepting Flask-style uppercase keys."""
27
+
28
+ normalized = {str(key).lower(): value for key, value in values.items()}
29
+ if not normalized.get('host'):
30
+ raise ConfigurationError(f'RABBITMQ_CONNECTIONS[{alias!r}] requires HOST')
31
+ try:
32
+ config = cls(
33
+ host=str(normalized['host']),
34
+ port=int(normalized.get('port', 5672)),
35
+ virtual_host=str(normalized.get('virtual_host', '/')),
36
+ user=str(normalized.get('user', 'guest')),
37
+ password=str(normalized.get('password', 'guest')),
38
+ heartbeat=int(normalized.get('heartbeat', 600)),
39
+ blocked_connection_timeout=float(normalized.get('blocked_connection_timeout', 300.0)),
40
+ reconnect_initial_backoff=float(normalized.get('reconnect_initial_backoff', 1.0)),
41
+ reconnect_max_backoff=float(normalized.get('reconnect_max_backoff', 30.0)),
42
+ )
43
+ except (TypeError, ValueError) as exc:
44
+ raise ConfigurationError(f'Invalid RabbitMQ values for alias {alias!r}: {exc}') from exc
45
+ if not 1 <= config.port <= 65535:
46
+ raise ConfigurationError(f'RabbitMQ PORT for {alias!r} must be between 1 and 65535')
47
+ if config.heartbeat < 0 or config.blocked_connection_timeout <= 0:
48
+ raise ConfigurationError(f'RabbitMQ timeouts for {alias!r} must be positive')
49
+ if config.reconnect_initial_backoff <= 0 or config.reconnect_max_backoff <= 0:
50
+ raise ConfigurationError(f'RabbitMQ reconnect backoffs for {alias!r} must be positive')
51
+ if config.reconnect_initial_backoff > config.reconnect_max_backoff:
52
+ raise ConfigurationError(
53
+ f'RabbitMQ RECONNECT_INITIAL_BACKOFF for {alias!r} cannot exceed RECONNECT_MAX_BACKOFF'
54
+ )
55
+ return config
@@ -0,0 +1,84 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import threading
5
+
6
+ from pika import BlockingConnection, ConnectionParameters, PlainCredentials
7
+ from pika.adapters.blocking_connection import BlockingChannel
8
+ from pika.exceptions import AMQPError
9
+
10
+ from flask_rmq.config import RabbitMQConfig
11
+ from flask_rmq.state import get_state, resolve_alias
12
+
13
+ logger = logging.getLogger('rabbitmq')
14
+
15
+
16
+ def get_connection_manager(using: str | None = None) -> RabbitMQConnectionManager:
17
+ return resolve_alias(get_state().connection_managers, using)
18
+
19
+
20
+ class RabbitMQConnectionManager:
21
+ """Own thread-local, role-separated Pika connections for one broker alias."""
22
+
23
+ def __init__(self, config: RabbitMQConfig) -> None:
24
+ self.config = config
25
+ self._parameters = ConnectionParameters(
26
+ host=config.host,
27
+ port=config.port,
28
+ virtual_host=config.virtual_host,
29
+ credentials=PlainCredentials(config.user, config.password),
30
+ heartbeat=config.heartbeat,
31
+ blocked_connection_timeout=config.blocked_connection_timeout,
32
+ )
33
+ self._local = threading.local()
34
+
35
+ def _connection(self, attr: str) -> BlockingConnection:
36
+ connection = getattr(self._local, attr, None)
37
+ if connection is None or not connection.is_open:
38
+ connection = BlockingConnection(self._parameters)
39
+ setattr(self._local, attr, connection)
40
+ return connection
41
+
42
+ def get_producer_connection(self) -> BlockingConnection:
43
+ return self._connection('producer_connection')
44
+
45
+ def get_consumer_connection(self) -> BlockingConnection:
46
+ return self._connection('consumer_connection')
47
+
48
+ def get_producer_channel(self) -> BlockingChannel:
49
+ connection = self.get_producer_connection()
50
+ channel = getattr(self._local, 'producer_channel', None)
51
+ if channel is None or not channel.is_open:
52
+ channel = connection.channel()
53
+ channel.confirm_delivery()
54
+ self._local.producer_channel = channel
55
+ return channel
56
+
57
+ def reset_producer_channel(self) -> None:
58
+ """Close and forget producer resources after a transport failure."""
59
+
60
+ for attr in ('producer_channel', 'producer_connection'):
61
+ resource = getattr(self._local, attr, None)
62
+ if resource is None:
63
+ continue
64
+ try:
65
+ if resource.is_open:
66
+ resource.close()
67
+ except (AMQPError, OSError):
68
+ logger.warning('Failed to close stale RabbitMQ %s', attr, exc_info=True)
69
+ finally:
70
+ delattr(self._local, attr)
71
+
72
+ def close_thread_connections(self) -> None:
73
+ """Close all resources owned by the calling thread."""
74
+
75
+ self.reset_producer_channel()
76
+ connection = getattr(self._local, 'consumer_connection', None)
77
+ if connection is not None:
78
+ try:
79
+ if connection.is_open:
80
+ connection.close()
81
+ except (AMQPError, OSError):
82
+ logger.warning('Failed to close consumer connection', exc_info=True)
83
+ finally:
84
+ delattr(self._local, 'consumer_connection')
flask_rmq/consumer.py ADDED
@@ -0,0 +1,137 @@
1
+ from __future__ import annotations
2
+
3
+ import functools
4
+ import logging
5
+ import threading
6
+ from collections.abc import Callable
7
+ from typing import TYPE_CHECKING
8
+
9
+ from pika.exceptions import (
10
+ AMQPConnectionError,
11
+ ChannelClosed,
12
+ ChannelClosedByBroker,
13
+ ConnectionClosed,
14
+ StreamLostError,
15
+ )
16
+
17
+ from flask_rmq.connections import get_connection_manager
18
+ from flask_rmq.queues import QueueConfig
19
+
20
+ if TYPE_CHECKING:
21
+ from pika.adapters.blocking_connection import BlockingChannel, BlockingConnection
22
+ from pika.spec import Basic, BasicProperties
23
+
24
+ MessageCallback = Callable[['BlockingChannel', 'Basic.Deliver', 'BasicProperties', bytes], None]
25
+ logger = logging.getLogger('rabbitmq')
26
+ _RECONNECTABLE_ERRORS = (
27
+ AMQPConnectionError,
28
+ ConnectionClosed,
29
+ ChannelClosed,
30
+ ChannelClosedByBroker,
31
+ StreamLostError,
32
+ ConnectionResetError,
33
+ )
34
+
35
+
36
+ class Consumer:
37
+ """Consume one queue with reconnect backoff and graceful shutdown."""
38
+
39
+ def __init__(
40
+ self,
41
+ queue: QueueConfig | str,
42
+ prefetch_count: int = 1,
43
+ reconnect_initial_backoff: float | None = None,
44
+ reconnect_max_backoff: float | None = None,
45
+ using: str | None = None,
46
+ ) -> None:
47
+ if prefetch_count < 1:
48
+ raise ValueError('prefetch_count must be at least 1')
49
+ self.queue = str(queue)
50
+ self._queue_config = queue
51
+ self._prefetch_count = prefetch_count
52
+ self._using = using
53
+ self._initial_override = reconnect_initial_backoff
54
+ self._max_override = reconnect_max_backoff
55
+ self._handler: MessageCallback | None = None
56
+
57
+ @property
58
+ def prefetch_count(self) -> int:
59
+ return self._prefetch_count
60
+
61
+ @property
62
+ def using(self) -> str | None:
63
+ return self._using
64
+
65
+ @property
66
+ def handler_name(self) -> str:
67
+ return self._handler.__name__ if self._handler else 'unregistered'
68
+
69
+ def handler(self, func: MessageCallback) -> MessageCallback:
70
+ if self._handler is not None:
71
+ raise RuntimeError(f'Consumer for queue {self.queue!r} already has a handler')
72
+ self._handler = func
73
+ return func
74
+
75
+ def __call__(self, func: MessageCallback) -> MessageCallback:
76
+ return self.handler(func)
77
+
78
+ def consume(self, stop_event: threading.Event | None = None) -> None:
79
+ if self._handler is None:
80
+ raise RuntimeError(f'Consumer for queue {self.queue!r} has no handler')
81
+ stop_event = stop_event or threading.Event()
82
+ config = get_connection_manager(self._using).config
83
+ initial = self._initial_override if self._initial_override is not None else config.reconnect_initial_backoff
84
+ maximum = self._max_override if self._max_override is not None else config.reconnect_max_backoff
85
+ backoff = initial
86
+ while not stop_event.is_set():
87
+ try:
88
+ self._run_session(self._handler, stop_event)
89
+ return
90
+ except _RECONNECTABLE_ERRORS:
91
+ logger.warning(
92
+ 'RabbitMQ consumer %s disconnected; retrying in %.2fs',
93
+ self.queue,
94
+ backoff,
95
+ exc_info=True,
96
+ )
97
+ stop_event.wait(backoff)
98
+ backoff = min(backoff * 2, maximum)
99
+
100
+ def _run_session(self, handler: MessageCallback, stop_event: threading.Event) -> None:
101
+ connection: BlockingConnection = get_connection_manager(self._using).get_consumer_connection()
102
+ channel: BlockingChannel = connection.channel()
103
+ try:
104
+ self._declare_queue(channel)
105
+ channel.basic_qos(prefetch_count=self._prefetch_count)
106
+ channel.basic_consume(queue=self.queue, on_message_callback=functools.partial(self._dispatch, handler))
107
+ logger.info('Consuming queue %s with %s', self.queue, handler.__name__)
108
+ while not stop_event.is_set():
109
+ connection.process_data_events(time_limit=1)
110
+ finally:
111
+ if channel.is_open:
112
+ channel.stop_consuming()
113
+ channel.close()
114
+
115
+ def _declare_queue(self, channel: BlockingChannel) -> None:
116
+ if isinstance(self._queue_config, QueueConfig):
117
+ channel.queue_declare(
118
+ queue=self.queue,
119
+ durable=self._queue_config.durable,
120
+ arguments=self._queue_config.arguments,
121
+ )
122
+ else:
123
+ channel.queue_declare(queue=self.queue, durable=True)
124
+
125
+ def _dispatch(
126
+ self,
127
+ handler: MessageCallback,
128
+ ch: BlockingChannel,
129
+ method: Basic.Deliver,
130
+ props: BasicProperties,
131
+ body: bytes,
132
+ ) -> None:
133
+ try:
134
+ handler(ch, method, props, body)
135
+ except Exception:
136
+ logger.exception('Handler %s failed for queue %s; nacking', handler.__name__, self.queue)
137
+ ch.basic_nack(delivery_tag=method.delivery_tag, requeue=False)
@@ -0,0 +1,10 @@
1
+ class FlaskRMQError(Exception):
2
+ """Base exception raised by Flask-RMQ."""
3
+
4
+
5
+ class ConfigurationError(FlaskRMQError):
6
+ """Raised when Flask-RMQ configuration is missing or invalid."""
7
+
8
+
9
+ class NotInitializedError(FlaskRMQError):
10
+ """Raised when an API is used before :class:`FlaskRMQ` initialization."""
flask_rmq/extension.py ADDED
@@ -0,0 +1,53 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Mapping
4
+ from typing import Any, cast
5
+
6
+ from flask import Flask
7
+
8
+ from flask_rmq.config import RabbitMQConfig
9
+ from flask_rmq.connections import RabbitMQConnectionManager
10
+ from flask_rmq.exceptions import ConfigurationError
11
+ from flask_rmq.registries import ConsumersRegistry, SetupRegistry
12
+ from flask_rmq.state import RMQState, register_state
13
+
14
+
15
+ class FlaskRMQ:
16
+ """Flask extension that configures RabbitMQ aliases and Click commands."""
17
+
18
+ def __init__(self, app: Flask | None = None) -> None:
19
+ if app is not None:
20
+ self.init_app(app)
21
+
22
+ def init_app(self, app: Flask) -> None:
23
+ if 'flask-rmq' in app.extensions:
24
+ raise ConfigurationError('Flask-RMQ is already initialized for this app')
25
+ raw_connections: Any = app.config.get('RABBITMQ_CONNECTIONS')
26
+ if not isinstance(raw_connections, Mapping) or not raw_connections:
27
+ raise ConfigurationError('Flask-RMQ requires a non-empty RABBITMQ_CONNECTIONS mapping in app.config')
28
+
29
+ managers: dict[str, RabbitMQConnectionManager] = {}
30
+ setups: dict[str, SetupRegistry] = {}
31
+ consumers: dict[str, ConsumersRegistry] = {}
32
+ for raw_alias, values in raw_connections.items():
33
+ alias = str(raw_alias)
34
+ if not isinstance(values, Mapping):
35
+ raise ConfigurationError(f'RABBITMQ_CONNECTIONS[{alias!r}] must be a mapping')
36
+ managers[alias] = RabbitMQConnectionManager(RabbitMQConfig.from_mapping(alias, values))
37
+ setups[alias] = SetupRegistry()
38
+ consumers[alias] = ConsumersRegistry()
39
+
40
+ state = RMQState(managers, setups, consumers)
41
+ app.extensions['flask-rmq'] = state
42
+ register_state(app, state)
43
+
44
+ from .cli import rmq_cli
45
+
46
+ app.cli.add_command(rmq_cli)
47
+
48
+ @staticmethod
49
+ def get_state(app: Flask) -> RMQState:
50
+ try:
51
+ return cast(RMQState, app.extensions['flask-rmq'])
52
+ except KeyError as exc:
53
+ raise ConfigurationError('Flask-RMQ is not initialized for this app') from exc