Skip to content
Merged
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
15 changes: 13 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ dependencies = [
"django>=4.2",
"django-ninja>=0.22",
"pydantic>=2.0",
"inflect>=7.5"
"inflect>=7.5",
"openapi-generator-cli>=7.14.0"
]

classifiers = [
"Development Status :: 3 - Alpha",
"Intended Audience :: Developers",
Expand All @@ -29,10 +31,19 @@ classifiers = [
"Framework :: Django :: 5.1",
]

[project.optional-dependencies]
lazy-jdk = [
"openapi-generator-cli[jdk4py]>=7.14.0"
]

[tool.setuptools.packages.find]
where = ["src"]
exclude = ["lazy_ninja.tests*"]

[tool.setuptools_scm]
version_scheme = "python-simplified-semver"
local_scheme = "no-local-version"
local_scheme = "no-local-version"

[project.scripts]
lazy-ninja = "lazy_ninja.cli.client_generator:main"

26 changes: 14 additions & 12 deletions requirements.dev.txt
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
pytest-cov==6.0.0
pytest-django==4.10.0
inflect==7.5.0
colorama==0.4.6
mkdocs-autorefs
mkdocs-get-deps
mkdocs-material
mkdocs-material-extensions
mkdocs-redirects
mkdocs-static-i18n
mkdocstrings
mkdocstrings-python
inflect>=7.5.0
openapi-generator-cli[jdk4py]>=7.0.0
pytest-cov>=6.0.0
colorama>=0.4.6
pytest-django>=4.10.0
mkdocs>=1.6.1
mkdocs-autorefs>=1.4.1
mkdocs-get-deps>=0.2.0
mkdocs-material>=9.6.9
mkdocs-material-extensions>=1.3.1
mkdocs-redirects>=1.2.2
mkdocs-static-i18n>=1.3.0
mkdocstrings>=0.29.0
mkdocstrings-python>=1.16.7
25 changes: 13 additions & 12 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
lazy-ninja
inflect==7.5.0
pytest-cov==6.0.0
colorama==0.4.6
pytest-django==4.10.0
mkdocs==1.6.1
mkdocs-autorefs==1.4.1
mkdocs-get-deps==0.2.0
mkdocs-material==9.6.9
mkdocs-material-extensions==1.3.1
mkdocs-redirects==1.2.2
mkdocs-static-i18n==1.3.0
mkdocstrings==0.29.0
mkdocstrings-python==1.16.7
openapi-generator-cli[jdk4py]>=7.0.0
pytest-cov>=6.0.0
colorama>=0.4.6
pytest-django>=4.10.0
mkdocs>=1.6.1
mkdocs-autorefs>=1.4.1
mkdocs-get-deps>=0.2.0
mkdocs-material>=9.6.9
mkdocs-material-extensions>=1.3.1
mkdocs-redirects>=1.2.2
mkdocs-static-i18n>=1.3.0
mkdocstrings>=0.29.0
mkdocstrings-python>=1.16.7
75 changes: 0 additions & 75 deletions src/lazy_ninja/__init__.py
Original file line number Diff line number Diff line change
@@ -1,75 +0,0 @@
from typing import Type, Optional

from django.db.models import Model

from ninja import NinjaAPI
from ninja import Schema

from .base import BaseModelController
from .helpers import get_hook
from .registry import ModelRegistry, controller_for
from .routes import register_model_routes_internal
from .file_upload import FileUploadConfig

def register_model_routes(
api: NinjaAPI,
model: Type[Model],
base_url: str,
list_schema: Type[Schema],
detail_schema: Type[Schema],
create_schema: Optional[Type[Schema]] = None,
update_schema: Optional[Type[Schema]] = None,
pagination_strategy: Optional[str] = None,
file_upload_config: Optional[FileUploadConfig] = None,
use_multipart_create: bool = False,
use_multipart_update: bool = False,
is_async: bool = True
) -> None:
"""
Main function to register CRUD routes for a Django model using Django Ninja.

Args:
api: NinjaAPI instance.
model: Django model class.
base_url: Base URL for the routes.
list_schema: Schema for list responses.
detail_schema: Schema for detail responses.
create_schema: (Optional) Schema for create requests.
update_schema: (Optional) Schema for update requests.
pagination_strategy: (Optional) Strategy for pagination.
file_upload_config: (Optional) Configuration for file uploads.
use_multipart_create: Whether to use multipart/form-data for create endpoint.
use_multipart_update: Whether to use multipart/form-data for update endpoint.
is_async: Whether to use async routes (default: True).

This function retrieves the registered controller for the model (if any)
and passes its hooks to the internal route registration function.
"""
ModelRegistry.discover_controllers()

controller = ModelRegistry.get_controller(model.__name__)
if not controller:
controller = BaseModelController

register_model_routes_internal(
api=api,
model=model,
base_url=base_url,
list_schema=list_schema,
detail_schema=detail_schema,
create_schema=create_schema,
update_schema=update_schema,
pre_list=get_hook(controller, 'pre_list'),
before_create=get_hook(controller, 'before_create'),
after_create=get_hook(controller, 'after_create'),
before_update=get_hook(controller, 'before_update'),
after_update=get_hook(controller, 'after_update'),
before_delete=get_hook(controller, 'before_delete'),
after_delete=get_hook(controller, 'after_delete'),
custom_response=get_hook(controller, 'custom_response'),
pagination_strategy=pagination_strategy,
file_upload_config=file_upload_config,
use_multipart_create=use_multipart_create,
use_multipart_update=use_multipart_update,
is_async=is_async
)
2 changes: 1 addition & 1 deletion src/lazy_ninja/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

from ninja import NinjaAPI, Schema

from . import register_model_routes
from .core import register_model_routes
from .utils import generate_schema
from .helpers import to_kebab_case
from .pagination import get_pagination_strategy
Expand Down
Empty file added src/lazy_ninja/cli/__init__.py
Empty file.
207 changes: 207 additions & 0 deletions src/lazy_ninja/cli/client_generator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
import os
import sys
import subprocess
import argparse
import importlib
from pathlib import Path

GENERATOR_CONFIG = {
"typescript-types": {
"cmd": ["npx", "openapi-typescript", "{schema}", "--output", "{out}"],
"ext": "ts",
},
"dart": {
"cmd": [
"openapi-generator-cli", "generate",
"-i", "{schema}",
"-g", "dart-dio",
"-o", "{out_dir}"
],
"ext": None
},
"python": {
"cmd": [
"openapi-generator-cli", "generate",
"-i", "{schema}",
"-g", "python",
"-o", "{out_dir}"
],
"ext": None
},
"typescript-axios": {
"cmd": [
"openapi-generator-cli", "generate",
"-i", "{schema}",
"-g", "typescript-axios",
"-o", "{out_dir}"
],
"ext": None
},
"typescript-fetch": {
"cmd": [
"openapi-generator-cli", "generate",
"-i", "{schema}",
"-g", "typescript-fetch",
"-o", "{out_dir}"
],
"ext": None
},
"java": {
"cmd": [
"openapi-generator-cli", "generate",
"-i", "{schema}",
"-g", "java",
"-o", "{out_dir}"
],
"ext": None
},
"kotlin": {
"cmd": [
"openapi-generator-cli", "generate",
"-i", "{schema}",
"-g", "kotlin",
"-o", "{out_dir}"
],
"ext": None
},
"go": {
"cmd": [
"openapi-generator-cli", "generate",
"-i", "{schema}",
"-g", "go",
"-o", "{out_dir}"
],
"ext": None
},
"csharp": {
"cmd": [
"openapi-generator-cli", "generate",
"-i", "{schema}",
"-g", "csharp",
"-o", "{out_dir}"
],
"ext": None
},
"ruby": {
"cmd": [
"openapi-generator-cli", "generate",
"-i", "{schema}",
"-g", "ruby",
"-o", "{out_dir}"
],
"ext": None
},
"swift5": {
"cmd": [
"openapi-generator-cli", "generate",
"-i", "{schema}",
"-g", "swift5",
"-o", "{out_dir}"
],
"ext": None
}
}


def setup_django(settings_module: str):
sys.path.insert(0, os.getcwd())
os.environ.setdefault("DJANGO_SETTINGS_MODULE", settings_module)

import django
django.setup()


def dump_openapi(api_module: str, api_var: str, out_file: Path):
from django.test import RequestFactory
from ninja.openapi.views import openapi_json

mod = importlib.import_module(api_module)
api = getattr(mod, api_var)

req = RequestFactory().get("/api/openapi.json")
resp = openapi_json(req, api)
out_file.write_bytes(resp.content)


def generate_client(schema_path: Path, language: str, output: str):
cfg = GENERATOR_CONFIG.get(language)

cmd = [p.format(schema=str(schema_path), out=output, out_dir=output) for p in cfg["cmd"]]
print(f"[LazyNinja] ▶ Running: {' '.join(cmd)}")
proc = subprocess.run(cmd)
if proc.returncode != 0:
print(f"[LazyNinja] ❌ Generator failed.")
sys.exit(proc.returncode)
print(f"[LazyNinja] ✅ Client ({language}) generated at {output}")


def main():
parser = argparse.ArgumentParser(
prog="lazy-ninja",
description=(
"🌀 Lazy Ninja CLI\n\n"
"Generate client code and SDKs from your Django + Ninja API schema.\n\n"
"Supports generating frontend clients (e.g., TypeScript for React) as well as backend SDKs\n"
"for server-to-server communication or internal services.\n\n"
"Example usage:\n"
" lazy-ninja generate-client typescript \\\n"
" --settings myproject.settings \\\n"
" --api-module myproject.api \\\n"
" --output ./client.ts"
),
epilog="🐛 Report issues at: https://github.com/AghastyGD/lazy-ninja/issues",
formatter_class=argparse.RawDescriptionHelpFormatter
)

sub = parser.add_subparsers(dest="cmd")

gen = sub.add_parser(
"generate-client",
help="Generate client code from OpenAPI schema",
description=(
"Generate client code from the OpenAPI schema exposed by your Django Ninja API.\n\n"
"Supports multiple languages. You must provide your Django settings module and\n"
"the path to the module where your `api = NinjaAPI()` instance is defined."
),
formatter_class=argparse.RawDescriptionHelpFormatter
)

gen.add_argument(
"language",
choices=list(GENERATOR_CONFIG.keys()),
help="Target language for client code (e.g. typescript, python)"
)
gen.add_argument(
"--settings",
required=True,
help="Django settings module (e.g. myproject.settings)"
)
gen.add_argument(
"--api-module",
default="settings.api",
help="Module path where your `api = NinjaAPI()` is defined (default: settings.api)"
)
gen.add_argument(
"--output",
default="./client",
help="Output file or folder (e.g. ./client.ts)"
)

args = parser.parse_args()

if not args.cmd:
parser.print_help()
sys.exit(1)

setup_django(args.settings)
schema_file = Path(".lazy_ninja_openapi.json")
dump_openapi(args.api_module, "api", schema_file)
generate_client(schema_file, args.language, args.output)
schema_file.unlink(missing_ok=True)


if __name__ == "__main__":
main()



Loading