Skip to main content

openapi_nexus/generators/python/requests/
project_files.rs

1//! Project-level files: pyproject.toml, README, __init__.py barrels, py.typed.
2
3use 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
9/// Generate all project-level files.
10pub 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 = ["requests>=2.32"]
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 ApiResponse as ApiResponse\n");
80    content.push_str("from .runtime import Client as Client\n");
81    content.push_str("from .runtime import ApiError as ApiError\n");
82    if include_upload_file {
83        content.push_str("from .runtime import UploadFile as UploadFile\n");
84    }
85    FileInfo::project(format!("{package_name}/__init__.py"), content)
86}
87
88fn models_init(
89    schemas: &IndexMap<String, IrSchema>,
90    request_inputs: &[RequestInputModel],
91    header: &str,
92) -> FileInfo {
93    let mut content = String::new();
94    content.push_str(header);
95
96    let mut entries: Vec<(String, String)> = Vec::new();
97    for (_key, schema) in schemas {
98        let py_name = schema.name.to_pascal_case();
99        let module = schema.name.to_snake_case();
100        entries.push((module, py_name));
101    }
102    for model in request_inputs {
103        let py_name = model.name.to_pascal_case();
104        let module = model.name.to_snake_case();
105        entries.push((module, py_name));
106    }
107
108    entries.sort_by(|a, b| a.0.cmp(&b.0));
109    for (module, name) in &entries {
110        content.push_str(&format!("from .{module} import {name} as {name}\n"));
111    }
112
113    FileInfo::model("__init__.py".to_string(), content)
114}
115
116fn apis_init(operations: &[IrOperation], header: &str) -> FileInfo {
117    let mut content = String::new();
118    content.push_str(header);
119
120    let mut tags: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
121    for op in operations {
122        if op.tags.is_empty() {
123            tags.insert("default".to_string());
124        } else {
125            for tag in &op.tags {
126                tags.insert(tag.clone());
127            }
128        }
129    }
130
131    for tag in &tags {
132        let module = tag.to_snake_case();
133        let class_name = format!("{}Api", tag.to_pascal_case());
134        content.push_str(&format!(
135            "from .{module}_api import {class_name} as {class_name}\n"
136        ));
137    }
138
139    FileInfo::api("__init__.py".to_string(), content)
140}