feat/#1 - refactor code for better design and testability

This commit was merged in pull request #11.
This commit is contained in:
Stanislav Hejny
2025-04-22 08:50:16 +01:00
parent 1de7afe4f5
commit 7f10905791
17 changed files with 1297 additions and 826 deletions
+87 -45
View File
@@ -19,17 +19,19 @@ import core.deps
from rest.v0 import api
from schemas.user_schema import UserSchema
from amqp.adapter.data_parser import AMQDataParser
# AMQP specific imports
from amqp.adapter.data_response_factory import AMQResponseFactory
from amqp.adapter.cleverthis_service_adapter import CleverThisServiceAdapter, \
parse_get_url, call_cleverthis_api, parse_request_body, AMQMessage
from amqp.adapter.cleverthis_service_adapter import CleverThisServiceAdapter, AMQMessage
from amqp.config.amq_configuration import AMQConfiguration
from amqp.model.model import AMQResponse
from amqp.service.amq_service import AMQService
# Suffix for the method name with serialization wrapper, prefer use this one over the orig endpoint
PREFERRED_SUFFIX = '_json'
PREFERRED_SUFFIX = "_json"
def clone_and_adapt_route(original_route: APIRoute) -> APIRoute:
"""
@@ -39,18 +41,25 @@ def clone_and_adapt_route(original_route: APIRoute) -> APIRoute:
# Check if the route has a preferred endpoint with json serialization
_preferred_endpoint = get_prefered_endpoint(
getattr(original_route.endpoint, '__module__'), original_route.endpoint.__name__ + PREFERRED_SUFFIX
getattr(original_route.endpoint, "__module__"),
original_route.endpoint.__name__ + PREFERRED_SUFFIX,
)
# 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_regex_string = (
"^.*" + original_route.path_regex.pattern.lstrip("^").rstrip("/$") + "$"
)
_new_route: APIRoute = APIRoute(
path=original_route.path,
endpoint=_preferred_endpoint if _preferred_endpoint else original_route.endpoint,
endpoint=(
_preferred_endpoint if _preferred_endpoint else original_route.endpoint
),
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,
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,
@@ -69,7 +78,11 @@ def clone_and_adapt_route(original_route: APIRoute) -> APIRoute:
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,
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)
@@ -91,9 +104,13 @@ def group_routes_by_method(api_routes: List[APIRoute]) -> Dict[str, List[APIRout
Groups API routes by their HTTP method. This is important to ensure that the 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
_method_to_routes = defaultdict(
list
) # Automatically initializes new lists for new keys
for _route in api_routes:
if _route.methods: # Ensure methods exist (should always be true for valid routes)
if (
_route.methods
): # Ensure methods exist (should always be true for valid routes)
for method in _route.methods:
_method_to_routes[method].append(clone_and_adapt_route(_route))
@@ -105,7 +122,9 @@ class CleverSwarmAmqpAdapter(CleverThisServiceAdapter):
CleverSwarm AMQP Adapter for CleverSwarm API. Implements the interface to CleverSwarm API.
"""
def __init__(self, amq_configuration: AMQConfiguration, session: ClientSession = None):
def __init__(
self, amq_configuration: AMQConfiguration, session: ClientSession = None
):
super().__init__(amq_configuration, session)
self.routes = group_routes_by_method(api_routes=api.api_router.routes)
_amq_service: AMQService = AMQService(amq_configuration, self)
@@ -130,7 +149,7 @@ class CleverSwarmAmqpAdapter(CleverThisServiceAdapter):
:param auth_user: user authenticated by the API Gateway, as UserSchem
:return: AMQResponse to be sent back to the API Gateway via AMQP
"""
return await self.handle_no_body_payload(message, auth_user, 'GET')
return await self.handle_no_body_payload(message, auth_user, "GET")
async def put(self, message: AMQMessage, auth_user: UserSchema) -> AMQResponse:
"""
@@ -139,7 +158,7 @@ class CleverSwarmAmqpAdapter(CleverThisServiceAdapter):
:param auth_user: user authenticated by the API Gateway, as UserSchem
:return: AMQResponse to be sent back to the API Gateway via AMQP
"""
return await self.handle_body_payload(message, auth_user, 'PUT')
return await self.handle_body_payload(message, auth_user, "PUT")
async def post(self, message: AMQMessage, auth_user: UserSchema) -> AMQResponse:
"""
@@ -148,33 +167,35 @@ class CleverSwarmAmqpAdapter(CleverThisServiceAdapter):
:param auth_user: user authenticated by the API Gateway, as UserSchem
:return: AMQResponse to be sent back to the API Gateway via AMQP
"""
return await self.handle_body_payload(message, auth_user, 'POST')
return await self.handle_body_payload(message, auth_user, "POST")
async def head(self, message: AMQMessage, auth_user: UserSchema) -> AMQResponse:
"""
Handles the HEAD requests for the CleverSwarm API.
"""
return await self.handle_no_body_payload(message, auth_user, 'HEAD')
return await self.handle_no_body_payload(message, auth_user, "HEAD")
async def delete(self, message: AMQMessage, auth_user: UserSchema) -> AMQResponse:
"""
Handles the DELETE requests for the CleverSwarm API.
"""
return await self.handle_no_body_payload(message, auth_user, 'DELETE')
return await self.handle_no_body_payload(message, auth_user, "DELETE")
async def options(self, message: AMQMessage, auth_user: UserSchema) -> AMQResponse:
"""
Handles the OPTIONS requests for the CleverSwarm API.
"""
return await self.handle_no_body_payload(message, auth_user, 'OPTIONS')
return await self.handle_no_body_payload(message, auth_user, "OPTIONS")
async def patch(self, message: AMQMessage, auth_user: UserSchema) -> AMQResponse:
"""
Handles the PATCH requests for the CleverSwarm API.
"""
return await self.handle_no_body_payload(message, auth_user, 'PATCH')
return await self.handle_no_body_payload(message, auth_user, "PATCH")
async def handle_no_body_payload(self, message: AMQMessage, auth_user: UserSchema, method: str) -> AMQResponse:
async def handle_no_body_payload(
self, message: AMQMessage, auth_user: UserSchema, method: str
) -> AMQResponse:
"""
Handles the requests that do not have a body payload.
:param message: message from the AMQP that contains the request
@@ -182,26 +203,34 @@ class CleverSwarmAmqpAdapter(CleverThisServiceAdapter):
:return: AMQResponse to be sent back to the API Gateway via AMQP
"""
_routes = self.routes.get(method, [])
_path, _args = parse_get_url(message.path)
_path, _args = AMQDataParser.parse_get_url(message.path)
for _route in _routes:
_match = _route.path_regex.match(_path)
if _match:
_args.update(_match.groupdict())
_args['authenticated_user'] = auth_user
_args["authenticated_user"] = auth_user
# If the path matches, call the corresponding function
return await call_cleverthis_api(
return await CleverThisServiceAdapter.call_cleverthis_api(
message,
_args,
api_method=_route.endpoint,
success_code=_route.status_code,
loop=self.loop
loop=self.loop,
)
return AMQResponseFactory.of(message.id, 404, "text/plain",
str(f"Destination location [{message.path}] does not exists").encode('utf-8'),
message.trace_info)
return AMQResponseFactory.of(
message.id,
404,
"text/plain",
str(f"Destination location [{message.path}] does not exists").encode(
"utf-8"
),
message.trace_info,
)
async def handle_body_payload(self, message: AMQMessage, auth_user: UserSchema, method: str) -> AMQResponse:
async def handle_body_payload(
self, message: AMQMessage, auth_user: UserSchema, method: str
) -> AMQResponse:
"""
Handles the requests that have a body payload, including Multipart data.
:param message: message from the AMQP that contains the request
@@ -209,9 +238,9 @@ class CleverSwarmAmqpAdapter(CleverThisServiceAdapter):
:param method: HTTP method (POST, PUT, etc.)
:return: AMQResponse to be sent back to the API Gateway via AMQP
"""
_form_data = parse_request_body(message)
_form_data = AMQDataParser.parse_request_body(message)
_routes = self.routes.get(method, [])
_path, _query_args = parse_get_url(message.path)
_path, _query_args = AMQDataParser.parse_get_url(message.path)
for _route in _routes:
_match = _route.path_regex.match(_path)
if _match:
@@ -222,37 +251,50 @@ class CleverSwarmAmqpAdapter(CleverThisServiceAdapter):
if _body_param.name in _form_data:
_file_data = _form_data[_body_param.name]
actual_filepath = message.extra_data[_body_param.name]
_form_data[_body_param.name] = _body_param.type_.__name__ == 'UploadFile' and \
UploadFile(
_form_data[_body_param.name] = (
_body_param.type_.__name__ == "UploadFile"
and UploadFile(
file=pathlib.Path(actual_filepath).open("rb"),
size=_file_data['size'],
filename=_file_data['filename'],
headers=Headers({"Content-Type": _file_data['mediaType']})
) or _form_data[_body_param.name]
size=_file_data["size"],
filename=_file_data["filename"],
headers=Headers(
{"Content-Type": _file_data["mediaType"]}
),
)
or _form_data[_body_param.name]
)
_verified_args = {}
_required_args = getattr(_route.endpoint, '__annotations__', {})
_required_args = getattr(_route.endpoint, "__annotations__", {})
for arg in _required_args:
if arg in _form_data:
_verified_args[arg] = _form_data[arg]
else:
# needed for /login input
if arg == 'authenticated_user':
_verified_args['authenticated_user'] = auth_user
if _required_args[arg].__name__ == 'OAuth2PasswordRequestForm':
if arg == "authenticated_user":
_verified_args["authenticated_user"] = auth_user
if _required_args[arg].__name__ == "OAuth2PasswordRequestForm":
try:
_verified_args[arg] = OAuth2PasswordRequestForm(**_form_data)
_verified_args[arg] = OAuth2PasswordRequestForm(
**_form_data
)
except Exception as e:
print(f"Error: {e}")
return await call_cleverthis_api(
return await CleverThisServiceAdapter.call_cleverthis_api(
message,
_verified_args,
api_method=_route.endpoint,
success_code=_route.status_code if _route.status_code else 200,
loop=self.loop
loop=self.loop,
)
return AMQResponseFactory.of(message.id, 404, "text/plain",
str(f"Destination location [{message.path}] does not exists").encode('utf-8'),
message.trace_info)
return AMQResponseFactory.of(
message.id,
404,
"text/plain",
str(f"Destination location [{message.path}] does not exists").encode(
"utf-8"
),
message.trace_info,
)