pysnapapi 0.3.0__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.
Files changed (48) hide show
  1. pysnapapi-0.3.0/LICENSE +21 -0
  2. pysnapapi-0.3.0/PKG-INFO +353 -0
  3. pysnapapi-0.3.0/README.md +303 -0
  4. pysnapapi-0.3.0/pyproject.toml +52 -0
  5. pysnapapi-0.3.0/pysnapapi.egg-info/PKG-INFO +353 -0
  6. pysnapapi-0.3.0/pysnapapi.egg-info/SOURCES.txt +46 -0
  7. pysnapapi-0.3.0/pysnapapi.egg-info/dependency_links.txt +1 -0
  8. pysnapapi-0.3.0/pysnapapi.egg-info/entry_points.txt +5 -0
  9. pysnapapi-0.3.0/pysnapapi.egg-info/requires.txt +10 -0
  10. pysnapapi-0.3.0/pysnapapi.egg-info/top_level.txt +1 -0
  11. pysnapapi-0.3.0/setup.cfg +4 -0
  12. pysnapapi-0.3.0/snapapi/__init__.py +8 -0
  13. pysnapapi-0.3.0/snapapi/__main__.py +4 -0
  14. pysnapapi-0.3.0/snapapi/api_client.py +125 -0
  15. pysnapapi-0.3.0/snapapi/cassette.py +81 -0
  16. pysnapapi-0.3.0/snapapi/cli.py +603 -0
  17. pysnapapi-0.3.0/snapapi/engine.py +1837 -0
  18. pysnapapi-0.3.0/snapapi/exceptions.py +22 -0
  19. pysnapapi-0.3.0/snapapi/fmt.py +102 -0
  20. pysnapapi-0.3.0/snapapi/helpers.py +63 -0
  21. pysnapapi-0.3.0/snapapi/history.py +155 -0
  22. pysnapapi-0.3.0/snapapi/jsonpath.py +173 -0
  23. pysnapapi-0.3.0/snapapi/lint.py +119 -0
  24. pysnapapi-0.3.0/snapapi/listeners.py +87 -0
  25. pysnapapi-0.3.0/snapapi/mock.py +207 -0
  26. pysnapapi-0.3.0/snapapi/openapi.py +236 -0
  27. pysnapapi-0.3.0/snapapi/parser.py +1214 -0
  28. pysnapapi-0.3.0/snapapi/profiles.py +27 -0
  29. pysnapapi-0.3.0/snapapi/pytest_plugin.py +52 -0
  30. pysnapapi-0.3.0/snapapi/redact.py +79 -0
  31. pysnapapi-0.3.0/snapapi/reports.py +541 -0
  32. pysnapapi-0.3.0/snapapi/safety.py +34 -0
  33. pysnapapi-0.3.0/snapapi/select.py +133 -0
  34. pysnapapi-0.3.0/snapapi/suites.py +29 -0
  35. pysnapapi-0.3.0/snapapi/variables.py +111 -0
  36. pysnapapi-0.3.0/snapapi/watch.py +68 -0
  37. pysnapapi-0.3.0/snapapi/xpath.py +66 -0
  38. pysnapapi-0.3.0/tests/test_api_client.py +33 -0
  39. pysnapapi-0.3.0/tests/test_cli.py +355 -0
  40. pysnapapi-0.3.0/tests/test_engine.py +856 -0
  41. pysnapapi-0.3.0/tests/test_jsonpath.py +46 -0
  42. pysnapapi-0.3.0/tests/test_listeners.py +147 -0
  43. pysnapapi-0.3.0/tests/test_p0_p1_p2.py +424 -0
  44. pysnapapi-0.3.0/tests/test_parser.py +878 -0
  45. pysnapapi-0.3.0/tests/test_sdet.py +744 -0
  46. pysnapapi-0.3.0/tests/test_sdet_next.py +559 -0
  47. pysnapapi-0.3.0/tests/test_select.py +40 -0
  48. pysnapapi-0.3.0/tests/test_variables.py +56 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Deekshith Poojary
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.
@@ -0,0 +1,353 @@
1
+ Metadata-Version: 2.4
2
+ Name: pysnapapi
3
+ Version: 0.3.0
4
+ Summary: Lightweight DSL for HTTP API testing
5
+ Author-email: Deekshith Poojary <deekshithpoojary355@gmail.com>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 Deekshith Poojary
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: Homepage, https://deekshith-poojary98.github.io/snapapi/
29
+ Project-URL: Documentation, https://github.com/deekshith-poojary98/snapapi#readme
30
+ Project-URL: Repository, https://github.com/deekshith-poojary98/snapapi
31
+ Project-URL: Issues, https://github.com/deekshith-poojary98/snapapi/issues
32
+ Classifier: Development Status :: 4 - Beta
33
+ Classifier: Environment :: Console
34
+ Classifier: Intended Audience :: Developers
35
+ Classifier: License :: OSI Approved :: MIT License
36
+ Classifier: Programming Language :: Python :: 3
37
+ Classifier: Topic :: Software Development :: Testing
38
+ Requires-Python: >=3.9
39
+ Description-Content-Type: text/markdown
40
+ License-File: LICENSE
41
+ Requires-Dist: requests>=2.28.0
42
+ Requires-Dist: colorama>=0.4.6
43
+ Requires-Dist: jsonschema>=4.18.0
44
+ Requires-Dist: PyYAML>=6.0
45
+ Provides-Extra: dev
46
+ Requires-Dist: pytest>=7.0; extra == "dev"
47
+ Provides-Extra: watch
48
+ Requires-Dist: watchdog>=2.1; extra == "watch"
49
+ Dynamic: license-file
50
+
51
+ # SnapAPI
52
+
53
+ [![PyPI version](https://badge.fury.io/py/snapapi.svg)](https://badge.fury.io/py/snapapi)
54
+ [![Python](https://img.shields.io/badge/python-3.9%20%7C%203.10%20%7C%203.11%20%7C%203.12%20%7C%203.13-blue.svg)](https://www.python.org/downloads/)
55
+ [![CI Tests](https://github.com/deekshith-poojary98/snapapi/actions/workflows/snapapi.yml/badge.svg)](https://github.com/deekshith-poojary98/snapapi/actions/workflows/snapapi.yml)
56
+ [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/deekshith-poojary98/snapapi)
57
+
58
+
59
+ SnapAPI is a lightweight HTTP API testing framework with a small custom DSL.
60
+ Write `.sapi` files, then run them from the CLI. The older `.snaptest` extension still works.
61
+
62
+ **[User guide](https://deekshith-poojary98.github.io/snapapi/)** — install, DSL reference, CLI, CI, VS Code, and an in-browser **[playground](https://deekshith-poojary98.github.io/snapapi/playground.html)**
63
+
64
+ ## Features
65
+
66
+ - Human-readable DSL for GET, POST, PUT, PATCH, DELETE, HEAD, and OPTIONS
67
+ - Attach `BODY`/`DATA`, `HEADER`s, `QUERY`/`PARAM`, and `AUTH` to the current request
68
+ - `EXPECT` checks: status, body contains, JSONPath (filters + collection asserts), XPath, response headers
69
+ - `SAVE` values from JSON responses and reuse them as `${var}`
70
+ - Setup / teardown with cycle detection
71
+ - Env files, tag filters, timeouts, retries, JSON and JUnit reports
72
+ - OpenAPI response/request contract checks, VCR cassettes, JSON mock server
73
+ - pytest plugin (`snapapi_run` / `@pytest.mark.snapapi`)
74
+ - VS Code syntax highlighting and diagnostics for `.sapi` files
75
+
76
+ ## Requirements
77
+
78
+ - Python 3.9 or newer
79
+
80
+ ## Installation
81
+
82
+ ```bash
83
+ git clone https://github.com/Deekshith-07/snapapi.git
84
+ cd snapapi
85
+ python3 -m venv .venv
86
+ source .venv/bin/activate # Windows: .venv\Scripts\activate
87
+ pip install -e ".[dev]"
88
+ ```
89
+
90
+ Or with the pinned runtime dependencies:
91
+
92
+ ```bash
93
+ pip install -r requirements.txt
94
+ pip install -e .
95
+ ```
96
+
97
+ ## CLI
98
+
99
+ ```bash
100
+ snapapi path/to/file.sapi
101
+ python -m snapapi path/to/file.sapi
102
+ ```
103
+
104
+ Pass multiple files or a directory of `.sapi` files:
105
+
106
+ ```bash
107
+ snapapi tests/test_suite.snaptest
108
+ snapapi tests/ suites/auth.sapi
109
+ ```
110
+
111
+ Options:
112
+
113
+ | Flag | Meaning |
114
+ | --- | --- |
115
+ | `-k "Login or not Health"` | Pytest-style keyword expression on name/description/tags |
116
+ | `-m "smoke and not slow"` | Pytest-style tag expression |
117
+ | `--tag user` | Run tests that have this tag (repeatable; all given tags must match) |
118
+ | `--exclude slow` | Skip tests with this tag (repeatable) |
119
+ | `--name "Create User"` | Run tests with this name (repeatable) |
120
+ | `--env .env` | Load `KEY=VALUE` pairs for `${VAR}`. If omitted, SnapAPI loads `<suite>.env`, then `.env` next to the file, then a single sibling `*.env` |
121
+ | `-D TOKEN=secret` | Set `${VAR}` from the CLI (repeatable) |
122
+ | `--timeout 10` | HTTP timeout in seconds (overrides `TIMEOUT`) |
123
+ | `--report json:report.json` | Write a JSON report |
124
+ | `--report junit:report.xml` | Write a JUnit XML report |
125
+ | `--report html:report.html` | Write a self-contained HTML report |
126
+ | `-x` / `--exitfirst` / `--stop-on-failure` | Stop each suite on the first failed test (off by default) |
127
+ | `--maxfail N` | Stop after N failures |
128
+ | `--collect-only` | List selected tests without HTTP |
129
+ | `-q` / `-v` | Quiet or verbose console |
130
+ | `--durations N` | Show the N slowest tests |
131
+ | `--profile stage` | Load `environments/stage.env`, `.snapapi/stage.env`, or `stage.env` |
132
+ | `--workers N` | Run independent tests in parallel (SAVE is isolated per test; sibling SAVE falls back to sequential) |
133
+ | `--grep regex` | Filter tests by name/description |
134
+ | `--lf` / `--last-failed` | Re-run failures from `.snapapi/last-run.json` (matches file + suite + name, including `Test [row]`) |
135
+ | `--ff` / `--failed-first` | Run last-failed tests first, then the rest |
136
+ | `--mode record\|replay\|record-on-miss` | VCR cassettes under `.snapapi/cassettes/` |
137
+ | `--record-on-miss` | With `--mode replay`, hit the network and save when a cassette is missing |
138
+ | `--vcr-match query,body,accept,authorization` | Cassette identity fields (default: query, content-type, accept, body) |
139
+ | `--contract-strict` | Fail when an OpenAPI path/method/schema is missing (default: skip/warn) |
140
+ | `--reruns N` | Re-run failed *tests* up to N times (distinct from `EXPECT RETRY`) |
141
+ | `--listener PATH[:Class]` | Python listener called after each test / suite (repeatable) |
142
+ | `--on-fail curl` / `--on-fail har:dir` | Emit a redacted curl or HAR on failure |
143
+ | `--safe-url` | Block private/metadata hosts |
144
+ | `--proxy URL` | HTTP/HTTPS proxy |
145
+ | `--insecure` | Skip TLS certificate verification |
146
+ | `--cert PATH` | Client certificate |
147
+ | `--cacert PATH` | CA bundle used to verify TLS |
148
+ | `snapapi lint PATH` | Parse/validate without HTTP |
149
+ | `snapapi fmt PATH` | Format `.sapi` files |
150
+ | `snapapi openapi spec.yaml` | Generate GET/POST/PUT/PATCH/DELETE smoke tests |
151
+ | `snapapi history [--failed] [--since 7d]` | Print `.snapapi/history.jsonl` |
152
+ | `snapapi mock mock.json [--port 0]` | Serve routes from a JSON mock file (prints the URL) |
153
+ | `snapapi watch PATH [--interval 0.5]` | Re-run when `.sapi` files change (poll; optional `watchdog` extra) |
154
+
155
+ The process exits `0` when every test passed, `1` when a test failed, and `2` on parse or usage errors.
156
+
157
+ ## DSL
158
+
159
+ Recommended form:
160
+
161
+ ```
162
+ SUITE: Book Store
163
+ DESC: Validates the user API
164
+ TIMEOUT: 10
165
+ URL: https://api.example.com
166
+ HEADER Content-Type: application/json
167
+
168
+ TEST: Create User
169
+ DESC: Create a user and keep the id
170
+ TAG: users write
171
+ POST: /users
172
+ AUTH: bearer ${TOKEN}
173
+ BODY: {"name": "Jane", "email": "jane@example.com"}
174
+ EXPECT: status == 201
175
+ EXPECT: body contains id
176
+ SAVE: userId FROM $.id
177
+
178
+ TEST: Get User
179
+ TAG: users
180
+ SETUP: Create User
181
+ GET: /users/${userId}
182
+ EXPECT: status == 200
183
+ EXPECT: json $.email == "jane@example.com"
184
+ EXPECT: header Content-Type contains json
185
+
186
+ TEST: List Users
187
+ GET: /users
188
+ QUERY: page=2&limit=10
189
+ PARAM: sort name
190
+ EXPECT: status == 200
191
+
192
+ TEST: Wait for ready
193
+ GET: /jobs/${id}
194
+ WAIT: json $.status == "ready" TIMEOUT 10s BACKOFF 0.5s
195
+ EXPECT: status == 200
196
+ ```
197
+
198
+ Comments are `//` lines. Indentation is cosmetic. JSON bodies may be one line or span multiple lines.
199
+
200
+ The older forms still parse:
201
+
202
+ ```
203
+ OPTIONS: {"TIMEOUT": 10}
204
+ HEADERS: {"Content-Type": "application/json"}
205
+ TAG: users, write
206
+ REQUEST: POST /users
207
+ HEADERS: {"Authorization": "Bearer ${TOKEN}"}
208
+ DATA: {"name": "Jane"}
209
+ EXPECT: STATUS 201
210
+ EXPECT: CONTAINS id
211
+ EXPECT: JSON $.email == "jane@example.com"
212
+ EXPECT: HEADER Content-Type CONTAINS json
213
+ ```
214
+
215
+ ### Keywords
216
+
217
+ - Suite: `SUITE`, `DESC`, `URL`, `TIMEOUT`, `FOLLOW-REDIRECTS`, `OPTIONS`, `IMPORT`, `SUITE-SETUP`, `SET`
218
+ - Test: `TEST`, `TAG`, `SETUP`, `TEARDOWN`, `DEPENDS`, `SKIP`, `ONLY`, `QUARANTINE`, `EXAMPLES`, `SET`
219
+ - Helper: `HELPER` (named procedure for `SUITE-SETUP` / `SETUP` / `TEARDOWN`; not a test case)
220
+ - Request: `REQUEST`, `GET`/`POST`/`PUT`/`PATCH`/`DELETE`/`HEAD`, `BODY`/`DATA`, `FILE`, `GRAPHQL`, `HEADER`/`HEADERS`, `QUERY`, `PARAM`, `AUTH`, `EXPECT`, `SAVE`, `WAIT`, `SET`
221
+
222
+ HTTP `OPTIONS` is written as `REQUEST: OPTIONS /path` so it does not collide with suite-level `OPTIONS: {...}` JSON. `HEAD: /x` is a request alias like `GET:`.
223
+
224
+ `SETUP` / `TEARDOWN` name a `HELPER` or `TEST`. Prefer `HELPER:` for procedures that should not appear as cases. A `TEST` named only as setup is still treated as a helper (legacy).
225
+
226
+ `DEPENDS: Create User` (comma-separated for several names) keeps both tests as primaries. SnapAPI reorders so named tests run first; unrelated tests keep file order. If a named test failed, skipped, or was not selected (for example `-k`), the dependent test is skipped with that reason. Cycles and unknown names are parse errors. `DEPENDS` cannot target a `HELPER`. This is not `SETUP:` — setup helpers always run first and fail the dependent test when they fail.
227
+
228
+ `IMPORT: other.sapi` pulls tests from another file (paths are relative to the current file).
229
+
230
+ `SET: orderId ${uuid()}` assigns an interpolated value (including helpers) without an HTTP call. It may appear at suite, test, or step level.
231
+
232
+ `WAIT: json $.status == "ready" TIMEOUT 10s BACKOFF 0.5s` reissues the current request until the check passes or the timeout expires. `EXPECT: json $.status == "ready" RETRY 20 BACKOFF 0.5s` also retries when the HTTP status is already 200.
233
+
234
+ `AUTH: bearer ${TOKEN}` sets `Authorization: Bearer ${TOKEN}`. Explicit `HEADER` lines still work.
235
+
236
+ `AUTH: oauth2 grant=client_credentials token_url=... client_id=...` and `grant=password username=... password=...` fetch a token (cached). If the token response includes `refresh_token`, a 401 retries once after refresh.
237
+
238
+ `AUTH: oauth2 grant=authorization_code token_url=... auth_url=... client_id=... redirect_uri=... code=${AUTH_CODE} pkce=true` exchanges an authorization code. SnapAPI does not open a browser; supply `${AUTH_CODE}` from the environment. With `pkce=true` the token request includes S256 `code_verifier` / `code_challenge` fields.
239
+
240
+ `AUTH: digest user:pass` uses `requests` HTTP Digest Auth.
241
+
242
+ `QUERY: page=2&limit=10` and `PARAM: page 2` attach query parameters to the current request (they merge with any query string already in the path).
243
+
244
+ `OPTIONS: {"OPENAPI": "spec.yaml"}` validates JSON responses (and request bodies/required params) against the matching path+method schema when present. Missing path/schema is skipped by default. Strict mode fails instead:
245
+
246
+ ```
247
+ OPTIONS: {"OPENAPI": "spec.yaml", "OPENAPI-STRICT": true}
248
+ EXPECT: openapi ./spec.yaml strict
249
+ ```
250
+
251
+ CLI `--contract-strict` is the same switch. Partial path match (`/users/{id}` vs `/users/1`) is allowed.
252
+
253
+ ### Checks
254
+
255
+ Check types are case-insensitive. Preferred:
256
+
257
+ ```
258
+ EXPECT: status == 200
259
+ EXPECT: status != 500
260
+ EXPECT: status == 200 RETRY 5 ON 5xx BACKOFF 1s
261
+ EXPECT: body contains userId
262
+ EXPECT: body not contains stack
263
+ EXPECT: json $.email matches ^.+@example\\.com$
264
+ EXPECT: json $.items length == 3
265
+ EXPECT: json $.items[*].id contains 3
266
+ EXPECT: json $.items[?(@.status=="open")].id contains 3
267
+ EXPECT: json $.tags contains-all ["a","b"]
268
+ EXPECT: json $.items each $.status == "active"
269
+ EXPECT: status == 400 OR status == 401
270
+ EXPECT: json $.success == false AND body contains error
271
+ EXPECT: (status == 400 OR status == 401) AND json $.success == false
272
+ EXPECT: schema ./schemas/user.json
273
+ EXPECT: duration < 200ms
274
+ EXPECT: header Content-Type contains json
275
+ EXPECT: openapi ./openapi.yaml
276
+ EXPECT: openapi ./openapi.yaml strict
277
+ EXPECT: xpath //Order/@id == "1"
278
+ ```
279
+
280
+ Also accepted:
281
+
282
+ ```
283
+ EXPECT: STATUS 200
284
+ EXPECT: CONTAINS userId
285
+ EXPECT: JSON $.data.email == "jane@example.com"
286
+ EXPECT: HEADER Content-Type CONTAINS json
287
+ ```
288
+
289
+ JSONPath is a small subset: `$.a.b`, `$.items.0.id`, `$.items[0].id`, `$.items[*].id`, and equality filters `$.items[?(@.status=="open")]` / `$.items[?(@.id==1)]`.
290
+
291
+ `AND` / `OR` combine checks on one line (`AND` binds tighter than `OR`; parentheses group). Quote a value if it contains those words. Multiple `EXPECT` lines on the same request still all have to pass.
292
+
293
+ XPath uses stdlib `xml.etree` (descendant tags and `/@attr`). Axes, namespaces, and functions are not implemented.
294
+
295
+ ### Variables
296
+
297
+ `${NAME}` is expanded in URLs, paths, headers, data, and expect values.
298
+
299
+ Lookup order: process environment, then `--env` / auto-discovered suite env file, then `SET` / `SAVE` values.
300
+
301
+ ## Sample suite
302
+
303
+ `tests/recommended.snaptest` shows the current DSL against a local mock server (pytest injects `BASE_URL`). `tests/test_suite.snaptest` is a classic-syntax example against [reqres.in](https://reqres.in) and needs network access. Automated tests in `tests/test_*.py` use a local mock HTTP server and do not call reqres. CI replays `tests/fixtures/offline.snaptest` from a checked-in cassette.
304
+
305
+ HTML reports include redacted request/response bodies. VCR cassette keys include method, path, and (by default) sorted query string, `Content-Type`/`Accept`, and body. `OPTIONS: {"VCR-MATCH": ["query","body","accept","authorization"]}` or `--vcr-match authorization,query` replaces that default. Replay restores `Set-Cookie` onto the session.
306
+
307
+ `snapapi mock tests/fixtures/mock.json --port 0` serves JSON routes. Routes may use path templates (`/users/{id}`), optional `match.query` / `match.body` subsets, and `delay_ms`. Exact paths win over templates. There is no language server, gRPC, or WebSocket support.
308
+
309
+ ### pytest plugin
310
+
311
+ Install with `pip install -e ".[dev]"`. Then:
312
+
313
+ ```python
314
+ def test_suite(snapapi_run):
315
+ result = snapapi_run("tests/foo.sapi")
316
+ assert result.ok
317
+
318
+ @pytest.mark.snapapi("tests/foo.sapi")
319
+ def test_marked(snapapi_run, request):
320
+ snapapi_run(request.node.get_closest_marker("snapapi").args[0])
321
+ ```
322
+
323
+ `snapapi_run(path, **engine_kwargs)` returns `SuiteResult` and fails the pytest case when the suite is not ok.
324
+
325
+ ## Project layout
326
+
327
+ ```
328
+ snapapi/
329
+ ├── snapapi/ # Python package
330
+ │ ├── parser.py # .sapi DSL parser
331
+ │ ├── engine.py # runner, checks, setup/teardown
332
+ │ ├── api_client.py # requests wrapper
333
+ │ └── cli.py # snapapi command
334
+ ├── tests/ # pytest + sample .sapi / .snaptest suites
335
+ ├── docs/ # User guide + in-browser playground
336
+ ├── snapapi-language/ # VS Code grammar / run command
337
+ ├── pyproject.toml
338
+ └── README.md
339
+ ```
340
+
341
+ ## Running the tests
342
+
343
+ ```bash
344
+ pytest
345
+ ```
346
+
347
+ ## VS Code
348
+
349
+ The `snapapi-language` extension is a language pack for `.sapi` files: syntax highlighting, snippets, completions, lightweight diagnostics (unknown keywords, unknown SETUP names, `HEAD:` / `REQUEST: OPTIONS`), and **SnapAPI: Run current file** / **Run test at cursor**. See [snapapi-language/README.md](snapapi-language/README.md). There is no separate language-server process.
350
+
351
+ ## License
352
+
353
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,303 @@
1
+ # SnapAPI
2
+
3
+ [![PyPI version](https://badge.fury.io/py/snapapi.svg)](https://badge.fury.io/py/snapapi)
4
+ [![Python](https://img.shields.io/badge/python-3.9%20%7C%203.10%20%7C%203.11%20%7C%203.12%20%7C%203.13-blue.svg)](https://www.python.org/downloads/)
5
+ [![CI Tests](https://github.com/deekshith-poojary98/snapapi/actions/workflows/snapapi.yml/badge.svg)](https://github.com/deekshith-poojary98/snapapi/actions/workflows/snapapi.yml)
6
+ [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/deekshith-poojary98/snapapi)
7
+
8
+
9
+ SnapAPI is a lightweight HTTP API testing framework with a small custom DSL.
10
+ Write `.sapi` files, then run them from the CLI. The older `.snaptest` extension still works.
11
+
12
+ **[User guide](https://deekshith-poojary98.github.io/snapapi/)** — install, DSL reference, CLI, CI, VS Code, and an in-browser **[playground](https://deekshith-poojary98.github.io/snapapi/playground.html)**
13
+
14
+ ## Features
15
+
16
+ - Human-readable DSL for GET, POST, PUT, PATCH, DELETE, HEAD, and OPTIONS
17
+ - Attach `BODY`/`DATA`, `HEADER`s, `QUERY`/`PARAM`, and `AUTH` to the current request
18
+ - `EXPECT` checks: status, body contains, JSONPath (filters + collection asserts), XPath, response headers
19
+ - `SAVE` values from JSON responses and reuse them as `${var}`
20
+ - Setup / teardown with cycle detection
21
+ - Env files, tag filters, timeouts, retries, JSON and JUnit reports
22
+ - OpenAPI response/request contract checks, VCR cassettes, JSON mock server
23
+ - pytest plugin (`snapapi_run` / `@pytest.mark.snapapi`)
24
+ - VS Code syntax highlighting and diagnostics for `.sapi` files
25
+
26
+ ## Requirements
27
+
28
+ - Python 3.9 or newer
29
+
30
+ ## Installation
31
+
32
+ ```bash
33
+ git clone https://github.com/Deekshith-07/snapapi.git
34
+ cd snapapi
35
+ python3 -m venv .venv
36
+ source .venv/bin/activate # Windows: .venv\Scripts\activate
37
+ pip install -e ".[dev]"
38
+ ```
39
+
40
+ Or with the pinned runtime dependencies:
41
+
42
+ ```bash
43
+ pip install -r requirements.txt
44
+ pip install -e .
45
+ ```
46
+
47
+ ## CLI
48
+
49
+ ```bash
50
+ snapapi path/to/file.sapi
51
+ python -m snapapi path/to/file.sapi
52
+ ```
53
+
54
+ Pass multiple files or a directory of `.sapi` files:
55
+
56
+ ```bash
57
+ snapapi tests/test_suite.snaptest
58
+ snapapi tests/ suites/auth.sapi
59
+ ```
60
+
61
+ Options:
62
+
63
+ | Flag | Meaning |
64
+ | --- | --- |
65
+ | `-k "Login or not Health"` | Pytest-style keyword expression on name/description/tags |
66
+ | `-m "smoke and not slow"` | Pytest-style tag expression |
67
+ | `--tag user` | Run tests that have this tag (repeatable; all given tags must match) |
68
+ | `--exclude slow` | Skip tests with this tag (repeatable) |
69
+ | `--name "Create User"` | Run tests with this name (repeatable) |
70
+ | `--env .env` | Load `KEY=VALUE` pairs for `${VAR}`. If omitted, SnapAPI loads `<suite>.env`, then `.env` next to the file, then a single sibling `*.env` |
71
+ | `-D TOKEN=secret` | Set `${VAR}` from the CLI (repeatable) |
72
+ | `--timeout 10` | HTTP timeout in seconds (overrides `TIMEOUT`) |
73
+ | `--report json:report.json` | Write a JSON report |
74
+ | `--report junit:report.xml` | Write a JUnit XML report |
75
+ | `--report html:report.html` | Write a self-contained HTML report |
76
+ | `-x` / `--exitfirst` / `--stop-on-failure` | Stop each suite on the first failed test (off by default) |
77
+ | `--maxfail N` | Stop after N failures |
78
+ | `--collect-only` | List selected tests without HTTP |
79
+ | `-q` / `-v` | Quiet or verbose console |
80
+ | `--durations N` | Show the N slowest tests |
81
+ | `--profile stage` | Load `environments/stage.env`, `.snapapi/stage.env`, or `stage.env` |
82
+ | `--workers N` | Run independent tests in parallel (SAVE is isolated per test; sibling SAVE falls back to sequential) |
83
+ | `--grep regex` | Filter tests by name/description |
84
+ | `--lf` / `--last-failed` | Re-run failures from `.snapapi/last-run.json` (matches file + suite + name, including `Test [row]`) |
85
+ | `--ff` / `--failed-first` | Run last-failed tests first, then the rest |
86
+ | `--mode record\|replay\|record-on-miss` | VCR cassettes under `.snapapi/cassettes/` |
87
+ | `--record-on-miss` | With `--mode replay`, hit the network and save when a cassette is missing |
88
+ | `--vcr-match query,body,accept,authorization` | Cassette identity fields (default: query, content-type, accept, body) |
89
+ | `--contract-strict` | Fail when an OpenAPI path/method/schema is missing (default: skip/warn) |
90
+ | `--reruns N` | Re-run failed *tests* up to N times (distinct from `EXPECT RETRY`) |
91
+ | `--listener PATH[:Class]` | Python listener called after each test / suite (repeatable) |
92
+ | `--on-fail curl` / `--on-fail har:dir` | Emit a redacted curl or HAR on failure |
93
+ | `--safe-url` | Block private/metadata hosts |
94
+ | `--proxy URL` | HTTP/HTTPS proxy |
95
+ | `--insecure` | Skip TLS certificate verification |
96
+ | `--cert PATH` | Client certificate |
97
+ | `--cacert PATH` | CA bundle used to verify TLS |
98
+ | `snapapi lint PATH` | Parse/validate without HTTP |
99
+ | `snapapi fmt PATH` | Format `.sapi` files |
100
+ | `snapapi openapi spec.yaml` | Generate GET/POST/PUT/PATCH/DELETE smoke tests |
101
+ | `snapapi history [--failed] [--since 7d]` | Print `.snapapi/history.jsonl` |
102
+ | `snapapi mock mock.json [--port 0]` | Serve routes from a JSON mock file (prints the URL) |
103
+ | `snapapi watch PATH [--interval 0.5]` | Re-run when `.sapi` files change (poll; optional `watchdog` extra) |
104
+
105
+ The process exits `0` when every test passed, `1` when a test failed, and `2` on parse or usage errors.
106
+
107
+ ## DSL
108
+
109
+ Recommended form:
110
+
111
+ ```
112
+ SUITE: Book Store
113
+ DESC: Validates the user API
114
+ TIMEOUT: 10
115
+ URL: https://api.example.com
116
+ HEADER Content-Type: application/json
117
+
118
+ TEST: Create User
119
+ DESC: Create a user and keep the id
120
+ TAG: users write
121
+ POST: /users
122
+ AUTH: bearer ${TOKEN}
123
+ BODY: {"name": "Jane", "email": "jane@example.com"}
124
+ EXPECT: status == 201
125
+ EXPECT: body contains id
126
+ SAVE: userId FROM $.id
127
+
128
+ TEST: Get User
129
+ TAG: users
130
+ SETUP: Create User
131
+ GET: /users/${userId}
132
+ EXPECT: status == 200
133
+ EXPECT: json $.email == "jane@example.com"
134
+ EXPECT: header Content-Type contains json
135
+
136
+ TEST: List Users
137
+ GET: /users
138
+ QUERY: page=2&limit=10
139
+ PARAM: sort name
140
+ EXPECT: status == 200
141
+
142
+ TEST: Wait for ready
143
+ GET: /jobs/${id}
144
+ WAIT: json $.status == "ready" TIMEOUT 10s BACKOFF 0.5s
145
+ EXPECT: status == 200
146
+ ```
147
+
148
+ Comments are `//` lines. Indentation is cosmetic. JSON bodies may be one line or span multiple lines.
149
+
150
+ The older forms still parse:
151
+
152
+ ```
153
+ OPTIONS: {"TIMEOUT": 10}
154
+ HEADERS: {"Content-Type": "application/json"}
155
+ TAG: users, write
156
+ REQUEST: POST /users
157
+ HEADERS: {"Authorization": "Bearer ${TOKEN}"}
158
+ DATA: {"name": "Jane"}
159
+ EXPECT: STATUS 201
160
+ EXPECT: CONTAINS id
161
+ EXPECT: JSON $.email == "jane@example.com"
162
+ EXPECT: HEADER Content-Type CONTAINS json
163
+ ```
164
+
165
+ ### Keywords
166
+
167
+ - Suite: `SUITE`, `DESC`, `URL`, `TIMEOUT`, `FOLLOW-REDIRECTS`, `OPTIONS`, `IMPORT`, `SUITE-SETUP`, `SET`
168
+ - Test: `TEST`, `TAG`, `SETUP`, `TEARDOWN`, `DEPENDS`, `SKIP`, `ONLY`, `QUARANTINE`, `EXAMPLES`, `SET`
169
+ - Helper: `HELPER` (named procedure for `SUITE-SETUP` / `SETUP` / `TEARDOWN`; not a test case)
170
+ - Request: `REQUEST`, `GET`/`POST`/`PUT`/`PATCH`/`DELETE`/`HEAD`, `BODY`/`DATA`, `FILE`, `GRAPHQL`, `HEADER`/`HEADERS`, `QUERY`, `PARAM`, `AUTH`, `EXPECT`, `SAVE`, `WAIT`, `SET`
171
+
172
+ HTTP `OPTIONS` is written as `REQUEST: OPTIONS /path` so it does not collide with suite-level `OPTIONS: {...}` JSON. `HEAD: /x` is a request alias like `GET:`.
173
+
174
+ `SETUP` / `TEARDOWN` name a `HELPER` or `TEST`. Prefer `HELPER:` for procedures that should not appear as cases. A `TEST` named only as setup is still treated as a helper (legacy).
175
+
176
+ `DEPENDS: Create User` (comma-separated for several names) keeps both tests as primaries. SnapAPI reorders so named tests run first; unrelated tests keep file order. If a named test failed, skipped, or was not selected (for example `-k`), the dependent test is skipped with that reason. Cycles and unknown names are parse errors. `DEPENDS` cannot target a `HELPER`. This is not `SETUP:` — setup helpers always run first and fail the dependent test when they fail.
177
+
178
+ `IMPORT: other.sapi` pulls tests from another file (paths are relative to the current file).
179
+
180
+ `SET: orderId ${uuid()}` assigns an interpolated value (including helpers) without an HTTP call. It may appear at suite, test, or step level.
181
+
182
+ `WAIT: json $.status == "ready" TIMEOUT 10s BACKOFF 0.5s` reissues the current request until the check passes or the timeout expires. `EXPECT: json $.status == "ready" RETRY 20 BACKOFF 0.5s` also retries when the HTTP status is already 200.
183
+
184
+ `AUTH: bearer ${TOKEN}` sets `Authorization: Bearer ${TOKEN}`. Explicit `HEADER` lines still work.
185
+
186
+ `AUTH: oauth2 grant=client_credentials token_url=... client_id=...` and `grant=password username=... password=...` fetch a token (cached). If the token response includes `refresh_token`, a 401 retries once after refresh.
187
+
188
+ `AUTH: oauth2 grant=authorization_code token_url=... auth_url=... client_id=... redirect_uri=... code=${AUTH_CODE} pkce=true` exchanges an authorization code. SnapAPI does not open a browser; supply `${AUTH_CODE}` from the environment. With `pkce=true` the token request includes S256 `code_verifier` / `code_challenge` fields.
189
+
190
+ `AUTH: digest user:pass` uses `requests` HTTP Digest Auth.
191
+
192
+ `QUERY: page=2&limit=10` and `PARAM: page 2` attach query parameters to the current request (they merge with any query string already in the path).
193
+
194
+ `OPTIONS: {"OPENAPI": "spec.yaml"}` validates JSON responses (and request bodies/required params) against the matching path+method schema when present. Missing path/schema is skipped by default. Strict mode fails instead:
195
+
196
+ ```
197
+ OPTIONS: {"OPENAPI": "spec.yaml", "OPENAPI-STRICT": true}
198
+ EXPECT: openapi ./spec.yaml strict
199
+ ```
200
+
201
+ CLI `--contract-strict` is the same switch. Partial path match (`/users/{id}` vs `/users/1`) is allowed.
202
+
203
+ ### Checks
204
+
205
+ Check types are case-insensitive. Preferred:
206
+
207
+ ```
208
+ EXPECT: status == 200
209
+ EXPECT: status != 500
210
+ EXPECT: status == 200 RETRY 5 ON 5xx BACKOFF 1s
211
+ EXPECT: body contains userId
212
+ EXPECT: body not contains stack
213
+ EXPECT: json $.email matches ^.+@example\\.com$
214
+ EXPECT: json $.items length == 3
215
+ EXPECT: json $.items[*].id contains 3
216
+ EXPECT: json $.items[?(@.status=="open")].id contains 3
217
+ EXPECT: json $.tags contains-all ["a","b"]
218
+ EXPECT: json $.items each $.status == "active"
219
+ EXPECT: status == 400 OR status == 401
220
+ EXPECT: json $.success == false AND body contains error
221
+ EXPECT: (status == 400 OR status == 401) AND json $.success == false
222
+ EXPECT: schema ./schemas/user.json
223
+ EXPECT: duration < 200ms
224
+ EXPECT: header Content-Type contains json
225
+ EXPECT: openapi ./openapi.yaml
226
+ EXPECT: openapi ./openapi.yaml strict
227
+ EXPECT: xpath //Order/@id == "1"
228
+ ```
229
+
230
+ Also accepted:
231
+
232
+ ```
233
+ EXPECT: STATUS 200
234
+ EXPECT: CONTAINS userId
235
+ EXPECT: JSON $.data.email == "jane@example.com"
236
+ EXPECT: HEADER Content-Type CONTAINS json
237
+ ```
238
+
239
+ JSONPath is a small subset: `$.a.b`, `$.items.0.id`, `$.items[0].id`, `$.items[*].id`, and equality filters `$.items[?(@.status=="open")]` / `$.items[?(@.id==1)]`.
240
+
241
+ `AND` / `OR` combine checks on one line (`AND` binds tighter than `OR`; parentheses group). Quote a value if it contains those words. Multiple `EXPECT` lines on the same request still all have to pass.
242
+
243
+ XPath uses stdlib `xml.etree` (descendant tags and `/@attr`). Axes, namespaces, and functions are not implemented.
244
+
245
+ ### Variables
246
+
247
+ `${NAME}` is expanded in URLs, paths, headers, data, and expect values.
248
+
249
+ Lookup order: process environment, then `--env` / auto-discovered suite env file, then `SET` / `SAVE` values.
250
+
251
+ ## Sample suite
252
+
253
+ `tests/recommended.snaptest` shows the current DSL against a local mock server (pytest injects `BASE_URL`). `tests/test_suite.snaptest` is a classic-syntax example against [reqres.in](https://reqres.in) and needs network access. Automated tests in `tests/test_*.py` use a local mock HTTP server and do not call reqres. CI replays `tests/fixtures/offline.snaptest` from a checked-in cassette.
254
+
255
+ HTML reports include redacted request/response bodies. VCR cassette keys include method, path, and (by default) sorted query string, `Content-Type`/`Accept`, and body. `OPTIONS: {"VCR-MATCH": ["query","body","accept","authorization"]}` or `--vcr-match authorization,query` replaces that default. Replay restores `Set-Cookie` onto the session.
256
+
257
+ `snapapi mock tests/fixtures/mock.json --port 0` serves JSON routes. Routes may use path templates (`/users/{id}`), optional `match.query` / `match.body` subsets, and `delay_ms`. Exact paths win over templates. There is no language server, gRPC, or WebSocket support.
258
+
259
+ ### pytest plugin
260
+
261
+ Install with `pip install -e ".[dev]"`. Then:
262
+
263
+ ```python
264
+ def test_suite(snapapi_run):
265
+ result = snapapi_run("tests/foo.sapi")
266
+ assert result.ok
267
+
268
+ @pytest.mark.snapapi("tests/foo.sapi")
269
+ def test_marked(snapapi_run, request):
270
+ snapapi_run(request.node.get_closest_marker("snapapi").args[0])
271
+ ```
272
+
273
+ `snapapi_run(path, **engine_kwargs)` returns `SuiteResult` and fails the pytest case when the suite is not ok.
274
+
275
+ ## Project layout
276
+
277
+ ```
278
+ snapapi/
279
+ ├── snapapi/ # Python package
280
+ │ ├── parser.py # .sapi DSL parser
281
+ │ ├── engine.py # runner, checks, setup/teardown
282
+ │ ├── api_client.py # requests wrapper
283
+ │ └── cli.py # snapapi command
284
+ ├── tests/ # pytest + sample .sapi / .snaptest suites
285
+ ├── docs/ # User guide + in-browser playground
286
+ ├── snapapi-language/ # VS Code grammar / run command
287
+ ├── pyproject.toml
288
+ └── README.md
289
+ ```
290
+
291
+ ## Running the tests
292
+
293
+ ```bash
294
+ pytest
295
+ ```
296
+
297
+ ## VS Code
298
+
299
+ The `snapapi-language` extension is a language pack for `.sapi` files: syntax highlighting, snippets, completions, lightweight diagnostics (unknown keywords, unknown SETUP names, `HEAD:` / `REQUEST: OPTIONS`), and **SnapAPI: Run current file** / **Run test at cursor**. See [snapapi-language/README.md](snapapi-language/README.md). There is no separate language-server process.
300
+
301
+ ## License
302
+
303
+ MIT — see [LICENSE](LICENSE).