tools/scripts/codegen/model_utils.py (133 lines of code) (raw):

#!/usr/bin/env python3 # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0. """ A set of utils to go through c2j models and their corresponding endpoint rules """ import datetime import json import os import re # Legacy table of service model remaps/name correction during the code generation SERVICE_NAME_REMAPS = {"runtime.lex": "lex", "runtime.lex.v2": "lexv2-runtime", "models.lex.v2": "lexv2-models", "transfer": "awstransfer", "transcribe-streaming": "transcribestreaming", "streams.dynamodb": "dynamodbstreams"} SMITHY_EXCLUSION_CLIENTS = { # multi auth "eventbridge" , "cloudfront-keyvaluestore" , "cognito-identity" , "cognito-idp" # customization , "machinelearning" , "apigatewayv2" , "apigateway" , "eventbridge" , "glacier" , "lambda" , "polly" , "sqs" # bearer token # ,"codecatalyst" # bidirectional streaming , "lexv2-runtime" , "qbusiness" , "transcribestreaming" , "s3-crt" , "s3" , "s3control" } # Regexp to parse C2J model filename to extract service name and date version SERVICE_MODEL_FILENAME_PATTERN = re.compile( "^" "(?P<service>.+)-" # service name "(?P<date>[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9])" # model date ".normal.json$" ) class ServiceModel(object): # A helper class to store C2j model info and metadata (endpoint rules and tests) def __init__(self, service_id: str, c2j_model: str, endpoint_rule_set: str, endpoint_tests: str, use_smithy: bool): self.service_id = service_id # For debugging purposes, not used atm # only filenames, no filesystem path self.c2j_model = c2j_model self.endpoint_rule_set = endpoint_rule_set self.endpoint_tests = endpoint_tests self.use_smithy = use_smithy class ModelUtils(object): """A helper utility to collect available for generation models: process models dir, find corresponding endpoint rules and do some legacy name remapping, filter models requested for generation. """ def __init__(self, args: dict): self.debug = args.get("debug", False) self.args = args self.models_available = self._collect_available_models(args["path_to_api_definitions"], args["path_to_endpoint_rules"]) self.models_to_generate = self._get_models_to_generate() def get_clients_to_build(self) -> set: """ Return a set of c2j client names to be generated :return: a set """ return set(self.models_to_generate.keys()) def _get_models_to_generate(self): # this method may return an empty dict if both all and client_list are missing # (for example, in case of defaults, partitions or tests generation) if self.args.get("all"): return self.models_available else: clients_to_build = self.args.get("client_list") if not clients_to_build: clients_to_build = [] clients_to_build_set = set(clients_to_build) available_models_set = set(self.models_available.keys()) not_found_models = clients_to_build_set - available_models_set if len(not_found_models): raise RuntimeError(f"Requested to build clients but their model files are not present: " f"{not_found_models}") return dict((k, self.models_available[k]) for k in clients_to_build if k in clients_to_build) @staticmethod def _collect_available_models(models_dir: str, endpoint_rules_dir: str) -> dict: """Return a dict of <service_name, ServiceModel> with all available c2j models in a models_dir :param models_dir: path to the directory with c2j models :param endpoint_rules_dir: path to the directory with endpoints dir models :return: dict<service_name, model_file_name> in models dir """ model_files = os.listdir(models_dir) service_name_to_model_filename_date = dict() for filename in model_files: if not os.path.isfile("/".join([models_dir, filename])): continue match = SERVICE_MODEL_FILENAME_PATTERN.match(filename) service_model_name = match.group("service") service_model_date = match.group("date") service_model_date = datetime.datetime.strptime(service_model_date, "%Y-%m-%d").date() already_found_model = service_name_to_model_filename_date.get(service_model_name, None) if already_found_model: already_found_date = already_found_model[1] if already_found_date < service_model_date: service_name_to_model_filename_date[service_model_name] = (filename, service_model_date) else: service_name_to_model_filename_date[service_model_name] = (filename, service_model_date) service_name_to_model_filename = dict() missing = set() for raw_key, model_file_date in service_name_to_model_filename_date.items(): key = SERVICE_NAME_REMAPS.get(raw_key, raw_key) if "." in key: key = "-".join(reversed(key.split("."))) # just replicating existing legacy behavior if ";" in key: key = key.replace(";", "-") # just in case... just replicating existing legacy behavior # fetch endpoint-rules filename which is based on ServiceId in c2j models: try: service_name_to_model_filename[key] = ModelUtils._build_service_model(models_dir, endpoint_rules_dir, model_file_date[0]) if key == "s3": service_name_to_model_filename["s3-crt"] = service_name_to_model_filename["s3"] except Exception as exc: # TODO: re-enable with endpoints introduction # print(f"C2J model does not have a corresponding endpoints ruleset: {exc}") missing.add(model_file_date[0]) service_name_to_model_filename[key] = ServiceModel(service_id=key, c2j_model=model_file_date[0], endpoint_rule_set=None, endpoint_tests=None, use_smithy=False) if missing: # TODO: re-enable with endpoints introduction # print(f"Missing endpoints for services: {missing}") pass if service_name_to_model_filename.get("s3") and "s3-crt" not in service_name_to_model_filename: service_name_to_model_filename["s3-crt"] = service_name_to_model_filename["s3"] return service_name_to_model_filename @staticmethod def is_smithy_enabled(service_id, models_dir, c2j_model_filename): """Return true if given service id and c2j model file should enable smithy client generation path :param service_id: :param models_dir: :param c2j_model_filename: :return: """ use_smithy = False if service_id not in SMITHY_EXCLUSION_CLIENTS: with open(models_dir + "/" + c2j_model_filename, 'r') as json_file: model = json.load(json_file) model_protocol = model.get("metadata", dict()).get("protocol", "UNKNOWN_PROTOCOL") #if model_protocol in {"json", "rest-json", "rest-xml", "query"}: # use_smithy = True return use_smithy @staticmethod def _build_service_model(models_dir: str, endpoint_rules_dir: str, c2j_model_filename) -> ServiceModel: """Return a ServiceModel containing paths to the Service models: C2J model and endpoints (rules and tests). :param models_dir (str): filepath (absolute or relative) to the dir with c2j models :param endpoint_rules_dir (str): filepath (absolute or relative) to the dir with dirs of endpoints :param c2j_model_filename (str): filename of a service C2J model (relative to models_dir, no separator) :return: ServiceModel, a descriptor class holding Service models filenames """ endpoint_rules_filename = c2j_model_filename.replace('.normal.json', '.endpoint-rule-set.json') endpoint_rules_filepath = f"{endpoint_rules_dir}/{endpoint_rules_filename}" endpoint_tests_filename = c2j_model_filename.replace('.normal.json', '.endpoint-tests.json') endpoint_tests_filepath = f"{endpoint_rules_dir}/{endpoint_tests_filename}" match = SERVICE_MODEL_FILENAME_PATTERN.match(c2j_model_filename) service_id = match.group("service") use_smithy = ModelUtils.is_smithy_enabled(service_id, models_dir, c2j_model_filename) if os.path.exists(endpoint_rules_filepath) and os.path.exists(endpoint_tests_filepath): return ServiceModel(service_id=service_id, c2j_model=c2j_model_filename, endpoint_rule_set=endpoint_rules_filename, endpoint_tests=endpoint_tests_filename, use_smithy=use_smithy)