#feat/1 - code cleanup for final release, part 1

This commit is contained in:
Stanislav Hejny
2025-04-17 13:20:56 +01:00
parent 7bc92f9614
commit a8d82b1e3d
5 changed files with 89 additions and 75 deletions
+20 -20
View File
@@ -28,27 +28,27 @@ class AMQRouteFactory:
:return: AMQRoute object :return: AMQRoute object
""" """
try: try:
keys = data.split(RS) _keys = data.split(RS)
if len(keys) >= 5: if len(_keys) >= 5:
i = 0 _i = 0
component_name = keys[i] _component_name = _keys[_i]
i += 1 _i += 1
exchange = keys[i] _exchange = _keys[_i]
i += 1 _i += 1
queue = keys[i] if i < len(keys) else "" _queue = _keys[_i] if _i < len(_keys) else ""
i += 1 _i += 1
routing_key = keys[i] if i < len(keys) else "" _routing_key = _keys[_i] if _i < len(_keys) else ""
i += 1 _i += 1
timeout = int(keys[i]) if i < len(keys) else 0 _timeout = int(_keys[_i]) if _i < len(_keys) else 0
i += 1 _i += 1
valid_until = int(keys[i]) if i < len(keys) else 0 _valid_until = int(_keys[_i]) if _i < len(_keys) else 0
return AMQRoute( return AMQRoute(
component_name=component_name, component_name=_component_name,
exchange=exchange, exchange=_exchange,
queue=queue, queue=_queue,
key=routing_key, key=_routing_key,
timeout=timeout, timeout=_timeout,
valid_until=valid_until valid_until=_valid_until
) )
logging.error( logging.error(
" [E] AMQRoute incorrect format. AMQRoute expects input as: " " [E] AMQRoute incorrect format. AMQRoute expects input as: "
+51 -54
View File
@@ -8,8 +8,7 @@ import json
import logging import logging
import sys import sys
import traceback import traceback
import pathlib from asyncio import AbstractEventLoop
from asyncio import Future, AbstractEventLoop
from typing import Dict, List, Tuple, Callable, Coroutine, Any from typing import Dict, List, Tuple, Callable, Coroutine, Any
from urllib.parse import parse_qs, urlparse from urllib.parse import parse_qs, urlparse
from xml.etree import ElementTree from xml.etree import ElementTree
@@ -25,11 +24,9 @@ from amqp.config.amq_configuration import AMQConfiguration
from amqp.model.model import DataMessage, DataResponse from amqp.model.model import DataMessage, DataResponse
from amqp.adapter.multipart_parser import CleverMultiPartParser, process_form_data from amqp.adapter.multipart_parser import CleverMultiPartParser, process_form_data
from amqp.adapter.file_uploader import publish_file_to_rabbitmq from amqp.adapter.file_uploader import publish_file_to_rabbitmq
from IPython.core import debugger
from amqp.router.router_base import AMQ_REPLY_TO_EXCHANGE from amqp.router.router_base import AMQ_REPLY_TO_EXCHANGE
from IPython.core import debugger
debug = debugger.Pdb().set_trace debug = debugger.Pdb().set_trace
@@ -54,10 +51,30 @@ class AMQMessage(DataMessage):
self.connection = connection self.connection = connection
self.extra_data = data_message.extra_data if hasattr(data_message, 'extra_data') else {} self.extra_data = data_message.extra_data if hasattr(data_message, 'extra_data') else {}
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
def parse_request_body(message: DataMessage): def parse_request_body(message: DataMessage):
""" """
Parses the HTTP POST request body based on the MIME type. 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: Args:
message (DataMessage): Contains raw request and MIME headers. message (DataMessage): Contains raw request and MIME headers.
@@ -117,32 +134,36 @@ def parse_request_body(message: DataMessage):
raise ValueError(f"Unsupported Content-Type: {content_type}") raise ValueError(f"Unsupported Content-Type: {content_type}")
async def convert_file_response( async def _convert_file_response(
obj: FileResponse, obj: FileResponse,
connection: AbstractRobustConnection, connection: AbstractRobustConnection,
loop: AbstractEventLoop | None loop: AbstractEventLoop | None
) -> str: ) -> str:
""" """
Converts a FileResponse object to a JSON string containing the filename and stream. 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.
""" """
async def wrap_connection_channel(connection: AbstractRobustConnection) -> tuple[AbstractRobustChannel, AbstractExchange, str]: async def _wrap_amq_api_calls(
""" Wrap calls to RAbbitMQ API to make it Awaitable, so it can be run thread-safe with correct event loop """ connection: AbstractRobustConnection
) -> tuple[AbstractRobustChannel, AbstractExchange, str]:
# 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
# the event loop the first RabbitMQ connection was created with.
_channel = await connection.channel() _channel = await connection.channel()
exchange_name: str = AMQ_REPLY_TO_EXCHANGE _exchange_name: str = AMQ_REPLY_TO_EXCHANGE
_exchange = await _channel.get_exchange(name=exchange_name, ensure=True) _exchange = await _channel.get_exchange(name=_exchange_name, ensure=True)
_queue = await _channel.declare_queue(exclusive=False) # Declare a unique, non-exclusive queue _queue = await _channel.declare_queue(exclusive=False) # Declare a unique, non-exclusive queue
_queue_name = _queue.name _queue_name = _queue.name
await _queue.bind( await _queue.bind(
exchange=exchange_name, exchange=_exchange_name,
routing_key=_queue_name, # Use queue name as routing key routing_key=_queue_name, # Use queue name as routing key
) )
logging.debug(f" [*] Publishing to exchange: {_exchange_name}, q: {_queue_name}")
logging.debug(f" [*] Publishing to exchange: {exchange_name}, q: {_queue_name}")
return _channel, _exchange, _queue_name return _channel, _exchange, _queue_name
# All calls to RabbittMQ API hav eto be called threadsafe, because those appear to be attached to parent # 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 one # event loop, and need to execute at that event loop, not the current one
_future = asyncio.run_coroutine_threadsafe(wrap_connection_channel(connection), loop) _future = asyncio.run_coroutine_threadsafe(_wrap_amq_api_calls(connection), loop)
while not _future.done(): while not _future.done():
await asyncio.sleep(0.1) # allow coroutine to execute. calling _future.result() will block the entire thread and the coroutine will never execute await asyncio.sleep(0.1) # allow coroutine to execute. calling _future.result() will block the entire thread and the coroutine will never execute
_channel, _exchange, _routing_key = _future.result() _channel, _exchange, _routing_key = _future.result()
@@ -199,7 +220,7 @@ async def call_cleverthis_api(
logging.info(f" [*] ENDPOINT RESULT: {api_method.__name__}, RESULT: {str(_result)}") logging.info(f" [*] ENDPOINT RESULT: {api_method.__name__}, RESULT: {str(_result)}")
if isinstance(_result, FileResponse): if isinstance(_result, FileResponse):
_json_result, _content_type = await convert_file_response( _json_result, _content_type = await _convert_file_response(
_result, message.connection, loop _result, message.connection, loop
), 'application/octet-stream' ), 'application/octet-stream'
else: else:
@@ -224,25 +245,6 @@ async def call_cleverthis_api(
return DataResponseFactory.of(message.id, 500, "text/plain", str(error).encode('utf-8'), message.trace_info) return DataResponseFactory.of(message.id, 500, "text/plain", str(error).encode('utf-8'), message.trace_info)
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
# ============= CleverThisServiceAdapter ============= # ============= CleverThisServiceAdapter =============
class CleverThisServiceAdapter: class CleverThisServiceAdapter:
""" """
@@ -293,10 +295,10 @@ class CleverThisServiceAdapter:
:return: AMQP response class with operation status code and optional error details. :return: AMQP response class with operation status code and optional error details.
""" """
_auth_user: Any | None = None _auth_user: Any | None = None
amq_response: Any | None = None _amq_response: Any | None = None
logging.warn( logging.debug(
f'Got here with: _auth_user={_auth_user} and require_authenticated_user={self.require_authenticated_user}') f' [*] on_message: _auth_user={_auth_user}, require_authenticated_user={self.require_authenticated_user}')
if self.require_authenticated_user: if self.require_authenticated_user:
_auth: str = message.headers.get('Authorization', '') _auth: str = message.headers.get('Authorization', '')
logging.info(f"Auth: {_auth}") logging.info(f"Auth: {_auth}")
@@ -308,31 +310,26 @@ class CleverThisServiceAdapter:
logging.warn(f'Token: {token}') logging.warn(f'Token: {token}')
_auth_user = await self.get_current_user(token) _auth_user = await self.get_current_user(token)
except Exception as error: except Exception as error:
logging.error(f"Error getting user: {error}") logging.error(f" [E] on_message: Error getting user: {error}")
# bearer_in_auth = 'Bearer' in _auth
# logging.warn(f'Got here2 with: Bearer_in_auth={bearer_in_auth}, _auth_user={_auth_user} and require_authenticated_user={self.require_authenticated_user}')
logging.warn(
f'Got here2 with: _auth_user={_auth_user} and require_authenticated_user={self.require_authenticated_user}')
if _auth_user or not self.require_authenticated_user or message.path.endswith('/login'): if _auth_user or not self.require_authenticated_user or message.path.endswith('/login'):
method = message.method.lower() method = message.method.lower()
if method == 'get': if method == 'get':
amq_response = await self.get(message, _auth_user) _amq_response = await self.get(message, _auth_user)
elif method == 'put': elif method == 'put':
amq_response = await self.put(message, _auth_user) _amq_response = await self.put(message, _auth_user)
elif method == 'post': elif method == 'post':
amq_response = await self.post(message, _auth_user) _amq_response = await self.post(message, _auth_user)
elif method == 'head': elif method == 'head':
amq_response = await self.head(message, _auth_user) _amq_response = await self.head(message, _auth_user)
elif method == 'delete': elif method == 'delete':
amq_response = await self.delete(message, _auth_user) _amq_response = await self.delete(message, _auth_user)
else: else:
raise ValueError(f"Unexpected HTTP method: {message.method}") raise ValueError(f"Unexpected HTTP method: {message.method}")
else: else:
amq_response = DataResponseFactory.of(message.id, 401, "text/plain", b"Unauthorized", message.trace_info) _amq_response = DataResponseFactory.of(message.id, 401, "text/plain", b"Unauthorized", message.trace_info)
logging.info(f" [x] ALL DONE in on_message id:{message.id}, resp code:{amq_response.response_code}") logging.info(f" [x] ALL DONE in on_message id:{message.id}, resp code:{_amq_response.response_code}")
return amq_response return _amq_response
# ================================================================================================== # ==================================================================================================
# ===================== C l e v e r T h i s A P I m e t h o d s ============================ # ===================== C l e v e r T h i s A P I m e t h o d s ============================
+14
View File
@@ -0,0 +1,14 @@
import logging
import sys
import traceback
def logging_error(msg: str, *args) -> None:
"""
Log an error message according to current 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.
"""
_tb = traceback.extract_tb(sys.exc_info()[2])[-1]
logging.error(f" [E] module '{_tb.filename}', line {_tb.lineno}, function '{_tb.name}': {msg}", *args)
+2 -1
View File
@@ -5,6 +5,7 @@ from aio_pika.abc import AbstractRobustQueue, AbstractRobustExchange
from pika.exchange_type import ExchangeType from pika.exchange_type import ExchangeType
from amqp.adapter.data_message_factory import DataMessageFactory from amqp.adapter.data_message_factory import DataMessageFactory
from amqp.adapter.logging_utils import logging_error
from amqp.model.model import DataMessage, AMQRoute from amqp.model.model import DataMessage, AMQRoute
from amqp.router.router_base import AMQ_REPLY_TO_EXCHANGE, DLQ_EXCHANGE, AMQ_RPC_EXCHANGE from amqp.router.router_base import AMQ_REPLY_TO_EXCHANGE, DLQ_EXCHANGE, AMQ_RPC_EXCHANGE
from amqp.router.router_consumer import RouterConsumer from amqp.router.router_consumer import RouterConsumer
@@ -54,7 +55,7 @@ class RabbitMQClient:
if not self.connection.is_closed: if not self.connection.is_closed:
self.connection.close() self.connection.close()
except Exception as e: except Exception as e:
logging.error(" [E] RabbitMQClient.PreDestroy cleanup - connection close error: %s", e) logging_error(" [E] RabbitMQClient.PreDestroy cleanup - connection close error: %s", e)
async def register_inbound_routes(self, route_mapping: str, callback): async def register_inbound_routes(self, route_mapping: str, callback):
""" """
+2
View File
@@ -38,3 +38,5 @@ def delete_queues_with_prefix_http(host='localhost', port=15672, username='sprin
if __name__ == "__main__": if __name__ == "__main__":
delete_queues_with_prefix_http() delete_queues_with_prefix_http()
delete_queues_with_prefix_http(prefix='amq.gen--')
delete_queues_with_prefix_http(prefix='amq_')