|
|
|
@@ -8,8 +8,7 @@ import json
|
|
|
|
|
import logging
|
|
|
|
|
import sys
|
|
|
|
|
import traceback
|
|
|
|
|
import pathlib
|
|
|
|
|
from asyncio import Future, AbstractEventLoop
|
|
|
|
|
from asyncio import AbstractEventLoop
|
|
|
|
|
from typing import Dict, List, Tuple, Callable, Coroutine, Any
|
|
|
|
|
from urllib.parse import parse_qs, urlparse
|
|
|
|
|
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.adapter.multipart_parser import CleverMultiPartParser, process_form_data
|
|
|
|
|
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 IPython.core import debugger
|
|
|
|
|
debug = debugger.Pdb().set_trace
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -54,10 +51,30 @@ class AMQMessage(DataMessage):
|
|
|
|
|
self.connection = connection
|
|
|
|
|
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):
|
|
|
|
|
"""
|
|
|
|
|
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.
|
|
|
|
|
|
|
|
|
@@ -117,32 +134,36 @@ def parse_request_body(message: DataMessage):
|
|
|
|
|
raise ValueError(f"Unsupported Content-Type: {content_type}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def convert_file_response(
|
|
|
|
|
async def _convert_file_response(
|
|
|
|
|
obj: FileResponse,
|
|
|
|
|
connection: AbstractRobustConnection,
|
|
|
|
|
loop: AbstractEventLoop | None
|
|
|
|
|
) -> 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]:
|
|
|
|
|
""" Wrap calls to RAbbitMQ API to make it Awaitable, so it can be run thread-safe with correct event loop """
|
|
|
|
|
async def _wrap_amq_api_calls(
|
|
|
|
|
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()
|
|
|
|
|
exchange_name: str = AMQ_REPLY_TO_EXCHANGE
|
|
|
|
|
_exchange = await _channel.get_exchange(name=exchange_name, ensure=True)
|
|
|
|
|
_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,
|
|
|
|
|
exchange=_exchange_name,
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
# 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
|
|
|
|
|
_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():
|
|
|
|
|
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()
|
|
|
|
@@ -199,7 +220,7 @@ async def call_cleverthis_api(
|
|
|
|
|
logging.info(f" [*] ENDPOINT RESULT: {api_method.__name__}, RESULT: {str(_result)}")
|
|
|
|
|
|
|
|
|
|
if isinstance(_result, FileResponse):
|
|
|
|
|
_json_result, _content_type = await convert_file_response(
|
|
|
|
|
_json_result, _content_type = await _convert_file_response(
|
|
|
|
|
_result, message.connection, loop
|
|
|
|
|
), 'application/octet-stream'
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 =============
|
|
|
|
|
class CleverThisServiceAdapter:
|
|
|
|
|
"""
|
|
|
|
@@ -293,10 +295,10 @@ class CleverThisServiceAdapter:
|
|
|
|
|
:return: AMQP response class with operation status code and optional error details.
|
|
|
|
|
"""
|
|
|
|
|
_auth_user: Any | None = None
|
|
|
|
|
amq_response: Any | None = None
|
|
|
|
|
_amq_response: Any | None = None
|
|
|
|
|
|
|
|
|
|
logging.warn(
|
|
|
|
|
f'Got here with: _auth_user={_auth_user} and require_authenticated_user={self.require_authenticated_user}')
|
|
|
|
|
logging.debug(
|
|
|
|
|
f' [*] on_message: _auth_user={_auth_user}, require_authenticated_user={self.require_authenticated_user}')
|
|
|
|
|
if self.require_authenticated_user:
|
|
|
|
|
_auth: str = message.headers.get('Authorization', '')
|
|
|
|
|
logging.info(f"Auth: {_auth}")
|
|
|
|
@@ -308,31 +310,26 @@ class CleverThisServiceAdapter:
|
|
|
|
|
logging.warn(f'Token: {token}')
|
|
|
|
|
_auth_user = await self.get_current_user(token)
|
|
|
|
|
except Exception as error:
|
|
|
|
|
logging.error(f"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}')
|
|
|
|
|
logging.error(f" [E] 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)
|
|
|
|
|
_amq_response = await self.get(message, _auth_user)
|
|
|
|
|
elif method == 'put':
|
|
|
|
|
amq_response = await self.put(message, _auth_user)
|
|
|
|
|
_amq_response = await self.put(message, _auth_user)
|
|
|
|
|
elif method == 'post':
|
|
|
|
|
amq_response = await self.post(message, _auth_user)
|
|
|
|
|
_amq_response = await self.post(message, _auth_user)
|
|
|
|
|
elif method == 'head':
|
|
|
|
|
amq_response = await self.head(message, _auth_user)
|
|
|
|
|
_amq_response = await self.head(message, _auth_user)
|
|
|
|
|
elif method == 'delete':
|
|
|
|
|
amq_response = await self.delete(message, _auth_user)
|
|
|
|
|
_amq_response = await self.delete(message, _auth_user)
|
|
|
|
|
else:
|
|
|
|
|
raise ValueError(f"Unexpected HTTP method: {message.method}")
|
|
|
|
|
else:
|
|
|
|
|
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}")
|
|
|
|
|
return amq_response
|
|
|
|
|
_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}")
|
|
|
|
|
return _amq_response
|
|
|
|
|
|
|
|
|
|
# ==================================================================================================
|
|
|
|
|
# ===================== C l e v e r T h i s A P I m e t h o d s ============================
|
|
|
|
|