openapi-to-agent-skills 0.3.2__tar.gz
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.
- openapi_to_agent_skills-0.3.2/.gitignore +31 -0
- openapi_to_agent_skills-0.3.2/PKG-INFO +112 -0
- openapi_to_agent_skills-0.3.2/README.md +83 -0
- openapi_to_agent_skills-0.3.2/pyproject.toml +63 -0
- openapi_to_agent_skills-0.3.2/src/openapi_to_skills/__init__.py +5 -0
- openapi_to_agent_skills-0.3.2/src/openapi_to_skills/__main__.py +5 -0
- openapi_to_agent_skills-0.3.2/src/openapi_to_skills/cli.py +133 -0
- openapi_to_agent_skills-0.3.2/src/openapi_to_skills/converter.py +139 -0
- openapi_to_agent_skills-0.3.2/src/openapi_to_skills/parser.py +501 -0
- openapi_to_agent_skills-0.3.2/src/openapi_to_skills/renderer.py +253 -0
- openapi_to_agent_skills-0.3.2/src/openapi_to_skills/spec_loader.py +53 -0
- openapi_to_agent_skills-0.3.2/src/openapi_to_skills/templates/authentication.md.j2 +46 -0
- openapi_to_agent_skills-0.3.2/src/openapi_to_skills/templates/operation.md.j2 +85 -0
- openapi_to_agent_skills-0.3.2/src/openapi_to_skills/templates/resource.md.j2 +13 -0
- openapi_to_agent_skills-0.3.2/src/openapi_to_skills/templates/schema.md.j2 +74 -0
- openapi_to_agent_skills-0.3.2/src/openapi_to_skills/templates/schema_index.md.j2 +9 -0
- openapi_to_agent_skills-0.3.2/src/openapi_to_skills/templates/skill.md.j2 +53 -0
- openapi_to_agent_skills-0.3.2/src/openapi_to_skills/types.py +201 -0
- openapi_to_agent_skills-0.3.2/src/openapi_to_skills/validate.py +33 -0
- openapi_to_agent_skills-0.3.2/src/openapi_to_skills/writer.py +23 -0
- openapi_to_agent_skills-0.3.2/tests/__init__.py +0 -0
- openapi_to_agent_skills-0.3.2/tests/e2e/__init__.py +0 -0
- openapi_to_agent_skills-0.3.2/tests/e2e/test_snapshots.py +86 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.egg-info/
|
|
6
|
+
dist/
|
|
7
|
+
build/
|
|
8
|
+
*.egg
|
|
9
|
+
.eggs/
|
|
10
|
+
|
|
11
|
+
# Virtual environments
|
|
12
|
+
.venv/
|
|
13
|
+
venv/
|
|
14
|
+
env/
|
|
15
|
+
|
|
16
|
+
# IDE
|
|
17
|
+
.idea/
|
|
18
|
+
.vscode/
|
|
19
|
+
*.swp
|
|
20
|
+
|
|
21
|
+
# Test output
|
|
22
|
+
test-output/
|
|
23
|
+
test-output-e2e/
|
|
24
|
+
|
|
25
|
+
# OS
|
|
26
|
+
.DS_Store
|
|
27
|
+
Thumbs.db
|
|
28
|
+
|
|
29
|
+
# Distribution
|
|
30
|
+
*.whl
|
|
31
|
+
*.tar.gz
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: openapi-to-agent-skills
|
|
3
|
+
Version: 0.3.2
|
|
4
|
+
Summary: Convert OpenAPI specifications to Agent Skills format
|
|
5
|
+
Project-URL: Homepage, https://github.com/xuyong/openapi-to-agent-skills
|
|
6
|
+
Project-URL: Repository, https://github.com/xuyong/openapi-to-agent-skills
|
|
7
|
+
Author: Xu Yong
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Topic :: Software Development :: Code Generators
|
|
17
|
+
Requires-Python: >=3.10
|
|
18
|
+
Requires-Dist: httpx>=0.27.0
|
|
19
|
+
Requires-Dist: jinja2>=3.1.0
|
|
20
|
+
Requires-Dist: pyyaml>=6.0
|
|
21
|
+
Requires-Dist: regex>=2024.0
|
|
22
|
+
Requires-Dist: rich>=13.0
|
|
23
|
+
Requires-Dist: typer>=0.12.0
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
26
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
27
|
+
Requires-Dist: ruff>=0.5.0; extra == 'dev'
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# openapi-to-agent-skills
|
|
31
|
+
|
|
32
|
+
Convert OpenAPI specifications to Agent Skills format — structured markdown documentation optimized for AI agents.
|
|
33
|
+
|
|
34
|
+
## Installation
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install openapi-to-agent-skills
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Usage
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
openapi-to-agent-skills <path-or-url-to-openapi-spec> [OPTIONS]
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Options
|
|
47
|
+
|
|
48
|
+
| Option | Description | Default |
|
|
49
|
+
|--------|-------------|---------|
|
|
50
|
+
| `-o, --output` | Output directory | `./output` |
|
|
51
|
+
| `-n, --name` | Skill name (derived from API title if omitted) | |
|
|
52
|
+
| `--include-tags` | Only include these tags (comma-separated) | |
|
|
53
|
+
| `--exclude-tags` | Exclude these tags (comma-separated) | |
|
|
54
|
+
| `--exclude-deprecated` | Exclude deprecated operations | `False` |
|
|
55
|
+
| `-t, --templates` | Custom templates directory | |
|
|
56
|
+
| `-a, --assets` | Static assets directory overlaid onto output | |
|
|
57
|
+
| `--exclude-paths` | Exclude paths matching prefixes (comma-separated) | |
|
|
58
|
+
| `-g, --group-by` | Grouping strategy: `tags`, `path`, `auto` | `auto` |
|
|
59
|
+
| `--case-strategy` | `lowercase` for case-insensitive filesystem safety | |
|
|
60
|
+
| `-f, --force` | Overwrite existing output directory | `False` |
|
|
61
|
+
| `-q, --quiet` | Suppress output except errors | `False` |
|
|
62
|
+
|
|
63
|
+
### Examples
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
# From a local file
|
|
67
|
+
openapi-to-agent-skills api.yaml -o ./skills
|
|
68
|
+
|
|
69
|
+
# From a URL
|
|
70
|
+
openapi-to-agent-skills https://petstore3.swagger.io/api/v3/openapi.json -o ./skills
|
|
71
|
+
|
|
72
|
+
# Filter by tags
|
|
73
|
+
openapi-to-agent-skills api.yaml --include-tags users,orders
|
|
74
|
+
|
|
75
|
+
# Case-safe output for Windows/macOS
|
|
76
|
+
openapi-to-agent-skills api.yaml --case-strategy lowercase
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Output Structure
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
<skill-name>/
|
|
83
|
+
├── SKILL.md # Entry point with YAML frontmatter
|
|
84
|
+
└── references/
|
|
85
|
+
├── authentication.md # Auth schemes (if any)
|
|
86
|
+
├── resources/
|
|
87
|
+
│ └── <tag>.md # One per resource group
|
|
88
|
+
├── operations/
|
|
89
|
+
│ └── <operationId>.md # One per operation
|
|
90
|
+
└── schemas/
|
|
91
|
+
└── <prefix>/
|
|
92
|
+
├── _index.md
|
|
93
|
+
└── <SchemaName>.md # One per schema
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Programmatic Usage
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
from openapi_to_skills import convert_openapi_to_skill
|
|
100
|
+
from openapi_to_skills.types import ConvertOptions
|
|
101
|
+
|
|
102
|
+
import yaml
|
|
103
|
+
|
|
104
|
+
with open("api.yaml") as f:
|
|
105
|
+
spec = yaml.safe_load(f)
|
|
106
|
+
|
|
107
|
+
convert_openapi_to_skill(spec, ConvertOptions(output_dir="./output"))
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## License
|
|
111
|
+
|
|
112
|
+
MIT
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# openapi-to-agent-skills
|
|
2
|
+
|
|
3
|
+
Convert OpenAPI specifications to Agent Skills format — structured markdown documentation optimized for AI agents.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install openapi-to-agent-skills
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
openapi-to-agent-skills <path-or-url-to-openapi-spec> [OPTIONS]
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
### Options
|
|
18
|
+
|
|
19
|
+
| Option | Description | Default |
|
|
20
|
+
|--------|-------------|---------|
|
|
21
|
+
| `-o, --output` | Output directory | `./output` |
|
|
22
|
+
| `-n, --name` | Skill name (derived from API title if omitted) | |
|
|
23
|
+
| `--include-tags` | Only include these tags (comma-separated) | |
|
|
24
|
+
| `--exclude-tags` | Exclude these tags (comma-separated) | |
|
|
25
|
+
| `--exclude-deprecated` | Exclude deprecated operations | `False` |
|
|
26
|
+
| `-t, --templates` | Custom templates directory | |
|
|
27
|
+
| `-a, --assets` | Static assets directory overlaid onto output | |
|
|
28
|
+
| `--exclude-paths` | Exclude paths matching prefixes (comma-separated) | |
|
|
29
|
+
| `-g, --group-by` | Grouping strategy: `tags`, `path`, `auto` | `auto` |
|
|
30
|
+
| `--case-strategy` | `lowercase` for case-insensitive filesystem safety | |
|
|
31
|
+
| `-f, --force` | Overwrite existing output directory | `False` |
|
|
32
|
+
| `-q, --quiet` | Suppress output except errors | `False` |
|
|
33
|
+
|
|
34
|
+
### Examples
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
# From a local file
|
|
38
|
+
openapi-to-agent-skills api.yaml -o ./skills
|
|
39
|
+
|
|
40
|
+
# From a URL
|
|
41
|
+
openapi-to-agent-skills https://petstore3.swagger.io/api/v3/openapi.json -o ./skills
|
|
42
|
+
|
|
43
|
+
# Filter by tags
|
|
44
|
+
openapi-to-agent-skills api.yaml --include-tags users,orders
|
|
45
|
+
|
|
46
|
+
# Case-safe output for Windows/macOS
|
|
47
|
+
openapi-to-agent-skills api.yaml --case-strategy lowercase
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Output Structure
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
<skill-name>/
|
|
54
|
+
├── SKILL.md # Entry point with YAML frontmatter
|
|
55
|
+
└── references/
|
|
56
|
+
├── authentication.md # Auth schemes (if any)
|
|
57
|
+
├── resources/
|
|
58
|
+
│ └── <tag>.md # One per resource group
|
|
59
|
+
├── operations/
|
|
60
|
+
│ └── <operationId>.md # One per operation
|
|
61
|
+
└── schemas/
|
|
62
|
+
└── <prefix>/
|
|
63
|
+
├── _index.md
|
|
64
|
+
└── <SchemaName>.md # One per schema
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Programmatic Usage
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
from openapi_to_skills import convert_openapi_to_skill
|
|
71
|
+
from openapi_to_skills.types import ConvertOptions
|
|
72
|
+
|
|
73
|
+
import yaml
|
|
74
|
+
|
|
75
|
+
with open("api.yaml") as f:
|
|
76
|
+
spec = yaml.safe_load(f)
|
|
77
|
+
|
|
78
|
+
convert_openapi_to_skill(spec, ConvertOptions(output_dir="./output"))
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## License
|
|
82
|
+
|
|
83
|
+
MIT
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "openapi-to-agent-skills"
|
|
3
|
+
version = "0.3.2"
|
|
4
|
+
description = "Convert OpenAPI specifications to Agent Skills format"
|
|
5
|
+
requires-python = ">=3.10"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
authors = [
|
|
8
|
+
{ name = "Xu Yong" },
|
|
9
|
+
]
|
|
10
|
+
readme = "README.md"
|
|
11
|
+
classifiers = [
|
|
12
|
+
"Development Status :: 4 - Beta",
|
|
13
|
+
"Intended Audience :: Developers",
|
|
14
|
+
"License :: OSI Approved :: MIT License",
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Programming Language :: Python :: 3.10",
|
|
17
|
+
"Programming Language :: Python :: 3.11",
|
|
18
|
+
"Programming Language :: Python :: 3.12",
|
|
19
|
+
"Topic :: Software Development :: Code Generators",
|
|
20
|
+
]
|
|
21
|
+
dependencies = [
|
|
22
|
+
"typer>=0.12.0",
|
|
23
|
+
"pyyaml>=6.0",
|
|
24
|
+
"jinja2>=3.1.0",
|
|
25
|
+
"httpx>=0.27.0",
|
|
26
|
+
"rich>=13.0",
|
|
27
|
+
"regex>=2024.0",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.urls]
|
|
31
|
+
Homepage = "https://github.com/xuyong/openapi-to-agent-skills"
|
|
32
|
+
Repository = "https://github.com/xuyong/openapi-to-agent-skills"
|
|
33
|
+
|
|
34
|
+
[project.scripts]
|
|
35
|
+
openapi-to-agent-skills = "openapi_to_skills.cli:app"
|
|
36
|
+
|
|
37
|
+
[project.optional-dependencies]
|
|
38
|
+
dev = [
|
|
39
|
+
"pytest>=8.0",
|
|
40
|
+
"ruff>=0.5.0",
|
|
41
|
+
"mypy>=1.10",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
[build-system]
|
|
45
|
+
requires = ["hatchling"]
|
|
46
|
+
build-backend = "hatchling.build"
|
|
47
|
+
|
|
48
|
+
[tool.hatch.build.targets.wheel]
|
|
49
|
+
packages = ["src/openapi_to_skills"]
|
|
50
|
+
|
|
51
|
+
[tool.ruff]
|
|
52
|
+
target-version = "py310"
|
|
53
|
+
line-length = 100
|
|
54
|
+
|
|
55
|
+
[tool.ruff.lint]
|
|
56
|
+
select = ["E", "F", "I", "N", "W"]
|
|
57
|
+
|
|
58
|
+
[tool.pytest.ini_options]
|
|
59
|
+
testpaths = ["tests"]
|
|
60
|
+
|
|
61
|
+
[tool.mypy]
|
|
62
|
+
python_version = "3.10"
|
|
63
|
+
strict = true
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""CLI entry point using Typer."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import shutil
|
|
7
|
+
import sys
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
import typer
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
|
|
13
|
+
from openapi_to_skills.converter import convert_openapi_to_skill
|
|
14
|
+
from openapi_to_skills.renderer import to_file_name
|
|
15
|
+
from openapi_to_skills.spec_loader import load_spec_from_input
|
|
16
|
+
from openapi_to_skills.types import ConvertOptions, ParserFilter, ParserOptions
|
|
17
|
+
from openapi_to_skills.validate import validate_case_strategy, validate_group_by, validate_spec
|
|
18
|
+
|
|
19
|
+
app = typer.Typer(add_completion=False)
|
|
20
|
+
console = Console()
|
|
21
|
+
err_console = Console(stderr=True)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@app.command()
|
|
25
|
+
def main(
|
|
26
|
+
input_path: str = typer.Argument(..., help="Path or URL to OpenAPI spec (JSON or YAML)"),
|
|
27
|
+
output: str = typer.Option("./output", "-o", "--output", help="Output directory"),
|
|
28
|
+
name: Optional[str] = typer.Option(None, "-n", "--name", help="Skill name (default: derived from API title)"),
|
|
29
|
+
include_tags: Optional[str] = typer.Option(None, "--include-tags", help="Only include these tags (comma-separated)"),
|
|
30
|
+
exclude_tags: Optional[str] = typer.Option(None, "--exclude-tags", help="Exclude these tags (comma-separated)"),
|
|
31
|
+
exclude_deprecated: bool = typer.Option(False, "--exclude-deprecated", help="Exclude deprecated operations"),
|
|
32
|
+
templates: Optional[str] = typer.Option(None, "-t", "--templates", help="Custom templates directory"),
|
|
33
|
+
assets: Optional[str] = typer.Option(None, "-a", "--assets", help="Static assets directory overlaid onto the skill after generation"),
|
|
34
|
+
exclude_paths: Optional[str] = typer.Option(None, "--exclude-paths", help="Exclude paths matching these prefixes (comma-separated)"),
|
|
35
|
+
group_by: str = typer.Option("auto", "-g", "--group-by", help="How to group operations: tags, path, auto"),
|
|
36
|
+
case_strategy: Optional[str] = typer.Option(None, "--case-strategy", help="Strategy for case-insensitive filesystem safety: lowercase"),
|
|
37
|
+
force: bool = typer.Option(False, "-f", "--force", help="Overwrite existing output directory"),
|
|
38
|
+
quiet: bool = typer.Option(False, "-q", "--quiet", help="Suppress output except errors"),
|
|
39
|
+
) -> None:
|
|
40
|
+
"""Convert OpenAPI specifications to Agent Skills format."""
|
|
41
|
+
if not quiet:
|
|
42
|
+
console.print(f"[bold]Reading OpenAPI spec:[/bold] {input_path}")
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
spec = load_spec_from_input(input_path)
|
|
46
|
+
except Exception as e:
|
|
47
|
+
err_console.print(f"[red]Error loading spec:[/red] {e}")
|
|
48
|
+
raise typer.Exit(1)
|
|
49
|
+
|
|
50
|
+
# Validate spec
|
|
51
|
+
spec_error = validate_spec(spec)
|
|
52
|
+
if spec_error:
|
|
53
|
+
err_console.print(f"[red]Error:[/red] {spec_error}")
|
|
54
|
+
raise typer.Exit(1)
|
|
55
|
+
|
|
56
|
+
# Validate options
|
|
57
|
+
try:
|
|
58
|
+
validated_group_by = validate_group_by(group_by)
|
|
59
|
+
validated_case_strategy = validate_case_strategy(case_strategy)
|
|
60
|
+
except ValueError as e:
|
|
61
|
+
err_console.print(f"[red]Error:[/red] {e}")
|
|
62
|
+
raise typer.Exit(1)
|
|
63
|
+
|
|
64
|
+
# Derive skill name for output path check
|
|
65
|
+
skill_name = name or to_file_name(spec["info"]["title"]).lower()[:64]
|
|
66
|
+
output_path = os.path.join(output, skill_name)
|
|
67
|
+
|
|
68
|
+
# Check if output exists
|
|
69
|
+
if os.path.exists(output_path):
|
|
70
|
+
if not force:
|
|
71
|
+
err_console.print(
|
|
72
|
+
f"[red]Error:[/red] Output directory already exists: {output_path}\nUse --force to overwrite.",
|
|
73
|
+
)
|
|
74
|
+
raise typer.Exit(1)
|
|
75
|
+
shutil.rmtree(output_path)
|
|
76
|
+
|
|
77
|
+
# Validate assets dir
|
|
78
|
+
if assets:
|
|
79
|
+
if not os.path.exists(assets):
|
|
80
|
+
console.print(f"[red]Error:[/red] Assets directory not found: {assets}")
|
|
81
|
+
raise typer.Exit(1)
|
|
82
|
+
assets_abs = os.path.abspath(assets)
|
|
83
|
+
output_abs = os.path.abspath(output_path)
|
|
84
|
+
if assets_abs == output_abs or assets_abs.startswith(output_abs + os.sep):
|
|
85
|
+
err_console.print(
|
|
86
|
+
f"[red]Error:[/red] Assets directory must be outside the output directory: {assets}",
|
|
87
|
+
)
|
|
88
|
+
raise typer.Exit(1)
|
|
89
|
+
|
|
90
|
+
if not quiet:
|
|
91
|
+
console.print(f" API: {spec['info']['title']} (v{spec['info'].get('version', '')})")
|
|
92
|
+
console.print(f" OpenAPI version: {spec.get('openapi', '')}")
|
|
93
|
+
console.print(f" Paths: {len(spec.get('paths', {}))}")
|
|
94
|
+
tags_list = spec.get("tags", [])
|
|
95
|
+
tag_names = ", ".join(t["name"] for t in tags_list) if tags_list else "none"
|
|
96
|
+
console.print(f" Tags: {tag_names}")
|
|
97
|
+
schemas = (spec.get("components") or {}).get("schemas")
|
|
98
|
+
if schemas:
|
|
99
|
+
console.print(f" Schemas: {len(schemas)}")
|
|
100
|
+
console.print("[bold]Converting to Agent Skill...[/bold]")
|
|
101
|
+
|
|
102
|
+
# Build options
|
|
103
|
+
parser_filter = ParserFilter(
|
|
104
|
+
include_tags=[t.strip() for t in include_tags.split(",")] if include_tags else None,
|
|
105
|
+
exclude_tags=[t.strip() for t in exclude_tags.split(",")] if exclude_tags else None,
|
|
106
|
+
exclude_deprecated=exclude_deprecated,
|
|
107
|
+
exclude_paths=[p.strip() for p in exclude_paths.split(",")] if exclude_paths else None,
|
|
108
|
+
)
|
|
109
|
+
parser_options = ParserOptions(
|
|
110
|
+
skill_name=name,
|
|
111
|
+
filter=parser_filter,
|
|
112
|
+
group_by=validated_group_by,
|
|
113
|
+
)
|
|
114
|
+
convert_options = ConvertOptions(
|
|
115
|
+
output_dir=output,
|
|
116
|
+
parser=parser_options,
|
|
117
|
+
template_dir=templates,
|
|
118
|
+
case_strategy=validated_case_strategy,
|
|
119
|
+
assets_dir=assets,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
convert_openapi_to_skill(spec, convert_options)
|
|
124
|
+
except Exception as e:
|
|
125
|
+
console.print(f"[red]Error during conversion:[/red] {e}")
|
|
126
|
+
raise typer.Exit(1)
|
|
127
|
+
|
|
128
|
+
if not quiet:
|
|
129
|
+
console.print(f"[green]Skill generated at:[/green] {output_path}")
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
if __name__ == "__main__":
|
|
133
|
+
app()
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""Converter: orchestrates Parser -> Renderer -> Writer pipeline."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
|
|
7
|
+
from openapi_to_skills.parser import create_parser
|
|
8
|
+
from openapi_to_skills.renderer import create_renderer, to_file_name
|
|
9
|
+
from openapi_to_skills.types import (
|
|
10
|
+
ConvertOptions,
|
|
11
|
+
SchemaDocument,
|
|
12
|
+
SchemaGroupDocument,
|
|
13
|
+
SkillDocument,
|
|
14
|
+
)
|
|
15
|
+
from openapi_to_skills.writer import create_writer
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def convert_openapi_to_skill(spec: dict, options: ConvertOptions) -> None:
|
|
19
|
+
parser = create_parser()
|
|
20
|
+
renderer = create_renderer(options.template_dir)
|
|
21
|
+
writer = create_writer()
|
|
22
|
+
|
|
23
|
+
# Parse OpenAPI to IR
|
|
24
|
+
doc = parser.parse(spec, options.parser)
|
|
25
|
+
|
|
26
|
+
# Apply case strategy to IR before rendering
|
|
27
|
+
if options.case_strategy == "lowercase":
|
|
28
|
+
doc = _apply_case_strategy_lowercase(doc)
|
|
29
|
+
|
|
30
|
+
# Write output
|
|
31
|
+
_write_skill_output(doc, options.output_dir, renderer, writer)
|
|
32
|
+
|
|
33
|
+
# Overlay hand-authored static assets
|
|
34
|
+
if options.assets_dir:
|
|
35
|
+
skill_dir = os.path.join(options.output_dir, doc.meta.name)
|
|
36
|
+
writer.copy_dir(options.assets_dir, skill_dir)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _apply_case_strategy_lowercase(doc: SkillDocument) -> SkillDocument:
|
|
40
|
+
"""Merge schema groups by case-insensitive prefix, lowercase names, disambiguate."""
|
|
41
|
+
merged_map: dict[str, dict] = {}
|
|
42
|
+
|
|
43
|
+
for group in doc.schema_groups:
|
|
44
|
+
key = group.prefix.lower()
|
|
45
|
+
if key in merged_map:
|
|
46
|
+
merged_map[key]["schemas"].extend(group.schemas)
|
|
47
|
+
else:
|
|
48
|
+
merged_map[key] = {"prefix": key, "schemas": list(group.schemas)}
|
|
49
|
+
|
|
50
|
+
new_groups: list[SchemaGroupDocument] = []
|
|
51
|
+
for entry in merged_map.values():
|
|
52
|
+
used_names: set[str] = set()
|
|
53
|
+
renamed_schemas: list[SchemaDocument] = []
|
|
54
|
+
|
|
55
|
+
for schema in entry["schemas"]:
|
|
56
|
+
base_name = to_file_name(schema.name).lower()
|
|
57
|
+
final_name = base_name
|
|
58
|
+
counter = 2
|
|
59
|
+
|
|
60
|
+
while final_name in used_names:
|
|
61
|
+
final_name = f"{base_name}-{counter}"
|
|
62
|
+
counter += 1
|
|
63
|
+
|
|
64
|
+
used_names.add(final_name)
|
|
65
|
+
renamed_schemas.append(
|
|
66
|
+
SchemaDocument(
|
|
67
|
+
name=final_name,
|
|
68
|
+
type=schema.type,
|
|
69
|
+
description=schema.description,
|
|
70
|
+
fields=schema.fields,
|
|
71
|
+
enum_values=schema.enum_values,
|
|
72
|
+
composition=schema.composition,
|
|
73
|
+
items=schema.items,
|
|
74
|
+
)
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
new_groups.append(SchemaGroupDocument(prefix=entry["prefix"], schemas=renamed_schemas))
|
|
78
|
+
|
|
79
|
+
return SkillDocument(
|
|
80
|
+
meta=doc.meta,
|
|
81
|
+
resources=doc.resources,
|
|
82
|
+
schema_groups=new_groups,
|
|
83
|
+
auth_schemes=doc.auth_schemes,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _write_skill_output(doc: SkillDocument, output_dir: str, renderer, writer) -> None:
|
|
88
|
+
skill_dir = os.path.join(output_dir, doc.meta.name)
|
|
89
|
+
references_dir = os.path.join(skill_dir, "references")
|
|
90
|
+
resources_dir = os.path.join(references_dir, "resources")
|
|
91
|
+
operations_dir = os.path.join(references_dir, "operations")
|
|
92
|
+
schemas_dir = os.path.join(references_dir, "schemas")
|
|
93
|
+
|
|
94
|
+
# Create directories
|
|
95
|
+
writer.mkdir(skill_dir)
|
|
96
|
+
if doc.resources:
|
|
97
|
+
writer.mkdir(resources_dir)
|
|
98
|
+
writer.mkdir(operations_dir)
|
|
99
|
+
if doc.schema_groups:
|
|
100
|
+
writer.mkdir(schemas_dir)
|
|
101
|
+
|
|
102
|
+
# Write SKILL.md
|
|
103
|
+
skill_md = renderer.render_skill(doc)
|
|
104
|
+
writer.write_file(os.path.join(skill_dir, "SKILL.md"), skill_md)
|
|
105
|
+
|
|
106
|
+
# Write resources and operations
|
|
107
|
+
for resource in doc.resources:
|
|
108
|
+
file_name = to_file_name(resource.tag)
|
|
109
|
+
|
|
110
|
+
# Resource index
|
|
111
|
+
resource_md = renderer.render_resource(resource)
|
|
112
|
+
writer.write_file(os.path.join(resources_dir, f"{file_name}.md"), resource_md)
|
|
113
|
+
|
|
114
|
+
# Individual operation files
|
|
115
|
+
for op in resource.operations:
|
|
116
|
+
op_file_name = to_file_name(op.operation_id)
|
|
117
|
+
op_md = renderer.render_operation(op)
|
|
118
|
+
writer.write_file(os.path.join(operations_dir, f"{op_file_name}.md"), op_md)
|
|
119
|
+
|
|
120
|
+
# Write schema groups
|
|
121
|
+
for group in doc.schema_groups:
|
|
122
|
+
prefix_dir = os.path.join(schemas_dir, to_file_name(group.prefix))
|
|
123
|
+
writer.mkdir(prefix_dir)
|
|
124
|
+
|
|
125
|
+
# Schema index
|
|
126
|
+
index_md = renderer.render_schema_index(group)
|
|
127
|
+
writer.write_file(os.path.join(prefix_dir, "_index.md"), index_md)
|
|
128
|
+
|
|
129
|
+
# Individual schema files
|
|
130
|
+
for schema in group.schemas:
|
|
131
|
+
schema_md = renderer.render_schema(schema)
|
|
132
|
+
writer.write_file(
|
|
133
|
+
os.path.join(prefix_dir, f"{to_file_name(schema.name)}.md"), schema_md
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
# Write authentication
|
|
137
|
+
if doc.auth_schemes:
|
|
138
|
+
auth_md = renderer.render_authentication(doc.auth_schemes)
|
|
139
|
+
writer.write_file(os.path.join(references_dir, "authentication.md"), auth_md)
|