bulk-post 0.1.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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Valerii Kirichenko
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,298 @@
1
+ Metadata-Version: 2.4
2
+ Name: bulk-post
3
+ Version: 0.1.0
4
+ Summary: A near-stdlib Python CLI that fires templated HTTP requests driven by CSV data, one request (or a multi-step workflow) per row.
5
+ Author-email: Valerii Kirichenko <true.monte.kristo@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/true-monte-kristo/bulk-post
8
+ Project-URL: Repository, https://github.com/true-monte-kristo/bulk-post
9
+ Project-URL: Issues, https://github.com/true-monte-kristo/bulk-post/issues
10
+ Keywords: http,csv,cli,bulk,requests,templating,workflow
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3 :: Only
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Internet :: WWW/HTTP
19
+ Classifier: Topic :: Utilities
20
+ Requires-Python: >=3.12
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Requires-Dist: jsonpath-ng>=1.8.0
24
+ Requires-Dist: pyyaml
25
+ Dynamic: license-file
26
+
27
+ # bulk-post
28
+
29
+ [![CI](https://github.com/true-monte-kristo/bulk-post/actions/workflows/ci.yml/badge.svg)](https://github.com/true-monte-kristo/bulk-post/actions/workflows/ci.yml)
30
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
31
+ [![Python](https://img.shields.io/badge/python-3.12%2B-blue.svg)](https://www.python.org/downloads/)
32
+ [![Security: report a vulnerability](https://img.shields.io/badge/security-report%20a%20vulnerability-red.svg)](https://github.com/true-monte-kristo/bulk-post/security/advisories/new)
33
+
34
+ A near-stdlib Python CLI that fires templated HTTP requests driven by CSV data. You define the request — URL, method, body, headers — with `{{placeholder}}` slots, and each CSV row supplies the values that fill them: one request per row, or a multi-step request workflow per row in `--workflow` mode. Supports bearer or basic auth (default: no auth) with automatic 401 re-prompt, a live terminal UI with pause/resume, parallel execution, and a retry file for failed rows. Third-party dependencies: PyYAML and jsonpath-ng — both always installed, but lazily imported (PyYAML only on the `--workflow` code path, jsonpath-ng only on the workflow-variables code path).
35
+
36
+ ## Requirements
37
+
38
+ - Python 3.12+
39
+ - [PyYAML](https://pypi.org/project/PyYAML/) — runtime dependency; lazily imported, only exercised in `--workflow` mode
40
+ - [jsonpath-ng](https://pypi.org/project/jsonpath-ng/) — runtime dependency; lazily imported, only exercised when a workflow declares variables
41
+
42
+ ## Installation
43
+
44
+ Install globally with [uv](https://docs.astral.sh/uv/):
45
+
46
+ ```bash
47
+ uv tool install .
48
+ bulk-post --help
49
+ ```
50
+
51
+ After changing the code, re-install with:
52
+
53
+ ```bash
54
+ uv tool install . --reinstall
55
+ ```
56
+
57
+ Or run directly without installing (from the repo root):
58
+
59
+ ```bash
60
+ python -m bulk_post --help # ensure pyyaml and jsonpath-ng are installed (uv run handles this)
61
+ ```
62
+
63
+ ## Usage
64
+
65
+ ```
66
+ bulk-post -u <url-template> -c <csv-file> [options]
67
+ ```
68
+
69
+ ### Examples
70
+
71
+ Cancel every invoice in a CSV using DELETE:
72
+
73
+ ```bash
74
+ bulk-post \
75
+ -u "https://api.example.com/invoices/{{id}}/cancel" \
76
+ -c invoices.csv \
77
+ -m DELETE
78
+ ```
79
+
80
+ PATCH with a JSON body, 200 ms between requests, verbose output:
81
+
82
+ ```bash
83
+ bulk-post \
84
+ -u "https://api.example.com/invoices/{{id}}/status" \
85
+ -c invoices.csv \
86
+ -m PATCH \
87
+ -b '{"status": "cancelled", "reason": "{{reason}}"}' \
88
+ -d 200 \
89
+ -v
90
+ ```
91
+
92
+ POST form-encoded data:
93
+
94
+ ```bash
95
+ bulk-post \
96
+ -u "https://api.example.com/items" \
97
+ -c items.csv \
98
+ -m POST \
99
+ -b "id={{id}}&status={{status}}" \
100
+ -C "application/x-www-form-urlencoded"
101
+ ```
102
+
103
+ Resume after a failure at row 47:
104
+
105
+ ```bash
106
+ bulk-post -u "https://api.example.com/items/{{id}}" -c items.csv -o 47
107
+ ```
108
+
109
+ ## CLI flags
110
+
111
+ | Flag | Short | Default | Description |
112
+ |------|-------|---------|-------------|
113
+ | `--url` | `-u` | required* | URL template; `{{col}}` is replaced with the value from that CSV column. *Provide either `--url` or `--workflow` (mutually exclusive) |
114
+ | `--csv` | `-c` | required | Path to the input CSV file |
115
+ | `--method` | `-m` | `POST` | HTTP method (`GET`, `POST`, `PUT`, `PATCH`, `DELETE`, …) |
116
+ | `--body` | `-b` | — | Request body; supports `{{col}}` placeholders |
117
+ | `--content-type` | `-C` | `application/json` | `Content-Type` header sent with the request body; ignored when no body is provided. When set to a JSON or XML type, the body template is validated once at startup before any requests are sent — the script exits immediately with an error if the template is structurally invalid |
118
+ | `--auth-type` | `-a` | `none` | Auth method: `bearer`, `basic`, or `none` |
119
+ | `--token` | `-t` | — | Bearer token; used with `--auth-type bearer` (see [Auth](#auth) below) |
120
+ | `--user` | `-U` | — | Basic auth credentials as `user:pass`; used with `--auth-type basic` (see [Auth](#auth) below) |
121
+ | `--delay` | `-d` | `0` | Milliseconds to wait between requests |
122
+ | `--offset` | `-o` | `0` | Skip the first N data rows (useful for resuming after a failure) |
123
+ | `--timeout` | `-T` | `30` | Per-request timeout in seconds |
124
+ | `--retry-file` | `-r` | `<stem>_failed.csv` | Where to write rows that failed; auto-named from the CSV path if omitted |
125
+ | `--verbose` | `-v` | false | Print URL, request/response headers (Authorization masked), body, status, and timing for every row |
126
+ | `--header` | `-H` | — | Add a custom request header in `Name: value` format; repeatable. Values support `{{col}}` placeholders |
127
+ | `--parallel` | `-p` | false | Process rows concurrently using multiple threads; `--delay` is ignored in this mode |
128
+ | `--concurrency-level` | `-n` | CPU count | Number of worker threads; only used with `--parallel` |
129
+ | `--debug` | `-D` | false | Print worker thread name on each row log line and show a live debug bar with queue depth, active thread count, and ok/fail counters; only meaningful with `--parallel` |
130
+ | `--workflow` | `-w` | — | Path to a workflow YAML file; mutually exclusive with `--url` |
131
+ | `--version` | `-V` | — | Print version and exit |
132
+
133
+ ## CSV format
134
+
135
+ The CSV must have a header row. Column names are used as placeholder names in `--url`, `--body`, and `--header` values. Every `{{placeholder}}` in the URL, body, or header values must match a column name; the script exits with an error if any are missing.
136
+
137
+ ```csv
138
+ id,reason
139
+ 1001,duplicate
140
+ 1002,customer_request
141
+ ```
142
+
143
+ ## Auth
144
+
145
+ Select the auth method with `--auth-type` / `-a` (default: `none`):
146
+
147
+ ### Bearer token
148
+
149
+ Pass `--auth-type bearer` (or `-a bearer`). Token resolution order: `--token` / `-t` flag → `BULK_TOKEN` env var → interactive prompt at startup.
150
+
151
+ If the server returns **401** mid-run, the script pauses, prompts for a fresh token, and retries the failed row automatically. This is handy when tokens are short-lived SSO bearer tokens that must be copied manually (e.g. from browser DevTools) and cannot be fetched programmatically.
152
+
153
+ ### Basic auth
154
+
155
+ Pass `--auth-type basic` (or `-a basic`). Credentials (`user:pass`) are resolved in the same order: `--user` / `-U` flag → `BULK_USER` env var → interactive prompt. On **401**, the script prompts for new credentials and retries.
156
+
157
+ ### No auth (default)
158
+
159
+ The default when `--auth-type` is omitted (or pass `--auth-type none` / `-a none` explicitly). No `Authorization` header is sent.
160
+
161
+ ## Terminal UI
162
+
163
+ When running in an interactive terminal, a live bottom bar shows:
164
+
165
+ - **Progress bar** — `current / total` rows with a visual fill bar
166
+ - **Command input** — type a command and press Enter; Tab autocompletes
167
+
168
+ Available commands:
169
+
170
+ | Command | Effect |
171
+ |---------|--------|
172
+ | `/pause` | Pause sending; script waits until you resume |
173
+ | `/resume` | Resume after a pause |
174
+ | `/exit` | Stop after the current row and print a summary |
175
+
176
+ In non-TTY mode (piped input, CI, test environments) the bottom bar is skipped and no interactive commands are available.
177
+
178
+ ## Retry file
179
+
180
+ Rows that fail (network error, non-2xx response, or substitution error) are written to the retry file. By default this is `<csv-stem>_failed.csv` next to the input file. Re-run with `-c <stem>_failed.csv` to retry only those rows.
181
+
182
+ If no rows fail, the retry file is deleted automatically.
183
+
184
+ ## Workflow mode
185
+
186
+ Instead of a single `--url` template, you can define a multi-step workflow in a YAML file and run it with `--workflow` / `-w`.
187
+
188
+ Each CSV row fires all steps in document order. Steps within a row are always sequential; `--parallel` controls per-row concurrency across rows.
189
+
190
+ ### Workflow YAML format
191
+
192
+ ```yaml
193
+ workflow:
194
+ description: Optional human-readable description # skipped at runtime
195
+
196
+ groupA: # logical grouping for shared auth
197
+ auth:
198
+ type: bearer # bearer | basic | none
199
+ token: some_token # optional — prompted if omitted
200
+ endpoints:
201
+ - step-name: # user-chosen name; unique within the group
202
+ url: https://api.example.com/{{id}}
203
+ method: POST # default POST
204
+ headers:
205
+ Content-Type: application/json
206
+ X-Custom: value
207
+ body: '{"key": "{{col}}"}'
208
+ on_error: stop # stop (default) | continue
209
+ auth: # step-level auth overrides group auth
210
+ type: bearer
211
+ token: override_token
212
+
213
+ groupB:
214
+ auth:
215
+ type: basic
216
+ user: alice
217
+ password: secret
218
+ endpoints:
219
+ - another-step:
220
+ url: https://other.example.com/{{id}}
221
+ method: DELETE
222
+
223
+ groupC: # no auth
224
+ endpoints:
225
+ - no-auth-step:
226
+ url: https://public.example.com/{{id}}
227
+ method: GET
228
+ ```
229
+
230
+ Key rules:
231
+
232
+ - **Execution order** — steps fire in the order they appear in the document (top to bottom across all groups).
233
+ - **Group auth** — all steps in a group inherit the group's auth unless they declare their own.
234
+ - **`on_error`** — `stop` (default) halts remaining steps for that row and writes it to the retry file; `continue` logs the failure, writes the row, and proceeds to the next step.
235
+ - **Placeholders** — `{{col}}` in `url`, `body`, and header values is replaced with the matching CSV column value, same as in single-URL mode.
236
+
237
+ ### Retry and resume
238
+
239
+ When a step fails, the row is written to the retry file with an extra column `_bulk_post_step` set to the path of the first failed step (e.g. `groupA/step-name`). Re-running with that retry CSV skips all steps before the failed one, resuming mid-workflow automatically.
240
+
241
+ ### Workflow variables
242
+
243
+ A step can capture a value from an earlier step's JSON response and pass it as a `{{$name}}` placeholder into that step's `url`, `headers`, or `body`. This lets you chain steps — for example, create a resource in step 1 and use the returned `id` in the URL of step 2.
244
+
245
+ Variables are declared under a `variables:` key at group level (inherited by all endpoints in the group) and/or at endpoint level (overrides the group on name conflict):
246
+
247
+ ```yaml
248
+ workflow:
249
+ groupA:
250
+ auth:
251
+ type: bearer
252
+ endpoints:
253
+ - create-item:
254
+ url: https://api.example.com/items
255
+ method: POST
256
+ body: '{"name": "{{name}}"}'
257
+
258
+ groupB:
259
+ endpoints:
260
+ - delete-item:
261
+ # Capture the id from create-item's response at endpoint level
262
+ variables:
263
+ $id:
264
+ source: .workflow.groupA.create-item # leading dot and "workflow." prefix are optional
265
+ jsonPath: $.id # JSONPath; first match is used
266
+ nullable: false # fail this step if id is missing
267
+ url: https://api.example.com/items/{{$id}}
268
+ method: DELETE
269
+ ```
270
+
271
+ **Variable rules:**
272
+
273
+ - Names must start with `$` (e.g. `$id`) and are referenced as `{{$id}}` in URL, headers, and body.
274
+ - `source` is written as `.workflow.<group>.<endpoint>` (or just `<group>/<endpoint>`). It must refer to an endpoint that runs before the current step — forward and self references are rejected at startup.
275
+ - `jsonPath` uses full JSONPath syntax (powered by [`jsonpath-ng`](https://pypi.org/project/jsonpath-ng/)). Only the first match is used. A match that is an object or array (non-scalar) fails the step.
276
+ - `nullable` defaults to `true`. When `false`, a null value or no-match fails the step (row written to retry file); when `true`, it resolves to an empty string.
277
+ - Variable values are scoped to a single CSV row and are never shared across rows.
278
+ - All variable declarations are validated at startup — undefined references, bad names, unreachable sources, and invalid JSONPath expressions all cause an immediate exit with a clear error.
279
+
280
+ **Resume/retry with variables:**
281
+
282
+ When a row fails, any resolved variable values are persisted into reserved retry-CSV columns named `_bulk_post_var/<source_path>/<name>`. Re-running the retry CSV skips completed steps and reads these persisted values for variables whose source step was skipped.
283
+
284
+ > **Security note:** retry CSVs may contain response-derived data (potentially sensitive) in plaintext. Do not share or commit retry CSVs that were produced from workflows using variables.
285
+
286
+ ### Example
287
+
288
+ ```bash
289
+ bulk-post -w workflow.yaml -c rows.csv
290
+ ```
291
+
292
+ ## Running tests
293
+
294
+ ```bash
295
+ uv run python -m unittest discover tests/
296
+ ```
297
+
298
+ Tests use stdlib `unittest`. PyYAML and jsonpath-ng are runtime dependencies (always installed); the workflow-parsing cases use PyYAML and the workflow-variable cases use jsonpath-ng. `uv run` provides both from `uv.lock` automatically; if you run `python -m unittest` directly, do so inside a virtualenv that has both packages.
@@ -0,0 +1,272 @@
1
+ # bulk-post
2
+
3
+ [![CI](https://github.com/true-monte-kristo/bulk-post/actions/workflows/ci.yml/badge.svg)](https://github.com/true-monte-kristo/bulk-post/actions/workflows/ci.yml)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
5
+ [![Python](https://img.shields.io/badge/python-3.12%2B-blue.svg)](https://www.python.org/downloads/)
6
+ [![Security: report a vulnerability](https://img.shields.io/badge/security-report%20a%20vulnerability-red.svg)](https://github.com/true-monte-kristo/bulk-post/security/advisories/new)
7
+
8
+ A near-stdlib Python CLI that fires templated HTTP requests driven by CSV data. You define the request — URL, method, body, headers — with `{{placeholder}}` slots, and each CSV row supplies the values that fill them: one request per row, or a multi-step request workflow per row in `--workflow` mode. Supports bearer or basic auth (default: no auth) with automatic 401 re-prompt, a live terminal UI with pause/resume, parallel execution, and a retry file for failed rows. Third-party dependencies: PyYAML and jsonpath-ng — both always installed, but lazily imported (PyYAML only on the `--workflow` code path, jsonpath-ng only on the workflow-variables code path).
9
+
10
+ ## Requirements
11
+
12
+ - Python 3.12+
13
+ - [PyYAML](https://pypi.org/project/PyYAML/) — runtime dependency; lazily imported, only exercised in `--workflow` mode
14
+ - [jsonpath-ng](https://pypi.org/project/jsonpath-ng/) — runtime dependency; lazily imported, only exercised when a workflow declares variables
15
+
16
+ ## Installation
17
+
18
+ Install globally with [uv](https://docs.astral.sh/uv/):
19
+
20
+ ```bash
21
+ uv tool install .
22
+ bulk-post --help
23
+ ```
24
+
25
+ After changing the code, re-install with:
26
+
27
+ ```bash
28
+ uv tool install . --reinstall
29
+ ```
30
+
31
+ Or run directly without installing (from the repo root):
32
+
33
+ ```bash
34
+ python -m bulk_post --help # ensure pyyaml and jsonpath-ng are installed (uv run handles this)
35
+ ```
36
+
37
+ ## Usage
38
+
39
+ ```
40
+ bulk-post -u <url-template> -c <csv-file> [options]
41
+ ```
42
+
43
+ ### Examples
44
+
45
+ Cancel every invoice in a CSV using DELETE:
46
+
47
+ ```bash
48
+ bulk-post \
49
+ -u "https://api.example.com/invoices/{{id}}/cancel" \
50
+ -c invoices.csv \
51
+ -m DELETE
52
+ ```
53
+
54
+ PATCH with a JSON body, 200 ms between requests, verbose output:
55
+
56
+ ```bash
57
+ bulk-post \
58
+ -u "https://api.example.com/invoices/{{id}}/status" \
59
+ -c invoices.csv \
60
+ -m PATCH \
61
+ -b '{"status": "cancelled", "reason": "{{reason}}"}' \
62
+ -d 200 \
63
+ -v
64
+ ```
65
+
66
+ POST form-encoded data:
67
+
68
+ ```bash
69
+ bulk-post \
70
+ -u "https://api.example.com/items" \
71
+ -c items.csv \
72
+ -m POST \
73
+ -b "id={{id}}&status={{status}}" \
74
+ -C "application/x-www-form-urlencoded"
75
+ ```
76
+
77
+ Resume after a failure at row 47:
78
+
79
+ ```bash
80
+ bulk-post -u "https://api.example.com/items/{{id}}" -c items.csv -o 47
81
+ ```
82
+
83
+ ## CLI flags
84
+
85
+ | Flag | Short | Default | Description |
86
+ |------|-------|---------|-------------|
87
+ | `--url` | `-u` | required* | URL template; `{{col}}` is replaced with the value from that CSV column. *Provide either `--url` or `--workflow` (mutually exclusive) |
88
+ | `--csv` | `-c` | required | Path to the input CSV file |
89
+ | `--method` | `-m` | `POST` | HTTP method (`GET`, `POST`, `PUT`, `PATCH`, `DELETE`, …) |
90
+ | `--body` | `-b` | — | Request body; supports `{{col}}` placeholders |
91
+ | `--content-type` | `-C` | `application/json` | `Content-Type` header sent with the request body; ignored when no body is provided. When set to a JSON or XML type, the body template is validated once at startup before any requests are sent — the script exits immediately with an error if the template is structurally invalid |
92
+ | `--auth-type` | `-a` | `none` | Auth method: `bearer`, `basic`, or `none` |
93
+ | `--token` | `-t` | — | Bearer token; used with `--auth-type bearer` (see [Auth](#auth) below) |
94
+ | `--user` | `-U` | — | Basic auth credentials as `user:pass`; used with `--auth-type basic` (see [Auth](#auth) below) |
95
+ | `--delay` | `-d` | `0` | Milliseconds to wait between requests |
96
+ | `--offset` | `-o` | `0` | Skip the first N data rows (useful for resuming after a failure) |
97
+ | `--timeout` | `-T` | `30` | Per-request timeout in seconds |
98
+ | `--retry-file` | `-r` | `<stem>_failed.csv` | Where to write rows that failed; auto-named from the CSV path if omitted |
99
+ | `--verbose` | `-v` | false | Print URL, request/response headers (Authorization masked), body, status, and timing for every row |
100
+ | `--header` | `-H` | — | Add a custom request header in `Name: value` format; repeatable. Values support `{{col}}` placeholders |
101
+ | `--parallel` | `-p` | false | Process rows concurrently using multiple threads; `--delay` is ignored in this mode |
102
+ | `--concurrency-level` | `-n` | CPU count | Number of worker threads; only used with `--parallel` |
103
+ | `--debug` | `-D` | false | Print worker thread name on each row log line and show a live debug bar with queue depth, active thread count, and ok/fail counters; only meaningful with `--parallel` |
104
+ | `--workflow` | `-w` | — | Path to a workflow YAML file; mutually exclusive with `--url` |
105
+ | `--version` | `-V` | — | Print version and exit |
106
+
107
+ ## CSV format
108
+
109
+ The CSV must have a header row. Column names are used as placeholder names in `--url`, `--body`, and `--header` values. Every `{{placeholder}}` in the URL, body, or header values must match a column name; the script exits with an error if any are missing.
110
+
111
+ ```csv
112
+ id,reason
113
+ 1001,duplicate
114
+ 1002,customer_request
115
+ ```
116
+
117
+ ## Auth
118
+
119
+ Select the auth method with `--auth-type` / `-a` (default: `none`):
120
+
121
+ ### Bearer token
122
+
123
+ Pass `--auth-type bearer` (or `-a bearer`). Token resolution order: `--token` / `-t` flag → `BULK_TOKEN` env var → interactive prompt at startup.
124
+
125
+ If the server returns **401** mid-run, the script pauses, prompts for a fresh token, and retries the failed row automatically. This is handy when tokens are short-lived SSO bearer tokens that must be copied manually (e.g. from browser DevTools) and cannot be fetched programmatically.
126
+
127
+ ### Basic auth
128
+
129
+ Pass `--auth-type basic` (or `-a basic`). Credentials (`user:pass`) are resolved in the same order: `--user` / `-U` flag → `BULK_USER` env var → interactive prompt. On **401**, the script prompts for new credentials and retries.
130
+
131
+ ### No auth (default)
132
+
133
+ The default when `--auth-type` is omitted (or pass `--auth-type none` / `-a none` explicitly). No `Authorization` header is sent.
134
+
135
+ ## Terminal UI
136
+
137
+ When running in an interactive terminal, a live bottom bar shows:
138
+
139
+ - **Progress bar** — `current / total` rows with a visual fill bar
140
+ - **Command input** — type a command and press Enter; Tab autocompletes
141
+
142
+ Available commands:
143
+
144
+ | Command | Effect |
145
+ |---------|--------|
146
+ | `/pause` | Pause sending; script waits until you resume |
147
+ | `/resume` | Resume after a pause |
148
+ | `/exit` | Stop after the current row and print a summary |
149
+
150
+ In non-TTY mode (piped input, CI, test environments) the bottom bar is skipped and no interactive commands are available.
151
+
152
+ ## Retry file
153
+
154
+ Rows that fail (network error, non-2xx response, or substitution error) are written to the retry file. By default this is `<csv-stem>_failed.csv` next to the input file. Re-run with `-c <stem>_failed.csv` to retry only those rows.
155
+
156
+ If no rows fail, the retry file is deleted automatically.
157
+
158
+ ## Workflow mode
159
+
160
+ Instead of a single `--url` template, you can define a multi-step workflow in a YAML file and run it with `--workflow` / `-w`.
161
+
162
+ Each CSV row fires all steps in document order. Steps within a row are always sequential; `--parallel` controls per-row concurrency across rows.
163
+
164
+ ### Workflow YAML format
165
+
166
+ ```yaml
167
+ workflow:
168
+ description: Optional human-readable description # skipped at runtime
169
+
170
+ groupA: # logical grouping for shared auth
171
+ auth:
172
+ type: bearer # bearer | basic | none
173
+ token: some_token # optional — prompted if omitted
174
+ endpoints:
175
+ - step-name: # user-chosen name; unique within the group
176
+ url: https://api.example.com/{{id}}
177
+ method: POST # default POST
178
+ headers:
179
+ Content-Type: application/json
180
+ X-Custom: value
181
+ body: '{"key": "{{col}}"}'
182
+ on_error: stop # stop (default) | continue
183
+ auth: # step-level auth overrides group auth
184
+ type: bearer
185
+ token: override_token
186
+
187
+ groupB:
188
+ auth:
189
+ type: basic
190
+ user: alice
191
+ password: secret
192
+ endpoints:
193
+ - another-step:
194
+ url: https://other.example.com/{{id}}
195
+ method: DELETE
196
+
197
+ groupC: # no auth
198
+ endpoints:
199
+ - no-auth-step:
200
+ url: https://public.example.com/{{id}}
201
+ method: GET
202
+ ```
203
+
204
+ Key rules:
205
+
206
+ - **Execution order** — steps fire in the order they appear in the document (top to bottom across all groups).
207
+ - **Group auth** — all steps in a group inherit the group's auth unless they declare their own.
208
+ - **`on_error`** — `stop` (default) halts remaining steps for that row and writes it to the retry file; `continue` logs the failure, writes the row, and proceeds to the next step.
209
+ - **Placeholders** — `{{col}}` in `url`, `body`, and header values is replaced with the matching CSV column value, same as in single-URL mode.
210
+
211
+ ### Retry and resume
212
+
213
+ When a step fails, the row is written to the retry file with an extra column `_bulk_post_step` set to the path of the first failed step (e.g. `groupA/step-name`). Re-running with that retry CSV skips all steps before the failed one, resuming mid-workflow automatically.
214
+
215
+ ### Workflow variables
216
+
217
+ A step can capture a value from an earlier step's JSON response and pass it as a `{{$name}}` placeholder into that step's `url`, `headers`, or `body`. This lets you chain steps — for example, create a resource in step 1 and use the returned `id` in the URL of step 2.
218
+
219
+ Variables are declared under a `variables:` key at group level (inherited by all endpoints in the group) and/or at endpoint level (overrides the group on name conflict):
220
+
221
+ ```yaml
222
+ workflow:
223
+ groupA:
224
+ auth:
225
+ type: bearer
226
+ endpoints:
227
+ - create-item:
228
+ url: https://api.example.com/items
229
+ method: POST
230
+ body: '{"name": "{{name}}"}'
231
+
232
+ groupB:
233
+ endpoints:
234
+ - delete-item:
235
+ # Capture the id from create-item's response at endpoint level
236
+ variables:
237
+ $id:
238
+ source: .workflow.groupA.create-item # leading dot and "workflow." prefix are optional
239
+ jsonPath: $.id # JSONPath; first match is used
240
+ nullable: false # fail this step if id is missing
241
+ url: https://api.example.com/items/{{$id}}
242
+ method: DELETE
243
+ ```
244
+
245
+ **Variable rules:**
246
+
247
+ - Names must start with `$` (e.g. `$id`) and are referenced as `{{$id}}` in URL, headers, and body.
248
+ - `source` is written as `.workflow.<group>.<endpoint>` (or just `<group>/<endpoint>`). It must refer to an endpoint that runs before the current step — forward and self references are rejected at startup.
249
+ - `jsonPath` uses full JSONPath syntax (powered by [`jsonpath-ng`](https://pypi.org/project/jsonpath-ng/)). Only the first match is used. A match that is an object or array (non-scalar) fails the step.
250
+ - `nullable` defaults to `true`. When `false`, a null value or no-match fails the step (row written to retry file); when `true`, it resolves to an empty string.
251
+ - Variable values are scoped to a single CSV row and are never shared across rows.
252
+ - All variable declarations are validated at startup — undefined references, bad names, unreachable sources, and invalid JSONPath expressions all cause an immediate exit with a clear error.
253
+
254
+ **Resume/retry with variables:**
255
+
256
+ When a row fails, any resolved variable values are persisted into reserved retry-CSV columns named `_bulk_post_var/<source_path>/<name>`. Re-running the retry CSV skips completed steps and reads these persisted values for variables whose source step was skipped.
257
+
258
+ > **Security note:** retry CSVs may contain response-derived data (potentially sensitive) in plaintext. Do not share or commit retry CSVs that were produced from workflows using variables.
259
+
260
+ ### Example
261
+
262
+ ```bash
263
+ bulk-post -w workflow.yaml -c rows.csv
264
+ ```
265
+
266
+ ## Running tests
267
+
268
+ ```bash
269
+ uv run python -m unittest discover tests/
270
+ ```
271
+
272
+ Tests use stdlib `unittest`. PyYAML and jsonpath-ng are runtime dependencies (always installed); the workflow-parsing cases use PyYAML and the workflow-variable cases use jsonpath-ng. `uv run` provides both from `uv.lock` automatically; if you run `python -m unittest` directly, do so inside a virtualenv that has both packages.
@@ -0,0 +1,67 @@
1
+ [project]
2
+ name = "bulk-post"
3
+ version = "0.1.0"
4
+ description = "A near-stdlib Python CLI that fires templated HTTP requests driven by CSV data, one request (or a multi-step workflow) per row."
5
+ readme = "README.md"
6
+ requires-python = ">=3.12"
7
+ license = "MIT"
8
+ license-files = ["LICENSE"]
9
+ authors = [{ name = "Valerii Kirichenko", email = "true.monte.kristo@gmail.com" }]
10
+ keywords = ["http", "csv", "cli", "bulk", "requests", "templating", "workflow"]
11
+ classifiers = [
12
+ "Development Status :: 4 - Beta",
13
+ "Environment :: Console",
14
+ "Intended Audience :: Developers",
15
+ "Operating System :: OS Independent",
16
+ "Programming Language :: Python :: 3 :: Only",
17
+ "Programming Language :: Python :: 3.12",
18
+ "Programming Language :: Python :: 3.13",
19
+ "Topic :: Internet :: WWW/HTTP",
20
+ "Topic :: Utilities",
21
+ ]
22
+ dependencies = [
23
+ "jsonpath-ng>=1.8.0",
24
+ "pyyaml",
25
+ ]
26
+
27
+ [project.urls]
28
+ Homepage = "https://github.com/true-monte-kristo/bulk-post"
29
+ Repository = "https://github.com/true-monte-kristo/bulk-post"
30
+ Issues = "https://github.com/true-monte-kristo/bulk-post/issues"
31
+
32
+ [project.scripts]
33
+ bulk-post = "bulk_post:main"
34
+
35
+ [build-system]
36
+ requires = ["setuptools"]
37
+ build-backend = "setuptools.build_meta"
38
+
39
+ [tool.setuptools.packages.find]
40
+ where = ["src"]
41
+
42
+ [dependency-groups]
43
+ dev = [
44
+ "mypy>=2.1.0",
45
+ "ruff>=0.15.17",
46
+ "types-pyyaml>=6.0.12.20260518",
47
+ ]
48
+
49
+ [tool.ruff]
50
+ line-length = 88
51
+ target-version = "py312"
52
+
53
+ [tool.ruff.lint]
54
+ select = ["E", "F", "I", "UP", "B", "SIM", "C4"]
55
+ ignore = ["E501"] # long help/URL string literals are fine; the formatter governs code layout
56
+
57
+ [tool.mypy]
58
+ python_version = "3.12"
59
+ warn_unused_ignores = true
60
+ warn_return_any = true
61
+ warn_redundant_casts = true
62
+ # Pragmatic start: do NOT enable disallow_untyped_defs / strict yet.
63
+ # Tighten in a later pass once the package split lands.
64
+
65
+ [[tool.mypy.overrides]]
66
+ module = ["jsonpath_ng.*"]
67
+ ignore_missing_imports = true
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+