|
| 1 | +# Copyright 2024 Google LLC |
| 2 | +# |
| 3 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | +# you may not use this file except in compliance with the License. |
| 5 | +# You may obtain a copy of the License at |
| 6 | +# |
| 7 | +# https://www.apache.org/licenses/LICENSE-2.0 |
| 8 | +# |
| 9 | +# Unless required by applicable law or agreed to in writing, software |
| 10 | +# distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | +# See the License for the specific language governing permissions and |
| 13 | +# limitations under the License. |
| 14 | + |
| 15 | +import json |
| 16 | +import os |
| 17 | +import re |
| 18 | +import sys |
| 19 | +import shutil |
| 20 | +import yaml |
| 21 | +from pathlib import Path |
| 22 | +from typing import Dict, List, Optional |
| 23 | +import jinja2 |
| 24 | +import logging |
| 25 | + |
| 26 | +from synthtool import _tracked_paths |
| 27 | +from synthtool.sources import templates |
| 28 | + |
| 29 | +logger = logging.getLogger() |
| 30 | +logger.setLevel(logging.DEBUG) |
| 31 | + |
| 32 | + |
| 33 | +DEFAULT_TEMPLATES_PATH = "synthtool/gcp/templates" |
| 34 | +LOCAL_TEMPLATES: Optional[str] = os.environ.get("SYNTHTOOL_TEMPLATES") |
| 35 | + |
| 36 | +# Originally brought from gcp/partials.py. |
| 37 | +# These are the default locations to look up |
| 38 | +_DEFAULT_PARTIAL_FILES = [ |
| 39 | + ".readme-partials.yml", |
| 40 | + ".readme-partials.yaml", |
| 41 | + ".integration-partials.yaml", |
| 42 | +] |
| 43 | + |
| 44 | + |
| 45 | +class CommonTemplates: |
| 46 | + def __init__(self, template_path: Optional[Path] = None): |
| 47 | + if LOCAL_TEMPLATES is None: |
| 48 | + logger.error("env var SYNTHTOOL_TEMPLATES must be set") |
| 49 | + sys.exit(1) |
| 50 | + logger.debug(f"Using local templates at {LOCAL_TEMPLATES}") |
| 51 | + self._template_root = Path(LOCAL_TEMPLATES) |
| 52 | + self._templates = templates.Templates(self._template_root) |
| 53 | + self.excludes = [] # type: List[str] |
| 54 | + |
| 55 | + def _generic_library(self, directory: str, relative_dir=None, **kwargs) -> Path: |
| 56 | + # load common repo meta information (metadata that's not language specific). |
| 57 | + if "metadata" in kwargs: |
| 58 | + self._load_generic_metadata(kwargs["metadata"], relative_dir=relative_dir) |
| 59 | + # if no samples were found, don't attempt to render a |
| 60 | + # samples/README.md. |
| 61 | + if "samples" not in kwargs["metadata"] or not kwargs["metadata"]["samples"]: |
| 62 | + self.excludes.append("samples/README.md") |
| 63 | + |
| 64 | + t = templates.TemplateGroup(self._template_root / directory, self.excludes) |
| 65 | + |
| 66 | + result = t.render(**kwargs) |
| 67 | + _tracked_paths.add(result) |
| 68 | + |
| 69 | + return result |
| 70 | + |
| 71 | + def java_library(self, **kwargs) -> Path: |
| 72 | + # kwargs["metadata"] is required to load values from .repo-metadata.json |
| 73 | + if "metadata" not in kwargs: |
| 74 | + kwargs["metadata"] = {} |
| 75 | + return self._generic_library("java_library", **kwargs) |
| 76 | + |
| 77 | + def render(self, template_name: str, **kwargs) -> Path: |
| 78 | + template = self._templates.render(template_name, **kwargs) |
| 79 | + _tracked_paths.add(template) |
| 80 | + return template |
| 81 | + |
| 82 | + def _load_generic_metadata(self, metadata: Dict, relative_dir=None): |
| 83 | + """ |
| 84 | + loads additional meta information from .repo-metadata.json. |
| 85 | + """ |
| 86 | + metadata["partials"] = load_partials() |
| 87 | + |
| 88 | + # Loads repo metadata information from the default location if it |
| 89 | + # hasn't already been set. Some callers may have already loaded repo |
| 90 | + # metadata, so we don't need to do it again or overwrite it. Also, only |
| 91 | + # set the "repo" key. |
| 92 | + if "repo" not in metadata: |
| 93 | + metadata["repo"] = _load_repo_metadata(relative_dir=relative_dir) |
| 94 | + |
| 95 | + |
| 96 | +def _load_repo_metadata( |
| 97 | + relative_dir=None, metadata_file: str = "./.repo-metadata.json" |
| 98 | +) -> Dict: |
| 99 | + """Parse a metadata JSON file into a Dict. |
| 100 | +
|
| 101 | + Currently, the defined fields are: |
| 102 | + * `name` - The service's API name |
| 103 | + * `name_pretty` - The service's API title. This will be used for generating titles on READMEs |
| 104 | + * `product_documentation` - The product documentation on cloud.google.com |
| 105 | + * `client_documentation` - The client library reference documentation |
| 106 | + * `issue_tracker` - The public issue tracker for the product |
| 107 | + * `release_level` - The release level of the client library. One of: alpha, beta, |
| 108 | + ga, deprecated, preview, stable |
| 109 | + * `language` - The repo language. One of dotnet, go, java, nodejs, php, python, ruby |
| 110 | + * `repo` - The GitHub repo in the format {owner}/{repo} |
| 111 | + * `distribution_name` - The language-idiomatic package/distribution name |
| 112 | + * `api_id` - The API ID associated with the service. Fully qualified identifier use to |
| 113 | + enable a service in the cloud platform (e.g. monitoring.googleapis.com) |
| 114 | + * `requires_billing` - Whether or not the API requires billing to be configured on the |
| 115 | + customer's acocunt |
| 116 | +
|
| 117 | + Args: |
| 118 | + metadata_file (str, optional): Path to the metadata json file |
| 119 | +
|
| 120 | + Returns: |
| 121 | + A dictionary of metadata. This may not necessarily include all the defined fields above. |
| 122 | + """ |
| 123 | + if relative_dir is not None: |
| 124 | + if os.path.exists(Path(relative_dir, metadata_file).resolve()): |
| 125 | + with open(Path(relative_dir, metadata_file).resolve()) as f: |
| 126 | + return json.load(f) |
| 127 | + elif os.path.exists(metadata_file): |
| 128 | + with open(metadata_file) as f: |
| 129 | + return json.load(f) |
| 130 | + return {} |
| 131 | + |
| 132 | + |
| 133 | +def load_partials(files: List[str] = []) -> Dict: |
| 134 | + """ |
| 135 | + hand-crafted artisanal markdown can be provided in a .readme-partials.yml. |
| 136 | + The following fields are currently supported: |
| 137 | +
|
| 138 | + body: custom body to include in the usage section of the document. |
| 139 | + samples_body: an optional body to place below the table of contents |
| 140 | + in samples/README.md. |
| 141 | + introduction: a more thorough introduction than metadata["description"]. |
| 142 | + title: provide markdown to use as a custom title. |
| 143 | + deprecation_warning: a warning to indicate that the library has been |
| 144 | + deprecated and a pointer to an alternate option |
| 145 | + """ |
| 146 | + result: Dict[str, Dict] = {} |
| 147 | + cwd_path = Path(os.getcwd()) |
| 148 | + for file in files + _DEFAULT_PARTIAL_FILES: |
| 149 | + partials_file = cwd_path / file |
| 150 | + if os.path.exists(partials_file): |
| 151 | + with open(partials_file) as f: |
| 152 | + result.update(yaml.load(f, Loader=yaml.SafeLoader)) |
| 153 | + return result |
0 commit comments