use std::fmt::Write;
use soothfast_spec::dialect::Info;
use crate::SdkOptions;
use crate::model::Sdk;
use crate::python::HEADER;
pub(crate) fn pyproject(info: &Info, opts: &SdkOptions) -> String {
let description = opts
.description
.clone()
.or_else(|| info.description.clone())
.unwrap_or_else(|| format!("Generated client for {}", info.title));
let mut out = String::new();
let _ = writeln!(out, "{HEADER}");
let _ = writeln!(out, "[build-system]");
let _ = writeln!(out, "requires = [\"hatchling\"]");
let _ = writeln!(out, "build-backend = \"hatchling.build\"");
let _ = writeln!(out);
let _ = writeln!(out, "[project]");
let _ = writeln!(out, "name = {:?}", opts.package);
let _ = writeln!(out, "version = {:?}", opts.version);
let _ = writeln!(out, "description = {description:?}");
let _ = writeln!(out, "readme = \"README.md\"");
let _ = writeln!(out, "requires-python = \">=3.10\"");
let _ = writeln!(out, "dependencies = [\"httpx>=0.27\"]");
if let Some(repo) = &opts.repository {
let _ = writeln!(out);
let _ = writeln!(out, "[project.urls]");
let _ = writeln!(out, "Repository = {repo:?}");
}
let _ = writeln!(out);
let _ = writeln!(out, "[tool.hatch.build]");
let _ = writeln!(out, "ignore-vcs = true");
let _ = writeln!(out);
let _ = writeln!(out, "[tool.hatch.build.targets.wheel]");
let _ = writeln!(out, "packages = [\"src/{}\"]", opts.module);
if opts.embed.is_some() {
let _ = writeln!(out);
let _ = writeln!(out, "[tool.hatch.build.targets.wheel.hooks.custom]");
let _ = writeln!(out, "path = \"hatch_build.py\"");
let _ = writeln!(out);
let _ = writeln!(out, "[tool.hatch.build.targets.sdist]");
let _ = writeln!(
out,
"include = [\"README.md\", \"hatch_build.py\", \"src\"]"
);
}
out
}
fn example_env(opts: &SdkOptions) -> String {
let unset: Vec<&crate::envtemplate::EnvVar> = opts
.embed_env_vars
.iter()
.filter(|v| v.optional())
.collect();
let picks = if unset.is_empty() {
opts.embed_env_vars.iter().collect()
} else {
unset
};
picks
.iter()
.take(2)
.map(|v| format!("{:?}: \"...\"", v.name))
.collect::<Vec<_>>()
.join(", ")
}
pub(crate) fn readme(info: &Info, sdk: &Sdk, opts: &SdkOptions, base_url: Option<&str>) -> String {
let mut out = String::new();
let _ = writeln!(
out,
"<!-- Code generated by `cargo soothfast sdk gen`. DO NOT EDIT. -->"
);
let _ = writeln!(out, "# {}", opts.package);
let _ = writeln!(out);
let _ = write!(
out,
"Generated Python client for **{}** ({})",
info.title, info.version
);
match (&opts.embed, base_url) {
(Some(_), _) => {
let _ = writeln!(
out,
", bundling the server it talks to — there is nothing to \
deploy and no endpoint to configure."
);
}
(None, Some(url)) => {
let _ = writeln!(out, ", talking to `{url}`.");
}
(None, None) => {
let _ = writeln!(out, ".");
}
}
let _ = writeln!(out);
let _ = writeln!(out, "```bash");
let _ = writeln!(out, "pip install {}", opts.package);
let _ = writeln!(out, "```");
let _ = writeln!(out);
let _ = writeln!(out, "```python");
let _ = writeln!(out, "from {} import Client", opts.module);
let _ = writeln!(out);
let _ = writeln!(out, "with Client() as client:");
match sdk.methods.first() {
Some(m) => {
let _ = writeln!(out, " result = client.{}(...)", m.name);
}
None => {
let _ = writeln!(out, " ...");
}
}
let _ = writeln!(out, "```");
let _ = writeln!(out);
let _ = writeln!(
out,
"An `AsyncClient` with the same methods is importable alongside \
`Client`. Errors raise subclasses of `ApiError`; rate limits retry \
automatically, honoring `Retry-After`."
);
if let Some(binary) = &opts.embed {
let prefix = crate::naming::env_prefix(&opts.package);
let _ = writeln!(out);
let _ = writeln!(
out,
"## Embedded server\n\n\
Constructing a client starts the bundled `{binary}` on a port \
it picks itself, and the process is reaped when yours exits. \
Two environment variables change that:\n\n\
- `{prefix}_BASE_URL` — talk to an already-running instance \
and spawn nothing.\n\
- `{prefix}_SERVER_BIN` — use a different server binary.\n\n\
`stop_embedded_servers()` shuts it down early; \
`EmbeddedServer.start()` manages one by hand."
);
if let Some(url) = base_url {
let _ = writeln!(
out,
"\nPass `DEFAULT_BASE_URL` (`{url}`) to use the hosted \
deployment instead."
);
}
if !opts.embed_env_vars.is_empty() {
let _ = writeln!(out);
let _ = writeln!(
out,
"### Configuring it\n\n\
The server reads the same environment a deployment sets it \
with. `server_env` supplies that per client:\n\n\
```python\n\
client = Client(server_env={{{}}})\n\
```\n\n\
Those values win over the ambient environment; anything left \
out falls back to what this process — or a `.env` file the \
server itself reads — already provides. Clients asking for \
different `server_env` get their own server process. The \
`ServerEnv` TypedDict spells every knob:\n",
example_env(opts),
);
let _ = write!(
out,
"{}",
crate::envtemplate::markdown_table(&opts.embed_env_vars)
);
}
}
let _ = writeln!(out);
let _ = writeln!(out, "## Methods");
let _ = writeln!(out);
for m in &sdk.methods {
let doc = m.doc.as_deref().unwrap_or("");
let _ = writeln!(
out,
"- `{}` — `{} {}`{}{}",
m.name,
m.http_method,
m.path,
if doc.is_empty() { "" } else { ": " },
doc,
);
}
out
}
pub(crate) fn init(info: &Info, sdk: &Sdk, opts: &SdkOptions) -> String {
let mut exports: Vec<String> = vec![
"ApiError".into(),
"AsyncClient".into(),
"BadRequestError".into(),
"Client".into(),
"NotFoundError".into(),
"Page".into(),
"PageInfo".into(),
"RateLimitError".into(),
"ServerError".into(),
"ServiceUnavailableError".into(),
];
if opts.embed.is_some() {
exports.extend(["EmbeddedServer".into(), "stop_embedded_servers".into()]);
}
let mut model_names: Vec<&str> = sdk.models.iter().map(|m| m.name.as_str()).collect();
model_names.extend(sdk.aliases.iter().map(|(n, _)| n.as_str()));
model_names.sort_unstable();
exports.extend(model_names.iter().map(|n| n.to_string()));
exports.sort_unstable();
let mut out = String::new();
let _ = writeln!(out, "{HEADER}");
let _ = writeln!(
out,
"\"\"\"{} — generated client for {}.\"\"\"",
opts.package, info.title
);
let _ = writeln!(out, "from __future__ import annotations");
let _ = writeln!(out);
let _ = writeln!(out, "from ._runtime import (");
for name in [
"ApiError",
"BadRequestError",
"NotFoundError",
"Page",
"PageInfo",
"RateLimitError",
"ServerError",
"ServiceUnavailableError",
] {
let _ = writeln!(out, " {name},");
}
let _ = writeln!(out, ")");
let _ = writeln!(out, "from .client import AsyncClient, Client");
if opts.embed.is_some() {
let _ = writeln!(
out,
"from ._server import EmbeddedServer, stop_embedded_servers"
);
}
if !model_names.is_empty() {
let _ = writeln!(out, "from .models import (");
for name in &model_names {
let _ = writeln!(out, " {name},");
}
let _ = writeln!(out, ")");
}
let _ = writeln!(out);
let _ = writeln!(out, "__version__ = {:?}", opts.version);
let _ = writeln!(out);
let _ = writeln!(out, "__all__ = [");
for name in &exports {
let _ = writeln!(out, " {name:?},");
}
let _ = writeln!(out, "]");
out
}