Skip to main content

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