0
# Server API (BlackD)
1
2
BlackD provides an HTTP server API for formatting Python code via REST endpoints, enabling integration with editors, IDEs, and other tools that need remote code formatting capabilities.
3
4
## Capabilities
5
6
### Server Management
7
8
Functions for starting and configuring the BlackD HTTP server.
9
10
```python { .api }
11
def main(bind_host: str, bind_port: int) -> None:
12
"""
13
Start blackd HTTP server.
14
15
Parameters:
16
- bind_host: Host address to bind to (e.g., "127.0.0.1", "0.0.0.0")
17
- bind_port: Port number to bind to (e.g., 45484)
18
19
Note:
20
- Runs asyncio event loop
21
- Handles SIGINT/SIGTERM for graceful shutdown
22
- Uses aiohttp web framework
23
"""
24
25
def make_app() -> web.Application:
26
"""
27
Create aiohttp application with BlackD routes.
28
29
Returns:
30
Configured aiohttp.web.Application instance
31
32
Note:
33
- Configures POST / route for formatting
34
- Sets up request handling and error responses
35
- Includes CORS headers and protocol versioning
36
"""
37
```
38
39
### Request Handling
40
41
Core HTTP request processing for code formatting.
42
43
```python { .api }
44
def handle(request: web.Request, executor: Executor) -> web.Response:
45
"""
46
Main HTTP request handler for formatting requests.
47
48
Parameters:
49
- request: aiohttp web request with Python code and headers
50
- executor: ThreadPoolExecutor for async processing
51
52
Returns:
53
aiohttp web response with formatted code or error
54
55
Request Format:
56
- Method: POST
57
- Path: /
58
- Body: Python source code to format
59
- Headers: Configuration options (see Header Constants)
60
61
Response Format:
62
- 200: Successfully formatted (body contains formatted code)
63
- 204: No changes needed (empty body)
64
- 400: Invalid request (syntax error, invalid headers)
65
- 500: Internal formatting error
66
67
Headers:
68
- X-Black-Version: Black version used for formatting
69
- Content-Type: text/plain for code, application/json for errors
70
"""
71
```
72
73
### Configuration Parsing
74
75
Functions for parsing Black configuration from HTTP headers.
76
77
```python { .api }
78
def parse_mode(headers: MultiMapping[str]) -> black.Mode:
79
"""
80
Parse Black Mode configuration from HTTP headers.
81
82
Parameters:
83
- headers: HTTP request headers containing configuration
84
85
Returns:
86
Mode object with configuration from headers
87
88
Supported Headers:
89
- X-Line-Length: Maximum line length (integer)
90
- X-Python-Variant: Target Python versions (comma-separated)
91
- X-Skip-String-Normalization: "true" to disable string normalization
92
- X-Skip-Magic-Trailing-Comma: "true" to disable trailing comma
93
- X-Skip-Source-First-Line: "true" to skip first line
94
- X-Preview: "true" to enable preview features
95
- X-Unstable: "true" to enable unstable features
96
- X-Enable-Unstable-Feature: Specific unstable features (comma-separated)
97
98
Raises:
99
- InvalidVariantHeader: If Python variant header is invalid
100
- HeaderError: If other headers contain invalid values
101
"""
102
```
103
104
## HTTP Protocol
105
106
### Header Constants
107
108
```python { .api }
109
# Protocol and versioning
110
PROTOCOL_VERSION_HEADER = "X-Protocol-Version"
111
BLACK_VERSION_HEADER = "X-Black-Version"
112
113
# Configuration headers
114
LINE_LENGTH_HEADER = "X-Line-Length"
115
PYTHON_VARIANT_HEADER = "X-Python-Variant"
116
SKIP_SOURCE_FIRST_LINE = "X-Skip-Source-First-Line"
117
SKIP_STRING_NORMALIZATION_HEADER = "X-Skip-String-Normalization"
118
SKIP_MAGIC_TRAILING_COMMA = "X-Skip-Magic-Trailing-Comma"
119
PREVIEW = "X-Preview"
120
UNSTABLE = "X-Unstable"
121
ENABLE_UNSTABLE_FEATURE = "X-Enable-Unstable-Feature"
122
123
# Processing options
124
FAST_OR_SAFE_HEADER = "X-Fast-Or-Safe"
125
DIFF_HEADER = "X-Diff"
126
```
127
128
### Exception Classes
129
130
```python { .api }
131
class HeaderError(Exception):
132
"""Raised when HTTP headers contain invalid values."""
133
pass
134
135
class InvalidVariantHeader(Exception):
136
"""Raised when X-Python-Variant header is invalid."""
137
pass
138
```
139
140
## Usage Examples
141
142
### Starting BlackD Server
143
144
```python
145
import blackd
146
147
# Start server on localhost:45484
148
blackd.main("127.0.0.1", 45484)
149
150
# Start server on all interfaces
151
blackd.main("0.0.0.0", 8080)
152
```
153
154
### Command Line Server Startup
155
156
```bash
157
# Default configuration
158
blackd
159
160
# Custom host and port
161
blackd --bind-host 0.0.0.0 --bind-port 8080
162
```
163
164
### HTTP Client Usage
165
166
```python
167
import requests
168
169
# Format code via HTTP API
170
code_to_format = '''
171
def hello(name):
172
print(f"Hello {name}!")
173
'''
174
175
response = requests.post(
176
"http://localhost:45484/",
177
data=code_to_format,
178
headers={
179
"Content-Type": "text/plain",
180
"X-Line-Length": "79",
181
"X-Python-Variant": "py39,py310",
182
"X-Skip-String-Normalization": "false",
183
"X-Fast-Or-Safe": "safe"
184
}
185
)
186
187
if response.status_code == 200:
188
formatted_code = response.text
189
print("Formatted successfully:")
190
print(formatted_code)
191
elif response.status_code == 204:
192
print("No changes needed")
193
elif response.status_code == 400:
194
print(f"Error: {response.text}")
195
```
196
197
### cURL Example
198
199
```bash
200
# Format code with cURL
201
curl -X POST http://localhost:45484/ \
202
-H "Content-Type: text/plain" \
203
-H "X-Line-Length: 88" \
204
-H "X-Python-Variant: py39" \
205
-d "def hello(name):print(f'Hello {name}!')"
206
```
207
208
### JavaScript/Node.js Client
209
210
```javascript
211
const axios = require('axios');
212
213
async function formatCode(code) {
214
try {
215
const response = await axios.post('http://localhost:45484/', code, {
216
headers: {
217
'Content-Type': 'text/plain',
218
'X-Line-Length': '88',
219
'X-Python-Variant': 'py39,py310',
220
'X-Preview': 'true'
221
}
222
});
223
224
if (response.status === 200) {
225
return response.data; // Formatted code
226
} else if (response.status === 204) {
227
return code; // No changes needed
228
}
229
} catch (error) {
230
if (error.response && error.response.status === 400) {
231
throw new Error(`Formatting error: ${error.response.data}`);
232
}
233
throw error;
234
}
235
}
236
237
// Usage
238
formatCode("def hello(name):print(f'Hello {name}!')")
239
.then(formatted => console.log(formatted))
240
.catch(error => console.error(error));
241
```
242
243
### Editor Integration Example
244
245
```python
246
import requests
247
import json
248
249
class BlackDFormatter:
250
def __init__(self, host="localhost", port=45484):
251
self.base_url = f"http://{host}:{port}/"
252
253
def format_code(self, code, **options):
254
"""Format code with optional configuration."""
255
headers = {"Content-Type": "text/plain"}
256
257
# Convert options to headers
258
if "line_length" in options:
259
headers["X-Line-Length"] = str(options["line_length"])
260
if "target_versions" in options:
261
headers["X-Python-Variant"] = ",".join(options["target_versions"])
262
if "preview" in options and options["preview"]:
263
headers["X-Preview"] = "true"
264
if "skip_string_normalization" in options:
265
headers["X-Skip-String-Normalization"] = str(options["skip_string_normalization"]).lower()
266
267
try:
268
response = requests.post(self.base_url, data=code, headers=headers, timeout=10)
269
270
if response.status_code == 200:
271
return response.text, True # (formatted_code, changed)
272
elif response.status_code == 204:
273
return code, False # (original_code, not_changed)
274
elif response.status_code == 400:
275
raise ValueError(f"Syntax error: {response.text}")
276
else:
277
raise RuntimeError(f"Server error: {response.status_code}")
278
279
except requests.RequestException as e:
280
raise ConnectionError(f"Cannot connect to BlackD server: {e}")
281
282
# Usage in editor plugin
283
formatter = BlackDFormatter()
284
285
def format_selection(editor_code, line_length=88):
286
try:
287
formatted, changed = formatter.format_code(
288
editor_code,
289
line_length=line_length,
290
target_versions=["py39", "py310"],
291
preview=False
292
)
293
if changed:
294
return formatted
295
else:
296
return None # No changes needed
297
except Exception as e:
298
# Show error to user
299
print(f"Formatting failed: {e}")
300
return None
301
```
302
303
### Async Server Application Integration
304
305
```python
306
import aiohttp
307
import asyncio
308
import blackd
309
310
async def integrate_with_blackd():
311
"""Example of integrating BlackD into another aiohttp application."""
312
313
# Create BlackD application
314
blackd_app = blackd.make_app()
315
316
# Create main application
317
main_app = aiohttp.web.Application()
318
319
# Add BlackD as a sub-application
320
main_app.add_subapp('/format/', blackd_app)
321
322
# Add other routes to main app
323
async def health_check(request):
324
return aiohttp.web.Response(text="OK")
325
326
main_app.router.add_get('/health', health_check)
327
328
return main_app
329
330
# Run integrated server
331
async def run_integrated_server():
332
app = await integrate_with_blackd()
333
runner = aiohttp.web.AppRunner(app)
334
await runner.setup()
335
336
site = aiohttp.web.TCPSite(runner, 'localhost', 8080)
337
await site.start()
338
339
print("Server running on http://localhost:8080")
340
print("BlackD available at http://localhost:8080/format/")
341
342
# Keep server running
343
await asyncio.Future() # Run forever
344
345
# Usage
346
# asyncio.run(run_integrated_server())
347
```
348
349
### Configuration Examples
350
351
```python
352
# Different formatting configurations via headers
353
354
# Strict Python 3.9 compatibility
355
headers = {
356
"X-Python-Variant": "py39",
357
"X-Line-Length": "79",
358
"X-Skip-String-Normalization": "false"
359
}
360
361
# Preview features enabled
362
headers = {
363
"X-Preview": "true",
364
"X-Line-Length": "100",
365
"X-Python-Variant": "py310,py311"
366
}
367
368
# Conservative formatting
369
headers = {
370
"X-Skip-String-Normalization": "true",
371
"X-Skip-Magic-Trailing-Comma": "true",
372
"X-Line-Length": "88"
373
}
374
375
# Unstable features
376
headers = {
377
"X-Unstable": "true",
378
"X-Enable-Unstable-Feature": "string_processing,wrap_long_dict_values_in_parens"
379
}
380
```
381
382
### Error Handling
383
384
```python
385
import requests
386
import json
387
388
def format_with_error_handling(code):
389
try:
390
response = requests.post("http://localhost:45484/", data=code)
391
392
if response.status_code == 200:
393
return response.text
394
elif response.status_code == 204:
395
return code # No changes
396
elif response.status_code == 400:
397
# Parse error details if JSON
398
try:
399
error_data = response.json()
400
raise SyntaxError(error_data.get("message", response.text))
401
except json.JSONDecodeError:
402
raise SyntaxError(response.text)
403
else:
404
raise RuntimeError(f"Server error {response.status_code}: {response.text}")
405
406
except requests.ConnectionError:
407
raise ConnectionError("BlackD server not available")
408
except requests.Timeout:
409
raise TimeoutError("BlackD server timeout")
410
```
411
412
## Types
413
414
```python { .api }
415
# aiohttp types
416
from aiohttp import web
417
from aiohttp.web import Request, Response, Application
418
from multidict import MultiMapping
419
420
# Async execution
421
from concurrent.futures import Executor, ThreadPoolExecutor
422
423
# Server configuration
424
ServerHost = str
425
ServerPort = int
426
HeaderDict = dict[str, str]
427
```