Best Python code snippet using localstack_python
helpers.py
Source:helpers.py  
1import contextlib2import datetime3import json4import logging5import re6import time7from typing import Any, Dict, List, Optional, Tuple, Union8from urllib import parse as urlparse9import pytz10from botocore.utils import InvalidArnException11from jsonpatch import apply_patch12from jsonpointer import JsonPointerException13from moto.apigateway import models as apigateway_models14from moto.apigateway.utils import create_id as create_resource_id15from requests.models import Response16from localstack import config17from localstack.constants import (18    APPLICATION_JSON,19    HEADER_LOCALSTACK_EDGE_URL,20    LOCALHOST_HOSTNAME,21    PATH_USER_REQUEST,22    TEST_AWS_ACCOUNT_ID,23)24from localstack.services.apigateway.context import ApiInvocationContext25from localstack.services.generic_proxy import RegionBackend26from localstack.utils import common27from localstack.utils.aws import aws_stack28from localstack.utils.aws.aws_responses import requests_error_response_json, requests_response29from localstack.utils.aws.aws_stack import parse_arn30from localstack.utils.aws.request_context import MARKER_APIGW_REQUEST_REGION, THREAD_LOCAL31from localstack.utils.strings import long_uid32LOG = logging.getLogger(__name__)33REQUEST_TIME_DATE_FORMAT = "%d/%b/%Y:%H:%M:%S %z"34# regex path pattern for user requests, handles stages like $default35PATH_REGEX_USER_REQUEST = (36    r"^/restapis/([A-Za-z0-9_\\-]+)(?:/([A-Za-z0-9\_($|%%24)\\-]+))?/%s/(.*)$" % PATH_USER_REQUEST37)38# URL pattern for invocations39HOST_REGEX_EXECUTE_API = r"(?:.*://)?([a-zA-Z0-9-]+)\.execute-api\.(localhost.localstack.cloud|([^\.]+)\.amazonaws\.com)(.*)"40# regex path patterns41PATH_REGEX_MAIN = r"^/restapis/([A-Za-z0-9_\-]+)/[a-z]+(\?.*)?"42PATH_REGEX_SUB = r"^/restapis/([A-Za-z0-9_\-]+)/[a-z]+/([A-Za-z0-9_\-]+)/.*"43# path regex patterns44PATH_REGEX_AUTHORIZERS = r"^/restapis/([A-Za-z0-9_\-]+)/authorizers/?([^?/]+)?(\?.*)?"45PATH_REGEX_VALIDATORS = r"^/restapis/([A-Za-z0-9_\-]+)/requestvalidators/?([^?/]+)?(\?.*)?"46PATH_REGEX_RESPONSES = r"^/restapis/([A-Za-z0-9_\-]+)/gatewayresponses(/[A-Za-z0-9_\-]+)?(\?.*)?"47PATH_REGEX_DOC_PARTS = r"^/restapis/([A-Za-z0-9_\-]+)/documentation/parts/?([^?/]+)?(\?.*)?"48PATH_REGEX_PATH_MAPPINGS = r"/domainnames/([^/]+)/basepathmappings/?(.*)"49PATH_REGEX_CLIENT_CERTS = r"/clientcertificates/?([^/]+)?$"50PATH_REGEX_VPC_LINKS = r"/vpclinks/([^/]+)?(.*)"51PATH_REGEX_TEST_INVOKE_API = r"^\/restapis\/([A-Za-z0-9_\-]+)\/resources\/([A-Za-z0-9_\-]+)\/methods\/([A-Za-z0-9_\-]+)/?(\?.*)?"52# template for SQS inbound data53APIGATEWAY_SQS_DATA_INBOUND_TEMPLATE = (54    "Action=SendMessage&MessageBody=$util.base64Encode($input.json('$'))"55)56# special tag name to allow specifying a custom ID for new REST APIs57TAG_KEY_CUSTOM_ID = "_custom_id_"58# map API IDs to region names - TODO remove and replace with in-memory lookup59API_REGIONS = {}60# TODO: make the CRUD operations in this file generic for the different model types (authorizes, validators, ...)61class APIGatewayRegion(RegionBackend):62    # TODO: introduce a RestAPI class to encapsulate the variables below63    # maps (API id) -> [authorizers]64    authorizers: Dict[str, List[Dict]]65    # maps (API id) -> [validators]66    validators: Dict[str, List[Dict]]67    # maps (API id) -> [documentation_parts]68    documentation_parts: Dict[str, List[Dict]]69    # maps (API id) -> [gateway_responses]70    gateway_responses: Dict[str, List[Dict]]71    # account details72    account: Dict[str, Any]73    # maps (domain_name) -> [path_mappings]74    base_path_mappings: Dict[str, List[Dict]]75    # maps ID to VPC link details76    vpc_links: Dict[str, Dict]77    # maps cert ID to client certificate details78    client_certificates: Dict[str, Dict]79    # maps resource ARN to tags80    TAGS: Dict[str, Dict[str, str]] = {}81    def __init__(self):82        self.authorizers = {}83        self.validators = {}84        self.documentation_parts = {}85        self.gateway_responses = {}86        self.account = {87            "cloudwatchRoleArn": aws_stack.role_arn("api-gw-cw-role"),88            "throttleSettings": {"burstLimit": 1000, "rateLimit": 500},89            "features": ["UsagePlans"],90            "apiKeyVersion": "1",91        }92        self.base_path_mappings = {}93        self.vpc_links = {}94        self.client_certificates = {}95class Resolver:96    def __init__(self, document: dict, allow_recursive=True):97        self.document = document98        self.allow_recursive = allow_recursive99        # cache which maps known refs to part of the document100        self._cache = {}101        self._refpaths = ["#"]102    def _is_ref(self, item) -> bool:103        return isinstance(item, dict) and "$ref" in item104    def _is_internal_ref(self, refpath) -> bool:105        return str(refpath).startswith("#/")106    @property107    def current_path(self):108        return self._refpaths[-1]109    @contextlib.contextmanager110    def _pathctx(self, refpath: str):111        if not self._is_internal_ref(refpath):112            refpath = "/".join((self.current_path, refpath))113        self._refpaths.append(refpath)114        yield115        self._refpaths.pop()116    def _resolve_refpath(self, refpath: str) -> dict:117        if refpath in self._refpaths and not self.allow_recursive:118            raise Exception("recursion detected with allow_recursive=False")119        if refpath in self._cache:120            return self._cache.get(refpath)121        with self._pathctx(refpath):122            if self._is_internal_ref(self.current_path):123                cur = self.document124            else:125                raise NotImplementedError("External references not yet supported.")126            for step in self.current_path.split("/")[1:]:127                cur = cur.get(step)128            self._cache[self.current_path] = cur129            return cur130    def _namespaced_resolution(self, namespace: str, data: Union[dict, list]) -> Union[dict, list]:131        with self._pathctx(namespace):132            return self._resolve_references(data)133    def _resolve_references(self, data) -> Union[dict, list]:134        if self._is_ref(data):135            return self._resolve_refpath(data["$ref"])136        if isinstance(data, dict):137            for k, v in data.items():138                data[k] = self._namespaced_resolution(k, v)139        elif isinstance(data, list):140            for i, v in enumerate(data):141                data[i] = self._namespaced_resolution(str(i), v)142        return data143    def resolve_references(self) -> dict:144        return self._resolve_references(self.document)145def resolve_references(data: dict, allow_recursive=True) -> dict:146    resolver = Resolver(data, allow_recursive=allow_recursive)147    return resolver.resolve_references()148def make_json_response(message):149    return requests_response(json.dumps(message), headers={"Content-Type": APPLICATION_JSON})150def make_error_response(message, code=400, error_type=None):151    if code == 404 and not error_type:152        error_type = "NotFoundException"153    error_type = error_type or "InvalidRequest"154    return requests_error_response_json(message, code=code, error_type=error_type)155def make_accepted_response():156    response = Response()157    response.status_code = 202158    return response159def get_api_id_from_path(path):160    if match := re.match(PATH_REGEX_SUB, path):161        return match.group(1)162    return re.match(PATH_REGEX_MAIN, path).group(1)163def is_test_invoke_method(method, path):164    return method == "POST" and bool(re.match(PATH_REGEX_TEST_INVOKE_API, path))165def get_stage_variables(context: ApiInvocationContext) -> Optional[Dict[str, str]]:166    if is_test_invoke_method(context.method, context.path):167        return None168    if not context.stage:169        return {}170    region_name = [171        name172        for name, region in apigateway_models.apigateway_backends.items()173        if context.api_id in region.apis174    ][0]175    api_gateway_client = aws_stack.connect_to_service("apigateway", region_name=region_name)176    try:177        response = api_gateway_client.get_stage(restApiId=context.api_id, stageName=context.stage)178        return response.get("variables")179    except Exception:180        LOG.info("Failed to get stage %s for API id %s", context.stage, context.api_id)181        return {}182# -----------------------183# GATEWAY RESPONSES APIs184# -----------------------185# TODO: merge with to_response_json(..) above186def gateway_response_to_response_json(item, api_id):187    base_path = "/restapis/%s/gatewayresponses" % api_id188    item["_links"] = {189        "self": {"href": "%s/%s" % (base_path, item["responseType"])},190        "gatewayresponse:put": {191            "href": "%s/{response_type}" % base_path,192            "templated": True,193        },194        "gatewayresponse:update": {"href": "%s/%s" % (base_path, item["responseType"])},195    }196    item["responseParameters"] = item.get("responseParameters", {})197    item["responseTemplates"] = item.get("responseTemplates", {})198    return item199def get_gateway_responses(api_id):200    region_details = APIGatewayRegion.get()201    result = region_details.gateway_responses.get(api_id, [])202    href = "http://docs.aws.amazon.com/apigateway/latest/developerguide/restapi-gatewayresponse-{rel}.html"203    base_path = "/restapis/%s/gatewayresponses" % api_id204    result = {205        "_links": {206            "curies": {"href": href, "name": "gatewayresponse", "templated": True},207            "self": {"href": base_path},208            "first": {"href": base_path},209            "gatewayresponse:by-type": {210                "href": "%s/{response_type}" % base_path,211                "templated": True,212            },213            "item": [{"href": "%s/%s" % (base_path, r["responseType"])} for r in result],214        },215        "_embedded": {"item": [gateway_response_to_response_json(i, api_id) for i in result]},216        # Note: Looks like the format required by aws CLI ("item" at top level) differs from the docs:217        # https://docs.aws.amazon.com/apigateway/api-reference/resource/gateway-responses/218        "item": [gateway_response_to_response_json(i, api_id) for i in result],219    }220    return result221def get_gateway_response(api_id, response_type):222    region_details = APIGatewayRegion.get()223    responses = region_details.gateway_responses.get(api_id, [])224    result = [r for r in responses if r["responseType"] == response_type]225    if result:226        return result[0]227    return make_error_response(228        "Gateway response %s for API Gateway %s not found" % (response_type, api_id),229        code=404,230    )231def put_gateway_response(api_id, response_type, data):232    region_details = APIGatewayRegion.get()233    responses = region_details.gateway_responses.setdefault(api_id, [])234    existing = ([r for r in responses if r["responseType"] == response_type] or [None])[0]235    if existing:236        existing.update(data)237    else:238        data["responseType"] = response_type239        responses.append(data)240    return data241def delete_gateway_response(api_id, response_type):242    region_details = APIGatewayRegion.get()243    responses = region_details.gateway_responses.get(api_id) or []244    region_details.gateway_responses[api_id] = [245        r for r in responses if r["responseType"] != response_type246    ]247    return make_accepted_response()248def update_gateway_response(api_id, response_type, data):249    region_details = APIGatewayRegion.get()250    responses = region_details.gateway_responses.setdefault(api_id, [])251    existing = ([r for r in responses if r["responseType"] == response_type] or [None])[0]252    if existing is None:253        return make_error_response(254            "Gateway response %s for API Gateway %s not found" % (response_type, api_id),255            code=404,256        )257    result = apply_json_patch_safe(existing, data["patchOperations"])258    return result259def handle_gateway_responses(method, path, data, headers):260    search_match = re.search(PATH_REGEX_RESPONSES, path)261    api_id = search_match.group(1)262    response_type = (search_match.group(2) or "").lstrip("/")263    if method == "GET":264        if response_type:265            return get_gateway_response(api_id, response_type)266        return get_gateway_responses(api_id)267    if method == "PUT":268        return put_gateway_response(api_id, response_type, data)269    if method == "PATCH":270        return update_gateway_response(api_id, response_type, data)271    if method == "DELETE":272        return delete_gateway_response(api_id, response_type)273    return make_error_response(274        "Not implemented for API Gateway gateway responses: %s" % method, code=404275    )276# ---------------277# UTIL FUNCTIONS278# ---------------279def find_api_subentity_by_id(api_id, entity_id, map_name):280    region_details = APIGatewayRegion.get()281    auth_list = getattr(region_details, map_name).get(api_id) or []282    entity = ([a for a in auth_list if a["id"] == entity_id] or [None])[0]283    return entity284def path_based_url(api_id, stage_name, path):285    """Return URL for inbound API gateway for given API ID, stage name, and path"""286    pattern = "%s/restapis/{api_id}/{stage_name}/%s{path}" % (287        config.service_url("apigateway"),288        PATH_USER_REQUEST,289    )290    return pattern.format(api_id=api_id, stage_name=stage_name, path=path)291def host_based_url(rest_api_id: str, path: str, stage_name: str = None):292    """Return URL for inbound API gateway for given API ID, stage name, and path with custom dns293    format"""294    pattern = "http://{endpoint}{stage}{path}"295    stage = stage_name and f"/{stage_name}" or ""296    return pattern.format(endpoint=get_execute_api_endpoint(rest_api_id), stage=stage, path=path)297def get_execute_api_endpoint(api_id: str, protocol: str = "") -> str:298    port = config.get_edge_port_http()299    return f"{protocol}{api_id}.execute-api.{LOCALHOST_HOSTNAME}:{port}"300def tokenize_path(path):301    return path.lstrip("/").split("/")302def extract_path_params(path: str, extracted_path: str) -> Dict[str, str]:303    tokenized_extracted_path = tokenize_path(extracted_path)304    # Looks for '{' in the tokenized extracted path305    path_params_list = [(i, v) for i, v in enumerate(tokenized_extracted_path) if "{" in v]306    tokenized_path = tokenize_path(path)307    path_params = {}308    for param in path_params_list:309        path_param_name = param[1][1:-1]310        path_param_position = param[0]311        if path_param_name.endswith("+"):312            path_params[path_param_name.rstrip("+")] = "/".join(313                tokenized_path[path_param_position:]314            )315        else:316            path_params[path_param_name] = tokenized_path[path_param_position]317    path_params = common.json_safe(path_params)318    return path_params319def extract_query_string_params(path: str) -> Tuple[str, Dict[str, str]]:320    parsed_path = urlparse.urlparse(path)321    path = parsed_path.path322    parsed_query_string_params = urlparse.parse_qs(parsed_path.query)323    query_string_params = {}324    for query_param_name, query_param_values in parsed_query_string_params.items():325        if len(query_param_values) == 1:326            query_string_params[query_param_name] = query_param_values[0]327        else:328            query_string_params[query_param_name] = query_param_values329    # strip trailing slashes from path to fix downstream lookups330    path = path.rstrip("/") or "/"331    return [path, query_string_params]332def get_cors_response(headers):333    # TODO: for now we simply return "allow-all" CORS headers, but in the future334    # we should implement custom headers for CORS rules, as supported by API Gateway:335    # http://docs.aws.amazon.com/apigateway/latest/developerguide/how-to-cors.html336    response = Response()337    response.status_code = 200338    response.headers["Access-Control-Allow-Origin"] = "*"339    response.headers["Access-Control-Allow-Methods"] = "GET, POST, PUT, DELETE, PATCH"340    response.headers["Access-Control-Allow-Headers"] = "*"341    response._content = ""342    return response343def get_rest_api_paths(rest_api_id, region_name=None):344    apigateway = aws_stack.connect_to_service(service_name="apigateway", region_name=region_name)345    resources = apigateway.get_resources(restApiId=rest_api_id, limit=100)346    resource_map = {}347    for resource in resources["items"]:348        path = resource.get("path")349        # TODO: check if this is still required in the general case (can we rely on "path" being350        #  present?)351        path = path or aws_stack.get_apigateway_path_for_resource(352            rest_api_id, resource["id"], region_name=region_name353        )354        resource_map[path] = resource355    return resource_map356# TODO: Extract this to a set of rules that have precedence and easy to test individually.357#358#  https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-method-settings359#  -method-request.html360#  https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-routes.html361def get_resource_for_path(path: str, path_map: Dict[str, Dict]) -> Optional[Tuple[str, dict]]:362    matches = []363    # creates a regex from the input path if there are parameters, e.g /foo/{bar}/baz -> /foo/[364    # ^\]+/baz, otherwise is a direct match.365    for api_path, details in path_map.items():366        api_path_regex = re.sub(r"{[^+]+\+}", r"[^\?#]+", api_path)367        api_path_regex = re.sub(r"{[^}]+}", r"[^/]+", api_path_regex)368        if re.match(r"^%s$" % api_path_regex, path):369            matches.append((api_path, details))370    # if there are no matches, it's not worth to proceed, bail here!371    if not matches:372        return None373    # so we have matches and perhaps more than one, e.g374    # /{proxy+} and /api/{proxy+} for inputs like /api/foo/bar375    # /foo/{param1}/baz and /foo/{param1}/{param2} for inputs like /for/bar/baz376    if len(matches) > 1:377        # check if we have an exact match (exact matches take precedence)378        for match in matches:379            if match[0] == path:380                return match381        # not an exact match but parameters can fit in382        for match in matches:383            if path_matches_pattern(path, match[0]):384                return match385        # at this stage, we have more than one match but we have an eager example like386        # /{proxy+} or /api/{proxy+}, so we pick the best match by sorting by length387        sorted_matches = sorted(matches, key=lambda x: len(x[0]), reverse=True)388        return sorted_matches[0]389    return matches[0]390def path_matches_pattern(path, api_path):391    api_paths = api_path.split("/")392    paths = path.split("/")393    reg_check = re.compile(r"{(.*)}")394    if len(api_paths) != len(paths):395        return False396    results = [397        part == paths[indx]398        for indx, part in enumerate(api_paths)399        if reg_check.match(part) is None and part400    ]401    return len(results) > 0 and all(results)402def connect_api_gateway_to_sqs(gateway_name, stage_name, queue_arn, path, region_name=None):403    resources = {}404    template = APIGATEWAY_SQS_DATA_INBOUND_TEMPLATE405    resource_path = path.replace("/", "")406    region_name = region_name or aws_stack.get_region()407    try:408        arn = parse_arn(queue_arn)409        queue_name = arn["resource"]410        sqs_region = arn["region"]411    except InvalidArnException:412        queue_name = queue_arn413        sqs_region = region_name414    resources[resource_path] = [415        {416            "httpMethod": "POST",417            "authorizationType": "NONE",418            "integrations": [419                {420                    "type": "AWS",421                    "uri": "arn:aws:apigateway:%s:sqs:path/%s/%s"422                    % (sqs_region, TEST_AWS_ACCOUNT_ID, queue_name),423                    "requestTemplates": {"application/json": template},424                }425            ],426        }427    ]428    return aws_stack.create_api_gateway(429        name=gateway_name,430        resources=resources,431        stage_name=stage_name,432        region_name=region_name,433    )434def apply_json_patch_safe(subject, patch_operations, in_place=True, return_list=False):435    """Apply JSONPatch operations, using some customizations for compatibility with API GW436    resources."""437    results = []438    patch_operations = (439        [patch_operations] if isinstance(patch_operations, dict) else patch_operations440    )441    for operation in patch_operations:442        try:443            # special case: for "replace" operations, assume "" as the default value444            if operation["op"] == "replace" and operation.get("value") is None:445                operation["value"] = ""446            if operation["op"] != "remove" and operation.get("value") is None:447                LOG.info('Missing "value" in JSONPatch operation for %s: %s', subject, operation)448                continue449            if operation["op"] == "add":450                path = operation["path"]451                target = subject.get(path.strip("/"))452                target = target or common.extract_from_jsonpointer_path(subject, path)453                if not isinstance(target, list):454                    # for "add" operations, we should ensure that the path target is a list instance455                    value = [] if target is None else [target]456                    common.assign_to_path(subject, path, value=value, delimiter="/")457                target = common.extract_from_jsonpointer_path(subject, path)458                if isinstance(target, list) and not path.endswith("/-"):459                    # if "path" is an attribute name pointing to an array in "subject", and we're running460                    # an "add" operation, then we should use the standard-compliant notation "/path/-"461                    operation["path"] = "%s/-" % path462            result = apply_patch(subject, [operation], in_place=in_place)463            if not in_place:464                subject = result465            results.append(result)466        except JsonPointerException:467            pass  # path cannot be found - ignore468        except Exception as e:469            if "non-existent object" in str(e):470                if operation["op"] == "replace":471                    # fall back to an ADD operation if the REPLACE fails472                    operation["op"] = "add"473                    result = apply_patch(subject, [operation], in_place=in_place)474                    results.append(result)475                    continue476                if operation["op"] == "remove" and isinstance(subject, dict):477                    result = subject.pop(operation["path"], None)478                    results.append(result)479                    continue480            raise481    if return_list:482        return results483    return (results or [subject])[-1]484def import_api_from_openapi_spec(485    rest_api: apigateway_models.RestAPI, body: Dict, query_params: Dict486) -> apigateway_models.RestAPI:487    """Import an API from an OpenAPI spec document"""488    resolved_schema = resolve_references(body)489    # XXX for some reason this makes cf tests fail that's why is commented.490    # test_cfn_handle_serverless_api_resource491    # rest_api.name = resolved_schema.get("info", {}).get("title")492    rest_api.description = resolved_schema.get("info", {}).get("description")493    # Remove default root, then add paths from API spec494    rest_api.resources = {}495    def get_or_create_path(path):496        parts = path.rstrip("/").replace("//", "/").split("/")497        parent_id = ""498        if len(parts) > 1:499            parent_path = "/".join(parts[:-1])500            parent = get_or_create_path(parent_path)501            parent_id = parent.id502        if existing := [503            r504            for r in rest_api.resources.values()505            if r.path_part == (parts[-1] or "/") and (r.parent_id or "") == (parent_id or "")506        ]:507            return existing[0]508        return add_path(path, parts, parent_id=parent_id)509    def add_path(path, parts, parent_id=""):510        child_id = create_resource_id()511        path = path or "/"512        child = apigateway_models.Resource(513            resource_id=child_id,514            region_name=rest_api.region_name,515            api_id=rest_api.id,516            path_part=parts[-1] or "/",517            parent_id=parent_id,518        )519        for method, method_schema in resolved_schema["paths"].get(path, {}).items():520            method = method.upper()521            method_resource = child.add_method(method, None, None)522            method_integration = method_schema.get("x-amazon-apigateway-integration", {})523            responses = method_schema.get("responses", {})524            for status_code in responses:525                response_model = None526                if model_schema := responses.get(status_code, {}).get("schema", {}):527                    response_model = {APPLICATION_JSON: model_schema}528                response_parameters = (529                    method_integration.get("responses", {})530                    .get("default", {})531                    .get("responseParameters")532                )533                method_resource.create_response(534                    status_code,535                    response_model,536                    response_parameters,537                )538            integration = apigateway_models.Integration(539                http_method=method,540                uri=method_integration.get("uri"),541                integration_type=method_integration["type"],542                passthrough_behavior=method_integration.get("passthroughBehavior"),543                request_templates=method_integration.get("requestTemplates") or {},544            )545            integration.create_integration_response(546                status_code=method_integration.get("default", {}).get("statusCode", 200),547                selection_pattern=None,548                response_templates=method_integration.get("default", {}).get(549                    "responseTemplates", None550                ),551                content_handling=None,552            )553            child.resource_methods[method]["methodIntegration"] = integration554        rest_api.resources[child_id] = child555        return child556    if definitions := resolved_schema.get("definitions", {}):557        for name, model in definitions.items():558            rest_api.add_model(name=name, schema=model, content_type=APPLICATION_JSON)559    basepath_mode = (query_params.get("basepath") or ["prepend"])[0]560    base_path = (resolved_schema.get("basePath") or "") if basepath_mode == "prepend" else ""561    for path in resolved_schema.get("paths", {}):562        get_or_create_path(base_path + path)563    policy = resolved_schema.get("x-amazon-apigateway-policy")564    if policy:565        policy = json.dumps(policy) if isinstance(policy, dict) else str(policy)566        rest_api.policy = policy567    minimum_compression_size = resolved_schema.get("x-amazon-apigateway-minimum-compression-size")568    if minimum_compression_size is not None:569        rest_api.minimum_compression_size = int(minimum_compression_size)570    endpoint_config = resolved_schema.get("x-amazon-apigateway-endpoint-configuration")571    if endpoint_config:572        if endpoint_config.get("vpcEndpointIds"):573            endpoint_config.setdefault("types", ["PRIVATE"])574        rest_api.endpoint_configuration = endpoint_config575    return rest_api576def get_target_resource_details(invocation_context: ApiInvocationContext) -> Tuple[str, Dict]:577    """Look up and return the API GW resource (path pattern + resource dict) for the given invocation context."""578    path_map = get_rest_api_paths(579        rest_api_id=invocation_context.api_id, region_name=invocation_context.region_name580    )581    relative_path = invocation_context.invocation_path582    try:583        extracted_path, resource = get_resource_for_path(path=relative_path, path_map=path_map)584        invocation_context.resource = resource585        return extracted_path, resource586    except Exception:587        return None, None588def get_target_resource_method(invocation_context: ApiInvocationContext) -> Optional[Dict]:589    """Look up and return the API GW resource method for the given invocation context."""590    _, resource = get_target_resource_details(invocation_context)591    if not resource:592        return None593    methods = resource.get("resourceMethods") or {}594    method_name = invocation_context.method.upper()595    return methods.get(method_name) or methods.get("ANY")596def get_event_request_context(invocation_context: ApiInvocationContext):597    method = invocation_context.method598    path = invocation_context.path599    headers = invocation_context.headers600    integration_uri = invocation_context.integration_uri601    resource_path = invocation_context.resource_path602    resource_id = invocation_context.resource_id603    set_api_id_stage_invocation_path(invocation_context)604    relative_path, query_string_params = extract_query_string_params(605        path=invocation_context.path_with_query_string606    )607    api_id = invocation_context.api_id608    stage = invocation_context.stage609    source_ip = headers.get("X-Forwarded-For", ",").split(",")[-2].strip()610    integration_uri = integration_uri or ""611    account_id = integration_uri.split(":lambda:path")[-1].split(":function:")[0].split(":")[-1]612    account_id = account_id or TEST_AWS_ACCOUNT_ID613    request_context = {614        "accountId": account_id,615        "apiId": api_id,616        "resourcePath": resource_path or relative_path,617        "domainPrefix": invocation_context.domain_prefix,618        "domainName": invocation_context.domain_name,619        "resourceId": resource_id,620        "requestId": long_uid(),621        "identity": {622            "accountId": account_id,623            "sourceIp": source_ip,624            "userAgent": headers.get("User-Agent"),625        },626        "httpMethod": method,627        "protocol": "HTTP/1.1",628        "requestTime": pytz.utc.localize(datetime.datetime.utcnow()).strftime(629            REQUEST_TIME_DATE_FORMAT630        ),631        "requestTimeEpoch": int(time.time() * 1000),632        "authorizer": {},633    }634    # set "authorizer" and "identity" event attributes from request context635    auth_context = invocation_context.auth_context636    if auth_context:637        request_context["authorizer"] = auth_context638    request_context["identity"].update(invocation_context.auth_identity or {})639    if not is_test_invoke_method(method, path):640        request_context["path"] = (f"/{stage}" if stage else "") + relative_path641        request_context["stage"] = stage642    return request_context643def set_api_id_stage_invocation_path(644    invocation_context: ApiInvocationContext,645) -> ApiInvocationContext:646    # skip if all details are already available647    values = (648        invocation_context.api_id,649        invocation_context.stage,650        invocation_context.path_with_query_string,651    )652    if all(values):653        return invocation_context654    # skip if this is a websocket request655    if invocation_context.is_websocket_request():656        return invocation_context657    path = invocation_context.path658    headers = invocation_context.headers659    path_match = re.search(PATH_REGEX_USER_REQUEST, path)660    host_header = headers.get(HEADER_LOCALSTACK_EDGE_URL, "") or headers.get("Host") or ""661    host_match = re.search(HOST_REGEX_EXECUTE_API, host_header)662    test_invoke_match = re.search(PATH_REGEX_TEST_INVOKE_API, path)663    if path_match:664        api_id = path_match.group(1)665        stage = path_match.group(2)666        relative_path_w_query_params = "/%s" % path_match.group(3)667    elif host_match:668        api_id = extract_api_id_from_hostname_in_url(host_header)669        stage = path.strip("/").split("/")[0]670        relative_path_w_query_params = "/%s" % path.lstrip("/").partition("/")[2]671    elif test_invoke_match:672        # special case: fetch the resource details for TestInvokeApi invocations673        stage = None674        region_name = invocation_context.region_name675        api_id = test_invoke_match.group(1)676        resource_id = test_invoke_match.group(2)677        query_string = test_invoke_match.group(4) or ""678        apigateway = aws_stack.connect_to_service(679            service_name="apigateway", region_name=region_name680        )681        resource = apigateway.get_resource(restApiId=api_id, resourceId=resource_id)682        resource_path = resource.get("path")683        relative_path_w_query_params = f"{resource_path}{query_string}"684    else:685        raise Exception(686            f"Unable to extract API Gateway details from request: {path} {dict(headers)}"687        )688    if api_id:689        # set current region in request thread local, to ensure aws_stack.get_region() works properly690        # TODO: replace with RequestContextManager691        if getattr(THREAD_LOCAL, "request_context", None) is not None:692            api_region = API_REGIONS.get(api_id, "")693            THREAD_LOCAL.request_context.headers[MARKER_APIGW_REQUEST_REGION] = api_region694    # set details in invocation context695    invocation_context.api_id = api_id696    invocation_context.stage = stage697    invocation_context.path_with_query_string = relative_path_w_query_params698    return invocation_context699def extract_api_id_from_hostname_in_url(hostname: str) -> str:700    """Extract API ID 'id123' from URLs like https://id123.execute-api.localhost.localstack.cloud:4566"""701    match = re.match(HOST_REGEX_EXECUTE_API, hostname)702    api_id = match.group(1)...Learn to execute automation testing from scratch with LambdaTest Learning Hub. Right from setting up the prerequisites to run your first automation test, to following best practices and diving deeper into advanced test scenarios. LambdaTest Learning Hubs compile a list of step-by-step guides to help you be proficient with different test automation frameworks i.e. Selenium, Cypress, TestNG etc.
You could also refer to video tutorials over LambdaTest YouTube channel to get step by step demonstration from industry experts.
Get 100 minutes of automation test minutes FREE!!
