Coverage for class_generator/cli.py: 85%
81 statements
« prev ^ index » next coverage.py v7.10.1, created at 2025-07-29 12:31 +0300
« prev ^ index » next coverage.py v7.10.1, created at 2025-07-29 12:31 +0300
1"""Command-line interface for the class generator."""
3import json
4import os
5import sys
6import time
7from concurrent.futures import Future, ThreadPoolExecutor, as_completed
8from tempfile import gettempdir
9from typing import Any
10from pathlib import Path
12import cloup
13import pytest
14from cloup.constraints import If, IsSet, accept_none, require_one
15from simple_logger.logger import get_logger
17from class_generator.constants import TESTS_MANIFESTS_DIR
18from class_generator.core.coverage import analyze_coverage, generate_report
19from class_generator.core.discovery import discover_cluster_resources
20from class_generator.core.generator import class_generator
21from class_generator.core.schema import update_kind_schema
22from class_generator.tests.test_generation import generate_class_generator_tests
24LOGGER = get_logger(name=__name__)
27@cloup.command("Resource class generator", show_constraints=True)
28@cloup.option(
29 "-k",
30 "--kind",
31 type=cloup.STRING,
32 help="""
33 \b
34 The Kind to generate the class for, Needs working cluster with admin privileges.
35 multiple kinds can be sent separated by comma (without psaces)
36 Example: -k Deployment,Pod,ConfigMap
37""",
38)
39@cloup.option(
40 "-o",
41 "--output-file",
42 help="The full filename path to generate a python resource file. If not sent, resource kind will be used",
43 type=cloup.Path(),
44)
45@cloup.option(
46 "--overwrite",
47 is_flag=True,
48 help="Output file overwrite existing file if passed",
49)
50@cloup.option("--dry-run", is_flag=True, help="Run the script without writing to file")
51@cloup.option(
52 "--add-tests",
53 help=f"Add a test to `test_class_generator.py` and test files to `{TESTS_MANIFESTS_DIR}` dir",
54 is_flag=True,
55 show_default=True,
56)
57@cloup.option(
58 "--update-schema",
59 help="Update kind schema files",
60 is_flag=True,
61 show_default=True,
62)
63@cloup.option(
64 "--discover-missing",
65 help="Discover resources in the cluster that don't have wrapper classes",
66 is_flag=True,
67 show_default=True,
68)
69@cloup.option(
70 "--coverage-report",
71 help="Generate a coverage report of implemented vs discovered resources",
72 is_flag=True,
73 show_default=True,
74)
75@cloup.option(
76 "--json",
77 "json_output",
78 help="Output reports in JSON format",
79 is_flag=True,
80 default=False,
81 show_default=True,
82)
83@cloup.option(
84 "--generate-missing",
85 help="Generate classes for all missing resources after discovery",
86 is_flag=True,
87 show_default=True,
88)
89@cloup.constraint(
90 If(IsSet("update_schema") & ~IsSet("generate_missing"), then=accept_none),
91 ["kind", "discover_missing", "coverage_report", "dry_run", "overwrite", "output_file", "add_tests"]
92)
93def main(
94 kind: str | None,
95 overwrite: bool,
96 output_file: str,
97 dry_run: bool,
98 add_tests: bool,
99 discover_missing: bool,
100 coverage_report: bool,
101 generate_missing: bool,
102 json_output: bool,
103 update_schema: bool,
104) -> None:
105 """Generate Python module for K8S resource."""
106 # Check that at least one action is specified
107 actions = [kind, update_schema, discover_missing, coverage_report, generate_missing]
108 if not any(actions):
109 LOGGER.error("At least one action must be specified (--kind, --update-schema, --discover-missing, --coverage-report, or --generate-missing)")
110 sys.exit(1)
112 # Handle schema update - either standalone or with --generate-missing
113 if update_schema:
114 LOGGER.info("Updating resource schema...")
115 update_kind_schema()
117 # If only updating schema (not generating), exit
118 if not generate_missing:
119 return
121 LOGGER.info("Schema updated. Proceeding with resource generation...")
123 # Check if we need coverage analysis (for --discover-missing, --coverage-report, or --generate-missing)
124 if discover_missing or coverage_report or generate_missing:
125 # Use schema mapping instead of cluster discovery
126 discovered_resources = None # Not needed anymore
128 # Analyze coverage (now based on schema mapping, not cluster discovery)
129 coverage_analysis = analyze_coverage()
131 # Generate report if requested
132 if coverage_report or discover_missing or generate_missing:
133 output_format = "json" if json_output else None
134 report = generate_report(coverage_data=coverage_analysis, output_format=output_format)
135 if report is not None: # Only print if report is not None (json format)
136 print(report)
138 # Generate missing resources if requested
139 if generate_missing and coverage_analysis["missing_resources"]:
140 LOGGER.info(f"Generating {len(coverage_analysis['missing_resources'])} missing resources...")
141 for resource_kind in coverage_analysis["missing_resources"]:
142 if isinstance(resource_kind, dict):
143 kind_to_generate = resource_kind.get("kind", resource_kind)
144 else:
145 kind_to_generate = resource_kind
147 try:
148 class_generator(
149 kind=kind_to_generate,
150 output_file="",
151 overwrite=overwrite,
152 add_tests=False,
153 dry_run=dry_run,
154 )
155 if not dry_run:
156 LOGGER.info(f"Generated {kind_to_generate}")
157 except Exception as e:
158 LOGGER.error(f"Failed to generate {kind_to_generate}: {e}")
160 # Exit if we only did discovery/report/generation
161 if discover_missing or coverage_report or generate_missing:
162 return
164 # Handle normal generation with -k/--kind option
165 if not kind:
166 LOGGER.error("No kind specified for generation")
167 return
169 # Handle add_tests option with kind
170 if add_tests:
171 generate_class_generator_tests()
172 return
174 # Generate class files for specified kinds
175 kind_list: list[str] = kind.split(",")
177 if len(kind_list) == 1:
178 # Single kind - run directly
179 class_generator(
180 kind=kind,
181 overwrite=overwrite,
182 dry_run=dry_run,
183 output_file=output_file,
184 add_tests=add_tests,
185 )
186 else:
187 # Multiple kinds - run in parallel
188 futures: list[Future] = []
189 with ThreadPoolExecutor(max_workers=10) as executor:
190 for _kind in kind_list:
191 futures.append(
192 executor.submit(
193 class_generator,
194 kind=_kind,
195 overwrite=overwrite,
196 dry_run=dry_run,
197 output_file=output_file,
198 add_tests=add_tests,
199 )
200 )
202 # Wait for all tasks to complete
203 for _ in as_completed(futures):
204 pass
207if __name__ == "__main__":
208 main()