api-forge-cli 1.0.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.
api_forge/parser.py ADDED
@@ -0,0 +1,291 @@
1
+ """YAML test suite parser."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ import yaml
8
+
9
+ from .models import (
10
+ Assertion,
11
+ AssertionType,
12
+ HttpMethod,
13
+ LoadTestConfig,
14
+ MockEndpoint,
15
+ MockServerConfig,
16
+ RequestConfig,
17
+ TestSuite,
18
+ )
19
+
20
+
21
+ def parse_test_suite(file_path: str | Path) -> TestSuite:
22
+ """Parse a YAML test suite file."""
23
+ path = Path(file_path)
24
+ if not path.exists():
25
+ raise FileNotFoundError(f"Test suite file not found: {path}")
26
+
27
+ with open(path, encoding="utf-8") as f:
28
+ data = yaml.safe_load(f)
29
+
30
+ if not isinstance(data, dict):
31
+ raise ValueError("Invalid test suite format: expected YAML object")
32
+
33
+ suite = TestSuite(
34
+ name=data.get("name", path.stem),
35
+ base_url=data.get("base_url", ""),
36
+ variables=data.get("variables", {}),
37
+ headers=data.get("headers", {}),
38
+ )
39
+
40
+ for req_data in data.get("requests", []):
41
+ suite.requests.append(_parse_request(req_data))
42
+
43
+ return suite
44
+
45
+
46
+ def _parse_request(data: dict) -> RequestConfig:
47
+ """Parse a single request configuration."""
48
+ method_str = data.get("method", "GET").upper()
49
+ try:
50
+ method = HttpMethod(method_str)
51
+ except ValueError:
52
+ method = HttpMethod.GET
53
+
54
+ config = RequestConfig(
55
+ name=data.get("name", "Unnamed Request"),
56
+ url=data.get("url", ""),
57
+ method=method,
58
+ headers=data.get("headers", {}),
59
+ body=data.get("body"),
60
+ timeout=data.get("timeout", 30.0),
61
+ variables=data.get("variables", {}),
62
+ depends_on=data.get("depends_on"),
63
+ )
64
+
65
+ # Parse assertions
66
+ for assertion_data in data.get("assertions", []):
67
+ config.assertions.append(_parse_assertion(assertion_data))
68
+
69
+ return config
70
+
71
+
72
+ def _parse_assertion(data: dict) -> Assertion:
73
+ """Parse a single assertion."""
74
+ type_str = data.get("type", "status").lower()
75
+ try:
76
+ assertion_type = AssertionType(type_str)
77
+ except ValueError:
78
+ assertion_type = AssertionType.STATUS
79
+
80
+ return Assertion(
81
+ type=assertion_type,
82
+ expected=data.get("expected", data.get("value")),
83
+ path=data.get("path", data.get("header")),
84
+ )
85
+
86
+
87
+ def parse_load_test_config(file_path: str | Path) -> LoadTestConfig:
88
+ """Parse a YAML load test configuration file."""
89
+ path = Path(file_path)
90
+ if not path.exists():
91
+ raise FileNotFoundError(f"Load test config file not found: {path}")
92
+
93
+ with open(path, encoding="utf-8") as f:
94
+ data = yaml.safe_load(f)
95
+
96
+ method_str = data.get("method", "GET").upper()
97
+ try:
98
+ method = HttpMethod(method_str)
99
+ except ValueError:
100
+ method = HttpMethod.GET
101
+
102
+ return LoadTestConfig(
103
+ url=data.get("url", ""),
104
+ method=method,
105
+ headers=data.get("headers", {}),
106
+ body=data.get("body"),
107
+ concurrency=data.get("concurrency", 10),
108
+ requests_per_second=data.get("requests_per_second", 0),
109
+ duration_seconds=data.get("duration_seconds", 10),
110
+ timeout=data.get("timeout", 30.0),
111
+ )
112
+
113
+
114
+ def parse_mock_config(file_path: str | Path) -> MockServerConfig:
115
+ """Parse a YAML mock server configuration file."""
116
+ path = Path(file_path)
117
+ if not path.exists():
118
+ raise FileNotFoundError(f"Mock config file not found: {path}")
119
+
120
+ with open(path, encoding="utf-8") as f:
121
+ data = yaml.safe_load(f)
122
+
123
+ config = MockServerConfig(
124
+ name=data.get("name", "api-forge-mock"),
125
+ host=data.get("host", "127.0.0.1"),
126
+ port=data.get("port", 8000),
127
+ default_status=data.get("default_status", 404),
128
+ default_body=data.get("default_body", '{"error": "Not Found"}'),
129
+ cors=data.get("cors", True),
130
+ )
131
+
132
+ for ep_data in data.get("endpoints", []):
133
+ config.endpoints.append(_parse_mock_endpoint(ep_data))
134
+
135
+ return config
136
+
137
+
138
+ def _parse_mock_endpoint(data: dict) -> MockEndpoint:
139
+ """Parse a single mock endpoint."""
140
+ method_str = data.get("method", "GET").upper()
141
+ try:
142
+ method = HttpMethod(method_str)
143
+ except ValueError:
144
+ method = HttpMethod.GET
145
+
146
+ body = data.get("body", "")
147
+ if isinstance(body, dict):
148
+ import json
149
+
150
+ body = json.dumps(body)
151
+
152
+ return MockEndpoint(
153
+ path=data.get("path", "/"),
154
+ method=method,
155
+ status_code=data.get("status", 200),
156
+ headers=data.get("headers", {}),
157
+ body=body,
158
+ delay_ms=data.get("delay_ms", 0),
159
+ match_headers=data.get("match_headers", {}),
160
+ match_body=data.get("match_body"),
161
+ )
162
+
163
+
164
+ def generate_example_suite() -> str:
165
+ """Generate an example test suite YAML."""
166
+ return """# API Test Suite Example
167
+ name: "User API Tests"
168
+ base_url: "https://jsonplaceholder.typicode.com"
169
+
170
+ # Global headers applied to all requests
171
+ headers:
172
+ Content-Type: "application/json"
173
+ Accept: "application/json"
174
+
175
+ # Global variables
176
+ variables:
177
+ api_version: "v1"
178
+
179
+ requests:
180
+ - name: "Get all users"
181
+ url: "/users"
182
+ method: GET
183
+ assertions:
184
+ - type: status
185
+ expected: 200
186
+ - type: response_time
187
+ expected: 2000
188
+ - type: body_contains
189
+ expected: '"email"'
190
+
191
+ - name: "Get single user"
192
+ url: "/users/1"
193
+ method: GET
194
+ assertions:
195
+ - type: status
196
+ expected: 200
197
+ - type: json_path
198
+ path: "id"
199
+ expected: 1
200
+ variables:
201
+ user_name: "name" # Extract name for next request
202
+
203
+ - name: "Create post"
204
+ url: "/posts"
205
+ method: POST
206
+ headers:
207
+ X-Custom-Header: "test"
208
+ body:
209
+ title: "Test Post"
210
+ body: "This is a test"
211
+ userId: 1
212
+ assertions:
213
+ - type: status
214
+ expected: 201
215
+ - type: json_path
216
+ path: "id"
217
+ expected: 101
218
+ """
219
+
220
+
221
+ def generate_example_load_test() -> str:
222
+ """Generate an example load test YAML."""
223
+ return """# Load Test Configuration
224
+ url: "https://jsonplaceholder.typicode.com/posts/1"
225
+ method: GET
226
+
227
+ # Concurrency settings
228
+ concurrency: 50
229
+ requests_per_second: 100 # 0 = unlimited
230
+ duration_seconds: 30
231
+
232
+ # Request configuration
233
+ timeout: 10.0
234
+ headers:
235
+ Accept: "application/json"
236
+ """
237
+
238
+
239
+ def generate_example_mock_config() -> str:
240
+ """Generate an example mock server YAML."""
241
+ return """# Mock Server Configuration
242
+ name: "Test API Mock"
243
+ host: "127.0.0.1"
244
+ port: 8000
245
+ cors: true
246
+
247
+ # Default response for unmatched routes
248
+ default_status: 404
249
+ default_body: '{"error": "Not Found"}'
250
+
251
+ endpoints:
252
+ - path: "/health"
253
+ method: GET
254
+ status: 200
255
+ body:
256
+ status: "healthy"
257
+ version: "1.0.0"
258
+
259
+ - path: "/users"
260
+ method: GET
261
+ status: 200
262
+ headers:
263
+ X-Total-Count: "100"
264
+ body:
265
+ - id: 1
266
+ name: "Alice"
267
+ email: "alice@example.com"
268
+ - id: 2
269
+ name: "Bob"
270
+ email: "bob@example.com"
271
+
272
+ - path: "/users"
273
+ method: POST
274
+ status: 201
275
+ body:
276
+ id: 3
277
+ message: "User created"
278
+
279
+ - path: "/slow"
280
+ method: GET
281
+ status: 200
282
+ delay_ms: 2000 # Simulate slow response
283
+ body:
284
+ message: "This took a while"
285
+
286
+ - path: "/error"
287
+ method: GET
288
+ status: 500
289
+ body:
290
+ error: "Internal Server Error"
291
+ """
@@ -0,0 +1,393 @@
1
+ Metadata-Version: 2.4
2
+ Name: api-forge-cli
3
+ Version: 1.0.0
4
+ Summary: CLI tool for API testing, load testing, and mock server generation
5
+ Project-URL: Homepage, https://github.com/SanjaySundarMurthy/api-forge
6
+ Project-URL: Repository, https://github.com/SanjaySundarMurthy/api-forge
7
+ Project-URL: Issues, https://github.com/SanjaySundarMurthy/api-forge/issues
8
+ Author-email: Sanjay Sundar Murthy <sanjaysundarmurthy@gmail.com>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: api,cli,http,load-testing,mock,rest,testing
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Topic :: Internet :: WWW/HTTP
22
+ Classifier: Topic :: Software Development :: Testing
23
+ Requires-Python: >=3.9
24
+ Requires-Dist: click>=8.0
25
+ Requires-Dist: httpx>=0.27
26
+ Requires-Dist: pyyaml>=6.0
27
+ Requires-Dist: rich>=13.0
28
+ Provides-Extra: dev
29
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
30
+ Requires-Dist: pytest-cov>=5.0; extra == 'dev'
31
+ Requires-Dist: pytest>=8.0; extra == 'dev'
32
+ Requires-Dist: respx>=0.21; extra == 'dev'
33
+ Requires-Dist: ruff>=0.4; extra == 'dev'
34
+ Description-Content-Type: text/markdown
35
+
36
+ # api-forge
37
+
38
+ **CLI tool for API testing, load testing, and mock server generation.**
39
+
40
+ [![PyPI version](https://badge.fury.io/py/api-forge-cli.svg)](https://pypi.org/project/api-forge-cli/)
41
+ [![Python 3.9+](https://img.shields.io/badge/python-3.9+-blue.svg)](https://www.python.org/downloads/)
42
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
43
+
44
+ ---
45
+
46
+ ## Why api-forge?
47
+
48
+ Testing APIs shouldn't require heavy frameworks or complex setup. **api-forge** is a lightweight CLI tool that provides:
49
+
50
+ - **Test Suites** — Define API tests in simple YAML files
51
+ - **Assertions** — Validate status codes, headers, JSON paths, response times
52
+ - **Variable Chaining** — Extract values from responses for use in subsequent requests
53
+ - **Load Testing** — Stress test endpoints with configurable concurrency
54
+ - **Mock Server** — Spin up mock APIs from YAML for development/testing
55
+ - **Quick Requests** — cURL-like commands with pretty output
56
+
57
+ ---
58
+
59
+ ## Installation
60
+
61
+ ```bash
62
+ pip install api-forge-cli
63
+ ```
64
+
65
+ ---
66
+
67
+ ## Quick Start
68
+
69
+ ```bash
70
+ # Make a quick GET request
71
+ api-forge get https://jsonplaceholder.typicode.com/posts/1
72
+
73
+ # Run a test suite
74
+ api-forge test suite.yaml
75
+
76
+ # Load test an endpoint
77
+ api-forge load https://api.example.com/health -n 50 -d 10
78
+
79
+ # Start a mock server
80
+ api-forge mock mock.yaml
81
+
82
+ # Generate example configs
83
+ api-forge init --type test -o suite.yaml
84
+ ```
85
+
86
+ ---
87
+
88
+ ## Commands
89
+
90
+ ### `get` / `post` / `put` / `delete` — Quick Requests
91
+
92
+ ```bash
93
+ # GET request
94
+ api-forge get https://api.example.com/users
95
+
96
+ # POST with JSON body
97
+ api-forge post https://api.example.com/users -d '{"name": "Alice"}'
98
+
99
+ # With custom headers
100
+ api-forge get https://api.example.com/users -H "Authorization:Bearer token"
101
+
102
+ # PUT request
103
+ api-forge put https://api.example.com/users/1 -d '{"name": "Bob"}'
104
+
105
+ # DELETE request
106
+ api-forge delete https://api.example.com/users/1
107
+ ```
108
+
109
+ ### `test` — Run API Test Suites
110
+
111
+ ```bash
112
+ api-forge test suite.yaml
113
+ api-forge test tests/api-tests.yaml -v
114
+ ```
115
+
116
+ Test suites are defined in YAML:
117
+
118
+ ```yaml
119
+ name: "User API Tests"
120
+ base_url: "https://api.example.com"
121
+
122
+ headers:
123
+ Authorization: "Bearer {{token}}"
124
+
125
+ variables:
126
+ token: "your-api-token"
127
+
128
+ requests:
129
+ - name: "Get all users"
130
+ url: "/users"
131
+ method: GET
132
+ assertions:
133
+ - type: status
134
+ expected: 200
135
+ - type: response_time
136
+ expected: 2000
137
+
138
+ - name: "Create user"
139
+ url: "/users"
140
+ method: POST
141
+ body:
142
+ name: "Alice"
143
+ email: "alice@example.com"
144
+ assertions:
145
+ - type: status
146
+ expected: 201
147
+ - type: json_path
148
+ path: "id"
149
+ expected: 1
150
+ variables:
151
+ new_user_id: "id" # Extract for next request
152
+
153
+ - name: "Get created user"
154
+ url: "/users/{{new_user_id}}"
155
+ method: GET
156
+ assertions:
157
+ - type: status
158
+ expected: 200
159
+ ```
160
+
161
+ ### `load` — Load Testing
162
+
163
+ ```bash
164
+ # Quick load test
165
+ api-forge load https://api.example.com/health -n 50 -d 30
166
+
167
+ # From config file
168
+ api-forge load -c loadtest.yaml
169
+ ```
170
+
171
+ | Option | Description |
172
+ |--------|-------------|
173
+ | `-n, --concurrency` | Number of concurrent connections (default: 10) |
174
+ | `-d, --duration` | Test duration in seconds (default: 10) |
175
+ | `--rps` | Requests per second limit (0 = unlimited) |
176
+ | `-c, --config` | Load test config YAML file |
177
+
178
+ Load test config:
179
+
180
+ ```yaml
181
+ url: "https://api.example.com/users"
182
+ method: GET
183
+ concurrency: 100
184
+ duration_seconds: 60
185
+ requests_per_second: 500
186
+ timeout: 10.0
187
+ headers:
188
+ Authorization: "Bearer token"
189
+ ```
190
+
191
+ **Output includes:**
192
+ - Total requests, success/failure counts
193
+ - Requests per second achieved
194
+ - Latency percentiles (min, avg, p50, p95, p99, max)
195
+ - Status code distribution
196
+ - Error samples
197
+
198
+ ### `mock` — Mock Server
199
+
200
+ ```bash
201
+ # Start mock server from config
202
+ api-forge mock mock.yaml
203
+
204
+ # Custom port
205
+ api-forge mock mock.yaml -p 3000
206
+
207
+ # Default server (returns 404 for all)
208
+ api-forge mock
209
+ ```
210
+
211
+ Mock server config:
212
+
213
+ ```yaml
214
+ name: "Test API"
215
+ port: 8000
216
+ cors: true
217
+
218
+ endpoints:
219
+ - path: "/health"
220
+ method: GET
221
+ status: 200
222
+ body:
223
+ status: "healthy"
224
+
225
+ - path: "/users"
226
+ method: GET
227
+ status: 200
228
+ headers:
229
+ X-Total-Count: "100"
230
+ body:
231
+ - id: 1
232
+ name: "Alice"
233
+ - id: 2
234
+ name: "Bob"
235
+
236
+ - path: "/users"
237
+ method: POST
238
+ status: 201
239
+ body:
240
+ id: 3
241
+ message: "Created"
242
+
243
+ - path: "/slow"
244
+ method: GET
245
+ status: 200
246
+ delay_ms: 2000 # Simulate latency
247
+ body:
248
+ message: "Slow response"
249
+
250
+ - path: "/users/*" # Wildcard matching
251
+ method: GET
252
+ status: 200
253
+ body:
254
+ id: 1
255
+ name: "User"
256
+ ```
257
+
258
+ ### `init` — Generate Examples
259
+
260
+ ```bash
261
+ api-forge init --type test -o suite.yaml
262
+ api-forge init --type load -o loadtest.yaml
263
+ api-forge init --type mock -o mock.yaml
264
+ ```
265
+
266
+ ---
267
+
268
+ ## Assertion Types
269
+
270
+ | Type | Description | Example |
271
+ |------|-------------|---------|
272
+ | `status` | HTTP status code | `expected: 200` or `expected: [200, 201]` |
273
+ | `header` | Header value contains | `header: Content-Type`, `expected: json` |
274
+ | `body_contains` | Body contains string | `expected: "success"` |
275
+ | `json_path` | JSON path equals value | `path: user.id`, `expected: 123` |
276
+ | `body_regex` | Body matches regex | `expected: "\\d{5}"` |
277
+ | `response_time` | Max response time (ms) | `expected: 500` |
278
+
279
+ ---
280
+
281
+ ## Variable Substitution
282
+
283
+ Use `{{variable_name}}` syntax in URLs, headers, and body:
284
+
285
+ ```yaml
286
+ variables:
287
+ api_key: "secret123"
288
+ user_id: "456"
289
+
290
+ requests:
291
+ - name: "Get user"
292
+ url: "/users/{{user_id}}"
293
+ headers:
294
+ X-API-Key: "{{api_key}}"
295
+ ```
296
+
297
+ **Extract variables from responses** for request chaining:
298
+
299
+ ```yaml
300
+ requests:
301
+ - name: "Login"
302
+ url: "/auth/login"
303
+ method: POST
304
+ body:
305
+ username: "admin"
306
+ password: "secret"
307
+ variables:
308
+ auth_token: "token" # Extract $.token from response
309
+
310
+ - name: "Protected endpoint"
311
+ url: "/protected"
312
+ headers:
313
+ Authorization: "Bearer {{auth_token}}"
314
+ ```
315
+
316
+ ---
317
+
318
+ ## Examples
319
+
320
+ ### Full Test Suite with Chaining
321
+
322
+ ```yaml
323
+ name: "E-commerce API"
324
+ base_url: "https://api.shop.example.com"
325
+
326
+ requests:
327
+ - name: "Create product"
328
+ url: "/products"
329
+ method: POST
330
+ body:
331
+ name: "Widget"
332
+ price: 29.99
333
+ assertions:
334
+ - type: status
335
+ expected: 201
336
+ variables:
337
+ product_id: "id"
338
+
339
+ - name: "Get product"
340
+ url: "/products/{{product_id}}"
341
+ assertions:
342
+ - type: json_path
343
+ path: "name"
344
+ expected: "Widget"
345
+
346
+ - name: "Delete product"
347
+ url: "/products/{{product_id}}"
348
+ method: DELETE
349
+ assertions:
350
+ - type: status
351
+ expected: 204
352
+ ```
353
+
354
+ ### Load Test with Detailed Config
355
+
356
+ ```yaml
357
+ url: "https://api.example.com/search"
358
+ method: POST
359
+ concurrency: 200
360
+ duration_seconds: 120
361
+ requests_per_second: 1000
362
+ timeout: 5.0
363
+ headers:
364
+ Content-Type: "application/json"
365
+ body:
366
+ query: "test"
367
+ limit: 10
368
+ ```
369
+
370
+ ---
371
+
372
+ ## Development
373
+
374
+ ```bash
375
+ git clone https://github.com/SanjaySundarMurthy/api-forge.git
376
+ cd api-forge
377
+ pip install -e ".[dev]"
378
+ pytest tests/ -v
379
+ ```
380
+
381
+ ---
382
+
383
+ ## Author
384
+
385
+ **Sanjay Sundar Murthy**
386
+ - GitHub: [@SanjaySundarMurthy](https://github.com/SanjaySundarMurthy)
387
+ - Email: sanjaysundarmurthy@gmail.com
388
+
389
+ ---
390
+
391
+ ## License
392
+
393
+ MIT License — see [LICENSE](LICENSE) for details.
@@ -0,0 +1,13 @@
1
+ api_forge/__init__.py,sha256=Pfo_n30AQ_JA8qP7gNez3yV3N7wqb7ephFe7mFVXMUI,77
2
+ api_forge/__main__.py,sha256=lG27aFqY6oNO-ZaZ5J-Ts3w4ZF3zaYhCngJ8Aa7MqEQ,119
3
+ api_forge/cli.py,sha256=Bipbg-K49klNJR_Q8XbEtKZeqSdrW67023HWywEBhdo,8591
4
+ api_forge/client.py,sha256=9Kxe5xVvRmKoNBrh9TFownQYL8Vl_YiU3eZREOivzIk,6214
5
+ api_forge/mock_server.py,sha256=D_ImoHO0MKAfl3zQ3peev12NZnLTGsKtpIhen_9LmqQ,6274
6
+ api_forge/models.py,sha256=1m0vcF0abwkVvUdvZT3H0WZtz8AGopyXYAwsN1ILYJg,9384
7
+ api_forge/output.py,sha256=ey5VbXFO1IFJNb2xx5ge1rkZdSJcnohz67J7XXuVHoQ,7339
8
+ api_forge/parser.py,sha256=Bi9afyX-RC1ythWxTHC9jNsYTMmdj5fqjGvbz8KoVLM,7399
9
+ api_forge_cli-1.0.0.dist-info/METADATA,sha256=ZVKl60movPZUli6zLNJUKJqbMvO32aWzzNarAyjvpJU,8607
10
+ api_forge_cli-1.0.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
11
+ api_forge_cli-1.0.0.dist-info/entry_points.txt,sha256=PsVFVdY6IzEsywDE2K0BvN5G8r7YGlq_Aq4ZGwVihhc,48
12
+ api_forge_cli-1.0.0.dist-info/licenses/LICENSE,sha256=kuYe0v68w5WHNJfYvCLtnsYEEr6NzB-jhnE_qRkxu1g,1098
13
+ api_forge_cli-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ api-forge = api_forge.cli:cli