Skip to content

Commit ea763b6

Browse files
committed
introduce rich for nicer cli
1 parent 08304d1 commit ea763b6

File tree

5 files changed

+216
-837
lines changed

5 files changed

+216
-837
lines changed

README.md

Lines changed: 40 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -33,30 +33,48 @@ python -m build
3333
pip install dist/serverless_openapi_generator-1.0.0-py3-none-any.whl
3434
```
3535

36-
or run as `uv` tool:
36+
### CLI
37+
38+
The tool now uses a sub-command structure for different stages of the generation process.
39+
40+
#### 1. `generate-schemas`
41+
42+
Generates JSON schemas from your Pydantic models.
3743

3844
```bash
39-
uvx --from git+https://github.com/tkfoss/python-serverless-openapi-documentation.git openapi-gen path/to/your/serverless.yml openapi.json
45+
openapi-gen generate-schemas --pydantic-source path/to/your/pydantic/models --output-dir path/to/your/schemas
4046
```
4147

42-
For more information on running tools with `uv`, see the [official documentation](https://docs.astral.sh/uv/guides/tools/#running-tools).
48+
**Arguments:**
49+
* `--pydantic-source`: (Required) Path to the Pydantic models source directory.
50+
* `--output-dir`: (Required) Directory to save the generated JSON schemas.
51+
52+
#### 2. `generate-serverless`
53+
54+
Generates a `serverless.yml` file from the JSON schemas and your project's metadata.
4355

44-
**CLI:**
4556
```bash
46-
openapi-gen openapi.json --serverless-yml-path path/to/your/serverless.yml
57+
openapi-gen generate-serverless --schema-dir path/to/your/schemas --project-dir path/to/your/project
4758
```
4859

49-
**Options:**
60+
**Arguments:**
61+
* `--schema-dir`: (Required) Directory containing the JSON schemas generated in the previous step.
62+
* `--project-dir`: (Optional) Path to the project root directory. This is used to find `pyproject.toml` for project metadata. If not provided, it's inferred from the schema directory.
5063

51-
```
52-
output_file_path Path to the output OpenAPI JSON file. (Required)
53-
--serverless-yml-path Path to the serverless.yml file. (Required if --pydantic-source is not used)
54-
--openApiVersion The OpenAPI version to generate for. Default: 3.0.3
55-
--pre-hook Path to a Python script to run before generation.
56-
--pydantic-source Path to the Pydantic models source directory.
57-
--validate Validate the generated OpenAPI spec.
64+
#### 3. `generate-spec`
65+
66+
Generates the final OpenAPI specification from a `serverless.yml` file.
67+
68+
```bash
69+
openapi-gen generate-spec openapi.json --serverless-yml-path path/to/your/serverless.yml
5870
```
5971

72+
**Arguments:**
73+
* `output_file_path`: (Required) Path to the output OpenAPI JSON file.
74+
* `--serverless-yml-path`: (Required) Path to the `serverless.yml` file.
75+
* `--openApiVersion`: The OpenAPI version to generate for. Default: `3.0.3`.
76+
* `--validate`: Validate the generated OpenAPI spec.
77+
6078
### Validation
6179

6280
This tool also includes a script to validate an OpenAPI specification file against the OpenAPI 3.0.3 specification.
@@ -66,10 +84,7 @@ This tool also includes a script to validate an OpenAPI specification file again
6684
openapi-validate path/to/your/openapi.json
6785
```
6886

69-
You can also use the `--validate` flag with the `openapi-gen` command to automatically validate the generated spec:
70-
```bash
71-
openapi-gen path/to/your/serverless.yml openapi.json --validate
72-
```
87+
You can also use the `--validate` flag with the `generate-spec` command to automatically validate the generated spec.
7388

7489
### Configuration
7590

@@ -133,49 +148,19 @@ The documentation format for functions, models, security schemes, and other prop
133148
* **Private Endpoints**: Automatically applies an `x-api-key` security scheme for functions marked with `private: true`.
134149
* **Inferred Request Bodies**: Generates `requestBody` documentation from a function's `request.schemas` configuration.
135150
* **Specification Extensions**: Supports custom `x-` fields in most sections of the documentation.
136-
* **Pre-processing Hooks**: Allows running a custom Python script to generate schemas or configurations before the main tool runs.
151+
* **Automatic Tagging**: If no tags are provided for an API operation, a tag is automatically generated from the function's handler path. For example, a handler at `src.api.users.handler` will be tagged with `users`. This helps in organizing the generated documentation.
137152

138-
### Pydantic Schema Generation
153+
### Pydantic-based Workflow
139154

140-
This tool can automatically generate JSON schemas from your Pydantic models and create a complete OpenAPI specification, even if your project does not use the Serverless Framework. To use this feature, provide the path to your Pydantic models' source directory using the `--pydantic-source` argument. The tool will generate a serverless configuration in memory to facilitate the OpenAPI generation process.
155+
This tool can automatically generate JSON schemas from your Pydantic models and create a complete OpenAPI specification. This is particularly useful for projects that do not use the Serverless Framework.
141156

142-
The tool will search for `dtos.py` files within the specified directory, generate JSON schemas for all Pydantic models found, and place them in an `openapi_models` directory at the root of your project. It will also extract project metadata from your `pyproject.toml` file to populate the `info` section of the OpenAPI document.
157+
The workflow is as follows:
143158

144-
> **Note:** For the Pydantic schema generation to work correctly, the Python environment where you run `openapi-gen` must have all the dependencies of your project installed. This is because the tool needs to import your Pydantic models to generate the schemas. You can typically install your project's dependencies using a command like `pip install -r requirements.txt` or `poetry install`.
159+
1. **Generate Schemas:** Use the `generate-schemas` command to create JSON schemas from your Pydantic models.
160+
2. **Generate `serverless.yml`:** Use the `generate-serverless` command to create a `serverless.yml` file. This command uses the schemas from the previous step and project metadata from your `pyproject.toml` file.
161+
3. **Generate OpenAPI Spec:** Use the `generate-spec` command with the newly created `serverless.yml` to generate the final `openapi.json`.
145162

146-
**To Run with Pydantic:**
147-
```bash
148-
openapi-gen openapi.json --pydantic-source path/to/your/pydantic/models
149-
```
150-
151-
### Pre-processing Hooks
152-
153-
You can use the `--pre-hook` argument to specify a Python script that will be executed before the OpenAPI generation begins. This is useful for programmatically generating parts of your `serverless.yml` or the schema files it references.
154-
155-
For example, you could have a script that generates JSON schemas from Pydantic models:
156-
157-
**`generate_schemas.py`:**
158-
```python
159-
# A simplified example to generate schemas from Pydantic models
160-
import json
161-
from pydantic import BaseModel
162-
163-
class MyModel(BaseModel):
164-
id: int
165-
name: str
166-
167-
if __name__ == "__main__":
168-
schema = MyModel.model_json_schema()
169-
with open("models/MyModel.json", "w") as f:
170-
json.dump(schema, f, indent=2)
171-
print("Generated schema for MyModel.")
172-
```
173-
174-
You would then run the tool like this:
175-
```bash
176-
openapi-gen openapi.json --serverless-yml-path serverless.yml --pre-hook generate_schemas.py
177-
```
178-
The `openapi-gen` tool will first execute `generate_schemas.py`, which creates the `models/MyModel.json` file. Then, when the generator processes your `serverless.yml`, it can reference that newly created schema via `${file(models/MyModel.json)}`.
163+
> **Note:** For the Pydantic schema generation to work correctly, the Python environment where you run `openapi-gen` must have all the dependencies of your project installed. This is because the tool needs to import your Pydantic models to generate the schemas.
179164

180165
## License
181166

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ dependencies = [
2323
"jsonschema-spec",
2424
"openapi-spec-validator",
2525
"pydantic>=2.10.6",
26+
"rich>=14.0.0",
2627
]
2728

2829
[tool.setuptools.packages.find]

src/serverless_openapi_generator/openapi_generator.py

Lines changed: 86 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
import yaml
44
import os
55
import re
6-
import subprocess
76
from pathlib import Path
7+
from rich import print as rprint
88
from . import owasp
99
from .schema_handler import SchemaHandler
1010
from . import pydantic_handler
@@ -41,7 +41,7 @@ def _resolve_file_references(self, value):
4141
else:
4242
return yaml.safe_load(f)
4343
except (IOError, yaml.YAMLError, json.JSONDecodeError) as e:
44-
print(f"Warning: Could not read or parse file reference {abs_path}: {e}")
44+
rprint(f"[yellow]Warning: Could not read or parse file reference {abs_path}: {e}[/yellow]")
4545
return value
4646
return value
4747

@@ -164,12 +164,21 @@ def create_operation_object(self, documentation, func, http_event):
164164
func_name = func['name']
165165
operation_id = f"{service_name}-{stage}-{func_name}"
166166

167+
tags = documentation.get('tags', [])
168+
if not tags:
169+
handler_path = func.get('details', {}).get('handler', '')
170+
if handler_path:
171+
# e.g. src.api.users.handler -> users
172+
parts = handler_path.split('.')
173+
if len(parts) > 2:
174+
tags.append(parts[-2])
175+
167176
obj = {
168177
'summary': documentation.get('summary', ''),
169178
'description': documentation.get('description', func['details'].get('description', '')),
170179
'operationId': operation_id,
171180
'parameters': [],
172-
'tags': documentation.get('tags', []),
181+
'tags': tags,
173182
}
174183

175184
if documentation.get('pathParams'):
@@ -291,8 +300,6 @@ def create_media_type_object(self, models):
291300
content = {}
292301
if models:
293302
for media_type, schema_name in models.items():
294-
print(f"Schema name: {schema_name}")
295-
print(f"Models: {self.schema_handler.models}")
296303
model_info = next((model for model in self.schema_handler.models if model['name'] == schema_name), None)
297304
if model_info:
298305
schema_ref = self.schema_handler.create_schema(schema_name, model_info.get('schema'))
@@ -402,77 +409,94 @@ def create_response_headers(self, headers_doc):
402409
headers[header_name] = header_obj
403410
return headers
404411

405-
def main():
406-
parser = argparse.ArgumentParser(description='Generate OpenAPI v3 documentation from a serverless.yml file.')
407-
parser.add_argument('output_file_path', type=str, help='Path to the output OpenAPI JSON file')
408-
parser.add_argument('--serverless-yml-path', type=str, help='Path to the serverless.yml file. Required if --pydantic-source is not used.')
409-
parser.add_argument('--openApiVersion', type=str, default='3.0.3', help='OpenAPI version to use')
410-
parser.add_argument('--pre-hook', type=str, help='Path to a Python script to run before generation')
411-
parser.add_argument('--pydantic-source', type=str, help='Path to the Pydantic models source directory')
412-
parser.add_argument('--validate', action='store_true', help='Validate the generated OpenAPI spec')
413-
args = parser.parse_args()
414-
415-
if not args.serverless_yml_path and not args.pydantic_source:
416-
parser.error("Either --serverless-yml-path or --pydantic-source must be provided.")
417-
418-
# Execute the pre-hook script if provided
419-
if args.pre_hook:
420-
print(f"--- Running pre-hook script: {args.pre_hook} ---")
421-
try:
422-
subprocess.run(['python', args.pre_hook], check=True, text=True)
423-
print("--- Pre-hook script finished successfully ---")
424-
except FileNotFoundError:
425-
print(f"Error: Pre-hook script not found at {args.pre_hook}")
426-
return
427-
except subprocess.CalledProcessError as e:
428-
print(f"Error executing pre-hook script: {e}")
429-
return
430-
431-
# Execute the Pydantic schema generation if the source is provided
432-
if args.pydantic_source:
433-
print(f"--- Running Pydantic schema generation from: {args.pydantic_source} ---")
434-
project_root = Path(args.pydantic_source)
435-
output_dir = project_root / "openapi_models"
436-
437-
generated_schemas = pydantic_handler.generate_dto_schemas(project_root, output_dir, project_root)
438-
project_meta = pydantic_handler.load_project_meta(project_root)
439-
440-
serverless_config = pydantic_handler.generate_serverless_config(generated_schemas, project_meta, project_root)
441-
442-
# Use a virtual file path in the project root for correct base directory resolution
443-
effective_sls_path = project_root / "serverless.yml"
444-
445-
print("--- Pydantic schema generation finished successfully ---")
446-
else:
447-
try:
448-
with open(args.serverless_yml_path, 'r') as f:
449-
serverless_config = yaml.safe_load(f)
450-
effective_sls_path = args.serverless_yml_path
451-
except FileNotFoundError:
452-
print(f"Error: The file {args.serverless_yml_path} was not found.")
453-
return
454-
except yaml.YAMLError as e:
455-
print(f"Error parsing YAML file: {e}")
456-
return
412+
def generate_schemas(args):
413+
rprint(f"[bold]--- Running Pydantic schema generation from: {args.pydantic_source} ---[/bold]")
414+
project_root = Path(args.pydantic_source)
415+
output_dir = Path(args.output_dir)
416+
417+
pydantic_handler.generate_dto_schemas(project_root, output_dir, project_root)
418+
rprint(f"[bold green]--- Pydantic schema generation finished successfully. Schemas are in {args.output_dir} ---[/bold green]")
419+
420+
def generate_serverless(args):
421+
rprint(f"[bold]--- Generating serverless.yml from schemas in {args.schema_dir} ---[/bold]")
422+
schema_dir = Path(args.schema_dir)
423+
project_root = Path(args.project_dir) if args.project_dir else schema_dir.parent
424+
425+
schemas = []
426+
for file_path in schema_dir.glob("*.json"):
427+
with open(file_path, 'r') as f:
428+
schema = json.load(f)
429+
schemas.append({
430+
"name": file_path.stem,
431+
"schema": schema
432+
})
433+
434+
project_meta = pydantic_handler.load_project_meta(project_root)
435+
serverless_config = pydantic_handler.generate_serverless_config(schemas, project_meta, project_root)
436+
437+
output_path = project_root / "serverless.yml"
438+
with open(output_path, 'w') as f:
439+
yaml.dump(serverless_config, f)
440+
441+
rprint(f"[bold green]--- serverless.yml generated at {output_path} ---[/bold green]")
442+
443+
def generate_spec(args):
444+
try:
445+
with open(args.serverless_yml_path, 'r') as f:
446+
serverless_config = yaml.safe_load(f)
447+
effective_sls_path = args.serverless_yml_path
448+
except FileNotFoundError:
449+
rprint(f"[red]Error: The file {args.serverless_yml_path} was not found.[/red]")
450+
return
451+
except yaml.YAMLError as e:
452+
rprint(f"[red]Error parsing YAML file: {e}[/red]")
453+
return
457454

458455
generator = DefinitionGenerator(serverless_config, str(effective_sls_path), args.openApiVersion)
459456
open_api_spec = generator.generate()
460457

461458
try:
462459
with open(args.output_file_path, 'w') as f:
463460
json.dump(open_api_spec, f, indent=2)
464-
print(f"OpenAPI specification successfully written to {args.output_file_path}")
461+
rprint(f"[bold green]OpenAPI specification successfully written to {args.output_file_path}[/bold green]")
465462
except IOError as e:
466-
print(f"Error writing to output file: {e}")
463+
rprint(f"[red]Error writing to output file: {e}[/red]")
467464

468465
if args.validate:
469466
from openapi_spec_validator import validate
470-
print("Validating generated spec...")
467+
rprint("[bold]Validating generated spec...[/bold]")
471468
try:
472469
validate(open_api_spec)
473-
print("Validation successful.")
470+
rprint("[bold green]Validation successful.[/bold green]")
474471
except Exception as e:
475-
print(f"Validation failed: {e}")
472+
rprint(f"[bold red]Validation failed: {e}[/bold red]")
473+
474+
def main():
475+
parser = argparse.ArgumentParser(description='Generate OpenAPI v3 documentation from a serverless.yml file.')
476+
subparsers = parser.add_subparsers(dest='command', required=True)
477+
478+
# Sub-parser for generating schemas
479+
parser_schemas = subparsers.add_parser('generate-schemas', help='Generate JSON schemas from Pydantic models.')
480+
parser_schemas.add_argument('--pydantic-source', type=str, required=True, help='Path to the Pydantic models source directory')
481+
parser_schemas.add_argument('--output-dir', type=str, required=True, help='Directory to save the generated JSON schemas')
482+
parser_schemas.set_defaults(func=generate_schemas)
483+
484+
# Sub-parser for generating serverless.yml
485+
parser_serverless = subparsers.add_parser('generate-serverless', help='Generate serverless.yml from JSON schemas.')
486+
parser_serverless.add_argument('--schema-dir', type=str, required=True, help='Directory containing the JSON schemas')
487+
parser_serverless.add_argument('--project-dir', type=str, help='Path to the project root directory (for pyproject.toml)')
488+
parser_serverless.set_defaults(func=generate_serverless)
489+
490+
# Sub-parser for generating the OpenAPI spec
491+
parser_spec = subparsers.add_parser('generate-spec', help='Generate the full OpenAPI spec from a serverless.yml file.')
492+
parser_spec.add_argument('output_file_path', type=str, help='Path to the output OpenAPI JSON file')
493+
parser_spec.add_argument('--serverless-yml-path', type=str, required=True, help='Path to the serverless.yml file.')
494+
parser_spec.add_argument('--openApiVersion', type=str, default='3.0.3', help='OpenAPI version to use')
495+
parser_spec.add_argument('--validate', action='store_true', help='Validate the generated OpenAPI spec')
496+
parser_spec.set_defaults(func=generate_spec)
497+
498+
args = parser.parse_args()
499+
args.func(args)
476500

477501
if __name__ == '__main__':
478502
main()

0 commit comments

Comments
 (0)