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

1"""Command-line interface for the class generator.""" 

2 

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 

11 

12import cloup 

13import pytest 

14from cloup.constraints import If, IsSet, accept_none, require_one 

15from simple_logger.logger import get_logger 

16 

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 

23 

24LOGGER = get_logger(name=__name__) 

25 

26 

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) 

111 

112 # Handle schema update - either standalone or with --generate-missing 

113 if update_schema: 

114 LOGGER.info("Updating resource schema...") 

115 update_kind_schema() 

116 

117 # If only updating schema (not generating), exit 

118 if not generate_missing: 

119 return 

120 

121 LOGGER.info("Schema updated. Proceeding with resource generation...") 

122 

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 

127 

128 # Analyze coverage (now based on schema mapping, not cluster discovery) 

129 coverage_analysis = analyze_coverage() 

130 

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) 

137 

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 

146 

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}") 

159 

160 # Exit if we only did discovery/report/generation 

161 if discover_missing or coverage_report or generate_missing: 

162 return 

163 

164 # Handle normal generation with -k/--kind option 

165 if not kind: 

166 LOGGER.error("No kind specified for generation") 

167 return 

168 

169 # Handle add_tests option with kind 

170 if add_tests: 

171 generate_class_generator_tests() 

172 return 

173 

174 # Generate class files for specified kinds 

175 kind_list: list[str] = kind.split(",") 

176 

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 ) 

201 

202 # Wait for all tasks to complete 

203 for _ in as_completed(futures): 

204 pass 

205 

206 

207if __name__ == "__main__": 

208 main()