You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Fastapi Startkit ships a `Process` facade that wraps Python's `subprocess` module behind a clean, fluent interface. It lets you run shell commands synchronously, stream output asynchronously, build pipelines, and run pools of concurrent processes — all with first-class support for test fakes so no real processes are spawned during your test suite.
10
+
Fastapi Startkit ships a `Process` facade that wraps Python's `subprocess` module behind a clean, fluent interface. It lets you run shell commands asynchronously, stream output in the background, build pipelines, and run pools of concurrent processes — all with first-class support for test fakes so no real processes are spawned during your test suite.
11
+
12
+
## Async vs Sync
13
+
14
+
`Process.run()` is **async by default** — it must be awaited and is designed for use inside FastAPI request handlers and other async contexts:
15
+
16
+
```python
17
+
result =await Process.run('ls -la')
18
+
```
19
+
20
+
If you are writing a CLI script or Cleo command that runs outside an event loop, use the synchronous fallback instead:
Every `run()` call returns a `ProcessResult` with the following methods:
61
+
Every `run()`/ `run_sync()`call returns a `ProcessResult` with the following methods:
40
62
41
63
| Method | Description |
42
64
|---|---|
43
65
|`output()`| Returns captured stdout as a string |
44
66
|`error_output()`| Returns captured stderr as a string |
67
+
|`error()`| Alias for `error_output()` — stderr as a string |
68
+
|`output_json()`| Parses stdout as JSON and returns the decoded value |
45
69
|`exit_code()`| Returns the integer exit code |
46
70
|`successful()`|`True` if exit code is `0`|
47
71
|`failed()`|`True` if exit code is non-zero |
48
72
|`throw()`| Raises `ProcessFailedException` if the process failed, otherwise returns `self`|
49
73
|`throw_if(condition)`| Raises `ProcessFailedException` if `condition` is truthy |
50
74
|`command()`| Returns the original command string |
51
75
52
-
`throw()` is useful for asserting success in a chain:
76
+
`throw()` is useful for asserting success after a run:
53
77
54
78
```python
55
-
result = Process.run('python migrate.py').throw()
79
+
result =await Process.run('python migrate.py')
80
+
result.throw()
56
81
# raises ProcessFailedException if migration fails
57
82
```
58
83
84
+
Use `output_json()` when a command returns JSON on stdout:
85
+
86
+
```python
87
+
result =await Process.run('aws s3api list-buckets')
88
+
data = result.output_json()
89
+
print(data['Buckets'])
90
+
```
91
+
59
92
## Fluent Builder Options
60
93
61
-
Any method on the `Process`facade that is not `run()` or `start()` returns a `PendingProcess` builder. Chain options before calling `run()`:
94
+
Any class method on `Process` that is not `run()`, `run_sync()`, `pipe()`, `pipe_sync()`, `start()`, or `pool()` returns a `PendingProcess` builder. Chain options before calling `run()`:
62
95
63
96
### `timeout(seconds)`
64
97
65
98
Sets how long to wait before killing the process and raising `ProcessTimedOutException`. The default is 60 seconds.
66
99
67
100
```python
68
-
result = Process.timeout(10).run('bash slow_script.sh')
101
+
result =awaitProcess.timeout(10).run('bash slow_script.sh')
69
102
```
70
103
71
104
### `forever()`
72
105
73
106
Disables the timeout entirely.
74
107
75
108
```python
76
-
result = Process.forever().run('bash long_import.sh')
109
+
result =awaitProcess.forever().run('bash long_import.sh')
77
110
```
78
111
79
112
### `quietly()`
80
113
81
114
Discards all stdout and stderr output. Useful when you only care about the exit code.
82
115
83
116
```python
84
-
result = Process.quietly().run('npm install')
117
+
result =awaitProcess.quietly().run('npm install')
85
118
```
86
119
87
120
### `tty()`
88
121
89
122
Passes stdin, stdout, and stderr directly through to the terminal. Output is not captured.
90
123
91
124
```python
92
-
Process.tty().run('vim file.txt')
125
+
awaitProcess.tty().run('vim file.txt')
93
126
```
94
127
95
128
### `env(variables)`
96
129
97
130
Merges additional environment variables into the process environment.
98
131
99
132
```python
100
-
result = Process.env({'APP_ENV': 'production', 'DEBUG': '0'}).run('python app.py')
133
+
result =awaitProcess.env({'APP_ENV': 'production', 'DEBUG': '0'}).run('python app.py')
101
134
```
102
135
103
136
### `path(directory)`
104
137
105
138
Sets the working directory for the process.
106
139
107
140
```python
108
-
result = Process.path('/var/www/app').run('git pull origin main')
141
+
result =awaitProcess.path('/var/www/app').run('git pull origin main')
109
142
```
110
143
111
144
### `input(data)`
112
145
113
146
Pipes a string into the process's stdin.
114
147
115
148
```python
116
-
result = Process.input('yes\n').run('apt-get install -y some-package')
149
+
result =awaitProcess.input('yes\n').run('apt-get install -y some-package')
117
150
```
118
151
119
152
### Combining Options
120
153
121
154
Options chain together — each returns the same `PendingProcess`:
122
155
123
156
```python
124
-
result = (
157
+
result =await(
125
158
Process
126
159
.timeout(30)
127
160
.path('/var/www/app')
@@ -131,9 +164,76 @@ result = (
131
164
)
132
165
```
133
166
134
-
## Async Execution
167
+
## Piping
168
+
169
+
`Process.pipe()` builds a shell pipeline from multiple commands. Pass a callback that receives a `Pipe` builder and calls `.command()` for each stage:
170
+
171
+
```python
172
+
result =await Process.pipe(lambdap: (
173
+
p.command('cat access.log'),
174
+
p.command('grep "ERROR"'),
175
+
p.command('wc -l'),
176
+
))
177
+
178
+
print(result.output()) # count of ERROR lines
179
+
```
180
+
181
+
The commands are joined with `|` and executed as a single shell command. All fluent builder options apply before calling `pipe()`:
182
+
183
+
```python
184
+
result =await Process.path('/var/www/app').timeout(15).pipe(lambdap: (
185
+
p.command('find . -name "*.py"'),
186
+
p.command('xargs wc -l'),
187
+
))
188
+
```
189
+
190
+
## Sync Execution
135
191
136
-
Use `Process.start()` to launch a process without blocking. It returns an `InvokedProcess` that streams output as lines arrive via a callback:
192
+
Use `run_sync()` and `pipe_sync()` when you are writing CLI scripts or Cleo commands that run outside an event loop.
193
+
194
+
### `run_sync()`
195
+
196
+
```python
197
+
from fastapi_startkit.process import Process
198
+
199
+
result = Process.run_sync('ls -la')
200
+
print(result.output())
201
+
print(result.exit_code())
202
+
```
203
+
204
+
All fluent builder options work the same way:
205
+
206
+
```python
207
+
result = (
208
+
Process
209
+
.timeout(30)
210
+
.path('/var/www/app')
211
+
.quietly()
212
+
.run_sync('bash deploy.sh')
213
+
)
214
+
```
215
+
216
+
### `pipe_sync()`
217
+
218
+
```python
219
+
result = Process.pipe_sync(lambdap: (
220
+
p.command('cat access.log'),
221
+
p.command('grep "ERROR"'),
222
+
p.command('wc -l'),
223
+
))
224
+
225
+
print(result.output()) # count of ERROR lines
226
+
```
227
+
228
+
::: tip When to use `run_sync` vs `run`
229
+
- Use **`run()`** inside FastAPI route handlers and any `async def` context.
230
+
- Use **`run_sync()`** in Cleo commands, standalone scripts, or any code that runs without an event loop.
231
+
Calling `run()` outside an async context will raise a `RuntimeError` because there is no running event loop.
232
+
:::
233
+
234
+
## Background Execution
235
+
236
+
Use `Process.start()` to launch a process in the background without blocking. It spawns a thread-backed `InvokedProcess` that streams output as lines arrive via a callback:
137
237
138
238
```python
139
239
import time
@@ -152,6 +252,10 @@ result = invoked.wait()
152
252
print("Exit code:", result.exit_code())
153
253
```
154
254
255
+
::: info Background execution uses threads
256
+
`Process.start()` launches the subprocess via `subprocess.Popen` and reads its output on background threads — it does **not** use asyncio. Use it for long-running tasks where you want to stream output in real time, not for general async usage inside FastAPI handlers (use `await Process.run()` for that).
|`signal(sig)`| Sends a signal (e.g. `signal.SIGTERM`) to the process |
164
268
|`ensure_not_timed_out()`| Raises `ProcessTimedOutException` if the configured timeout has elapsed |
165
269
166
-
## Piping
167
-
168
-
`Process.pipe()` builds a shell pipeline from multiple commands. Pass a callback that receives a `Pipe` builder and calls `.command()` for each stage:
169
-
170
-
```python
171
-
result = Process.pipe(lambdap: (
172
-
p.command('cat access.log'),
173
-
p.command('grep "ERROR"'),
174
-
p.command('wc -l'),
175
-
))
176
-
177
-
print(result.output()) # count of ERROR lines
178
-
```
179
-
180
-
The commands are joined with `|` and executed as a single shell command. All fluent builder options apply before calling `pipe()`:
181
-
182
-
```python
183
-
result = Process.path('/var/www/app').timeout(15).pipe(lambdap: (
184
-
p.command('find . -name "*.py"'),
185
-
p.command('xargs wc -l'),
186
-
))
187
-
```
188
-
189
270
## Pools (Concurrent Processes)
190
271
191
-
`Process.pool()` runs multiple commands concurrently. Pass a callback that adds commands to a `Pool`, then call `.start()` to launch all of them and `.wait()` to collect results:
272
+
`Process.pool()` runs multiple commands concurrently using background threads. Pass a callback that adds commands to a `Pool`, then call `.start()` to launch all of them and `.wait()` to collect results:
192
273
193
274
```python
194
275
pool = Process.pool(lambdap: (
@@ -239,12 +320,12 @@ results = pool.wait()
239
320
240
321
## Testing with Fakes
241
322
242
-
Call `Process.fake()` at the start of a test to intercept all process calls. No real subprocesses are spawned.
323
+
Call `Process.fake()` at the start of a test to intercept all process calls. No real subprocesses are spawned. Because `Process.run()` is async, test functions must be declared with `async def` and await every call.
243
324
244
325
```python
245
326
from fastapi_startkit.process import Process
246
327
247
-
deftest_deploy_script():
328
+
asyncdeftest_deploy_script():
248
329
fake = Process.fake()
249
330
250
331
# ... call code that internally runs Process.run('bash deploy.sh')
0 commit comments