bulk-post 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.
bulk_post/__init__.py ADDED
@@ -0,0 +1,238 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Bulk HTTP request runner — fires templated requests whose {{placeholder}} slots are
4
+ filled from each CSV row: one request per row, or a multi-step workflow per row in
5
+ --workflow mode.
6
+
7
+ Usage:
8
+ python -m bulk_post \
9
+ -u "https://example.com/api/invoices/{{id}}/cancel" \
10
+ -c rows.csv \
11
+ -m DELETE \
12
+ -d 200
13
+
14
+ python -m bulk_post \
15
+ -u "https://example.com/api/invoices/{{id}}/status" \
16
+ -c rows.csv \
17
+ -m PATCH \
18
+ -b '{"status": "cancelled"}'
19
+
20
+ Token resolution order: --token/-t flag → BULK_TOKEN env var → interactive prompt.
21
+ If the token expires mid-run (401), the script pauses and asks for a new one.
22
+
23
+ The CSV header must contain columns matching every {{variable}} in the URL.
24
+ Failed rows are logged and skipped; execution always continues.
25
+ """
26
+
27
+ from .auth import (
28
+ _make_auth_refresh_fn as _make_auth_refresh_fn,
29
+ )
30
+ from .auth import (
31
+ prompt_new_basic_creds as prompt_new_basic_creds,
32
+ )
33
+ from .auth import (
34
+ prompt_new_token as prompt_new_token,
35
+ )
36
+ from .auth import (
37
+ resolve_auth_header as resolve_auth_header,
38
+ )
39
+ from .auth import (
40
+ resolve_basic_creds as resolve_basic_creds,
41
+ )
42
+ from .auth import (
43
+ resolve_token as resolve_token,
44
+ )
45
+ from .cli import (
46
+ _get_version as _get_version,
47
+ )
48
+ from .cli import (
49
+ _run as _run,
50
+ )
51
+ from .cli import (
52
+ build_parser as build_parser,
53
+ )
54
+ from .cli import (
55
+ main as main,
56
+ )
57
+ from .csvio import (
58
+ _open_log_file as _open_log_file,
59
+ )
60
+ from .csvio import (
61
+ _open_retry_writer as _open_retry_writer,
62
+ )
63
+ from .csvio import (
64
+ _skip_rows as _skip_rows,
65
+ )
66
+ from .csvio import (
67
+ _write_failure_log as _write_failure_log,
68
+ )
69
+ from .csvio import (
70
+ count_csv_rows as count_csv_rows,
71
+ )
72
+ from .http import _mask_headers as _mask_headers
73
+ from .http import http_request as http_request
74
+ from .runner import (
75
+ _fire as _fire,
76
+ )
77
+ from .runner import (
78
+ _handle_cmd_in_loop as _handle_cmd_in_loop,
79
+ )
80
+ from .runner import (
81
+ _log_row as _log_row,
82
+ )
83
+ from .runner import (
84
+ _parallel_worker as _parallel_worker,
85
+ )
86
+ from .runner import (
87
+ _run_loop as _run_loop,
88
+ )
89
+ from .runner import (
90
+ _run_parallel as _run_parallel,
91
+ )
92
+ from .runner import (
93
+ _run_parallel_main_loop as _run_parallel_main_loop,
94
+ )
95
+ from .state import _ParallelState as _ParallelState
96
+ from .state import _WorkflowParallelState as _WorkflowParallelState
97
+ from .templating import (
98
+ PLACEHOLDER_RE as PLACEHOLDER_RE,
99
+ )
100
+ from .templating import (
101
+ VAR_RE as VAR_RE,
102
+ )
103
+ from .templating import (
104
+ _validate_body_template as _validate_body_template,
105
+ )
106
+ from .templating import (
107
+ _validate_placeholders as _validate_placeholders,
108
+ )
109
+ from .templating import (
110
+ render_template as render_template,
111
+ )
112
+ from .templating import (
113
+ substitute as substitute,
114
+ )
115
+ from .templating import (
116
+ substitute_vars as substitute_vars,
117
+ )
118
+ from .terminal import (
119
+ _CMD_EXIT as _CMD_EXIT,
120
+ )
121
+ from .terminal import (
122
+ _CMD_PAUSE as _CMD_PAUSE,
123
+ )
124
+ from .terminal import (
125
+ _CMD_PROMPT as _CMD_PROMPT,
126
+ )
127
+ from .terminal import (
128
+ _CMD_RESUME as _CMD_RESUME,
129
+ )
130
+ from .terminal import (
131
+ _COMMANDS as _COMMANDS,
132
+ )
133
+ from .terminal import (
134
+ _CYAN as _CYAN,
135
+ )
136
+ from .terminal import (
137
+ _GHOST as _GHOST,
138
+ )
139
+ from .terminal import (
140
+ _GREEN as _GREEN,
141
+ )
142
+ from .terminal import (
143
+ _GREY as _GREY,
144
+ )
145
+ from .terminal import (
146
+ _HAS_TERMIOS as _HAS_TERMIOS,
147
+ )
148
+ from .terminal import (
149
+ _RED as _RED,
150
+ )
151
+ from .terminal import (
152
+ _RESET as _RESET,
153
+ )
154
+ from .terminal import (
155
+ _TERRACOTTA as _TERRACOTTA,
156
+ )
157
+ from .terminal import (
158
+ BAR_WIDTH as BAR_WIDTH,
159
+ )
160
+ from .terminal import (
161
+ _BottomBar as _BottomBar,
162
+ )
163
+ from .terminal import (
164
+ _get_suggestion as _get_suggestion,
165
+ )
166
+ from .terminal import (
167
+ _out as _out,
168
+ )
169
+ from .terminal import (
170
+ _poll_cmd as _poll_cmd,
171
+ )
172
+ from .terminal import (
173
+ _progress as _progress,
174
+ )
175
+ from .terminal import (
176
+ _render_bar as _render_bar,
177
+ )
178
+ from .terminal import (
179
+ _stdin_command as _stdin_command,
180
+ )
181
+ from .terminal import (
182
+ _wait_for_resume as _wait_for_resume,
183
+ )
184
+ from .terminal import (
185
+ print_progress as print_progress,
186
+ )
187
+ from .terminal import (
188
+ print_verbose as print_verbose,
189
+ )
190
+ from .variables import (
191
+ _WORKFLOW_VAR_PREFIX as _WORKFLOW_VAR_PREFIX,
192
+ )
193
+ from .variables import (
194
+ VarDef as VarDef,
195
+ )
196
+ from .variables import (
197
+ _var_col as _var_col,
198
+ )
199
+ from .variables import (
200
+ persist_vars as persist_vars,
201
+ )
202
+ from .variables import (
203
+ resolve_variables as resolve_variables,
204
+ )
205
+ from .variables import (
206
+ validate_jsonpath as validate_jsonpath,
207
+ )
208
+ from .workflow import _WORKFLOW_STEP_COL as _WORKFLOW_STEP_COL
209
+ from .workflow import (
210
+ WorkflowStep as WorkflowStep,
211
+ )
212
+ from .workflow import (
213
+ _fire_workflow_step as _fire_workflow_step,
214
+ )
215
+ from .workflow import (
216
+ _resolve_workflow_auth_headers as _resolve_workflow_auth_headers,
217
+ )
218
+ from .workflow import (
219
+ _validate_workflow_placeholders as _validate_workflow_placeholders,
220
+ )
221
+ from .workflow import (
222
+ parse_workflow as parse_workflow,
223
+ )
224
+ from .workflow import (
225
+ workflow_var_columns as workflow_var_columns,
226
+ )
227
+ from .workflow_runner import (
228
+ _make_workflow_auth_refresh_fns as _make_workflow_auth_refresh_fns,
229
+ )
230
+ from .workflow_runner import (
231
+ _run_workflow_loop as _run_workflow_loop,
232
+ )
233
+ from .workflow_runner import (
234
+ _run_workflow_parallel as _run_workflow_parallel,
235
+ )
236
+ from .workflow_runner import (
237
+ _workflow_parallel_worker as _workflow_parallel_worker,
238
+ )
bulk_post/__main__.py ADDED
@@ -0,0 +1,6 @@
1
+ import sys
2
+
3
+ from . import main
4
+
5
+ if __name__ == "__main__":
6
+ sys.exit(main())
bulk_post/auth.py ADDED
@@ -0,0 +1,159 @@
1
+ """Token / basic-auth resolution and 401 refresh callbacks."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import base64
7
+ import os
8
+ import sys
9
+ from collections.abc import Callable
10
+
11
+ from .state import _ParallelState
12
+
13
+
14
+ def resolve_token(
15
+ flag_value: str | None,
16
+ suspend: Callable | None = None,
17
+ resume: Callable | None = None,
18
+ ) -> str:
19
+ """Resolve a bearer token: ``--token`` flag -> ``BULK_TOKEN`` env -> prompt.
20
+
21
+ Exits with code 1 if none is provided. ``suspend``/``resume`` pause the
22
+ bottom bar around the interactive prompt.
23
+ """
24
+ if flag_value:
25
+ return flag_value
26
+ env = os.environ.get("BULK_TOKEN", "").strip()
27
+ if env:
28
+ return env
29
+ if suspend:
30
+ suspend()
31
+ try:
32
+ token = input("Paste your Bearer token: ").strip()
33
+ except EOFError:
34
+ token = ""
35
+ if resume:
36
+ resume()
37
+ if not token:
38
+ print("[ERROR] No token provided.", file=sys.stderr)
39
+ sys.exit(1)
40
+ return token
41
+
42
+
43
+ def prompt_new_token(
44
+ suspend: Callable | None = None,
45
+ resume: Callable | None = None,
46
+ ) -> str:
47
+ """Prompt for a fresh bearer token after a 401 (exits 1 if none given)."""
48
+ if suspend:
49
+ suspend()
50
+ print("\n[AUTH] Token expired (401). Grab a fresh token from browser DevTools.")
51
+ try:
52
+ token = input("Paste new Bearer token: ").strip()
53
+ except EOFError:
54
+ token = ""
55
+ if resume:
56
+ resume()
57
+ if not token:
58
+ print("[ERROR] No token provided — aborting.", file=sys.stderr)
59
+ sys.exit(1)
60
+ return token
61
+
62
+
63
+ def resolve_basic_creds(
64
+ flag_value: str | None,
65
+ suspend: Callable | None = None,
66
+ resume: Callable | None = None,
67
+ ) -> str:
68
+ """Resolve basic-auth ``user:pass``: ``--user`` flag -> ``BULK_USER`` env -> prompt.
69
+
70
+ Exits with code 1 if none is provided.
71
+ """
72
+ if flag_value:
73
+ return flag_value
74
+ env = os.environ.get("BULK_USER", "").strip()
75
+ if env:
76
+ return env
77
+ if suspend:
78
+ suspend()
79
+ try:
80
+ creds = input("Basic auth credentials (user:pass): ").strip()
81
+ except EOFError:
82
+ creds = ""
83
+ if resume:
84
+ resume()
85
+ if not creds:
86
+ print("[ERROR] No credentials provided.", file=sys.stderr)
87
+ sys.exit(1)
88
+ return creds
89
+
90
+
91
+ def prompt_new_basic_creds(
92
+ suspend: Callable | None = None,
93
+ resume: Callable | None = None,
94
+ ) -> str:
95
+ """Prompt for new basic-auth credentials after a 401 (exits 1 if none given)."""
96
+ if suspend:
97
+ suspend()
98
+ print("\n[AUTH] Credentials rejected (401). Enter new credentials.")
99
+ try:
100
+ creds = input("Basic auth credentials (user:pass): ").strip()
101
+ except EOFError:
102
+ creds = ""
103
+ if resume:
104
+ resume()
105
+ if not creds:
106
+ print("[ERROR] No credentials provided — aborting.", file=sys.stderr)
107
+ sys.exit(1)
108
+ return creds
109
+
110
+
111
+ def resolve_auth_header(
112
+ args: argparse.Namespace,
113
+ suspend: Callable | None = None,
114
+ resume: Callable | None = None,
115
+ ) -> str | None:
116
+ """Build the ``Authorization`` header for ``args.auth_type``.
117
+
118
+ Returns ``None`` for ``none``; otherwise resolves the token/credentials
119
+ (prompting once at startup if needed) into a ``Bearer``/``Basic`` value.
120
+ """
121
+ if args.auth_type == "none":
122
+ return None
123
+ if args.auth_type == "bearer":
124
+ token = resolve_token(args.token, suspend=suspend, resume=resume)
125
+ return f"Bearer {token}"
126
+ creds = resolve_basic_creds(args.user, suspend=suspend, resume=resume)
127
+ return f"Basic {base64.b64encode(creds.encode()).decode()}"
128
+
129
+
130
+ def _make_auth_refresh_fn(
131
+ args,
132
+ state: _ParallelState,
133
+ suspend: Callable | None,
134
+ resume: Callable | None,
135
+ ) -> Callable:
136
+ """Return a thread-safe 401-refresh closure for parallel workers."""
137
+
138
+ def refresh(old_auth_header: str | None) -> str | None:
139
+ with state.auth_lock:
140
+ # Another thread already refreshed while we waited for the lock.
141
+ if state.auth_header != old_auth_header:
142
+ return state.auth_header
143
+ with state.output_lock:
144
+ if suspend:
145
+ suspend()
146
+ try:
147
+ if args.auth_type == "bearer":
148
+ refreshed = prompt_new_token()
149
+ new = f"Bearer {refreshed}"
150
+ else:
151
+ refreshed = prompt_new_basic_creds()
152
+ new = f"Basic {base64.b64encode(refreshed.encode()).decode()}"
153
+ finally:
154
+ if resume:
155
+ resume()
156
+ state.auth_header = new
157
+ return new
158
+
159
+ return refresh