Skip to content

Commit

Permalink
refactoring:
Browse files Browse the repository at this point in the history
- use logger in geoservercloud library
- use fastAPI exception handlers
  • Loading branch information
mki-c2c committed Feb 5, 2025
1 parent 7d76683 commit e1c7e83
Show file tree
Hide file tree
Showing 6 changed files with 101 additions and 138 deletions.
12 changes: 6 additions & 6 deletions backend/maelstro/core/clone.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
from maelstro.config import app_config as config
from maelstro.common.types import GsLayer
from .georchestra import GeorchestraHandler, GsRestWrapper, get_georchestra_handler
from .exceptions import ParamError, MaelstroDetail, raise_for_status
from .exceptions import ParamError, MaelstroDetail
from .operations import raise_for_status


logger = logging.getLogger()
Expand Down Expand Up @@ -60,28 +61,27 @@ def _clone_dataset(self, output_format: str) -> str | list[Any]:
"destinations": [src["gs_url"] for src in config.get_destinations()],
}
pre_info, post_info = self.meta.update_geoverver_urls(mapping)
self.geo_hnd.response_handler.responses.append(
self.geo_hnd.log_handler.responses.append(
{
"operation": "Update of geoserver links in zip archive",
"before": pre_info,
"after": post_info,
}
)
results = gn_dst.put_record_zip(BytesIO(self.meta.get_zip()))
self.geo_hnd.response_handler.responses.append(
self.geo_hnd.log_handler.responses.append(
{
"message": results["msg"],
"detail": results["detail"],
}
)

if output_format == "text/plain":
return self.geo_hnd.response_handler.get_formatted_responses()
return self.geo_hnd.response_handler.get_json_responses()
return self.geo_hnd.log_handler.get_formatted_responses()
return self.geo_hnd.log_handler.get_json_responses()

def clone_layers(self) -> None:
server_layers = self.meta.get_gs_layers(config.get_gs_sources())

gs_dst = self.geo_hnd.get_gs_service(self.dst_name, False)
for gs_url, layer_names in server_layers.items():
if layer_names:
Expand Down
12 changes: 0 additions & 12 deletions backend/maelstro/core/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from fastapi import HTTPException
from dataclasses import dataclass, asdict
from requests import Response


@dataclass
Expand Down Expand Up @@ -37,14 +36,3 @@ class ParamError(MaelstroException):
def __init__(self, err_detail: MaelstroDetail):
err_detail.status_code = 406
super().__init__(err_detail)


def raise_for_status(response: Response) -> None:
if 400 <= response.status_code < 600:
raise HTTPException(
response.status_code,
{
"message": f"HTTP error in [{response.request.method}] {response.url}",
"info": response.text,
},
)
55 changes: 14 additions & 41 deletions backend/maelstro/core/georchestra.py
Original file line number Diff line number Diff line change
@@ -1,76 +1,49 @@
from contextlib import contextmanager
import inspect
import json
from typing import Any, Iterator
from geonetwork import GnApi
from geoservercloud.services import RestService # type: ignore
from geoservercloud.services.restclient import RestClient # type: ignore
from requests.exceptions import HTTPError
from maelstro.config import ConfigError, app_config as config
from .operations import (
ResponseHandler,
LoggedRequests,
add_gn_handling,
add_gs_handling,
LogCollectionHandler,
connect_log_handler,
gs_logger,
)
from .exceptions import ParamError, MaelstroDetail, AuthError


WRAPPERS = {"GnApiWrapper": add_gn_handling, "GsClientWrapper": add_gs_handling}


class MethodWrapper:
def __init__(self, response_handler: ResponseHandler, *args: Any, **kwargs: Any):
super().__init__(*args, **kwargs)
self.response_handler = response_handler

def __init_subclass__(cls, **kwargs: Any):
super().__init_subclass__(**kwargs)
for k, v in inspect.getmembers(cls, inspect.isfunction):
setattr(cls, k, WRAPPERS[cls.__name__](v))


class GnApiWrapper(GnApi, MethodWrapper):
def __init__(self, response_handler: ResponseHandler, *args: Any, **kwargs: Any):
super().__init__(*args, **kwargs)
self.response_handler = response_handler


class GsClientWrapper(RestClient, MethodWrapper): # type: ignore
class GnApiWrapper(GnApi):
pass


class GsRestWrapper(RestService): # type: ignore
def __init__(self, *args: Any, **kwargs: Any):
super().__init__(*args, **kwargs)
setattr(
self,
"rest_client",
GsClientWrapper(self.rest_client.url, self.rest_client.auth),
)
pass


class GeorchestraHandler:
def __init__(self, response_handler: ResponseHandler):
self.response_handler = response_handler
def __init__(self, log_handler: LogCollectionHandler):
self.log_handler = log_handler

def get_gn_service(self, instance_name: str, is_source: bool) -> GnApiWrapper:
if not self.response_handler.valid:
if not self.log_handler.valid:
raise RuntimeError(
"GeorchestraHandler context invalid, handler already close"
)
gn_info = self.get_service_info(instance_name, is_source, True)
return GnApiWrapper(self.response_handler, gn_info["url"], gn_info["auth"])
return GnApiWrapper(gn_info["url"], gn_info["auth"])

def get_gs_service(self, instance_name: str, is_source: bool) -> GsRestWrapper:
if not self.response_handler.valid:
if not self.log_handler.valid:
raise RuntimeError(
"GeorchestraHandler context invalid, handler already close"
)
gs_info = self.get_service_info(instance_name, is_source, False)
gsapi = GsRestWrapper(gs_info["url"], gs_info["auth"])
try:
import geoservercloud.services.restclient # type: ignore

geoservercloud.services.restclient.TIMEOUT = 15
resp = gsapi.rest_client.get("/rest/about/version.json")
except HTTPError as err:
if err.response.status_code == 401:
Expand Down Expand Up @@ -116,5 +89,5 @@ def get_service_info(

@contextmanager
def get_georchestra_handler() -> Iterator[GeorchestraHandler]:
with LoggedRequests() as response_handler:
yield GeorchestraHandler(response_handler)
with connect_log_handler() as log_handler:
yield GeorchestraHandler(log_handler)
156 changes: 78 additions & 78 deletions backend/maelstro/core/operations.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,71 @@
from typing import Any, Callable
from contextlib import contextmanager
from typing import Any, Iterator
import logging
from logging import Handler
from requests import Response
from requests.exceptions import RequestException
from fastapi import HTTPException
from fastapi import FastAPI, HTTPException, Request
from geonetwork.gn_logger import logger as gn_logger
from geoservercloud.services.restlogger import gs_logger as gs_logger # type: ignore
from geonetwork.exceptions import GnException


gs_logger = logging.getLogger("GeoOperations")
gs_logger.addHandler(logging.StreamHandler())
gs_logger.setLevel(logging.DEBUG)


class ResponseHandler(Handler):
parent_app = None


def setup_exception_handlers(app: FastAPI) -> None:
global parent_app # pylint: disable=global-statement
parent_app = app

@app.exception_handler(GnException)
def handle_gn_exception(request: Request, err: GnException) -> None:
raise HTTPException(
# 404 not found on lib level will rather be a bad request here
# error 400 is not truncated by the gateway, whereas 404 is
err.code if err.code != 404 else 400,
{
"msg": err.detail.message,
"url": err.parent_response.url,
"operations": parent_app.state.log_handler.get_json_responses(),
"content": err.detail.info,
},
) from err

@app.exception_handler(RequestException)
def handle_gs_exception(request: Request, err: RequestException) -> None:
gs_logger.debug(
"[%s] %s: %s",
request.method,
str(request.url),
err.__class__.__name__,
extra={"response": request},
)
assert parent_app is not None
raise HTTPException(
status_code=504,
detail={
"message": f"HTTP error {err.__class__.__name__} at {request.url}",
"operations": parent_app.state.log_handler.get_json_responses(),
"info": str(err),
},
) from err


def raise_for_status(response: Response) -> None:
global parent_app # pylint: disable=global-variable-not-assigned
if 400 <= response.status_code < 600:
assert parent_app is not None
raise HTTPException(
response.status_code if response.status_code != 404 else 400,
{
"message": f"HTTP error in [{response.request.method}] {response.url}",
"operations": parent_app.state.log_handler.get_json_responses(),
"info": response.text,
},
)


class LogCollectionHandler(Handler):
def __init__(self) -> None:
super().__init__()
self.responses: list[Response | None | dict[str, Any]] = []
Expand Down Expand Up @@ -48,73 +100,21 @@ def get_json_responses(self) -> list[dict[str, Any]]:
return [self.json_response(r) for r in self.responses if r is not None]


def add_gn_handling(app_function: Callable[..., Any]) -> Callable[..., Any]:
def wrapped_function(self, *args, **kwargs): # type: ignore
try:
result = app_function(self, *args, **kwargs)
return result
except GnException as err:
raise HTTPException(
err.code,
{
"msg": err.detail.message,
"url": err.parent_response.url,
"operations": self.response_handler.get_json_responses(),
"content": err.detail.info,
},
) from err

return wrapped_function


def add_gs_handling(app_function: Callable[..., Any]) -> Callable[..., Any]:
METHODS_TO_LOG = ["get", "post", "put", "delete"]

if app_function.__name__ in METHODS_TO_LOG:

def wrapped_function(path, *args, **kwargs): # type: ignore
try:
result = app_function(path, *args, **kwargs)

gs_logger.debug(
"[%s] %s: %s",
app_function.__name__,
result.status_code,
path,
extra={"response": result},
)
return result
except RequestException as err:
gs_logger.debug(
"[%s] %s: %s",
app_function.__name__,
path,
err.__class__.__name__,
extra={"response": err.request},
)
raise HTTPException(
status_code=504,
detail={
"message": f"HTTP error {err.__class__.__name__} at {path}",
"info": err,
},
) from err

return wrapped_function
return app_function


class LoggedRequests:
def __init__(self) -> None:
self.handler = ResponseHandler()

def __enter__(self) -> ResponseHandler:
gn_logger.addHandler(self.handler)
gs_logger.addHandler(self.handler)
self.handler.valid = True
return self.handler

def __exit__(self, *args: Any) -> None:
self.handler.valid = False
gn_logger.removeHandler(self.handler)
gs_logger.removeHandler(self.handler)
@contextmanager
def connect_log_handler() -> Iterator[LogCollectionHandler]:
global parent_app # pylint: disable=global-variable-not-assigned
handler = LogCollectionHandler()
gn_logger.addHandler(handler)
gs_logger.addHandler(handler)
assert parent_app is not None
parent_app.state.log_handler = handler
handler.valid = True
try:
yield handler
except GeneratorExit:
pass
finally:
handler.valid = False
parent_app.state.log_handler = None
gn_logger.removeHandler(handler)
gs_logger.removeHandler(handler)
2 changes: 2 additions & 0 deletions backend/maelstro/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,12 @@
from maelstro.config import app_config as config
from maelstro.metadata import Meta
from maelstro.core import CloneDataset
from maelstro.core.operations import setup_exception_handlers
from maelstro.common.models import SearchQuery


app = FastAPI(root_path="/maelstro-backend")
setup_exception_handlers(app)

app.state.health_countdown = 5

Expand Down
2 changes: 1 addition & 1 deletion backend/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ dependencies = [
"types-pyyaml (>=6.0.12.20241230,<7.0.0.0)",
"lxml-stubs (>=0.5.1,<0.6.0)",
"geonetwork @ git+https://github.com/camptocamp/python-geonetwork@a07d7fba1a53fc9b5840b7bc139fbec418f35366",
"geoservercloud @ git+https://github.com/camptocamp/python-geoservercloud",
"geoservercloud @ git+https://github.com/camptocamp/python-geoservercloud@6defa49bf959d91883ecd43ed1e2e976566e9495",
]

[tool.poetry.group.check]
Expand Down

0 comments on commit e1c7e83

Please sign in to comment.