//! Python API method generation. use std::fmt::Write; use crate::{Endpoint, Parameter, escape_python_keyword, generators::{normalize_return_type, write_description}, to_snake_case}; use super::client::generate_class_constants; use super::types::js_type_to_python; /// Generate the main client class pub fn generate_main_client(output: &mut String, endpoints: &[Endpoint]) { writeln!(output, "class BrkClient(BrkClientBase):").unwrap(); writeln!( output, " \"\"\"Main BRK client with metrics tree and API methods.\"\"\"" ) .unwrap(); writeln!(output).unwrap(); // Generate class-level constants generate_class_constants(output); writeln!( output, " def __init__(self, base_url: str = 'http://localhost:3000', timeout: float = 30.0):" ) .unwrap(); writeln!(output, " super().__init__(base_url, timeout)").unwrap(); writeln!(output, " self.metrics = MetricsTree(self)").unwrap(); writeln!(output).unwrap(); // Generate metric() method for dynamic metric access writeln!(output, " def metric(self, metric: str, index: Index) -> MetricEndpointBuilder[Any]:").unwrap(); writeln!(output, " \"\"\"Create a dynamic metric endpoint builder for any metric/index combination.").unwrap(); writeln!(output).unwrap(); writeln!(output, " Use this for programmatic access when the metric name is determined at runtime.").unwrap(); writeln!(output, " For type-safe access, use the `metrics` tree instead.").unwrap(); writeln!(output, " \"\"\"").unwrap(); writeln!(output, " return MetricEndpointBuilder(self, metric, index)").unwrap(); writeln!(output).unwrap(); // Generate helper methods writeln!(output, " def index_to_date(self, index: Index, i: int) -> date:").unwrap(); writeln!(output, " \"\"\"Convert an index value to a date for date-based indexes.\"\"\"").unwrap(); writeln!(output, " return index_to_date(index, i)").unwrap(); writeln!(output).unwrap(); writeln!(output, " def is_date_index(self, index: Index) -> bool:").unwrap(); writeln!(output, " \"\"\"Check if an index type is date-based.\"\"\"").unwrap(); writeln!(output, " return is_date_index(index)").unwrap(); writeln!(output).unwrap(); // Generate API methods generate_api_methods(output, endpoints); } /// Generate API methods from OpenAPI endpoints pub fn generate_api_methods(output: &mut String, endpoints: &[Endpoint]) { for endpoint in endpoints { if !endpoint.should_generate() { continue; } let method_name = endpoint_to_method_name(endpoint); let base_return_type = normalize_return_type( &endpoint .response_type .as_deref() .map(js_type_to_python) .unwrap_or_else(|| "Any".to_string()), ); let return_type = if endpoint.supports_csv { format!("Union[{}, str]", base_return_type) } else { base_return_type }; // Build method signature let params = build_method_params(endpoint); writeln!( output, " def {}(self{}) -> {}:", method_name, params, return_type ) .unwrap(); // Docstring match (&endpoint.summary, &endpoint.description) { (Some(summary), Some(desc)) if summary != desc => { writeln!(output, " \"\"\"{}.", summary.trim_end_matches('.')).unwrap(); writeln!(output).unwrap(); write_description(output, desc, " ", ""); } (Some(summary), _) => { writeln!(output, " \"\"\"{}", summary).unwrap(); } (None, Some(desc)) => { // First line includes opening quotes let mut lines = desc.lines(); if let Some(first) = lines.next() { writeln!(output, " \"\"\"{}", first).unwrap(); } for line in lines { if line.is_empty() { writeln!(output).unwrap(); } else { writeln!(output, " {}", line).unwrap(); } } } (None, None) => { write!(output, " \"\"\"").unwrap(); } } writeln!(output).unwrap(); writeln!(output, " Endpoint: `{} {}`\"\"\"", endpoint.method.to_uppercase(), endpoint.path).unwrap(); // Build path let path = build_path_template(&endpoint.path, &endpoint.path_params); if endpoint.query_params.is_empty() { if endpoint.path_params.is_empty() { writeln!(output, " return self.get_json('{}')", path).unwrap(); } else { writeln!(output, " return self.get_json(f'{}')", path).unwrap(); } } else { writeln!(output, " params = []").unwrap(); for param in &endpoint.query_params { // Use safe name for Python variable, original name for API query parameter let safe_name = escape_python_keyword(¶m.name); if param.required { writeln!( output, " params.append(f'{}={{{}}}')", param.name, safe_name ) .unwrap(); } else { writeln!( output, " if {} is not None: params.append(f'{}={{{}}}')", safe_name, param.name, safe_name ) .unwrap(); } } writeln!(output, " query = '&'.join(params)").unwrap(); writeln!( output, " path = f'{}{{\"?\" + query if query else \"\"}}'", path ) .unwrap(); if endpoint.supports_csv { writeln!(output, " if format == 'csv':").unwrap(); writeln!(output, " return self.get_text(path)").unwrap(); writeln!(output, " return self.get_json(path)").unwrap(); } else { writeln!(output, " return self.get_json(path)").unwrap(); } } writeln!(output).unwrap(); } } fn endpoint_to_method_name(endpoint: &Endpoint) -> String { to_snake_case(&endpoint.operation_name()) } fn build_method_params(endpoint: &Endpoint) -> String { let mut params = Vec::new(); // Path params are always required for param in &endpoint.path_params { let safe_name = escape_python_keyword(¶m.name); let py_type = js_type_to_python(¶m.param_type); params.push(format!(", {}: {}", safe_name, py_type)); } // Required query params must come before optional ones (Python syntax requirement) for param in &endpoint.query_params { if param.required { let safe_name = escape_python_keyword(¶m.name); let py_type = js_type_to_python(¶m.param_type); params.push(format!(", {}: {}", safe_name, py_type)); } } for param in &endpoint.query_params { if !param.required { let safe_name = escape_python_keyword(¶m.name); let py_type = js_type_to_python(¶m.param_type); params.push(format!(", {}: Optional[{}] = None", safe_name, py_type)); } } params.join("") } fn build_path_template(path: &str, path_params: &[Parameter]) -> String { let mut result = path.to_string(); for param in path_params { let placeholder = format!("{{{}}}", param.name); // Use escaped name for Python variable interpolation in f-string let safe_name = escape_python_keyword(¶m.name); let interpolation = format!("{{{}}}", safe_name); result = result.replace(&placeholder, &interpolation); } result }