openapi_nexus/generators/python/httpx/
project_files.rs1use crate::codegen::traits::file_writer::FileInfo;
4use crate::generators::request_inputs::RequestInputModel;
5use crate::ir::types::{IrInfo, IrOperation, IrSchema};
6use heck::{ToPascalCase, ToSnakeCase};
7use indexmap::IndexMap;
8
9pub fn generate_project_files(
11 info: &IrInfo,
12 package_name: &str,
13 header: &str,
14 schemas: &IndexMap<String, IrSchema>,
15 operations: &[IrOperation],
16 request_inputs: &[RequestInputModel],
17 include_upload_file: bool,
18) -> Vec<FileInfo> {
19 let files = vec![
20 pyproject_toml(info, package_name),
21 readme_file(info, package_name),
22 py_typed(package_name),
23 top_level_init(package_name, header, include_upload_file),
24 models_init(schemas, request_inputs, header),
25 apis_init(operations, header),
26 ];
27
28 files
29}
30
31fn pyproject_toml(info: &IrInfo, package_name: &str) -> FileInfo {
32 let description = info
33 .description
34 .as_deref()
35 .unwrap_or("Generated Python SDK.")
36 .lines()
37 .next()
38 .unwrap_or("Generated Python SDK.");
39 let content = format!(
40 r#"[build-system]
41requires = ["hatchling"]
42build-backend = "hatchling.build"
43
44[project]
45name = "{package_name}"
46version = "{version}"
47description = "{description}"
48requires-python = ">=3.12"
49dependencies = ["httpx>=0.27"]
50"#,
51 version = info.version,
52 );
53 FileInfo::project("pyproject.toml".to_string(), content)
54}
55
56fn readme_file(info: &IrInfo, package_name: &str) -> FileInfo {
57 let title = &info.title;
58 let version = &info.version;
59 let description = info
60 .description
61 .clone()
62 .unwrap_or_else(|| "Generated Python SDK.".to_string());
63 let content = format!(
64 "# {title}\n\n{description}\n\nVersion: `{version}`\n\nGenerated by [openapi-nexus](https://github.com/rust-codegen-group/openapi-nexus) for `{package_name}`.\n"
65 );
66 FileInfo::readme("README.md".to_string(), content)
67}
68
69fn py_typed(package_name: &str) -> FileInfo {
70 FileInfo::project(format!("{package_name}/py.typed"), String::new())
71}
72
73fn top_level_init(package_name: &str, header: &str, include_upload_file: bool) -> FileInfo {
74 let mut content = String::new();
75 content.push_str(header);
76 content.push_str("from .runtime import ApiKeyAuth as ApiKeyAuth\n");
77 content.push_str("from .runtime import Authenticator as Authenticator\n");
78 content.push_str("from .runtime import BearerAuth as BearerAuth\n");
79 content.push_str("from .runtime import Client as Client\n");
80 content.push_str("from .runtime import ApiError as ApiError\n");
81 if include_upload_file {
82 content.push_str("from .runtime import UploadFile as UploadFile\n");
83 }
84 FileInfo::project(format!("{package_name}/__init__.py"), content)
85}
86
87fn models_init(
88 schemas: &IndexMap<String, IrSchema>,
89 request_inputs: &[RequestInputModel],
90 header: &str,
91) -> FileInfo {
92 let mut content = String::new();
93 content.push_str(header);
94
95 let mut entries: Vec<(String, String)> = Vec::new();
96 for (_key, schema) in schemas {
97 let py_name = schema.name.to_pascal_case();
98 let module = schema.name.to_snake_case();
99 entries.push((module, py_name));
100 }
101 for model in request_inputs {
102 let py_name = model.name.to_pascal_case();
103 let module = model.name.to_snake_case();
104 entries.push((module, py_name));
105 }
106
107 entries.sort_by(|a, b| a.0.cmp(&b.0));
108 for (module, name) in &entries {
109 content.push_str(&format!("from .{module} import {name} as {name}\n"));
110 }
111
112 FileInfo::model("__init__.py".to_string(), content)
113}
114
115fn apis_init(operations: &[IrOperation], header: &str) -> FileInfo {
116 let mut content = String::new();
117 content.push_str(header);
118
119 let mut tags: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
120 for op in operations {
121 if op.tags.is_empty() {
122 tags.insert("default".to_string());
123 } else {
124 for tag in &op.tags {
125 tags.insert(tag.clone());
126 }
127 }
128 }
129
130 for tag in &tags {
131 let module = tag.to_snake_case();
132 let class_name = format!("{}Api", tag.to_pascal_case());
133 content.push_str(&format!(
134 "from .{module}_api import {class_name} as {class_name}\n"
135 ));
136 }
137
138 FileInfo::api("__init__.py".to_string(), content)
139}