def apply_overrides(manifest_json, overrides, marketplace): """ Overrides fields in the manifest file. Args: manifest_json (dict): a json dict of the entire manifest overrides (dict): a json dict of override parameters. If this dict contains invalid marketplace (str): the marketplace name Raises: UnloggedException: if the marketplace doesn't exist ValidationError: if a non-integer is passed as the price or port parameter. Returns: dict: a json dict of the manifest with fields overridden. """ manifest_json = copy.deepcopy(manifest_json) if overrides is None: return manifest_json if "title" in overrides: manifest_json["info"]["title"] = overrides["title"] if "description" in overrides: manifest_json["info"]["description"] = overrides["description"] if "price" in overrides: invalid_price_format = "Price should be a non-negative integer." try: price = int(overrides["price"]) manifest_json["info"]["x-21-total-price"]["min"] = price manifest_json["info"]["x-21-total-price"]["max"] = price if price < 0: raise exceptions.ValidationError(invalid_price_format) except ValueError: raise exceptions.ValidationError(invalid_price_format) if "name" in overrides: manifest_json["info"]["contact"]["name"] = overrides["name"] if "email" in overrides: manifest_json["info"]["contact"]["email"] = overrides["email"] if "host" in overrides: manifest_json["host"] = overrides["host"] if "port" in overrides: host = manifest_json["host"] # if the host is in the form of https://x.com/ remove the trailing slash host = host.strip("/") invalid_port_format = "Port should be an integer between 0 and 65536." try: port = int(overrides["port"]) if port <= 0 or port > 65536: raise exceptions.ValidationError(invalid_port_format) except ValueError: raise exceptions.ValidationError(invalid_port_format) host += ":{}".format(port) manifest_json["host"] = host if "basePath" in overrides: manifest_json["basePath"] = overrides["basePath"] return manifest_json
def check_app_manifest(api_docs_path, overrides, marketplace): """ Runs validate_manifest and handles any errors that could occur Args: api_docs_path (str): path to the manifest file overrides (dict): Dictionary containing the key/value pairs will be overridden in the manifest. marketplace (str): the marketplace name Raises: ValidationError: If manifest is not valid, bad, missing, is a directory, or too large """ if not os.path.exists(api_docs_path): raise exceptions.ValidationError( uxstring.UxString.manifest_missing.format(api_docs_path)) if os.path.isdir(api_docs_path): raise exceptions.ValidationError( uxstring.UxString.manifest_is_directory.format(api_docs_path)) file_size = os.path.getsize(api_docs_path) / 1e6 if file_size > 2: raise exceptions.ValidationError( uxstring.UxString.large_manifest.format(api_docs_path)) try: with open(api_docs_path, "r") as f: manifest_dict = yaml.load(f.read()) # empty yaml files do not raise an error, so do it here if not manifest_dict: raise YAMLError manifest_dict = clean_manifest(manifest_dict) if overrides is not None: manifest_dict = override_manifest(manifest_dict, overrides, marketplace) # ensure the manifest is valid validate_manifest(manifest_dict) # write back the manifest in case some clean up or overriding has happend if overrides is not None: with open(api_docs_path, "w") as f: yaml.dump(manifest_dict, f) return manifest_dict except (YAMLError, ValueError): raise exceptions.ValidationError( uxstring.UxString.malformed_yaml.format(api_docs_path))
def check_app_manifest(api_docs_path, overrides, marketplace): """ Runs validate_manifest and handles any errors that could occur Args: api_docs_path (str): path to the manifest file overrides (dict): Dictionary containing the key/value pairs will be overridden in the manifest. marketplace (str): the marketplace name Raises: ValidationError: If manifest is not valid, bad, missing, is a directory, or too large """ if not os.path.exists(api_docs_path): raise exceptions.ValidationError( click.style("Could not find the manifest file at {}.", fg="red").format(api_docs_path)) if os.path.isdir(api_docs_path): raise exceptions.ValidationError( click.style( "{} is a directory. Please enter the direct path to the manifest file.", fg="red").format(api_docs_path)) file_size = os.path.getsize(api_docs_path) / 1e6 if file_size > 2: raise exceptions.ValidationError( click.style( "The size of the manifest file at {} exceeds the maximum limit of 2MB.", fg="red").format(api_docs_path)) try: with open(api_docs_path, "r") as f: original_manifest_dict = yaml.load(f.read()) manifest_dict = transform_manifest(original_manifest_dict, overrides, marketplace) # write back the manifest in case some clean up or overriding has happend with open(api_docs_path, "w") as f: yaml.dump(manifest_dict, f) return manifest_dict except (YAMLError, ValueError): raise exceptions.ValidationError( click.style("Your manifest file at {} is not valid YAML.", fg="red").format(api_docs_path))
def validate_manifest(manifest_json): """ Validates the manifest file Ensures that the required fields in the manifest are present and valid Args: manifest_json (dict): a json dict of the entire manifest Raises: ValueError: if a required field is not valid or present in the manifest """ manifest_json = copy.deepcopy(manifest_json) for field in ["schemes", "host", "basePath", "info"]: if field not in manifest_json: raise exceptions.ValidationError( click.style("Field '{}' is missing from the manifest file.", fg="red").format(field), json=manifest_json) for field in ["contact", "title", "description", "x-21-total-price", "x-21-quick-buy", "x-21-category"]: if field not in manifest_json["info"]: raise exceptions.ValidationError( click.style( "Field '{}' is missing from the manifest file under the 'info' section.", fg="red").format(field), json=manifest_json) for field in {"name", "email"}: if field not in manifest_json["info"]["contact"]: raise exceptions.ValidationError( click.style( "Field '{}' is missing from the manifest file under the 'contact' section.", fg="red") .format(field), json=manifest_json) for field in ["min", "max"]: if field not in manifest_json["info"]["x-21-total-price"]: raise exceptions.ValidationError( click.style("Field '{}' is missing from the manifest file under the " "'x-21-total-price' section.", fg="red"), json=manifest_json) if len(manifest_json["schemes"]) == 0: raise exceptions.ValidationError( click.style( "You have to specify either HTTP or HTTPS for your endpoint under the " "`schemes` section.", fg="red"), json=manifest_json) valid_app_categories = {'blockchain', 'entertainment', 'social', 'markets', 'utilities', 'iot'} if manifest_json["info"]["x-21-category"].lower() not in valid_app_categories: valid_categories = ", ".join(valid_app_categories) raise exceptions.ValidationError( click.style("'{}' is not a valid category for the 21 marketplace. Valid categories are {}.", fg="red").format( manifest_json["info"]["x-21-category"], valid_categories), json=manifest_json)
def validate_manifest(manifest_json): """ Validates the manifest file Ensures that the required fields in the manifest are present and valid Args: manifest_json (dict): a json dict of the entire manifest Raises: ValueError: if a required field is not valid or present in the manifest """ for field in uxstring.UxString.valid_top_level_manifest_fields: if field not in manifest_json: raise exceptions.ValidationError( uxstring.UxString.top_level_manifest_field_missing.format( field), json=manifest_json) for field in uxstring.UxString.manifest_info_fields: if field not in manifest_json["info"]: raise exceptions.ValidationError( uxstring.UxString.manifest_info_field_missing.format(field), json=manifest_json) for field in uxstring.UxString.manifest_contact_fields: if field not in manifest_json["info"]["contact"]: raise exceptions.ValidationError( uxstring.UxString.manifest_contact_field_missing.format(field), json=manifest_json) for field in uxstring.UxString.price_fields: if field not in manifest_json["info"]["x-21-total-price"]: raise exceptions.ValidationError( uxstring.UxString.price_fields_missing.format(field), json=manifest_json) if len(manifest_json["schemes"]) == 0: raise exceptions.ValidationError(uxstring.UxString.scheme_missing, json=manifest_json) if manifest_json["info"]["x-21-category"].lower( ) not in uxstring.UxString.valid_app_categories: valid_categories = ", ".join(uxstring.UxString.valid_app_categories) raise exceptions.ValidationError( uxstring.UxString.invalid_category.format( manifest_json["info"]["x-21-category"], valid_categories), json=manifest_json)
def _publish(client, manifest_path, marketplace, skip, overrides): """ Publishes application by uploading the manifest to the given marketplace Args: client (two1.server.rest_client.TwentyOneRestClient) an object for sending authenticated requests to the TwentyOne backend. manifest_path (str): the path to the manifest file. marketplace (str): the zerotier marketplace name. skip (bool): skips strict checking of manifest file. overrides (dict): Dictionary containing the key/value pairs will be overridden in the manifest. Raises: ValidationError: if an error occurs while parsing the manifest file """ try: manifest_json = check_app_manifest(manifest_path, overrides, marketplace) app_url = "{}://{}".format(manifest_json["schemes"][0], manifest_json["host"]) app_ip = urlparse(app_url).hostname if not skip: address = get_zerotier_address(marketplace) if address != app_ip: wrong_ip = click.style("It seems that the IP address that you put in your manifest file (") +\ click.style("{}", bold=True) +\ click.style(") is different than your current 21market IP (") +\ click.style("{}", bold=True) +\ click.style(")\nAre you sure you want to continue publishing with ") +\ click.style("{}", bold=True) +\ click.style("?") if not click.confirm(wrong_ip.format(app_ip, address, app_ip)): switch_host = click.style("Please edit ") +\ click.style("{}", bold=True) +\ click.style(" and replace ") +\ click.style("{}", bold=True) +\ click.style(" with ") +\ click.style("[{}].", bold=True) logger.info( switch_host.format(manifest_path, app_ip, address)) return except exceptions.ValidationError as ex: # catches and re-raises the same exception to enhance the error message publish_docs_url = click.style("https://21.co/learn/21-publish/", bold=True) publish_instructions = "For instructions on publishing your app, please refer to {}".format( publish_docs_url) raise exceptions.ValidationError( "The following error occurred while reading your manifest file at {}:\n{}\n\n{}" .format(manifest_path, ex.args[0], publish_instructions), json=ex.json) app_name = manifest_json["info"]["title"] app_endpoint = "{}://{}{}".format(manifest_json["schemes"][0], manifest_json["host"], manifest_json["basePath"]) logger.info( (click.style("Publishing {} at ") + click.style("{}", bold=True) + click.style(" to {}.")).format(app_name, app_endpoint, marketplace)) payload = {"manifest": manifest_json, "marketplace": marketplace} try: response = client.publish(payload) except ServerRequestError as e: if e.status_code == 403 and e.data.get("error") == "TO600": logger.info( "The endpoint {} specified in your manifest has already been registered in " "the marketplace by another user.\nPlease check your manifest file and make " "sure your 'host' field is correct.\nIf the problem persists please contact " "[email protected].".format(app_endpoint), fg="red") return else: raise e if response.status_code == 201: response_data = response.json() mkt_url = response_data['mkt_url'] permalink = response_data['permalink'] logger.info( click.style( "\n" "You have successfully published {} to {}. " "You should be able to view the listing within a few minutes at {}\n\n" "Users will be able to purchase it, using 21 buy, at {} ", fg="magenta").format(app_name, marketplace, permalink, mkt_url))
def override_manifest(manifest_json, overrides, marketplace): """ Overrides fields in the manifest file. Args: manifest_json (dict): a json dict of the entire manifest overrides (dict): a json dict of override parameters. If this dict contains invalid marketplace (str): the marketplace name Raises: UnloggedException: if the marketplace doesn't exist ValidationError: if a non-integer is passed as the price or port parameter. Returns: dict: a json dict of the manifest with fields overridden. """ old_host = manifest_json["host"].strip("/") if "title" in overrides: manifest_json["info"]["title"] = overrides["title"] if "description" in overrides: manifest_json["info"]["description"] = overrides["description"] if "price" in overrides: try: price = int(overrides["price"]) manifest_json["info"]["x-21-total-price"]["min"] = price manifest_json["info"]["x-21-total-price"]["max"] = price if price < 0: raise exceptions.ValidationError( uxstring.UxString.invalid_price_format) except ValueError: raise exceptions.ValidationError( uxstring.UxString.invalid_price_format) if "name" in overrides: manifest_json["info"]["contact"]["name"] = overrides["name"] if "email" in overrides: manifest_json["info"]["contact"]["email"] = overrides["email"] if "host" in overrides: host = overrides["host"] if host == "AUTO": host = get_zerotier_address(marketplace) manifest_json["host"] = host if "port" in overrides: host = manifest_json["host"] # if the host is in the form of https://x.com/ remove the trailing slash host = host.strip("/") try: port = int(overrides["port"]) if port <= 0 or port > 65536: raise exceptions.ValidationError( uxstring.UxString.invalid_port_format) except ValueError: raise exceptions.ValidationError( uxstring.UxString.invalid_port_format) host += ":{}".format(port) manifest_json["host"] = host if "basePath" in overrides: manifest_json["basePath"] = overrides["basePath"] new_host = manifest_json["host"] if new_host != old_host: manifest_json = replace_host_in_docs(manifest_json, new_host, old_host) return manifest_json
def _publish(client, manifest_path, marketplace, skip, overrides): """ Publishes application by uploading the manifest to the given marketplace Args: client (two1.server.rest_client.TwentyOneRestClient) an object for sending authenticated requests to the TwentyOne backend. manifest_path (str): the path to the manifest file. marketplace (str): the zerotier marketplace name. skip (bool): skips strict checking of manifest file. overrides (dict): Dictionary containing the key/value pairs will be overridden in the manifest. Raises: ValidationError: if an error occurs while parsing the manifest file """ try: manifest_json = check_app_manifest(manifest_path, overrides, marketplace) app_url = urlparse(manifest_json["host"]) app_ip = app_url.path.split(":")[0] if not skip: address = get_zerotier_address(marketplace) if address != app_ip: if not click.confirm( uxstring.UxString.wrong_ip.format( app_ip, address, app_ip)): logger.info( uxstring.UxString.switch_host.format( manifest_path, app_ip, address)) return except exceptions.ValidationError as ex: # catches and re-raises the same exception to enhance the error message raise exceptions.ValidationError(uxstring.UxString.bad_manifest.format( manifest_path, ex.args[0]), json=ex._json) app_name = manifest_json["info"]["title"] app_endpoint = "{}://{}{}".format(manifest_json["schemes"][0], manifest_json["host"], manifest_json["basePath"]) logger.info( uxstring.UxString.publish_start.format(app_name, app_endpoint, marketplace)) payload = {"manifest": manifest_json, "marketplace": marketplace} try: response = client.publish(payload) except ServerRequestError as e: if e.status_code == 403 and e.data.get("error") == "TO600": logger.info(uxstring.UxString.app_url_claimed.format(app_endpoint), fg="red") return else: raise e if response.status_code == 201: logger.info( uxstring.UxString.publish_success.format(app_name, marketplace))