"""Application Handlers. Requests are redirected to handlers, which are responsible for getting information from the URL and passing it down to the logic layer. The way each layer talks to each other is through Response objects which defines the type status of the data and the data itself. Please note: the Orchard uses the term handlers overviews as convention for clarity See: owsresponse.response for more details. """ import ast import json from flask import Response, jsonify, request from owsresponse import response, status from owsresponse.adaptors.flask import flaskify from pydantic import ValidationError from requests import HTTPError, RequestException from werkzeug.exceptions import HTTPException, NotFound from vectororder import config from vectororder.api import app from vectororder.constants import STATUS_BY_CATEGORY, fields from vectororder.constants.delivery_statuses import DELIVERY_STATUSES from vectororder.constants.encoding_statuses import ENCODING_STATUSES from vectororder.exceptions import InvalidOrderRequest, RequestError from vectororder.logic import encoders, orders, priorities from vectororder.models.schemas import OrderData, OrderStatus @app.route(config.HEALTH_CHECK, methods=["GET"]) def health() -> Response: """Check the health of the application.""" return jsonify({"status": "ok"}) @app.errorhandler(Exception) def exception_handler(error: Exception) -> Response: """Handle error when uncaught exception is raised. Default exception handler. Note: Exception will also be sent to Sentry if config.SENTRY is set. Returns: flask.Response: A 500 response with JSON 'code' & 'message' payload. """ message = ( "The server encountered an internal error " "and was unable to complete your request." ) app.logger.exception(error) return flaskify(response.create_fatal_response(message)) @app.errorhandler(HTTPException) def http_exception_handler(exc: HTTPException) -> Response: """Handle error when HTTPException is raised. Returns: Response: A HTTP status code response with JSON 'code' & 'message' payload. """ if isinstance(exc, NotFound): app.logger.warning(exc) else: app.logger.exception(exc) return flaskify( response.create_error_response( code=type(exc).__name__, message=str(exc.description), status=exc.code, ) ) @app.errorhandler(RequestException) @app.errorhandler(HTTPError) def request_exception_handler( error: RequestException | HTTPError, ) -> Response: """Handle request errors.""" return flaskify( response.create_error_response( code=type(error).__name__, message=str(error), status=status.BAD_GATEWAY, ) ) @app.errorhandler(RequestError) def handle_request_error(error: RequestError) -> Response: """Handle request and validation errors.""" return error.make_flask_response() @app.route("/priorities", methods=["GET"]) def get_priorities() -> Response: """Get possible priorities for a Vector order.""" return jsonify({fields.RESPONSE_ITEMS: priorities.get_priorities()}) @app.route("/statuses", methods=["GET"]) def get_statuses() -> Response: """Get possible Vector order statuses by category.""" return Response( STATUS_BY_CATEGORY.model_dump_json(indent=2), mimetype="application/json" ) @app.route("/orders/", methods=["GET"]) def get_order(order_id: int) -> Response: """Get vector order general information. Args: order_id (int): Vector order ID. """ try: include_stores = ast.literal_eval(request.args.get("stores", "False")) except ValueError: include_stores = False return flaskify(response.Response(orders.get_order(int(order_id), include_stores))) @app.route("/orders", methods=["GET"]) def get_orders() -> Response: """Get Vector orders with basic information.""" try: include_stores = ast.literal_eval(request.args.get("stores", "False")) except ValueError: include_stores = False return flaskify( response.Response( { fields.RESPONSE_ITEMS: orders.get_orders( [int(v) for v in request.args.get("order_id", "").split(",")], bool(include_stores), ), } ) ) @app.route("/encoders", methods=["GET"]) def get_encoders() -> Response: """Get possible Vector order encoders.""" return flaskify(response.Response(encoders.get_encoders())) @app.route("/encoding-statuses", methods=["GET"]) def get_encoding_statuses() -> Response: """Get possible Vector order encoding statuses.""" return flaskify(response.Response(ENCODING_STATUSES)) @app.route("/delivery-statuses", methods=["GET"]) def get_delivery_statuses() -> Response: """Get possible Vector order delivery statuses.""" return flaskify(response.Response(DELIVERY_STATUSES)) @app.route("/orders", methods=["POST"]) def create_order() -> Response: """Create vector order.""" payload = request.get_json() order_data = OrderData(**payload) if order_data.order_status == OrderStatus.CLOSED and ( not order_data.upcs or not order_data.store_ids ): raise InvalidOrderRequest( "Order status cannot be closed when there's no UPC's or stores provided." ) order_id = orders.create_order(order_data) return jsonify({"order_id": order_id}) @app.route("/orders/", methods=["PATCH"]) def update_order(order_id: int) -> Response: """Close vector order.""" payload = request.get_json() try: order_status = OrderStatus(payload.get("order_status", OrderStatus.OPEN)) except ValueError as e: raise InvalidOrderRequest( f"Order status '{payload.get('order_status')}' not allowed." ) from e orders.update_order(order_id, set(payload.get("upcs", [])), order_status) return Response(status=204) @app.errorhandler(ValidationError) def pydantic_validation_error_handler(exc: ValidationError) -> Response: app.logger.exception(exc) return Response( response=json.dumps( { "code": type(exc).__name__, "message": str(exc), } ), status=422, )