Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion mypy.ini
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ plugins = pydantic.mypy
[mypy-nox.*,pytest]
ignore_missing_imports = True

[mypy-openapi_schema_pydantic.*]
[mypy-openapi_pydantic.*]
ignore_missing_imports = True

[mypy-autopep8.*]
Expand Down
3,125 changes: 1,787 additions & 1,338 deletions poetry.lock

Large diffs are not rendered by default.

29 changes: 17 additions & 12 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,17 @@ keywords = ["OpenAPI", "Generator", "Python", "async"]
Changelog = "https://github.com/MarcoMuellner/openapi-python-generator/releases"

[tool.poetry.dependencies]
python = "^3.7"
httpx = {extras = ["all"], version = "^0.23.0"}
pydantic = "^1.9.1"
orjson = "^3.7.2"
openapi-schema-pydantic = "^1.2.3"
Jinja2 = "^3.1.2"
click = "^8.1.3"
black = ">=21.10b0"
isort = ">=5.10.1"
python = "^3.9"
fastapi = "^0.115.0"
orjson = "^3.10.7"
jinja2 = "^3.1.4"
click = "^8.1.7"
isort = "^5.13.2"
backoff = "^2.2.1"
xmltodict = "^0.13.0"
structlog = "23.1.0"
httpx = "^0.23.0"
openapi-pydantic = "^0.4.1"

[tool.poetry.dev-dependencies]
Pygments = ">=2.10.0"
Expand All @@ -47,16 +49,19 @@ safety = ">=1.10.3"
typeguard = ">=2.13.3"
xdoctest = {extras = ["colors"], version = ">=0.15.10"}
myst-parser = {version = ">=0.16.1"}
pytest-cov = "^3.0.0"
fastapi = "^0.78.0"
uvicorn = "^0.18.1"
pytest-cov = "^5.0.0"
uvicorn = "^0.31.0"
respx = "^0.20.1"
aiohttp = "^3.8.3"

[tool.poetry.scripts]
openapi-python-generator = "openapi_python_generator.__main__:main"



[tool.poetry.group.dev.dependencies]
black = "^24.8.0"

[tool.coverage.paths]
source = ["src", "*/site-packages"]
tests = ["tests", "*/tests"]
Expand Down
21 changes: 18 additions & 3 deletions src/openapi_python_generator/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
import click

from openapi_python_generator import __version__
from openapi_python_generator.common import HTTPLibrary
from openapi_python_generator.generate_data import generate_data
from openapi_python_generator.common import HTTPLibrary, library_config_dict
from openapi_python_generator.generate_data import generate_data, get_open_api, generator, write_data


@click.command()
Expand All @@ -24,6 +24,13 @@
help="Name of the environment variable that contains the token. If you set this, the code expects this environment "
"variable to be set and will raise an error if it is not.",
)
@click.option(
"--use-class",
is_flag=True,
show_default=True,
default=False,
help="Use the service class generator when creating apis for each controller",
)
@click.option(
"--use-orjson",
is_flag=True,
Expand All @@ -32,6 +39,12 @@
help="Use the orjson library to serialize the data. This is faster than the default json library and provides "
"serialization of datetimes and other types that are not supported by the default json library.",
)
@click.option(
"--enum-path",
type=str,
multiple=True,
help="path for Custom Enums to use",
)
@click.option(
"--custom-template-path",
type=str,
Expand All @@ -45,6 +58,8 @@ def main(
library: Optional[HTTPLibrary] = HTTPLibrary.httpx,
env_token_name: Optional[str] = None,
use_orjson: bool = False,
use_class: bool = False,
enum_path: Optional[tuple[str]] = None,
custom_template_path: Optional[str] = None,
) -> None:
"""
Expand All @@ -54,7 +69,7 @@ def main(
an OUTPUT path, where the resulting client is created.
"""
generate_data(
source, output, library, env_token_name, use_orjson, custom_template_path
source, output, library, env_token_name, use_orjson, use_class, enum_path, custom_template_path
)


Expand Down
56 changes: 51 additions & 5 deletions src/openapi_python_generator/generate_data.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from pathlib import Path
from typing import Optional
from typing import Union

import shutil
import black
import click
import httpx
Expand All @@ -10,7 +10,7 @@
from black import NothingChanged
from httpx import ConnectError
from httpx import ConnectTimeout
from openapi_schema_pydantic import OpenAPI
from openapi_pydantic import OpenAPI
from pydantic import ValidationError

from .common import HTTPLibrary
Expand All @@ -21,6 +21,13 @@
from .models import ConversionResult


def create_clean_directory(path: Path):
"""Remove the directory if it exists and create a fresh one."""
if path.exists():
shutil.rmtree(path) # Remove the existing directory and its contents
path.mkdir(parents=True, exist_ok=True) # Recreate the directory


def write_code(path: Path, content) -> None:
"""
Write the content to the file at the given path.
Expand All @@ -34,9 +41,12 @@ def write_code(path: Path, content) -> None:
formatted_contend = black.format_file_contents(
content, fast=False, mode=black.FileMode(line_length=120)
)

except NothingChanged:
formatted_contend = content
except Exception as e:
f.write(content)
print(f"Error formatting {path}: {e}")
raise e
formatted_contend = isort.code(formatted_contend, line_length=120)
f.write(formatted_contend)
except Exception as e:
Expand Down Expand Up @@ -88,10 +98,17 @@ def write_data(data: ConversionResult, output: Union[str, Path]) -> None:
# Create the models module.
models_path = Path(output) / "models"
models_path.mkdir(parents=True, exist_ok=True)
create_clean_directory(Path(models_path))

# Create the services module.
services_path = Path(output) / "services"
services_path.mkdir(parents=True, exist_ok=True)
create_clean_directory(Path(services_path))

# Create the enums.
enums_path = Path(output) / "enums"
if len(data.enum_files) > 0:
create_clean_directory(Path(enums_path))

files = []

Expand All @@ -116,15 +133,40 @@ def write_data(data: ConversionResult, output: Union[str, Path]) -> None:
files.append(service.file_name)
write_code(
services_path / f"{service.file_name}.py",
jinja_env.get_template(SERVICE_TEMPLATE).render(**service.dict()),
jinja_env.get_template(SERVICE_TEMPLATE).render(**service.model_dump()),
)

files = []

for enum in data.enum_files:
files.append(enum.file_name)
write_code(
enums_path / f"{enum.file_name}.py",
enum.content,
)
if len(files) > 0:
write_code(
enums_path / "__init__.py",
"\n".join([f"from .{file} import *" for file in files]),
)

# Create services.__init__.py file containing imports to all services.
write_code(services_path / "__init__.py", "")
write_code(
services_path / "__init__.py",
"\n".join([f"from .{service.file_name} import *" for service in data.services]),
)

# Write the api_config.py file.
write_code(Path(output) / "api_config.py", data.api_config.content)

# Write the sdk.py file.
if data.sdk is not None:
write_code(Path(output) / "sdk.py", data.sdk.content)

# Write the rest_client.py file.
if data.rest_client is not None:
write_code(Path(output) / "rest_client.py", data.rest_client.content)

# Write the __init__.py file.
write_code(
Path(output) / "__init__.py",
Expand All @@ -138,6 +180,8 @@ def generate_data(
library: Optional[HTTPLibrary] = HTTPLibrary.httpx,
env_token_name: Optional[str] = None,
use_orjson: bool = False,
use_class: bool = False,
enum_path: Optional[tuple[str]] = None,
custom_template_path: Optional[str] = None,
) -> None:
"""
Expand All @@ -151,6 +195,8 @@ def generate_data(
library_config_dict[library],
env_token_name,
use_orjson,
use_class,
enum_path,
custom_template_path,
)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from typing import Optional

from openapi_schema_pydantic import OpenAPI

from openapi_pydantic import OpenAPI

from openapi_python_generator.language_converters.python.jinja_config import (
API_CONFIG_TEMPLATE,
Expand All @@ -24,4 +25,5 @@ def generate_api_config(
env_token_name=env_token_name, **data.dict()
),
base_url=data.servers[0].url if len(data.servers) > 0 else "NO SERVER",
)
env_token_name=env_token_name, **data.model_dump()
)
11 changes: 11 additions & 0 deletions src/openapi_python_generator/language_converters/python/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,14 @@ def normalize_symbol(symbol: str) -> str:
if normalized_symbol in keyword.kwlist:
normalized_symbol = normalized_symbol + "_"
return normalized_symbol


def camel_case_split(identifier):
matches = re.finditer(
".+?(?:(?<=[a-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])|$)", identifier
)
result = [m.group(0) for m in matches]
if len(result) > 1:
return "_".join(result).lower()
else:
return identifier.lower()
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from openapi_python_generator.models import EnumFiles
from typing import Optional

def generate_enum_file(
enum_path: str
) -> Optional[EnumFiles] :


"""
Generate the API SDK.
"""
try:
# read the enum file
with open(enum_path, "r") as file:
content = file.read()
file_name = enum_path.split("/")[-1].split(".")[0]
# extract classes names in the file
classes = [line.split(" ")[1].split("(")[0] for line in content.split("\n") if "class" in line]

return EnumFiles(
file_name=file_name,
content=content,
classes=classes

)
return None
except Exception as e:
return None
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from typing import Optional

from openapi_schema_pydantic import OpenAPI
from openapi_pydantic import OpenAPI

from openapi_python_generator.language_converters.python import common
from openapi_python_generator.language_converters.python.api_config_generator import (
Expand All @@ -12,6 +12,14 @@
from openapi_python_generator.language_converters.python.service_generator import (
generate_services,
)
from openapi_python_generator.language_converters.python.service_class_generator import (
generate_class_services,
)
from openapi_python_generator.language_converters.python.rest_client_generator import (
generate_rest_client,
)
from openapi_python_generator.language_converters.python.enum_file_generator import generate_enum_file
from openapi_python_generator.language_converters.python.sdk_generator import generate_sdk
from openapi_python_generator.models import ConversionResult
from openapi_python_generator.models import LibraryConfig

Expand All @@ -21,6 +29,8 @@ def generator(
library_config: LibraryConfig,
env_token_name: Optional[str] = None,
use_orjson: bool = False,
use_class: bool = False,
enum_path: Optional[tuple[str]] = None,
custom_template_path: Optional[str] = None,
) -> ConversionResult:
"""
Expand All @@ -36,14 +46,35 @@ def generator(
models = []

if data.paths is not None:
services = generate_services(data.paths, library_config)
services = generate_services(data.paths, library_config) if not use_class else generate_class_services(data.paths, library_config)
else:
services = []

api_config = generate_api_config(data, env_token_name)

enum_classes = []
if enum_path is not None:
enum_files = [generate_enum_file(path) for path in enum_path]
for enum in enum_files:
if enum is not None:
enum_classes += enum.classes
else:
enum_files = []

sdk = None
rest_client = None
if use_class:
sdk = generate_sdk(data, env_token_name,
classes=[ {"class_name" :service.class_name , "file_name": service.file_name} for service in services ],
enum_classes=[ {"class_name" :enum , "method_name": common.camel_case_split(enum)} for enum in enum_classes])

rest_client = generate_rest_client(data, library_config)

return ConversionResult(
models=models,
services=services,
api_config=api_config,
sdk=sdk,
rest_client=rest_client,
enum_files=[enum_file for enum_file in enum_files if enum_file is not None],
)
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
HTTPX_TEMPLATE = "httpx.jinja2"
API_CONFIG_TEMPLATE = "apiconfig.jinja2"
TEMPLATE_PATH = Path(__file__).parent / "templates"
SDK_TEMPLATE = "sdk.jinja2"
REST_CLIENT_TEMPLATE = "rest_client.jinja2"


def create_jinja_env():
Expand Down
Loading
Loading