openapi2cli 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.
@@ -0,0 +1,17 @@
1
+ """api2cli - Generate CLI tools from OpenAPI specs."""
2
+
3
+ __version__ = "0.1.0"
4
+
5
+ from .generator import CLIGenerator, GeneratedCLI
6
+ from .parser import Endpoint, OpenAPIParser, Parameter, ParsedSpec
7
+ from .runtime import APIClient
8
+
9
+ __all__ = [
10
+ "OpenAPIParser",
11
+ "ParsedSpec",
12
+ "Endpoint",
13
+ "Parameter",
14
+ "CLIGenerator",
15
+ "GeneratedCLI",
16
+ "APIClient",
17
+ ]
@@ -0,0 +1,6 @@
1
+ """Allow running as `python -m api2cli`."""
2
+
3
+ from .cli import main
4
+
5
+ if __name__ == "__main__":
6
+ main()
openapi2cli/cli.py ADDED
@@ -0,0 +1,117 @@
1
+ """Main CLI for openapi2cli."""
2
+
3
+ import sys
4
+ from pathlib import Path
5
+
6
+ import click
7
+
8
+ from . import __version__
9
+ from .generator import CLIGenerator
10
+ from .parser import OpenAPIParser
11
+
12
+
13
+ @click.group()
14
+ @click.version_option(version=__version__)
15
+ def main():
16
+ """openapi2cli - Generate CLI tools from OpenAPI specs.
17
+
18
+ Built for AI agents who need to interact with APIs.
19
+
20
+ Example:
21
+
22
+ # Generate a CLI from a spec
23
+ openapi2cli generate https://httpbin.org/spec.json --name httpbin
24
+
25
+ # Then use it
26
+ ./httpbin get --output json
27
+ """
28
+ pass
29
+
30
+
31
+ @main.command()
32
+ @click.argument("spec", type=str)
33
+ @click.option("--name", "-n", required=True, help="Name for the generated CLI")
34
+ @click.option("--output", "-o", type=click.Path(), help="Output file path")
35
+ @click.option("--stdout", is_flag=True, help="Print to stdout instead of file")
36
+ def generate(spec: str, name: str, output: str, stdout: bool):
37
+ """Generate a CLI from an OpenAPI spec.
38
+
39
+ SPEC can be a file path or URL to an OpenAPI 3.x specification.
40
+
41
+ Examples:
42
+
43
+ openapi2cli generate petstore.yaml --name petstore
44
+ openapi2cli generate https://api.example.com/openapi.json --name example -o example_cli.py
45
+ """
46
+ try:
47
+ # Parse the spec
48
+ parser = OpenAPIParser()
49
+ parsed = parser.parse(spec)
50
+
51
+ click.echo(f"Parsed: {parsed.title} v{parsed.version}", err=True)
52
+ click.echo(f"Found {len(parsed.endpoints)} endpoints", err=True)
53
+
54
+ # Generate CLI
55
+ generator = CLIGenerator()
56
+ cli = generator.generate(parsed, name=name)
57
+
58
+ click.echo(f"Generated {len(cli.groups)} command groups", err=True)
59
+
60
+ # Output
61
+ if stdout:
62
+ click.echo(cli.to_python())
63
+ else:
64
+ output_path = Path(output) if output else Path(f"{name}_cli.py")
65
+ cli.save(output_path)
66
+ click.echo(f"Saved to: {output_path}", err=True)
67
+ click.echo(f"\nUsage: python {output_path} --help", err=True)
68
+
69
+ except Exception as e:
70
+ click.echo(f"Error: {e}", err=True)
71
+ sys.exit(1)
72
+
73
+
74
+ @main.command()
75
+ @click.argument("spec", type=str)
76
+ def inspect(spec: str):
77
+ """Inspect an OpenAPI spec without generating code.
78
+
79
+ Shows the structure of the API: endpoints, parameters, auth, etc.
80
+ """
81
+ try:
82
+ parser = OpenAPIParser()
83
+ parsed = parser.parse(spec)
84
+
85
+ click.echo(f"\nšŸ“‹ {parsed.title} v{parsed.version}")
86
+ click.echo(f" {parsed.description[:100]}..." if len(parsed.description) > 100 else f" {parsed.description}")
87
+ click.echo(f"\n🌐 Base URL: {parsed.base_url}")
88
+
89
+ # Auth schemes
90
+ if parsed.auth_schemes:
91
+ click.echo("\nšŸ” Authentication:")
92
+ for scheme in parsed.auth_schemes:
93
+ click.echo(f" - {scheme.name}: {scheme.type}")
94
+
95
+ # Endpoints by tag
96
+ grouped = parsed.group_by_tag()
97
+ click.echo(f"\nšŸ“” Endpoints ({len(parsed.endpoints)} total):")
98
+
99
+ for tag, endpoints in sorted(grouped.items()):
100
+ click.echo(f"\n [{tag}]")
101
+ for ep in endpoints[:5]: # Show first 5
102
+ params = ", ".join(p.name for p in ep.parameters[:3])
103
+ if len(ep.parameters) > 3:
104
+ params += "..."
105
+ click.echo(f" • {ep.method:6} {ep.path}")
106
+ if params:
107
+ click.echo(f" params: {params}")
108
+ if len(endpoints) > 5:
109
+ click.echo(f" ... and {len(endpoints) - 5} more")
110
+
111
+ except Exception as e:
112
+ click.echo(f"Error: {e}", err=True)
113
+ sys.exit(1)
114
+
115
+
116
+ if __name__ == "__main__":
117
+ main()
@@ -0,0 +1,430 @@
1
+ """CLI code generator."""
2
+
3
+ import re
4
+ from dataclasses import dataclass, field
5
+ from pathlib import Path
6
+ from typing import List, Optional, Union
7
+
8
+ from jinja2 import Template
9
+
10
+ from .parser import AuthScheme, Endpoint, ParsedSpec
11
+
12
+
13
+ @dataclass
14
+ class CLIOption:
15
+ """A CLI option/argument."""
16
+
17
+ name: str
18
+ param_type: str = "str"
19
+ required: bool = False
20
+ default: Optional[str] = None
21
+ help: str = ""
22
+ is_flag: bool = False
23
+ multiple: bool = False
24
+
25
+
26
+ @dataclass
27
+ class CLICommand:
28
+ """A CLI command."""
29
+
30
+ name: str
31
+ method: str
32
+ path: str
33
+ help: str = ""
34
+ options: List[CLIOption] = field(default_factory=list)
35
+ has_body: bool = False
36
+
37
+
38
+ @dataclass
39
+ class CLIGroup:
40
+ """A group of CLI commands (tag)."""
41
+
42
+ name: str
43
+ help: str = ""
44
+ commands: List[CLICommand] = field(default_factory=list)
45
+
46
+
47
+ @dataclass
48
+ class GeneratedCLI:
49
+ """A generated CLI structure."""
50
+
51
+ name: str
52
+ version: str = "1.0.0"
53
+ description: str = ""
54
+ base_url: str = ""
55
+ groups: List[CLIGroup] = field(default_factory=list)
56
+ global_options: List[CLIOption] = field(default_factory=list)
57
+ auth_schemes: List[AuthScheme] = field(default_factory=list)
58
+
59
+ def to_python(self) -> str:
60
+ """Generate Python code for the CLI."""
61
+ return CLI_TEMPLATE.render(cli=self)
62
+
63
+ def to_standalone_script(self) -> str:
64
+ """Generate a standalone executable script."""
65
+ code = self.to_python()
66
+ return f"#!/usr/bin/env python3\n{code}"
67
+
68
+ def save(self, path: Union[Path, str]) -> None:
69
+ """Save the generated CLI to a file."""
70
+ path = Path(path)
71
+ code = self.to_standalone_script()
72
+ path.write_text(code)
73
+ # Make executable
74
+ path.chmod(0o755)
75
+
76
+
77
+ class CLIGenerator:
78
+ """Generates CLI code from a parsed OpenAPI spec."""
79
+
80
+ def generate(self, spec: ParsedSpec, name: str) -> GeneratedCLI:
81
+ """Generate a CLI from a parsed spec."""
82
+ # Generate global options (auth, output format, base URL)
83
+ global_options = self._generate_global_options(spec)
84
+
85
+ # Group endpoints by tag and generate command groups
86
+ grouped = spec.group_by_tag()
87
+ groups = []
88
+
89
+ for tag, endpoints in grouped.items():
90
+ group = self._generate_group(tag, endpoints)
91
+ groups.append(group)
92
+
93
+ return GeneratedCLI(
94
+ name=name,
95
+ version=spec.version,
96
+ description=spec.description or f"CLI for {spec.title}",
97
+ base_url=spec.base_url,
98
+ groups=groups,
99
+ global_options=global_options,
100
+ auth_schemes=spec.auth_schemes,
101
+ )
102
+
103
+ def _generate_global_options(self, spec: ParsedSpec) -> List[CLIOption]:
104
+ """Generate global CLI options."""
105
+ options = [
106
+ CLIOption(
107
+ name="--output",
108
+ param_type="str",
109
+ default="table",
110
+ help="Output format (json, table, raw)",
111
+ ),
112
+ CLIOption(
113
+ name="--base-url",
114
+ param_type="str",
115
+ default=spec.base_url,
116
+ help="API base URL",
117
+ ),
118
+ ]
119
+
120
+ # Add auth options based on security schemes
121
+ for scheme in spec.auth_schemes:
122
+ if scheme.type == "apiKey":
123
+ options.append(CLIOption(
124
+ name="--api-key",
125
+ param_type="str",
126
+ help=f"API key ({scheme.param_name})",
127
+ ))
128
+ elif scheme.type == "http" and scheme.scheme == "bearer":
129
+ options.append(CLIOption(
130
+ name="--token",
131
+ param_type="str",
132
+ help="Bearer token for authentication",
133
+ ))
134
+
135
+ # Default auth option if no schemes defined
136
+ if not any(o.name in ("--api-key", "--token") for o in options):
137
+ options.append(CLIOption(
138
+ name="--api-key",
139
+ param_type="str",
140
+ help="API key for authentication",
141
+ ))
142
+
143
+ return options
144
+
145
+ def _generate_group(self, tag: str, endpoints: List[Endpoint]) -> CLIGroup:
146
+ """Generate a command group from a tag."""
147
+ commands = []
148
+
149
+ for endpoint in endpoints:
150
+ cmd = self._generate_command(endpoint)
151
+ commands.append(cmd)
152
+
153
+ return CLIGroup(
154
+ name=self._sanitize_name(tag),
155
+ help=f"Commands for {tag}",
156
+ commands=commands,
157
+ )
158
+
159
+ def _generate_command(self, endpoint: Endpoint) -> CLICommand:
160
+ """Generate a CLI command from an endpoint."""
161
+ options = []
162
+ seen_names = set()
163
+
164
+ def add_option(opt: CLIOption) -> None:
165
+ """Add option if name not already used."""
166
+ if opt.name not in seen_names:
167
+ seen_names.add(opt.name)
168
+ options.append(opt)
169
+
170
+ # Add options for parameters
171
+ for param in endpoint.parameters:
172
+ add_option(CLIOption(
173
+ name=param.cli_name,
174
+ param_type=self._map_type(param.schema_type),
175
+ required=param.required,
176
+ default=str(param.default) if param.default is not None else None,
177
+ help=param.description or f"{param.name} parameter",
178
+ ))
179
+
180
+ # Add options for request body properties
181
+ has_body = False
182
+ if endpoint.request_body:
183
+ has_body = True
184
+ for prop_name, prop_schema in endpoint.request_body.properties.items():
185
+ required = prop_name in endpoint.request_body.required_props
186
+ add_option(CLIOption(
187
+ name=f"--{self._sanitize_name(prop_name)}",
188
+ param_type=self._map_type(prop_schema.get('type', 'string')),
189
+ required=required,
190
+ help=prop_schema.get('description', f"{prop_name} field"),
191
+ ))
192
+
193
+ # Also add a --data option for raw JSON input
194
+ add_option(CLIOption(
195
+ name="--data",
196
+ param_type="str",
197
+ help="Raw JSON data for request body",
198
+ ))
199
+
200
+ return CLICommand(
201
+ name=endpoint.cli_name,
202
+ method=endpoint.method,
203
+ path=endpoint.path,
204
+ help=endpoint.summary or endpoint.description or f"{endpoint.method} {endpoint.path}",
205
+ options=options,
206
+ has_body=has_body,
207
+ )
208
+
209
+ def _sanitize_name(self, name: str) -> str:
210
+ """Sanitize a name for use as a CLI command/option."""
211
+ # Convert camelCase to kebab-case
212
+ name = re.sub(r'([a-z])([A-Z])', r'\1-\2', name)
213
+ # Replace underscores, spaces, dots with hyphens
214
+ name = name.replace('_', '-').replace(' ', '-').replace('.', '-')
215
+ # Remove invalid characters
216
+ name = re.sub(r'[^a-zA-Z0-9-]', '', name)
217
+ # Remove consecutive hyphens
218
+ name = re.sub(r'-+', '-', name)
219
+ # Remove leading/trailing hyphens
220
+ name = name.strip('-')
221
+ return name.lower()
222
+
223
+ def _map_type(self, schema_type: str) -> str:
224
+ """Map OpenAPI type to Python/Click type."""
225
+ mapping = {
226
+ 'integer': 'int',
227
+ 'number': 'float',
228
+ 'boolean': 'bool',
229
+ 'array': 'str', # JSON string for arrays
230
+ 'object': 'str', # JSON string for objects
231
+ }
232
+ return mapping.get(schema_type, 'str')
233
+
234
+
235
+ # Template for generated CLI - use raw strings to avoid escaping issues
236
+ CLI_TEMPLATE_STR = '''
237
+ """{{ cli.name }} - Generated CLI for {{ cli.description }}
238
+
239
+ Auto-generated by openapi2cli. Do not edit manually.
240
+ """
241
+
242
+ import json
243
+ import os
244
+ import sys
245
+ from typing import Optional
246
+
247
+ import click
248
+ import requests
249
+
250
+ try:
251
+ from rich.console import Console
252
+ from rich.table import Table
253
+ RICH_AVAILABLE = True
254
+ except ImportError:
255
+ RICH_AVAILABLE = False
256
+
257
+
258
+ # Configuration
259
+ BASE_URL = "{{ cli.base_url }}"
260
+ ENV_PREFIX = "{{ cli.name.upper().replace('-', '_') }}"
261
+
262
+
263
+ def get_auth_headers(api_key: Optional[str] = None, token: Optional[str] = None) -> dict:
264
+ """Get authentication headers."""
265
+ headers = {}
266
+
267
+ # Try CLI args first, then env vars
268
+ key = api_key or os.environ.get(ENV_PREFIX + "_API_KEY")
269
+ tok = token or os.environ.get(ENV_PREFIX + "_TOKEN")
270
+
271
+ if tok:
272
+ headers["Authorization"] = "Bearer " + tok
273
+ elif key:
274
+ {%- for scheme in cli.auth_schemes %}
275
+ {%- if scheme.type == "apiKey" and scheme.location == "header" %}
276
+ headers["{{ scheme.param_name }}"] = key
277
+ {%- endif %}
278
+ {%- endfor %}
279
+ {%- if not cli.auth_schemes %}
280
+ headers["X-API-Key"] = key
281
+ {%- endif %}
282
+
283
+ return headers
284
+
285
+
286
+ def format_output(data, output_format: str):
287
+ """Format output based on requested format."""
288
+ if output_format == "json":
289
+ click.echo(json.dumps(data, indent=2))
290
+ elif output_format == "raw":
291
+ click.echo(data)
292
+ elif output_format == "table" and RICH_AVAILABLE:
293
+ console = Console()
294
+ if isinstance(data, list) and data:
295
+ table = Table()
296
+ first = data[0]
297
+ if isinstance(first, dict):
298
+ for key in first.keys():
299
+ table.add_column(str(key))
300
+ for item in data:
301
+ if isinstance(item, dict):
302
+ table.add_row(*[str(v) for v in item.values()])
303
+ else:
304
+ table.add_column("value")
305
+ for item in data:
306
+ table.add_row(str(item))
307
+ console.print(table)
308
+ elif isinstance(data, dict):
309
+ table = Table()
310
+ table.add_column("Key")
311
+ table.add_column("Value")
312
+ for k, v in data.items():
313
+ table.add_row(str(k), str(v))
314
+ console.print(table)
315
+ else:
316
+ console.print(data)
317
+ else:
318
+ click.echo(json.dumps(data, indent=2))
319
+
320
+
321
+ def make_request(
322
+ method: str,
323
+ path: str,
324
+ base_url: str,
325
+ params: dict = None,
326
+ json_data: dict = None,
327
+ headers: dict = None,
328
+ path_params: dict = None,
329
+ ):
330
+ """Make an HTTP request to the API."""
331
+ if path_params:
332
+ for key, value in path_params.items():
333
+ path = path.replace("{" + key + "}", str(value))
334
+
335
+ url = base_url.rstrip("/") + path
336
+
337
+ try:
338
+ response = requests.request(
339
+ method=method,
340
+ url=url,
341
+ params=params,
342
+ json=json_data,
343
+ headers=headers,
344
+ timeout=30,
345
+ )
346
+ response.raise_for_status()
347
+
348
+ try:
349
+ return response.json()
350
+ except json.JSONDecodeError:
351
+ return response.text
352
+
353
+ except requests.exceptions.RequestException as e:
354
+ click.echo("Error: " + str(e), err=True)
355
+ sys.exit(1)
356
+
357
+
358
+ @click.group()
359
+ @click.option("--output", "-o", default="json", help="Output format (json, table, raw)")
360
+ @click.option("--base-url", default=BASE_URL, help="API base URL")
361
+ @click.option("--api-key", envvar=ENV_PREFIX + "_API_KEY", help="API key")
362
+ @click.option("--token", envvar=ENV_PREFIX + "_TOKEN", help="Bearer token")
363
+ @click.version_option(version="{{ cli.version }}")
364
+ @click.pass_context
365
+ def cli(ctx, output, base_url, api_key, token):
366
+ """{{ cli.description }}"""
367
+ ctx.ensure_object(dict)
368
+ ctx.obj["output"] = output
369
+ ctx.obj["base_url"] = base_url
370
+ ctx.obj["headers"] = get_auth_headers(api_key, token)
371
+
372
+ {% for group in cli.groups %}
373
+
374
+ @cli.group()
375
+ def {{ group.name | replace("-", "_") }}():
376
+ """{{ group.help }}"""
377
+ pass
378
+
379
+ {% for cmd in group.commands %}
380
+
381
+ @{{ group.name | replace("-", "_") }}.command("{{ cmd.name }}")
382
+ {%- for opt in cmd.options %}
383
+ @click.option("{{ opt.name }}"{% if opt.required %}, required=True{% endif %}{% if opt.default %}, default="{{ opt.default }}"{% endif %}, help="{{ opt.help | replace('"', '\\"') }}")
384
+ {%- endfor %}
385
+ @click.pass_context
386
+ def {{ group.name | replace("-", "_") | replace(".", "_") }}_{{ cmd.name | replace("-", "_") | replace(".", "_") }}(ctx{% for opt in cmd.options %}, {{ opt.name | replace("--", "") | replace("-", "_") | replace(".", "_") }}{% endfor %}):
387
+ """{{ cmd.help | replace('"', '\\"') }}"""
388
+ path_params = {}
389
+ query_params = {}
390
+ body_data = {}
391
+
392
+ {%- for opt in cmd.options %}
393
+ {%- set var_name = opt.name | replace("--", "") | replace("-", "_") %}
394
+ if {{ var_name }} is not None:
395
+ {%- if "id" in opt.name.lower() and "{" in cmd.path %}
396
+ path_params["{{ opt.name | replace("--", "") | replace("-", "") }}"] = {{ var_name }}
397
+ {%- elif opt.name == "--data" %}
398
+ body_data = json.loads({{ var_name }})
399
+ {%- elif cmd.has_body and opt.name != "--data" %}
400
+ body_data["{{ opt.name | replace("--", "") | replace("-", "_") }}"] = {{ var_name }}
401
+ {%- else %}
402
+ query_params["{{ opt.name | replace("--", "") }}"] = {{ var_name }}
403
+ {%- endif %}
404
+ {%- endfor %}
405
+
406
+ result = make_request(
407
+ method="{{ cmd.method }}",
408
+ path="{{ cmd.path }}",
409
+ base_url=ctx.obj["base_url"],
410
+ params=query_params or None,
411
+ json_data=body_data or None,
412
+ headers=ctx.obj["headers"],
413
+ path_params=path_params or None,
414
+ )
415
+
416
+ format_output(result, ctx.obj["output"])
417
+ {% endfor %}
418
+ {% endfor %}
419
+
420
+
421
+ def main():
422
+ """Main entry point."""
423
+ cli()
424
+
425
+
426
+ if __name__ == "__main__":
427
+ main()
428
+ '''
429
+
430
+ CLI_TEMPLATE = Template(CLI_TEMPLATE_STR)
openapi2cli/parser.py ADDED
@@ -0,0 +1,325 @@
1
+ """OpenAPI spec parser."""
2
+
3
+ import json
4
+ import re
5
+ from dataclasses import dataclass, field
6
+ from pathlib import Path
7
+ from typing import Any, Dict, List, Optional, Union
8
+
9
+ import requests
10
+ import yaml
11
+
12
+
13
+ @dataclass
14
+ class Parameter:
15
+ """An API endpoint parameter."""
16
+
17
+ name: str
18
+ location: str # path, query, header, cookie
19
+ required: bool = False
20
+ description: str = ""
21
+ schema_type: str = "string"
22
+ default: Any = None
23
+ enum: List[str] = field(default_factory=list)
24
+
25
+ @property
26
+ def cli_name(self) -> str:
27
+ """Convert to CLI option name."""
28
+ # petId -> pet-id, api_key -> api-key
29
+ name = re.sub(r'([a-z])([A-Z])', r'\1-\2', self.name)
30
+ name = name.replace('_', '-').lower()
31
+ return f"--{name}"
32
+
33
+
34
+ @dataclass
35
+ class RequestBody:
36
+ """Request body schema."""
37
+
38
+ content_type: str = "application/json"
39
+ required: bool = False
40
+ properties: Dict[str, Any] = field(default_factory=dict)
41
+ required_props: List[str] = field(default_factory=list)
42
+
43
+
44
+ @dataclass
45
+ class AuthScheme:
46
+ """Authentication scheme."""
47
+
48
+ name: str
49
+ type: str # apiKey, http, oauth2, openIdConnect
50
+ location: str = "" # header, query, cookie (for apiKey)
51
+ scheme: str = "" # bearer, basic (for http)
52
+ param_name: str = "" # name of the header/query param
53
+
54
+
55
+ @dataclass
56
+ class Endpoint:
57
+ """An API endpoint."""
58
+
59
+ path: str
60
+ method: str
61
+ operation_id: str = ""
62
+ summary: str = ""
63
+ description: str = ""
64
+ tags: List[str] = field(default_factory=list)
65
+ parameters: List[Parameter] = field(default_factory=list)
66
+ request_body: Optional[RequestBody] = None
67
+ security: List[str] = field(default_factory=list)
68
+
69
+ @property
70
+ def cli_name(self) -> str:
71
+ """Generate CLI command name."""
72
+ if self.operation_id:
73
+ # getPetById -> get-pet-by-id -> get (simplified)
74
+ name = re.sub(r'([a-z])([A-Z])', r'\1-\2', self.operation_id)
75
+ name = name.lower()
76
+
77
+ # Simplify common patterns
78
+ # getPetById -> get, listPets -> list, addPet -> add
79
+ parts = name.split('-')
80
+ if len(parts) >= 2:
81
+ # Keep first part and maybe second if it's meaningful
82
+ verb = parts[0]
83
+ if verb in ('get', 'list', 'find', 'add', 'create', 'update', 'delete', 'remove'):
84
+ if len(parts) > 2 and parts[-1] not in ('by', 'all'):
85
+ # get-pet-by-id -> get-by-id
86
+ return '-'.join([verb] + parts[2:])
87
+ return verb
88
+ return name
89
+
90
+ # Fallback: method + simplified path
91
+ path_parts = [p for p in self.path.split('/') if p and not p.startswith('{')]
92
+ if path_parts:
93
+ return f"{self.method.lower()}-{'-'.join(path_parts[-2:])}"
94
+ return self.method.lower()
95
+
96
+
97
+ @dataclass
98
+ class ParsedSpec:
99
+ """A parsed OpenAPI specification."""
100
+
101
+ title: str
102
+ version: str
103
+ description: str = ""
104
+ base_url: str = ""
105
+ endpoints: List[Endpoint] = field(default_factory=list)
106
+ auth_schemes: List[AuthScheme] = field(default_factory=list)
107
+
108
+ def group_by_tag(self) -> Dict[str, List[Endpoint]]:
109
+ """Group endpoints by their tags."""
110
+ groups: Dict[str, List[Endpoint]] = {}
111
+
112
+ for endpoint in self.endpoints:
113
+ tags = endpoint.tags or ["default"]
114
+ for tag in tags:
115
+ if tag not in groups:
116
+ groups[tag] = []
117
+ groups[tag].append(endpoint)
118
+
119
+ return groups
120
+
121
+
122
+ class OpenAPIParser:
123
+ """Parser for OpenAPI 3.x specifications."""
124
+
125
+ def parse(self, source: Union[str, Path]) -> ParsedSpec:
126
+ """Parse an OpenAPI spec from a file path or URL."""
127
+ raw = self._load_spec(source)
128
+ return self._parse_spec(raw)
129
+
130
+ def _load_spec(self, source: Union[str, Path]) -> dict:
131
+ """Load spec from file or URL."""
132
+ if isinstance(source, Path):
133
+ source = str(source)
134
+
135
+ # Check if URL
136
+ if source.startswith(('http://', 'https://')):
137
+ response = requests.get(source, timeout=30)
138
+ response.raise_for_status()
139
+ content = response.text
140
+ # Detect format
141
+ if source.endswith('.yaml') or source.endswith('.yml'):
142
+ return yaml.safe_load(content)
143
+ try:
144
+ return json.loads(content)
145
+ except json.JSONDecodeError:
146
+ return yaml.safe_load(content)
147
+
148
+ # Local file
149
+ path = Path(source)
150
+ content = path.read_text()
151
+
152
+ if path.suffix in ('.yaml', '.yml'):
153
+ return yaml.safe_load(content)
154
+ return json.loads(content)
155
+
156
+ def _parse_spec(self, raw: dict) -> ParsedSpec:
157
+ """Parse raw spec dict into ParsedSpec."""
158
+ info = raw.get('info', {})
159
+
160
+ # Get base URL from servers
161
+ servers = raw.get('servers', [])
162
+ base_url = servers[0]['url'] if servers else ""
163
+
164
+ # Parse endpoints
165
+ endpoints = self._parse_paths(raw.get('paths', {}), raw)
166
+
167
+ # Parse auth schemes
168
+ auth_schemes = self._parse_security_schemes(
169
+ raw.get('components', {}).get('securitySchemes', {})
170
+ )
171
+
172
+ return ParsedSpec(
173
+ title=info.get('title', 'API'),
174
+ version=info.get('version', '1.0.0'),
175
+ description=info.get('description', ''),
176
+ base_url=base_url,
177
+ endpoints=endpoints,
178
+ auth_schemes=auth_schemes,
179
+ )
180
+
181
+ def _parse_paths(self, paths: dict, spec: dict) -> List[Endpoint]:
182
+ """Parse paths into endpoints."""
183
+ endpoints = []
184
+
185
+ for path, methods in paths.items():
186
+ # Handle path-level parameters
187
+ path_params = self._parse_parameters(
188
+ methods.get('parameters', []), spec
189
+ )
190
+
191
+ for method, details in methods.items():
192
+ if method in ('get', 'post', 'put', 'patch', 'delete', 'head', 'options'):
193
+ endpoint = self._parse_endpoint(
194
+ path, method.upper(), details, spec, path_params
195
+ )
196
+ endpoints.append(endpoint)
197
+
198
+ return endpoints
199
+
200
+ def _parse_endpoint(
201
+ self,
202
+ path: str,
203
+ method: str,
204
+ details: dict,
205
+ spec: dict,
206
+ path_params: List[Parameter]
207
+ ) -> Endpoint:
208
+ """Parse a single endpoint."""
209
+ # Parse parameters (combine path-level and operation-level)
210
+ params = path_params.copy()
211
+ params.extend(
212
+ self._parse_parameters(details.get('parameters', []), spec)
213
+ )
214
+
215
+ # Parse request body
216
+ request_body = None
217
+ if 'requestBody' in details:
218
+ request_body = self._parse_request_body(details['requestBody'], spec)
219
+
220
+ # Get security requirements
221
+ security = []
222
+ for sec in details.get('security', []):
223
+ security.extend(sec.keys())
224
+
225
+ return Endpoint(
226
+ path=path,
227
+ method=method,
228
+ operation_id=details.get('operationId', ''),
229
+ summary=details.get('summary', ''),
230
+ description=details.get('description', ''),
231
+ tags=details.get('tags', []),
232
+ parameters=params,
233
+ request_body=request_body,
234
+ security=security,
235
+ )
236
+
237
+ def _parse_parameters(self, params: list, spec: dict) -> List[Parameter]:
238
+ """Parse parameters."""
239
+ result = []
240
+
241
+ for param in params:
242
+ # Handle $ref
243
+ if '$ref' in param:
244
+ param = self._resolve_ref(param['$ref'], spec)
245
+
246
+ schema = param.get('schema', {})
247
+
248
+ result.append(Parameter(
249
+ name=param.get('name', ''),
250
+ location=param.get('in', 'query'),
251
+ required=param.get('required', False),
252
+ description=param.get('description', ''),
253
+ schema_type=schema.get('type', 'string'),
254
+ default=schema.get('default'),
255
+ enum=schema.get('enum', []),
256
+ ))
257
+
258
+ return result
259
+
260
+ def _parse_request_body(self, body: dict, spec: dict) -> RequestBody:
261
+ """Parse request body."""
262
+ # Handle $ref
263
+ if '$ref' in body:
264
+ body = self._resolve_ref(body['$ref'], spec)
265
+
266
+ content = body.get('content', {})
267
+
268
+ # Prefer JSON
269
+ content_type = 'application/json'
270
+ if content_type not in content:
271
+ content_type = next(iter(content.keys()), 'application/json')
272
+
273
+ schema = content.get(content_type, {}).get('schema', {})
274
+
275
+ # Handle $ref in schema
276
+ if '$ref' in schema:
277
+ schema = self._resolve_ref(schema['$ref'], spec)
278
+
279
+ properties = schema.get('properties', {})
280
+ required_props = schema.get('required', [])
281
+
282
+ return RequestBody(
283
+ content_type=content_type,
284
+ required=body.get('required', False),
285
+ properties=properties,
286
+ required_props=required_props,
287
+ )
288
+
289
+ def _parse_security_schemes(self, schemes: dict) -> List[AuthScheme]:
290
+ """Parse security schemes."""
291
+ result = []
292
+
293
+ for name, details in schemes.items():
294
+ scheme_type = details.get('type', '')
295
+
296
+ auth = AuthScheme(
297
+ name=name,
298
+ type=scheme_type,
299
+ )
300
+
301
+ if scheme_type == 'apiKey':
302
+ auth.location = details.get('in', 'header')
303
+ auth.param_name = details.get('name', '')
304
+ elif scheme_type == 'http':
305
+ auth.scheme = details.get('scheme', 'bearer')
306
+
307
+ result.append(auth)
308
+
309
+ return result
310
+
311
+ def _resolve_ref(self, ref: str, spec: dict) -> dict:
312
+ """Resolve a $ref pointer."""
313
+ if not ref.startswith('#/'):
314
+ return {}
315
+
316
+ parts = ref[2:].split('/')
317
+ current = spec
318
+
319
+ for part in parts:
320
+ if isinstance(current, dict) and part in current:
321
+ current = current[part]
322
+ else:
323
+ return {}
324
+
325
+ return current if isinstance(current, dict) else {}
openapi2cli/runtime.py ADDED
@@ -0,0 +1,170 @@
1
+ """Runtime for executing API calls and running generated CLIs."""
2
+
3
+ import subprocess
4
+ import sys
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+ from typing import List, Optional, Union
8
+
9
+ import requests
10
+
11
+
12
+ @dataclass
13
+ class CLIResult:
14
+ """Result of running a CLI command."""
15
+
16
+ exit_code: int
17
+ output: str
18
+ error: str = ""
19
+
20
+
21
+ class APIClient:
22
+ """HTTP client for making API requests."""
23
+
24
+ def __init__(
25
+ self,
26
+ base_url: str,
27
+ auth_header: Optional[str] = None,
28
+ auth_value: Optional[str] = None,
29
+ api_key_param: Optional[str] = None,
30
+ api_key_value: Optional[str] = None,
31
+ timeout: int = 30,
32
+ ):
33
+ self.base_url = base_url.rstrip("/")
34
+ self.auth_header = auth_header
35
+ self.auth_value = auth_value
36
+ self.api_key_param = api_key_param
37
+ self.api_key_value = api_key_value
38
+ self.timeout = timeout
39
+ self.session = requests.Session()
40
+
41
+ def _get_headers(self) -> dict:
42
+ """Get request headers including auth."""
43
+ headers = {"Content-Type": "application/json"}
44
+
45
+ if self.auth_header and self.auth_value:
46
+ headers[self.auth_header] = self.auth_value
47
+
48
+ return headers
49
+
50
+ def _get_params(self, params: Optional[dict]) -> dict:
51
+ """Get query parameters including API key if configured."""
52
+ result = params.copy() if params else {}
53
+
54
+ if self.api_key_param and self.api_key_value:
55
+ result[self.api_key_param] = self.api_key_value
56
+
57
+ return result
58
+
59
+ def _build_url(self, path: str, path_params: Optional[dict] = None) -> str:
60
+ """Build full URL with path parameter substitution."""
61
+ if path_params:
62
+ for key, value in path_params.items():
63
+ path = path.replace(f"{{{key}}}", str(value))
64
+
65
+ return f"{self.base_url}{path}"
66
+
67
+ def request(
68
+ self,
69
+ method: str,
70
+ path: str,
71
+ params: Optional[dict] = None,
72
+ json_data: Optional[dict] = None,
73
+ path_params: Optional[dict] = None,
74
+ ) -> requests.Response:
75
+ """Make an HTTP request."""
76
+ url = self._build_url(path, path_params)
77
+ headers = self._get_headers()
78
+ query_params = self._get_params(params)
79
+
80
+ return self.session.request(
81
+ method=method,
82
+ url=url,
83
+ params=query_params or None,
84
+ json=json_data,
85
+ headers=headers,
86
+ timeout=self.timeout,
87
+ )
88
+
89
+ def get(
90
+ self,
91
+ path: str,
92
+ params: Optional[dict] = None,
93
+ path_params: Optional[dict] = None,
94
+ ) -> requests.Response:
95
+ """Make a GET request."""
96
+ return self.request("GET", path, params=params, path_params=path_params)
97
+
98
+ def post(
99
+ self,
100
+ path: str,
101
+ json_data: Optional[dict] = None,
102
+ params: Optional[dict] = None,
103
+ path_params: Optional[dict] = None,
104
+ ) -> requests.Response:
105
+ """Make a POST request."""
106
+ return self.request(
107
+ "POST", path, params=params, json_data=json_data, path_params=path_params
108
+ )
109
+
110
+ def put(
111
+ self,
112
+ path: str,
113
+ json_data: Optional[dict] = None,
114
+ params: Optional[dict] = None,
115
+ path_params: Optional[dict] = None,
116
+ ) -> requests.Response:
117
+ """Make a PUT request."""
118
+ return self.request(
119
+ "PUT", path, params=params, json_data=json_data, path_params=path_params
120
+ )
121
+
122
+ def delete(
123
+ self,
124
+ path: str,
125
+ params: Optional[dict] = None,
126
+ path_params: Optional[dict] = None,
127
+ ) -> requests.Response:
128
+ """Make a DELETE request."""
129
+ return self.request("DELETE", path, params=params, path_params=path_params)
130
+
131
+ def patch(
132
+ self,
133
+ path: str,
134
+ json_data: Optional[dict] = None,
135
+ params: Optional[dict] = None,
136
+ path_params: Optional[dict] = None,
137
+ ) -> requests.Response:
138
+ """Make a PATCH request."""
139
+ return self.request(
140
+ "PATCH", path, params=params, json_data=json_data, path_params=path_params
141
+ )
142
+
143
+
144
+ class CLIRunner:
145
+ """Runner for executing generated CLIs."""
146
+
147
+ def __init__(self, script_path: Union[Path, str]):
148
+ self.script_path = Path(script_path)
149
+
150
+ def run(self, args: List[str], env: Optional[dict] = None) -> CLIResult:
151
+ """Run the CLI with given arguments."""
152
+ import os
153
+
154
+ full_env = os.environ.copy()
155
+ if env:
156
+ full_env.update(env)
157
+
158
+ result = subprocess.run(
159
+ [sys.executable, str(self.script_path)] + args,
160
+ capture_output=True,
161
+ text=True,
162
+ env=full_env,
163
+ timeout=60,
164
+ )
165
+
166
+ return CLIResult(
167
+ exit_code=result.returncode,
168
+ output=result.stdout,
169
+ error=result.stderr,
170
+ )
@@ -0,0 +1,267 @@
1
+ Metadata-Version: 2.4
2
+ Name: openapi2cli
3
+ Version: 0.1.0
4
+ Summary: Generate CLI tools from OpenAPI specs — built for AI agents
5
+ Project-URL: Homepage, https://github.com/Olafs-World/openapi2cli
6
+ Project-URL: Repository, https://github.com/Olafs-World/openapi2cli
7
+ Project-URL: Documentation, https://github.com/Olafs-World/openapi2cli#readme
8
+ Author-email: Olaf <olaf.bot@agentmail.to>
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: agents,api,cli,code-generation,openapi,swagger
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Topic :: Internet :: WWW/HTTP
21
+ Classifier: Topic :: Software Development :: Code Generators
22
+ Requires-Python: >=3.9
23
+ Requires-Dist: click>=8.0
24
+ Requires-Dist: jinja2>=3.0
25
+ Requires-Dist: pyyaml>=6.0
26
+ Requires-Dist: requests>=2.25
27
+ Requires-Dist: rich>=13.0
28
+ Provides-Extra: dev
29
+ Requires-Dist: pytest-cov>=4.0; extra == 'dev'
30
+ Requires-Dist: pytest>=7.0; extra == 'dev'
31
+ Requires-Dist: ruff>=0.1; extra == 'dev'
32
+ Description-Content-Type: text/markdown
33
+
34
+ # openapi2cli šŸ”§
35
+
36
+ [![CI](https://github.com/Olafs-World/openapi2cli/actions/workflows/ci.yml/badge.svg)](https://github.com/Olafs-World/openapi2cli/actions/workflows/ci.yml)
37
+ [![PyPI version](https://badge.fury.io/py/openapi2cli.svg)](https://pypi.org/project/openapi2cli/)
38
+ [![Python 3.9+](https://img.shields.io/badge/python-3.9+-blue.svg)](https://www.python.org/downloads/)
39
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
40
+
41
+ **Generate CLI tools from OpenAPI specs.** Built for AI agents who need to interact with APIs.
42
+
43
+ ```bash
44
+ # Generate a CLI from any OpenAPI spec
45
+ $ openapi2cli generate https://httpbin.org/spec.json --name httpbin
46
+
47
+ # Use it immediately
48
+ $ ./httpbin_cli.py get --output json
49
+ {
50
+ "url": "https://httpbin.org/get",
51
+ "headers": { ... }
52
+ }
53
+ ```
54
+
55
+ ## Why?
56
+
57
+ AI agents are great at executing CLI commands. They're less great at crafting HTTP requests from memory. This tool bridges the gap:
58
+
59
+ 1. **OpenAPI spec** → any API with a spec becomes usable
60
+ 2. **CLI generation** → instant `--help`, tab completion, validation
61
+ 3. **No code changes** → just point at a spec and go
62
+
63
+ ## Installation
64
+
65
+ ```bash
66
+ pip install openapi2cli
67
+ ```
68
+
69
+ ## Quick Start
70
+
71
+ ### Generate a CLI
72
+
73
+ ```bash
74
+ # From a URL
75
+ openapi2cli generate https://petstore3.swagger.io/api/v3/openapi.json --name petstore
76
+
77
+ # From a local file
78
+ openapi2cli generate ./api-spec.yaml --name myapi --output myapi
79
+ ```
80
+
81
+ ### Use the Generated CLI
82
+
83
+ ```bash
84
+ # See available commands
85
+ ./petstore --help
86
+
87
+ # List pets
88
+ ./petstore pet find-by-status --status available
89
+
90
+ # Add a pet (with auth)
91
+ export PETSTORE_API_KEY=your-key
92
+ ./petstore pet add --name "Fluffy" --status available
93
+
94
+ # JSON output for scripting
95
+ ./petstore pet get --pet-id 123 --output json | jq '.name'
96
+ ```
97
+
98
+ ### Inspect a Spec
99
+
100
+ ```bash
101
+ # See what's in a spec without generating
102
+ openapi2cli inspect https://httpbin.org/spec.json
103
+ ```
104
+
105
+ ## Features
106
+
107
+ | Feature | Description |
108
+ |---------|-------------|
109
+ | šŸ” **Auto-discovery** | Parses OpenAPI 3.x specs (YAML or JSON) |
110
+ | šŸ·ļø **Smart grouping** | Commands grouped by API tags |
111
+ | šŸ” **Auth support** | API keys, Bearer tokens, env vars |
112
+ | šŸ“Š **Output formats** | JSON, table, or raw |
113
+ | ⚔ **Fast generation** | Single command, instant CLI |
114
+ | šŸ¤– **Agent-friendly** | Self-documenting with `--help` |
115
+
116
+ ## Configuration
117
+
118
+ ### Authentication
119
+
120
+ Generated CLIs support multiple auth methods:
121
+
122
+ ```bash
123
+ # Via environment variable (recommended)
124
+ export PETSTORE_API_KEY=your-key
125
+ ./petstore pet list
126
+
127
+ # Via CLI option
128
+ ./petstore --api-key your-key pet list
129
+
130
+ # Bearer token
131
+ export PETSTORE_TOKEN=your-bearer-token
132
+ ./petstore pet list
133
+ ```
134
+
135
+ The env var prefix is derived from the CLI name (uppercase, underscores).
136
+
137
+ ### Base URL Override
138
+
139
+ ```bash
140
+ # Use a different API server
141
+ ./petstore --base-url https://staging.petstore.io/api pet list
142
+ ```
143
+
144
+ ### Output Formats
145
+
146
+ ```bash
147
+ # JSON (default, good for piping)
148
+ ./petstore pet list --output json
149
+
150
+ # Table (human-readable, requires rich)
151
+ ./petstore pet list --output table
152
+
153
+ # Raw (API response as-is)
154
+ ./petstore pet list --output raw
155
+ ```
156
+
157
+ ## Generated CLI Structure
158
+
159
+ For a spec with tags `pet`, `store`, `user`:
160
+
161
+ ```
162
+ petstore
163
+ ā”œā”€ā”€ pet
164
+ │ ā”œā”€ā”€ add # POST /pet
165
+ │ ā”œā”€ā”€ get # GET /pet/{petId}
166
+ │ ā”œā”€ā”€ update # PUT /pet
167
+ │ ā”œā”€ā”€ delete # DELETE /pet/{petId}
168
+ │ └── find-by-status
169
+ ā”œā”€ā”€ store
170
+ │ ā”œā”€ā”€ order
171
+ │ └── inventory
172
+ └── user
173
+ ā”œā”€ā”€ create
174
+ ā”œā”€ā”€ login
175
+ └── logout
176
+ ```
177
+
178
+ ## API Reference
179
+
180
+ ### `openapi2cli generate`
181
+
182
+ ```
183
+ openapi2cli generate SPEC --name NAME [--output PATH] [--stdout]
184
+
185
+ Arguments:
186
+ SPEC OpenAPI spec (file path or URL)
187
+
188
+ Options:
189
+ -n, --name CLI name (required)
190
+ -o, --output Output file path (default: {name}_cli.py)
191
+ --stdout Print to stdout instead of file
192
+ ```
193
+
194
+ ### `openapi2cli inspect`
195
+
196
+ ```
197
+ openapi2cli inspect SPEC
198
+
199
+ Arguments:
200
+ SPEC OpenAPI spec (file path or URL)
201
+ ```
202
+
203
+ ### Python API
204
+
205
+ ```python
206
+ from openapi2cli import OpenAPIParser, CLIGenerator
207
+
208
+ # Parse a spec
209
+ parser = OpenAPIParser()
210
+ spec = parser.parse("https://api.example.com/openapi.json")
211
+
212
+ print(f"API: {spec.title}")
213
+ print(f"Endpoints: {len(spec.endpoints)}")
214
+
215
+ # Generate CLI
216
+ generator = CLIGenerator()
217
+ cli = generator.generate(spec, name="example")
218
+
219
+ # Save to file
220
+ cli.save("example_cli.py")
221
+
222
+ # Or get the code
223
+ code = cli.to_python()
224
+ ```
225
+
226
+ ## Development
227
+
228
+ ```bash
229
+ # Clone
230
+ git clone https://github.com/Olafs-World/openapi2cli.git
231
+ cd openapi2cli
232
+
233
+ # Install dev dependencies
234
+ pip install -e ".[dev]"
235
+
236
+ # Run tests
237
+ pytest tests/ -v
238
+
239
+ # Run only unit tests (no API calls)
240
+ pytest tests/ -v -m "not integration"
241
+ ```
242
+
243
+ ## How It Works
244
+
245
+ 1. **Parse** - Load OpenAPI 3.x spec (YAML/JSON, local/URL)
246
+ 2. **Extract** - Pull endpoints, parameters, auth schemes, request bodies
247
+ 3. **Generate** - Create Click-based CLI with proper groups and options
248
+ 4. **Output** - Save as standalone Python script (executable)
249
+
250
+ The generated CLI uses `requests` for HTTP and optionally `rich` for pretty output.
251
+
252
+ ## Limitations
253
+
254
+ - OpenAPI 3.x only (not Swagger 2.0)
255
+ - No file upload support yet
256
+ - Complex nested request bodies may need `--data` JSON flag
257
+ - OAuth2 flows not fully implemented (use `--token` with pre-obtained tokens)
258
+
259
+ ## License
260
+
261
+ MIT Ā© [Olaf](https://olafs-world.vercel.app)
262
+
263
+ ---
264
+
265
+ <p align="center">
266
+ <i>Built by an AI who got tired of writing curl commands šŸ¤–</i>
267
+ </p>
@@ -0,0 +1,11 @@
1
+ openapi2cli/__init__.py,sha256=tbcK5dHpbnX8UP8b9yLidCZF02eOVwb2Z15w80XatIs,371
2
+ openapi2cli/__main__.py,sha256=YemrHLS5fqMtkTuPMAnCmPLHN0nqfB6OC-mRMGFCccA,106
3
+ openapi2cli/cli.py,sha256=szav-4w2LMlogvTBfDKilPerCvrnyZCDRSlZpqHSZMg,3660
4
+ openapi2cli/generator.py,sha256=IhVSCHEh6KfNRmaeK8_xU8ZhExU7I0Ok0RJj-6XcnBI,13476
5
+ openapi2cli/parser.py,sha256=02q24MUBKss-1CDJ0vvI5qwDlio3FBpV7PbtOpa_Qo4,10155
6
+ openapi2cli/runtime.py,sha256=5Vo6pt-KShC2MjX63BvfJ-e4TW670owbDvMVvbrf2Zk,4877
7
+ openapi2cli-0.1.0.dist-info/METADATA,sha256=OaPAPIEp3cyW7G5GWW6QXEuxkpejJLBHLdxje1ekCKY,6778
8
+ openapi2cli-0.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
9
+ openapi2cli-0.1.0.dist-info/entry_points.txt,sha256=PYoT4cTlCmdI6m1-yOM58pA2rRpkt667UKBaq386tEk,53
10
+ openapi2cli-0.1.0.dist-info/licenses/LICENSE,sha256=R6wp-QnWz5hcOS9xs_DAAzChQm-ir9s5IQyVsVRXzz4,1061
11
+ openapi2cli-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ openapi2cli = openapi2cli.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Olaf
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.