"""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 over views as convention for clarity See: oto.response for more details. """ from connector_neo4j import Neo4jSession from flask import g as flask_global from flask import request from oto import response from oto.adaptors.flask import flaskify from owsrequest import flask_request from owsrequest.constants import headers as header_constants from product_digital import config from product_digital import api from product_digital.api import app from product_digital.connectors.sentry import sentry_client from product_digital.constants import error from product_digital.constants import header from python_pdp_sdk import ForwardKwargsGetter, UnauthenticatedException from product_digital.exceptions import RequestError from product_digital.logic import hello from product_digital.logic import meta_language from product_digital.logic import product from product_digital.logic import product_workflow from product_digital.logic import release_spatial from product_digital.logic import validation from product_digital.models import ows_product from product_digital.utils import handler_util from product_digital.utils.handler_util import get_account_details, get_orchard_user_id from product_digital.validation import json_schema from product_digital.logic.validate_audio_product_feature_flag_context import ValidateAudioProductFeatureFlagContext @app.route(config.HEALTH_CHECK, methods=['GET']) def health(): """Check the health of the application.""" return flaskify(hello.health_check()) @app.route('/public/meta-language', methods=['GET']) @json_schema.validate_request_headers def get_public_meta_languages(): """Fetch all of the possible meta language options.""" headers = {'Cache-Control': 'max-age=86400'} product_id = request.args.get('productId') return flaskify(meta_language.get_languages(product_id), headers) @app.route('/meta-language', methods=['GET']) @json_schema.validate_request_headers def get_meta_languages(): """Fetch all of the possible meta language options.""" # PP check is observability-only during rollout; legacy fallback is no auth. try: # TODO: once PP is fully trusted, a False result should raise 403 _ = api.authorization_backend.is_authorized( action="view", resource_id=0, resource_type="language", resource_getter=ForwardKwargsGetter() ) except UnauthenticatedException: pass except Exception as e: # PP is observability-only; swallow (but log!) all errors to preserve legacy no-auth behavior flask_global.ows.log.info( f"Error checking PP authorization for meta-language endpoint: {e}", exc_info=True) headers = {'Cache-Control': 'max-age=86400'} product_id = request.args.get('productId') include_instrumental = request.args.get('include_instrumental') == 'true' return flaskify(meta_language.get_languages(product_id, include_instrumental), headers) @app.errorhandler(RequestError) def request_error_handler(error): """Default handler when uncaught exception is raised. Note: Exception will also be sent to Sentry if config.SENTRY is set. Returns: flask.Response: A 500 response with JSON 'code' & 'message' payload. """ flask_global.log.exception(error) sentry_client.captureException() return flaskify(response.create_error_response(error.error_code, error.message, error.status)) @app.errorhandler(500) def exception_handler(error): """Default handler when uncaught exception is raised. 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.') flask_global.log.exception(error) return flaskify(response.create_fatal_response(message)) @app.route('/product/audio', methods=['POST']) @json_schema.validate_request_headers @json_schema.validate_request_body def create_product(): """Create a new audio product. Returns: flask.Response: on successful creation of the prouct, returns a 201 with a JSON representation of the created product. """ headers_response = flask_request.verify_grass_headers(request) if not headers_response: return flaskify(headers_response) product_data = request.get_json() flask_global.ows.log.info('User-assigned UPC: {}'.format(product_data.get('upc'))) orchard_user_id = request.headers.get(header.ORCHARD_USER_ID) if orchard_user_id and orchard_user_id.startswith(header.OA_USER_PREFIX): subaccount_id = product_data.get('subaccount_id') account_id = product_data.get('account_id') if subaccount_id: product_data['account_id'] = subaccount_id product_data['account_type'] = header.GRASS_ACCOUNT_TYPE_SUBACCOUNT else: product_data['account_id'] = account_id product_data['account_type'] = header.GRASS_ACCOUNT_TYPE_VENDOR elif handler_util.both_grass_headers_present(request): product_data['account_type'], product_data['account_id'] = \ handler_util.get_grass_headers(request) return flaskify(product.create(product_data=product_data)) @app.route('/product//copy', methods=['POST']) @json_schema.reject_grass_headers @json_schema.validate_request_headers @json_schema.validate_request_body def copy_product(product_id): """Create a product copy from product_id. Returns: flask.Response: on successful creation of the prouct copy, returns a 201 with a JSON representation of the created product. """ data = request.get_json() copy_product_response = product.copy_product(product_id, data) return flaskify(copy_product_response) @app.route('/product/audio/', methods=['PUT']) @json_schema.validate_request_body def update_product(product_id): """Update an existing audio product. Returns: flask.Response: on successful, 200 status with JSON body. """ headers_response = flask_request.verify_grass_headers(request) if not headers_response: return flaskify(headers_response) flask_global.is_non_destructive_update = request.args.to_dict().get( 'is_non_destructive_update') == 'true' product_data = request.get_json() orchard_user_id = get_orchard_user_id(request) if orchard_user_id and orchard_user_id.startswith(header.OA_USER_PREFIX): subaccount_id = product_data.get('subaccount_id') account_id = product_data.get('account_id') if not subaccount_id and not account_id: # If OA we autopopulate the real owner for validation purposes product_response = ows_product.get_product(product_id) if not product_response: # Throw response as we cannot proceed return flaskify(product_response) subaccount_id = product_response.message.get('subaccount_id') account_id = product_response.message.get('vendor_id') context_type = product_response.message.get('context_type') if subaccount_id: product_data['account_id'] = subaccount_id product_data['account_type'] = \ header.GRASS_ACCOUNT_TYPE_SUBACCOUNT else: product_data['account_id'] = account_id product_data['account_type'] = \ header.GRASS_ACCOUNT_TYPE_VENDOR if context_type: product_data['context_type'] = context_type else: ownership_response = flask_request.verify_grass_ownership( request, handler_util.check_product_ownership, product_id=product_id) if not ownership_response: return flaskify(ownership_response) if handler_util.both_grass_headers_present(request): product_data['account_type'], product_data['account_id'] = \ handler_util.get_grass_headers(request) return flaskify(product.update_audio_product( product_id=product_id, product_data=product_data, orchard_user_id=orchard_user_id, )) @app.route('/product/audio//submit', methods=['POST']) def simple_submit_product(product_id): """Submit a product without validations or side-effects.""" account_id, account_type = get_account_details(request) return flaskify(product.product_submission_workflow(product_id, account_type, account_id, False)) @app.route('/product/audio//status', methods=['PUT']) def submit_product(product_id): """Change the status of an existing product 'transfer_to_content'. :param product_id: The primary key of a product. :return: Response object contain JSON body and a 200 status code. """ headers_response = flask_request.verify_grass_headers(request) if not headers_response: return flaskify(headers_response) orchard_user_id = request.headers.get(header.ORCHARD_USER_ID, '') account_id, account_type = get_account_details(request) identity_id = request.headers.get(header_constants.ORCHARD_IDENTITY_ID, '') valid_identity = handler_util.is_identity_authorized(identity_id) if not valid_identity: ownership_response = flask_request.verify_grass_ownership( request, handler_util.check_product_ownership, product_id=product_id) if not ownership_response: return flaskify(ownership_response) product_response = ows_product.get_product(product_id) if not product_response: return flaskify(product_response) vendor_id = product_response.message.get('vendor_id') ff_context=ValidateAudioProductFeatureFlagContext(vendor_id=vendor_id) return flaskify(product.submit_audio_product( product_id=product_id, account_type=account_type, account_id=account_id, vendor_id=vendor_id, orchard_user_id=orchard_user_id, identity_id=identity_id, ff_context=ff_context, )) @app.route('/product/audio/', methods=['GET']) @json_schema.validate_request_headers def get_product(product_id): """Fetch an existing audio product. Returns: flask.Response: on successful, 200 status with JSON body. """ headers_response = flask_request.verify_grass_headers(request) if not headers_response: return flaskify(headers_response) ownership_response = flask_request.verify_grass_ownership( request, handler_util.check_product_ownership, product_id=product_id) if not ownership_response: return flaskify(ownership_response) orchard_user_id = '' if header.ORCHARD_USER_ID in request.headers: orchard_user_id = request.headers[header.ORCHARD_USER_ID] return flaskify(product.get_audio_product(product_id, orchard_user_id)) @app.route('/product/audio/dataloader', methods=['POST']) @json_schema.validate_request_headers def dataload_products(): """Fetch an existing audio products. Returns: flask.Response: on successful, 201 status with JSON body. """ product_ids = request.get_json() if not isinstance(product_ids, list) or not all( isinstance(item, int) for item in product_ids ): return flaskify( response.create_error_response( error.ERROR_CODE_INVALID_INPUT, error.ERROR_MESSAGE_INVALID_DATALOAD, status=400, ) ) headers_response = flask_request.verify_grass_headers(request) if not headers_response: return flaskify(headers_response) for product_id in product_ids: ownership_response = flask_request.verify_grass_ownership( request, handler_util.check_product_ownership, product_id=product_id) if not ownership_response: return flaskify(ownership_response) orchard_user_id = '' if header.ORCHARD_USER_ID in request.headers: orchard_user_id = request.headers[header.ORCHARD_USER_ID] return flaskify(product.get_audio_products(product_ids, orchard_user_id)) @app.route('/product/audio/validate/dataloader', methods=['POST']) @json_schema.validate_request_headers @Neo4jSession(database=config.NEO4J_DATABASE_NAME) def dataload_product_audio_validation(): """Dataload the validation information for products. Returns: flask.Response: on success, 200 status with JSON body """ headers_response = flask_request.verify_grass_headers(request) if not headers_response: return flaskify(headers_response) payload = request.get_json() if ( not isinstance(payload, dict) or not payload.get('product_ids') or not all(isinstance(x, int) for x in payload.get('product_ids', [])) ): return flaskify( response.create_error_response( error.ERROR_CODE_INVALID_INPUT, error.ERROR_MESSAGE_INVALID_DATALOAD, status=400, ) ) product_ids = payload.get('product_ids', []) for product_id in product_ids: ownership_response = flask_request.verify_grass_ownership( request, handler_util.check_product_ownership, product_id=product_id) if not ownership_response: return flaskify(ownership_response) validation_contexts = { 'pre_submission', 'post_submission', } validation_context = payload.get('validation_context', 'pre_submission') is_known_validation_context = ( validation_context and validation_context in validation_contexts) if not is_known_validation_context: raise Exception('Unknown validation context.') elif validation_context == 'post_submission': has_access = flask_global.request_context.profile_type == ( header_constants.PROFILE_TYPE_CONTENT) if not has_access: raise Exception('No access to validation context.') setattr(flask_global, 'validation_context', validation_context) account_type = request.headers.get(header.GRASS_ACCOUNT_TYPE, '') account_id = request.headers.get(header.GRASS_ACCOUNT_ID, '') orchard_user_id = request.headers.get(header.ORCHARD_USER_ID, '') response_format = request.args.get('format', 'original') result = product.dataload_product_audio_validation( product_ids=product_ids, account_type=account_type, account_id=account_id, response_format=response_format, orchard_user_id=orchard_user_id, validation_context=validation_context, ) return flaskify(result) @app.route('/product/audio//validate', methods=['GET']) @json_schema.validate_request_headers @Neo4jSession(database=config.NEO4J_DATABASE_NAME) def get_product_audio_validate(product_id): """Get the validation information for a product. Args: product_id (int): ID of the product to validate Returns: flask.Response: on success, 200 status with JSON body """ headers_response = flask_request.verify_grass_headers(request) if not headers_response: return flaskify(headers_response) ownership_response = flask_request.verify_grass_ownership( request, handler_util.check_product_ownership, product_id=product_id) if not ownership_response: return flaskify(ownership_response) validation_contexts = { 'pre_submission', 'post_submission', } validation_context = request.args.get( 'validation_context', 'pre_submission') is_known_validation_context = ( validation_context and validation_context in validation_contexts) if not is_known_validation_context: raise Exception('Unknown validation context.') elif validation_context == 'post_submission': has_access = flask_global.request_context.profile_type == ( header_constants.PROFILE_TYPE_CONTENT) if not has_access: raise Exception('No access to validation context.') setattr(flask_global, 'validation_context', validation_context) account_type = request.headers.get(header.GRASS_ACCOUNT_TYPE, '') account_id = request.headers.get(header.GRASS_ACCOUNT_ID, '') orchard_user_id = request.headers.get(header.ORCHARD_USER_ID, '') product_response = ows_product.get_product(product_id) if not product_response: # Throw response as we cannot proceed return flaskify(product_response) vendor_id = product_response.message.get('vendor_id') ff_context=ValidateAudioProductFeatureFlagContext(vendor_id=vendor_id) original_response = product.validate_audio_product( product_id, account_type, account_id, vendor_id, orchard_user_id, validation_context, ff_context=ff_context, ) response_format = request.args.get('format', 'original') if response_format == 'list': return flaskify(validation.format_as_list(original_response)) return flaskify(original_response) @app.route('/product//scheduling-and-pricing', methods=['GET']) @json_schema.validate_request_headers def get_scheduling_and_pricing(product_id): """Get scheduling and pricing information for a product. Args: product_id (int): ID of the product to validate Returns: flask.Response: on success, 200 status with JSON body """ headers_response = flask_request.verify_grass_headers(request) if not headers_response: return flaskify(headers_response) ownership_response = flask_request.verify_grass_ownership( request, handler_util.check_product_ownership, product_id=product_id) if not ownership_response: return flaskify(ownership_response) result = product.get_product_scheduling_and_pricing(product_id) return flaskify(result) @app.route('/release-approval/rejections', methods=['PUT']) @json_schema.validate_request_headers @json_schema.validate_request_body def update_rejections(): """Update properties for a set of rejections. Returns: flask.Response: on success, 200 status with JSON body """ headers_response = flask_request.verify_grass_headers(request) if not headers_response: return flaskify(headers_response) return flaskify(product_workflow.update_rejections(request.get_json())) @app.route('/product//tracks/grats', methods=['GET']) def get_grats_for_product(product_id): """Fetch all active instant grats for a product. Returns: flask.Response: on success, 200 status with JSON body """ headers_response = flask_request.verify_grass_headers(request) if not headers_response: return flaskify(headers_response) ownership_response = flask_request.verify_grass_ownership( request, handler_util.check_product_ownership, product_id=product_id) if not ownership_response: return flaskify(ownership_response) return flaskify(product.get_product_instant_grats(product_id)) @app.route('/product//correction', methods=['POST']) @json_schema.validate_request_headers def create_product_correction(product_id): """Create a new error correction for the given product. Args: product_id (int): ID of the product to validate Returns: flask.Response: on success, 200 status with JSON body """ headers_response = flask_request.verify_grass_headers(request) if not headers_response: return flaskify(headers_response) ownership_response = flask_request.verify_grass_ownership( request, handler_util.check_product_ownership, product_id=product_id) if not ownership_response: return flaskify(ownership_response) correction_data = request.get_json() orchard_user_id = request.headers.get(header.ORCHARD_USER_ID, '') return flaskify(product_workflow.create_release_correction( product_id, correction_data, orchard_user_id)) @app.route('/product//correction/', methods=['DELETE']) @json_schema.validate_request_headers def delete_release_correction(product_id, release_correction_id): """Delete an active release correction for a product. Args: product_id (int): ID of the product to validate release_correction_id (int): ID of the release correction to delete Returns: flask.Response: on success, 200 status with JSON body """ headers_response = flask_request.verify_grass_headers(request) if not headers_response: return flaskify(headers_response) ownership_response = flask_request.verify_grass_ownership( request, handler_util.check_product_ownership, product_id=product_id) if not ownership_response: return flaskify(ownership_response) product_response = ows_product.get_product(product_id) if not product_response: return flaskify(product_response) return flaskify(product_workflow.delete_release_correction( product_id, release_correction_id)) @app.route( '/product//correction//details', methods=['POST']) @json_schema.validate_request_headers def create_correction_details(product_id, release_correction_id): """Create the correction details for the given id and details. Args: product_id (int): ID of the product to validate release_correction_id (int): ID of the release correction Returns: flask.Response: on success, 200 status with JSON body """ headers_response = flask_request.verify_grass_headers(request) if not headers_response: return flaskify(headers_response) ownership_response = flask_request.verify_grass_ownership( request, handler_util.check_product_ownership, product_id=product_id) if not ownership_response: return flaskify(ownership_response) correction_data = request.get_json() orchard_user_id = request.headers.get(header.ORCHARD_USER_ID, '') return flaskify(product_workflow.create_correction_details_records( product_id=product_id, release_correction_id=release_correction_id, correction_data=correction_data, orchard_user_id=orchard_user_id, )) @app.route('/product//mkt_priority', methods=['GET']) def get_mkt_priority(product_id): """Fetch an mkt priority of product. Returns: flask.Response: on successful, 200 status with JSON body. """ headers_response = flask_request.verify_grass_headers(request) if not headers_response: return flaskify(headers_response) return flaskify(product.get_mkt_priority( product_id)) @app.route('/product/mkt_priority/dataloader', methods=['POST']) def dataload_mkt_priority(): """Fetch bulk product mkt priorities. Returns: flask.Response: on successful, 200 status with JSON body. """ product_ids = request.get_json() if not isinstance(product_ids, list) or not all( isinstance(item, int) for item in product_ids ): return flaskify( response.create_error_response( error.ERROR_CODE_INVALID_INPUT, error.ERROR_MESSAGE_INVALID_DATALOAD, status=400, ) ) headers_response = flask_request.verify_grass_headers(request) if not headers_response: return flaskify(headers_response) return flaskify(product.get_bulk_mkt_priority( product_ids)) @app.route('/product//mkt_priority', methods=['POST']) def set_mkt_priority(product_id): """Set an mkt priority of product. Must be an oa user or a microservice-to-microservice call. Returns: flask.Response: on successful, 200 status with JSON body. """ headers_response = flask_request.verify_grass_headers(request, required=False) if not headers_response: return flaskify(headers_response) orchard_user_id = request.headers.get(header.ORCHARD_USER_ID) user_id = None if orchard_user_id: is_oa = orchard_user_id.startswith(header.OA_USER_PREFIX) if not is_oa: return flaskify(response.create_fatal_response(error.ERROR_MESSAGE_FORBIDDEN_USER)) user_id = int(orchard_user_id.split(':')[1]) data = request.get_json() return flaskify(product.set_mkt_priority( product_id, data['country_id'], data['priority'], user_id )) @app.route('/product//mkt_priority/', methods=['DELETE']) def delete_mkt_priority(product_id, country_id): """Delete a mkt priority of product. Must be an oa user or a microservice-to-microservice call. Returns: flask.Response: on successful, 200 status with JSON body. """ headers_response = flask_request.verify_grass_headers(request, required=False) if not headers_response: return flaskify(headers_response) orchard_user_id = request.headers.get(header.ORCHARD_USER_ID) if orchard_user_id: is_oa = orchard_user_id.startswith(header.OA_USER_PREFIX) if not is_oa: return flaskify(response.create_fatal_response(error.ERROR_MESSAGE_FORBIDDEN_USER)) return flaskify(product.delete_mkt_priority( product_id, country_id)) @app.route('/product/audio//unsubmit', methods=['PUT']) def unsubmit_product(product_id): """Change the status of an existing product to 'label_processing'. :param product_id: The primary key of a product. :return: Response object contain JSON body and a 200 status code. """ headers_response = flask_request.verify_grass_headers(request) if not headers_response: return flaskify(headers_response) ownership_response = flask_request.verify_grass_ownership( request, handler_util.check_product_ownership, product_id=product_id) if not ownership_response: return flaskify(ownership_response) account_id = request.headers.get(header.GRASS_ACCOUNT_ID, '') return flaskify(product.unsubmit_audio_product( product_id, account_id, )) @app.route('/product/audio//reject', methods=['PUT']) def reject_product_from_productreview(product_id): """Change the status of an existing product to 'label_processing'. Does not remove the record from the `release_approval_queue` table. Used when rejecting product via ows-product-review. :param product_id: The primary key of a product. :return: Response object contain JSON body and a 200 status code. """ headers_response = flask_request.verify_grass_headers(request) if not headers_response: return flaskify(headers_response) ownership_response = flask_request.verify_grass_ownership( request, handler_util.check_product_ownership, product_id=product_id) if not ownership_response: return flaskify(ownership_response) return flaskify(product.reject_audio_product_from_productreview( product_id )) @app.route('/product/audio//approve', methods=['PUT']) def approve_product_from_productreview(product_id): """Emulate the approval process in OA. Changes the status of an existing product to 'in_content'. Sets ingestion_completed field to current time. Adds new release_status table record. Does not remove the record from the `release_approval_queue` table. Used when approving product via ows-product-review. :param product_id: The primary key of a product. :return: Response object contain JSON body and a 200 status code. """ headers_response = flask_request.verify_grass_headers(request) if not headers_response: return flaskify(headers_response) ownership_response = flask_request.verify_grass_ownership( request, handler_util.check_product_ownership, product_id=product_id) if not ownership_response: return flaskify(ownership_response) return flaskify(product.approve_audio_product_from_productreview( product_id )) @app.route('/product//not_for_distribution', methods=['PUT']) def update_not_for_distribution(product_id): """Update the not_for_distribution flag and product status of a given product. :param product_id: The primary key of a product. :return: Response object contain JSON body and a 200 status code. """ headers_response = flask_request.verify_grass_headers(request) if not headers_response: return flaskify(headers_response) if handler_util.both_grass_headers_present(request): response_object = response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message=error.ERROR_MESSAGE_FORBIDDEN_USER, status=403) return flaskify(response_object) product_data = request.get_json() not_for_distribution = product_data.get('not_for_distribution') release_status = product_data.get('release_status') return flaskify(product.update_not_for_distribution( product_id, not_for_distribution, release_status)) @app.route('/product//approve', methods=['PUT']) def approve(product_id): """Change the release_status of an existing product to in_content. :param product_id: The primary key of a product. :return: Response object contain JSON body and a 200 status code. """ headers_response = flask_request.verify_grass_headers(request) if not headers_response: return flaskify(headers_response) # Don't allow other microservices to approve a product. if handler_util.both_grass_headers_present(request): response_object = response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message=error.ERROR_MESSAGE_FORBIDDEN_USER, status=403) return flaskify(response_object) # Only allow OA users to approve a product. orchard_user_id = request.headers.get(header.ORCHARD_USER_ID) if orchard_user_id and not orchard_user_id.startswith(header.OA_USER_PREFIX): response_object = response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message=error.ERROR_MESSAGE_FORBIDDEN_USER, status=403) return flaskify(response_object) split_user_id = orchard_user_id.split(':') changed_by = split_user_id[1] changed_by_type = split_user_id[0] return flaskify(product.approve(product_id, changed_by, changed_by_type)) @app.route('/product/', methods=['DELETE']) @json_schema.validate_request_headers def delete(product_id): """Delete an audio product. Returns: flask.Response: on successful deletion of the prouct, returns 200. """ account_type = request.headers.get(header.GRASS_ACCOUNT_TYPE) account_id = request.headers.get(header.GRASS_ACCOUNT_ID) return flaskify( product.delete( product_id, account_type=account_type, account_id=account_id, )) @app.route('/product//mkt_priority', methods=['DELETE']) def delete_mkt_priority_in_bulk(product_id): """Delete a mkt priority of product. Must be an oa user or a microservice-to-microservice call. Returns: flask.Response: on successful, 200 status with JSON body. """ headers_response = flask_request.verify_grass_headers(request, required=False) if not headers_response: return flaskify(headers_response) orchard_user_id = request.headers.get(header.ORCHARD_USER_ID) if orchard_user_id: is_oa = orchard_user_id.startswith(header.OA_USER_PREFIX) if not is_oa: return flaskify(response.create_fatal_response(error.ERROR_MESSAGE_FORBIDDEN_USER)) return flaskify(product.delete_mkt_priority_for_product(product_id)) @app.route('/product//spatial', methods=['GET']) @json_schema.validate_request_headers def get_product_spatial(product_id): """Get the spatial UPC for a product. Args: product_id (int): ID of the product. Returns: flask.Response: on success, 200 status with JSON body. """ headers_response = flask_request.verify_grass_headers(request) if not headers_response: return flaskify(headers_response) ownership_response = flask_request.verify_grass_ownership( request, handler_util.check_product_ownership, product_id=product_id) if not ownership_response: return flaskify(ownership_response) return flaskify(release_spatial.get_release_spatial(product_id)) @app.route('/products//spatial', methods=['DELETE']) @json_schema.validate_request_headers def delete_product_spatial(product_id): """Delete the spatial UPC record for a product. Args: product_id (int): ID of the product. Returns: flask.Response: on success, 200 status with JSON body. """ headers_response = flask_request.verify_grass_headers(request) if not headers_response: return flaskify(headers_response) ownership_response = flask_request.verify_grass_ownership( request, handler_util.check_product_ownership, product_id=product_id) if not ownership_response: return flaskify(ownership_response) return flaskify(release_spatial.delete_release_spatial(product_id)) @app.route('/product//spatial', methods=['POST']) @json_schema.validate_request_headers def create_product_spatial(product_id): """Create a spatial UPC record for a product. If no spatial_upc is provided in the request body, one is provisioned automatically. Args: product_id (int): ID of the product. Returns: flask.Response: 200 with spatial data, 409 if a record already exists. """ headers_response = flask_request.verify_grass_headers(request) if not headers_response: return flaskify(headers_response) ownership_response = flask_request.verify_grass_ownership( request, handler_util.check_product_ownership, product_id=product_id) if not ownership_response: return flaskify(ownership_response) data = request.get_json() or {} return flaskify(release_spatial.create_release_spatial(product_id, data))