httpxgen 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- httpxgen/__init__.py +12 -0
- httpxgen/cli.py +78 -0
- httpxgen/generator/__init__.py +4 -0
- httpxgen/generator/build.py +162 -0
- httpxgen/generator/client.py +729 -0
- httpxgen/generator/errors.py +2 -0
- httpxgen/generator/models.py +493 -0
- httpxgen/generator/naming.py +40 -0
- httpxgen/generator/normalize.py +665 -0
- httpxgen/generator/operations.py +599 -0
- httpxgen/generator/package.py +202 -0
- httpxgen/generator/schema.py +142 -0
- httpxgen/generator/templates.py +332 -0
- httpxgen/io.py +5 -0
- httpxgen/loading.py +31 -0
- httpxgen/openapi.py +117 -0
- httpxgen/output.py +66 -0
- httpxgen/selection.py +162 -0
- httpxgen-0.1.0.dist-info/METADATA +445 -0
- httpxgen-0.1.0.dist-info/RECORD +23 -0
- httpxgen-0.1.0.dist-info/WHEEL +4 -0
- httpxgen-0.1.0.dist-info/entry_points.txt +2 -0
- httpxgen-0.1.0.dist-info/licenses/LICENSE +21 -0
httpxgen/__init__.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from .generator import GenerationError, generate_client
|
|
2
|
+
from .io import filter_operations_by_tags, load_openapi, write_client
|
|
3
|
+
from .openapi import OpenAPISpec
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"GenerationError",
|
|
7
|
+
"OpenAPISpec",
|
|
8
|
+
"filter_operations_by_tags",
|
|
9
|
+
"generate_client",
|
|
10
|
+
"load_openapi",
|
|
11
|
+
"write_client",
|
|
12
|
+
]
|
httpxgen/cli.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from httpxgen.generator import GenerationError
|
|
5
|
+
from httpxgen.io import (
|
|
6
|
+
filter_operations_by_tags,
|
|
7
|
+
load_openapi,
|
|
8
|
+
write_client,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def main() -> int:
|
|
13
|
+
parser = argparse.ArgumentParser(
|
|
14
|
+
description="Generate a typed async Python client from OpenAPI JSON or YAML."
|
|
15
|
+
)
|
|
16
|
+
parser.add_argument("openapi", type=Path, help="OpenAPI JSON or YAML file")
|
|
17
|
+
parser.add_argument(
|
|
18
|
+
"output",
|
|
19
|
+
type=Path,
|
|
20
|
+
help="target package directory, or the root holding one package per tag "
|
|
21
|
+
"when more than one --tag is given",
|
|
22
|
+
)
|
|
23
|
+
parser.add_argument(
|
|
24
|
+
"--package-name",
|
|
25
|
+
help="generated import package name; defaults to the output directory name",
|
|
26
|
+
)
|
|
27
|
+
parser.add_argument(
|
|
28
|
+
"--check",
|
|
29
|
+
action="store_true",
|
|
30
|
+
help="do not write; fail when generated output is stale",
|
|
31
|
+
)
|
|
32
|
+
parser.add_argument(
|
|
33
|
+
"--tag",
|
|
34
|
+
dest="tags",
|
|
35
|
+
action="append",
|
|
36
|
+
default=[],
|
|
37
|
+
metavar="TAG",
|
|
38
|
+
help="include only operations with this OpenAPI tag; repeat it to generate "
|
|
39
|
+
"one package per tag around a shared support package",
|
|
40
|
+
)
|
|
41
|
+
parser.add_argument(
|
|
42
|
+
"--schema-tag",
|
|
43
|
+
dest="schema_tags",
|
|
44
|
+
action="append",
|
|
45
|
+
default=[],
|
|
46
|
+
metavar="TAG",
|
|
47
|
+
help="retain schemas referenced by this tag without generating its operations",
|
|
48
|
+
)
|
|
49
|
+
args = parser.parse_args()
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
spec = load_openapi(args.openapi)
|
|
53
|
+
if len(args.tags) <= 1:
|
|
54
|
+
spec = filter_operations_by_tags(
|
|
55
|
+
spec,
|
|
56
|
+
args.tags,
|
|
57
|
+
schema_tags=args.schema_tags,
|
|
58
|
+
)
|
|
59
|
+
changed = write_client(
|
|
60
|
+
spec=spec,
|
|
61
|
+
package_dir=args.output,
|
|
62
|
+
package_name=args.package_name,
|
|
63
|
+
tags=args.tags,
|
|
64
|
+
schema_tags=args.schema_tags,
|
|
65
|
+
check=args.check,
|
|
66
|
+
)
|
|
67
|
+
except (GenerationError, OSError, ValueError) as error:
|
|
68
|
+
parser.exit(1, f"error: {error}\n")
|
|
69
|
+
|
|
70
|
+
if args.check:
|
|
71
|
+
print("Generated HTTP client is current.")
|
|
72
|
+
else:
|
|
73
|
+
print(f"Generated {len(changed)} file(s) in package {args.output}.")
|
|
74
|
+
return 0
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
if __name__ == "__main__":
|
|
78
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
from collections import Counter
|
|
2
|
+
from collections.abc import Sequence
|
|
3
|
+
|
|
4
|
+
from httpxgen.generator.client import Layout, render_client, render_serialization
|
|
5
|
+
from httpxgen.generator.models import exported_model_names, render_models
|
|
6
|
+
from httpxgen.generator.naming import class_name, identifier
|
|
7
|
+
from httpxgen.generator.normalize import normalize_inline_schemas
|
|
8
|
+
from httpxgen.generator.operations import (
|
|
9
|
+
read_operations,
|
|
10
|
+
read_security_schemes,
|
|
11
|
+
response_model_names,
|
|
12
|
+
)
|
|
13
|
+
from httpxgen.generator.package import (
|
|
14
|
+
render_client_package_init,
|
|
15
|
+
render_package_init,
|
|
16
|
+
render_workspace_init,
|
|
17
|
+
validate_package_names,
|
|
18
|
+
validate_workspace_names,
|
|
19
|
+
)
|
|
20
|
+
from httpxgen.generator.templates import (
|
|
21
|
+
GENERATED_HEADER,
|
|
22
|
+
TemplateName,
|
|
23
|
+
render_template,
|
|
24
|
+
)
|
|
25
|
+
from httpxgen.openapi import OpenAPISpec
|
|
26
|
+
from httpxgen.selection import filter_operations_by_tags
|
|
27
|
+
|
|
28
|
+
_SHARED = "shared"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def generate_client(spec: OpenAPISpec, package_name: str) -> dict[str, str]:
|
|
32
|
+
"""Render a single client package as {relative path: file content}."""
|
|
33
|
+
spec = normalize_inline_schemas(spec)
|
|
34
|
+
schemas = spec.components.schemas
|
|
35
|
+
operations = read_operations(spec)
|
|
36
|
+
client_name = f"{class_name(package_name)}Client"
|
|
37
|
+
validate_package_names(schemas, operations, client_name)
|
|
38
|
+
layout = Layout(
|
|
39
|
+
exceptions=f"{package_name}.exceptions",
|
|
40
|
+
serialization=f"{package_name}.serialization",
|
|
41
|
+
models=f"{package_name}.models",
|
|
42
|
+
http_methods=f"{package_name}.http_methods",
|
|
43
|
+
)
|
|
44
|
+
return _finish(
|
|
45
|
+
{
|
|
46
|
+
"client.py": render_client(operations, schemas, client_name, layout),
|
|
47
|
+
"models.py": render_models(schemas, operations),
|
|
48
|
+
"exceptions.py": render_template(TemplateName.EXCEPTIONS),
|
|
49
|
+
"http_methods.py": render_template(TemplateName.HTTP_METHODS),
|
|
50
|
+
"serialization.py": render_serialization(read_security_schemes(spec)),
|
|
51
|
+
"__init__.py": render_package_init(schemas, client_name, operations),
|
|
52
|
+
}
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def generate_workspace(
|
|
57
|
+
spec: OpenAPISpec,
|
|
58
|
+
tags: Sequence[str],
|
|
59
|
+
package_name: str,
|
|
60
|
+
*,
|
|
61
|
+
schema_tags: Sequence[str] = (),
|
|
62
|
+
) -> dict[str, str]:
|
|
63
|
+
"""Render one client package per tag around a shared support package."""
|
|
64
|
+
tags = tuple(dict.fromkeys(tags))
|
|
65
|
+
spec = normalize_inline_schemas(
|
|
66
|
+
filter_operations_by_tags(spec, tags, schema_tags=schema_tags)
|
|
67
|
+
)
|
|
68
|
+
schemas = spec.components.schemas
|
|
69
|
+
operations_by_tag = {
|
|
70
|
+
tag: read_operations(_only(spec, tag, tags, schema_tags)) for tag in tags
|
|
71
|
+
}
|
|
72
|
+
validate_workspace_names(
|
|
73
|
+
schemas,
|
|
74
|
+
operations_by_tag,
|
|
75
|
+
[f"{class_name(identifier(tag))}Client" for tag in tags],
|
|
76
|
+
)
|
|
77
|
+
owners = _schema_owners(spec, tags)
|
|
78
|
+
shared_schemas = {
|
|
79
|
+
name: schema for name, schema in schemas.items() if owners[name] is None
|
|
80
|
+
}
|
|
81
|
+
shared_names = frozenset(exported_model_names(shared_schemas))
|
|
82
|
+
shared_module = f"{package_name}.{_SHARED}"
|
|
83
|
+
|
|
84
|
+
files = {
|
|
85
|
+
f"{_SHARED}/exceptions.py": render_template(TemplateName.EXCEPTIONS),
|
|
86
|
+
f"{_SHARED}/http_methods.py": render_template(TemplateName.HTTP_METHODS),
|
|
87
|
+
f"{_SHARED}/serialization.py": render_serialization(
|
|
88
|
+
read_security_schemes(spec)
|
|
89
|
+
),
|
|
90
|
+
f"{_SHARED}/models.py": render_models(schemas, defined=shared_schemas),
|
|
91
|
+
f"{_SHARED}/__init__.py": render_template(TemplateName.SHARED_INIT),
|
|
92
|
+
}
|
|
93
|
+
clients: list[tuple[str, str]] = []
|
|
94
|
+
exports: dict[str, list[str]] = {}
|
|
95
|
+
for tag in tags:
|
|
96
|
+
module = identifier(tag)
|
|
97
|
+
client_name = f"{class_name(module)}Client"
|
|
98
|
+
operations = operations_by_tag[tag]
|
|
99
|
+
validate_package_names(schemas, operations, client_name)
|
|
100
|
+
defined = {name for name, owner in owners.items() if owner == tag}
|
|
101
|
+
layout = Layout(
|
|
102
|
+
exceptions=shared_module,
|
|
103
|
+
serialization=shared_module,
|
|
104
|
+
models=f"{package_name}.{module}.models",
|
|
105
|
+
http_methods=shared_module,
|
|
106
|
+
shared_models=f"{shared_module}.models",
|
|
107
|
+
shared_names=shared_names,
|
|
108
|
+
)
|
|
109
|
+
models = sorted(
|
|
110
|
+
[
|
|
111
|
+
*exported_model_names({name: schemas[name] for name in defined}),
|
|
112
|
+
*(
|
|
113
|
+
name
|
|
114
|
+
for operation in operations
|
|
115
|
+
for name in response_model_names(operation)
|
|
116
|
+
),
|
|
117
|
+
]
|
|
118
|
+
)
|
|
119
|
+
files[f"{module}/client.py"] = render_client(
|
|
120
|
+
operations, schemas, client_name, layout
|
|
121
|
+
)
|
|
122
|
+
files[f"{module}/models.py"] = render_models(
|
|
123
|
+
schemas,
|
|
124
|
+
operations,
|
|
125
|
+
defined=defined,
|
|
126
|
+
external=[(f"{shared_module}.models", sorted(shared_names))],
|
|
127
|
+
)
|
|
128
|
+
files[f"{module}/__init__.py"] = render_client_package_init(client_name, models)
|
|
129
|
+
clients.append((module, client_name))
|
|
130
|
+
exports[module] = models
|
|
131
|
+
files["__init__.py"] = render_workspace_init(clients, exports, sorted(shared_names))
|
|
132
|
+
return _finish(files)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _schema_owners(spec: OpenAPISpec, tags: Sequence[str]) -> dict[str, str | None]:
|
|
136
|
+
"""Map every schema to the single tag using it, or None when it is shared."""
|
|
137
|
+
reachable = {
|
|
138
|
+
tag: set(filter_operations_by_tags(spec, [tag]).components.schemas)
|
|
139
|
+
for tag in tags
|
|
140
|
+
}
|
|
141
|
+
counts = Counter(name for names in reachable.values() for name in names)
|
|
142
|
+
return {
|
|
143
|
+
name: next(
|
|
144
|
+
(tag for tag in tags if counts[name] == 1 and name in reachable[tag]),
|
|
145
|
+
None,
|
|
146
|
+
)
|
|
147
|
+
for name in spec.components.schemas
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _only(
|
|
152
|
+
spec: OpenAPISpec, tag: str, tags: Sequence[str], schema_tags: Sequence[str]
|
|
153
|
+
) -> OpenAPISpec:
|
|
154
|
+
others = [item for item in tags if item != tag]
|
|
155
|
+
return filter_operations_by_tags(spec, [tag], schema_tags=[*others, *schema_tags])
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _finish(files: dict[str, str]) -> dict[str, str]:
|
|
159
|
+
return {
|
|
160
|
+
relative: f"{GENERATED_HEADER}\n\n{content.rstrip()}\n"
|
|
161
|
+
for relative, content in files.items()
|
|
162
|
+
} | {"py.typed": ""}
|