|
| 1 | +import os |
| 2 | +import sys |
| 3 | +from pathlib import Path |
| 4 | + |
| 5 | +import yaml |
| 6 | + |
| 7 | +DEFAULT_STATUS = 'unsupported' |
| 8 | +DEFAULT_EMULATION_LEVEL = 'CRUD' |
| 9 | +FEATURES_FILE_NAME='features.yml' |
| 10 | + |
| 11 | +MD_FILE_HEADER = """--- |
| 12 | +title: "AWS Service Feature Coverage" |
| 13 | +linkTitle: "⭐ Feature Coverage" |
| 14 | +weight: 1 |
| 15 | +description: > |
| 16 | + Overview of the implemented AWS APIs and their level of parity with the AWS cloud |
| 17 | +aliases: |
| 18 | + - /localstack/coverage/ |
| 19 | + - /aws/feature-coverage/ |
| 20 | +hide_readingtime: true |
| 21 | +--- |
| 22 | +
|
| 23 | +
|
| 24 | +## Emulation Levels |
| 25 | +
|
| 26 | +* CRUD: The service accepts requests and returns proper (potentially static) responses. |
| 27 | + No additional business logic besides storing entities. |
| 28 | +* Emulated: The service imitates the functionality, including synchronous and asynchronous business logic operating on service entities. |
| 29 | +
|
| 30 | +| Service / Feature | Implementation status | Emulation Level | Limitations | |
| 31 | +|-------------------|----------------|-----------------|--------------------------|""" |
| 32 | + |
| 33 | +class FeatureCatalogMarkdownGenerator: |
| 34 | + md_content = [MD_FILE_HEADER] |
| 35 | + |
| 36 | + def __init__(self, file_path: str): |
| 37 | + self.file_path = file_path |
| 38 | + pass |
| 39 | + |
| 40 | + def add_service_section(self, feature_file_content: str): |
| 41 | + service_name = feature_file_content.get('name') |
| 42 | + emulation_level = feature_file_content.get('emulation_level', DEFAULT_EMULATION_LEVEL) |
| 43 | + self.md_content.append(f"| **{service_name}** | [Details 🔍] | {emulation_level} | |") |
| 44 | + |
| 45 | + def add_features_rows(self, feature_file_content: str): |
| 46 | + for feature in feature_file_content.get('features', []): |
| 47 | + feature_name = feature.get('name', '') |
| 48 | + documentation_page = feature.get('documentation_page') |
| 49 | + if documentation_page: |
| 50 | + feature_name = f'[{feature_name}]({documentation_page})' |
| 51 | + status = feature.get('status', DEFAULT_STATUS) |
| 52 | + |
| 53 | + limitations = feature.get('limitations', []) |
| 54 | + limitations_md = '\n '.join(limitations) if limitations else '' |
| 55 | + |
| 56 | + self.md_content.append(f"| {feature_name} | {status} | | {limitations_md} |") |
| 57 | + |
| 58 | + def generate_file(self): |
| 59 | + try: |
| 60 | + with open(self.file_path, "w") as feature_coverage_md_file: |
| 61 | + feature_coverage_md_file.writelines(s + '\n' for s in self.md_content) |
| 62 | + except Exception as e: |
| 63 | + print(f"Error writing to file: {e}") |
| 64 | + sys.exit(1) |
| 65 | + |
| 66 | +def load_yaml_file(file_path: str): |
| 67 | + try: |
| 68 | + with open(file_path, 'r') as file: |
| 69 | + return yaml.safe_load(file) |
| 70 | + except yaml.YAMLError as e: |
| 71 | + sys.stdout.write(f"::error title=Failed to parse features file::An error occurred while parsing {file_path}: {e}") |
| 72 | + sys.exit(1) |
| 73 | + except FileNotFoundError: |
| 74 | + sys.stdout.write(f"::error title=Missing features file::No features file found at {file_path}") |
| 75 | + sys.exit(1) |
| 76 | + |
| 77 | +def get_service_path_to_abs_community_ext_paths(community_files_path: str, ext_files_path: str) -> dict[str, (str, str)]: |
| 78 | + relative_to_abs_paths = {} |
| 79 | + for community_abs_path in Path(community_files_path).rglob(FEATURES_FILE_NAME): |
| 80 | + rel_path = str(community_abs_path.relative_to(community_files_path)) |
| 81 | + relative_to_abs_paths[rel_path] = (community_abs_path, None) |
| 82 | + |
| 83 | + for abs_path_ext in Path(ext_files_path).rglob(FEATURES_FILE_NAME): |
| 84 | + rel_path = str(abs_path_ext.relative_to(ext_files_path)) |
| 85 | + if rel_path in relative_to_abs_paths: |
| 86 | + community_abs_path, _ = relative_to_abs_paths[rel_path] |
| 87 | + relative_to_abs_paths[rel_path] = (community_abs_path, abs_path_ext) |
| 88 | + else: |
| 89 | + relative_to_abs_paths[rel_path] = (None, abs_path_ext) |
| 90 | + return relative_to_abs_paths |
| 91 | + |
| 92 | +def main(): |
| 93 | + community_feature_files_path = os.getenv('PATH_FEATURE_FILES_COMMUNITY') |
| 94 | + ext_feature_files_path = os.getenv('PATH_FEATURE_FILES_EXT') |
| 95 | + feature_catalog_md_file_path = os.getenv('PATH_FEATURE_CATALOG_MD') |
| 96 | + |
| 97 | + service_path_to_abs_paths = get_service_path_to_abs_community_ext_paths(community_feature_files_path, ext_feature_files_path) |
| 98 | + md_generator = FeatureCatalogMarkdownGenerator(feature_catalog_md_file_path) |
| 99 | + |
| 100 | + for service_name in sorted(service_path_to_abs_paths): |
| 101 | + abs_path_community, abs_path_ext = service_path_to_abs_paths.get(service_name) |
| 102 | + service_definition_created = False |
| 103 | + if abs_path_community: |
| 104 | + feature_file_community = load_yaml_file(abs_path_community) |
| 105 | + md_generator.add_service_section(feature_file_community) |
| 106 | + service_definition_created = True |
| 107 | + md_generator.add_features_rows(feature_file_community) |
| 108 | + if abs_path_ext: |
| 109 | + feature_file_ext = load_yaml_file(abs_path_ext) |
| 110 | + if not service_definition_created: |
| 111 | + md_generator.add_service_section(feature_file_community) |
| 112 | + md_generator.add_features_rows(feature_file_ext) |
| 113 | + md_generator.generate_file() |
| 114 | + |
| 115 | +if __name__ == "__main__": |
| 116 | + main() |
0 commit comments