asyagent 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,262 @@
1
+ Metadata-Version: 2.4
2
+ Name: asyagent
3
+ Version: 0.1.0
4
+ Summary: An HTTP service that compiles Asymptote (*.asy) sources into rendered output (PDF, SVG, EPS, PNG, JPG).
5
+ Author: asyagent contributors
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/yaochi/asyagent
8
+ Project-URL: Bug Tracker, https://github.com/yaochi/asyagent/issues
9
+ Keywords: asymptote,asy,vector-graphics,compiler,http-service,pdf,svg,png
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
20
+ Classifier: Topic :: Multimedia :: Graphics
21
+ Classifier: Topic :: Scientific/Engineering :: Visualization
22
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
+ Classifier: Typing :: Typed
24
+ Requires-Python: >=3.10
25
+ Description-Content-Type: text/markdown
26
+
27
+ # asyagent
28
+
29
+ An HTTP service that compiles [Asymptote](https://asymptote.sourceforge.io/) (`*.asy`) vector-graphics source code into rendered output (PDF, SVG, EPS, PNG, JPG) and returns it either inline (raw binary or base64 JSON) or as an object-storage URL.
30
+
31
+ AsyAgent is a proxy that wraps a local tool and exposes it as an API with optional object-storage upload for Asymptote: the `asy` compiler doesn't have a server mode, so `asyagent` wraps it in an HTTP API.
32
+
33
+ ## Key characteristics
34
+
35
+ - **Zero third-party dependencies** — pure Python 3.10+ standard library. No `pip install`, no virtualenv required.
36
+ - **Header-driven control plane** — all request behaviour (output format, response mode, encoding, DPI, timeout, storage prefix) is controlled by `X-Asy-*` request headers.
37
+ - **Dual response modes** — return compiled output inline (binary or base64 JSON) or upload to storage and return a URL.
38
+ - **Multiple storage backends** — local filesystem (zero-config), S3-compatible object storage (hand-rolled AWS Signature V4), or disabled.
39
+ - **Multi-format** — native `pdf`/`svg`/`eps`, plus `png`/`jpg` via Ghostscript rasterization. Multi-output (multiple `shipout()` calls) is auto-bundled as a ZIP.
40
+
41
+ ## Quick start
42
+
43
+ ```bash
44
+ # Start the server (zero config — uses local storage)
45
+ python3 -m asyagent
46
+
47
+ # Compile an asy source, get PDF inline
48
+ curl -s http://127.0.0.1:8787/v1/render \
49
+ --data-binary 'size(5cm); draw(unitcircle);' \
50
+ -H 'Content-Type: text/plain' \
51
+ -o circle.pdf
52
+
53
+ # Get a PNG instead
54
+ curl -s http://127.0.0.1:8787/v1/render \
55
+ --data-binary 'size(5cm); draw(unitcircle);' \
56
+ -H 'Content-Type: text/plain' \
57
+ -H 'X-Asy-Format: png' \
58
+ -o circle.png
59
+
60
+ # Return base64 JSON
61
+ curl -s http://127.0.0.1:8787/v1/render \
62
+ -d '{"source": "size(5cm); draw(unitcircle);"}' \
63
+ -H 'Content-Type: application/json' \
64
+ -H 'X-Asy-Encoding: base64'
65
+
66
+ # Upload to storage, return URL
67
+ curl -s http://127.0.0.1:8787/v1/render \
68
+ -d '{"source": "size(5cm); draw(unitcircle);"}' \
69
+ -H 'Content-Type: application/json' \
70
+ -H 'X-Asy-Mode: url'
71
+ ```
72
+
73
+ ## API
74
+
75
+ ### `POST /v1/render`
76
+
77
+ Accepts an Asymptote source and returns the compiled output.
78
+
79
+ **Request body** (one of):
80
+
81
+ | Body type | Content-Type | Description |
82
+ |-----------|-------------|-------------|
83
+ | Raw source text | `text/plain` (or any non-JSON) | The `.asy` source code directly. Auto-detection: if the body is a single line starting with `http://` or `https://`, it's treated as a URL. |
84
+ | JSON object | `application/json` | `{"source": "..."}` or `{"url": "https://..."}` |
85
+
86
+ **Control headers:**
87
+
88
+ | Header | Values | Default | Description |
89
+ |--------|--------|---------|-------------|
90
+ | `X-Asy-Format` | `pdf` `svg` `eps` `png` `jpg` `jpeg` | `pdf` (or inferred from `Accept`) | Output format |
91
+ | `X-Asy-Mode` | `inline` `url` | `inline` | `inline` = return binary/base64; `url` = upload to storage, return URL |
92
+ | `X-Asy-Encoding` | `binary` `base64` | `binary` | Only for inline mode. `binary` returns raw bytes with proper Content-Type; `base64` returns JSON with base64-encoded data |
93
+ | `X-Asy-Input` | `auto` `source` `url` | `auto` | How to interpret a non-JSON body |
94
+ | `X-Asy-Dpi` | `1`–`4096` | `150` | DPI for raster formats (png/jpg) |
95
+ | `X-Asy-Timeout` | seconds | `60` | Compile timeout (capped by `ASYAGENT_MAX_TIMEOUT`) |
96
+ | `X-Asy-Filename` | string | — | Suggested filename for Content-Disposition / storage key |
97
+ | `X-Asy-Disposition` | `inline` `attachment` | `inline` | Content-Disposition value |
98
+ | `X-Asy-Storage-Prefix` | string | env `S3_PREFIX` | Override storage key prefix for this request |
99
+ | `X-Asy-Storage-Bucket` | string | env `S3_BUCKET` | Override S3 bucket for this request |
100
+ | `Accept` | MIME type | — | Alternative to `X-Asy-Format` (e.g. `Accept: image/png`) |
101
+
102
+ **Response (inline/binary):** Raw bytes with `Content-Type` matching the format (e.g. `application/pdf`, `image/png`).
103
+
104
+ **Response (inline/base64):**
105
+ ```json
106
+ {
107
+ "ok": true,
108
+ "mode": "inline",
109
+ "encoding": "base64",
110
+ "format": "pdf",
111
+ "mime": "application/pdf",
112
+ "size": 5989,
113
+ "data": "JVBERi0xLjU..."
114
+ }
115
+ ```
116
+
117
+ **Response (url mode):**
118
+ ```json
119
+ {
120
+ "ok": true,
121
+ "mode": "url",
122
+ "format": "pdf",
123
+ "mime": "application/pdf",
124
+ "size": 5989,
125
+ "url": "http://host/files/...",
126
+ "key": "files/abc123.pdf",
127
+ "urls": [{"url": "...", "key": "...", "mime": "...", "size": 5989}]
128
+ }
129
+ ```
130
+
131
+ ### `GET /`
132
+
133
+ Service info (version, formats, storage health, defaults).
134
+
135
+ ### `GET /healthz`
136
+
137
+ Health check — returns 200 if storage is writable, 503 otherwise.
138
+
139
+ ### `GET /files/{key}`
140
+
141
+ Serves files from local storage (only available when `ASYAGENT_STORAGE=local`).
142
+
143
+ ## Configuration
144
+
145
+ All configuration via environment variables:
146
+
147
+ ### Server
148
+
149
+ | Variable | Default | Description |
150
+ |----------|---------|-------------|
151
+ | `ASYAGENT_HOST` | `0.0.0.0` | Listen address |
152
+ | `ASYAGENT_PORT` | `8787` | Listen port |
153
+ | `ASYAGENT_MAX_WORKERS` | `16` | Max concurrent compiles (semaphore) |
154
+ | `ASYAGENT_COMPILE_TIMEOUT` | `60` | Default compile timeout (s) |
155
+ | `ASYAGENT_MAX_TIMEOUT` | `300` | Max allowed client-requested timeout |
156
+ | `ASYAGENT_FETCH_TIMEOUT` | `20` | URL fetch timeout (s) |
157
+ | `ASYAGENT_MAX_SOURCE_BYTES` | `1048576` | Max request body size |
158
+ | `ASYAGENT_MAX_FETCH_BYTES` | `5242880` | Max remote file size for URL input |
159
+ | `ASYAGENT_ASY_BIN` | `asy` | Path to asy binary |
160
+ | `ASYAGENT_GS_BIN` | `gs` | Path to ghostscript binary |
161
+ | `ASYAGENT_DEFAULT_FORMAT` | `pdf` | Default output format |
162
+ | `ASYAGENT_DEFAULT_MODE` | `inline` | Default response mode |
163
+ | `ASYAGENT_DEFAULT_ENCODING` | `binary` | Default inline encoding |
164
+ | `ASYAGENT_DEFAULT_DPI` | `150` | Default raster DPI |
165
+ | `ASYAGENT_TMP_DIR` | — | Override temp directory for compile working dirs |
166
+
167
+ ### Storage
168
+
169
+ | Variable | Default | Description |
170
+ |----------|---------|-------------|
171
+ | `ASYAGENT_STORAGE` | `local` | Backend: `local`, `s3`, or `none` |
172
+ | `ASYAGENT_LOCAL_DIR` | `./storage` | Local storage directory |
173
+ | `ASYAGENT_LOCAL_BASE_URL` | auto | Base URL for local file serving |
174
+
175
+ ### S3 / Object Storage
176
+
177
+ | Variable | Default | Description |
178
+ |----------|---------|-------------|
179
+ | `S3_BUCKET` | — | Bucket name (required for S3) |
180
+ | `S3_PREFIX` | `asyagent/` | Key prefix |
181
+ | `S3_ENDPOINT` | auto | Custom endpoint (e.g. `http://minio:9000`) |
182
+ | `S3_URL_STYLE` | `path` | `path` or `virtual` (virtual-hosted) |
183
+ | `S3_PUBLIC_BASE_URL` | — | Override the public URL base (e.g. CDN domain) |
184
+ | `S3_PRESIGN` | `false` | Return presigned URLs instead of public URLs |
185
+ | `S3_PRESIGN_EXPIRES` | `3600` | Presigned URL expiry (s) |
186
+ | `S3_USE_TLS` | `true` | Use HTTPS for S3 API calls |
187
+ | `AWS_ACCESS_KEY_ID` | — | Access key |
188
+ | `AWS_SECRET_ACCESS_KEY` | — | Secret key |
189
+ | `AWS_SESSION_TOKEN` | — | STS session token (optional) |
190
+ | `AWS_REGION` | `us-east-1` | Region |
191
+
192
+ ## Architecture
193
+
194
+ ```
195
+ asyagent/
196
+ __init__.py # package metadata
197
+ __main__.py # python -m asyagent entry point
198
+ config.py # Settings dataclass, env-driven
199
+ errors.py # typed exception hierarchy
200
+ sigv4.py # AWS Signature V4 (sign + presign), zero-dep
201
+ fetcher.py # URL -> source text fetcher
202
+ compiler.py # asy invocation + gs rasterization + ZIP bundling
203
+ storage.py # Local / S3 / None storage backends
204
+ server.py # ThreadingHTTPServer, header-driven rendering
205
+ tests/
206
+ test_sigv4.py # KAT against AWS test vector (AKIDEXAMPLE)
207
+ test_compiler.py # all formats, multi-shipout, errors
208
+ test_server.py # end-to-end integration tests
209
+ examples/
210
+ unit_circle.asy # example source
211
+ function_plot.asy # example source
212
+ multi_page.asy # multi-shipout example
213
+ client.py # example client script
214
+ ```
215
+
216
+ ### Request flow
217
+
218
+ ```
219
+ Client ──POST /v1/render──▶ server.py
220
+ │ │
221
+ │ Headers: X-Asy-Format, │ RenderContext (parses all X-Asy-* headers)
222
+ │ X-Asy-Mode, etc. │
223
+ │ ├── text/plain body ──▶ _resolve_source (auto-detect url/source)
224
+ │ ├── application/json ──▶ _resolve_source (json.source or json.url)
225
+ │ │ └── fetcher.py (if url)
226
+ │ │
227
+ │ ├── compile_source (semaphore-gated)
228
+ │ │ ├── asy -f pdf -o out input.asy (native: pdf/svg/eps)
229
+ │ │ ├── gs -sDEVICE=pngalpha ... (raster: png/jpg)
230
+ │ │ └── select_or_bundle (ZIP if multiple outputs)
231
+ │ │
232
+ │ ├── mode=inline,encoding=binary ──▶ raw bytes + Content-Type
233
+ │ ├── mode=inline,encoding=base64 ──▶ JSON {data: base64...}
234
+ │ └── mode=url ──▶ storage.upload ──▶ JSON {url: ...}
235
+
236
+ ◀── response (binary / JSON) ─┘
237
+ ```
238
+
239
+ ### Response mode comparison
240
+
241
+ | Mode | Encoding | Response body | Content-Type | Use case |
242
+ |------|----------|---------------|-------------|----------|
243
+ | `inline` | `binary` | Raw compiled bytes | matches format | Direct download, browser display |
244
+ | `inline` | `base64` | JSON `{data: "..."}` | `application/json` | API compositing, embedding in JSON workflows |
245
+ | `url` | — | JSON `{url: "..."}` | `application/json` | Large files, CDN delivery, async workflows |
246
+
247
+ ## Running tests
248
+
249
+ ```bash
250
+ python3 -m unittest discover -s tests -v
251
+ ```
252
+
253
+ ## Docker
254
+
255
+ ```bash
256
+ docker build -t asyagent .
257
+ docker run -p 8787:8787 asyagent
258
+ ```
259
+
260
+ ## License
261
+
262
+ MIT
@@ -0,0 +1,236 @@
1
+ # asyagent
2
+
3
+ An HTTP service that compiles [Asymptote](https://asymptote.sourceforge.io/) (`*.asy`) vector-graphics source code into rendered output (PDF, SVG, EPS, PNG, JPG) and returns it either inline (raw binary or base64 JSON) or as an object-storage URL.
4
+
5
+ AsyAgent is a proxy that wraps a local tool and exposes it as an API with optional object-storage upload for Asymptote: the `asy` compiler doesn't have a server mode, so `asyagent` wraps it in an HTTP API.
6
+
7
+ ## Key characteristics
8
+
9
+ - **Zero third-party dependencies** — pure Python 3.10+ standard library. No `pip install`, no virtualenv required.
10
+ - **Header-driven control plane** — all request behaviour (output format, response mode, encoding, DPI, timeout, storage prefix) is controlled by `X-Asy-*` request headers.
11
+ - **Dual response modes** — return compiled output inline (binary or base64 JSON) or upload to storage and return a URL.
12
+ - **Multiple storage backends** — local filesystem (zero-config), S3-compatible object storage (hand-rolled AWS Signature V4), or disabled.
13
+ - **Multi-format** — native `pdf`/`svg`/`eps`, plus `png`/`jpg` via Ghostscript rasterization. Multi-output (multiple `shipout()` calls) is auto-bundled as a ZIP.
14
+
15
+ ## Quick start
16
+
17
+ ```bash
18
+ # Start the server (zero config — uses local storage)
19
+ python3 -m asyagent
20
+
21
+ # Compile an asy source, get PDF inline
22
+ curl -s http://127.0.0.1:8787/v1/render \
23
+ --data-binary 'size(5cm); draw(unitcircle);' \
24
+ -H 'Content-Type: text/plain' \
25
+ -o circle.pdf
26
+
27
+ # Get a PNG instead
28
+ curl -s http://127.0.0.1:8787/v1/render \
29
+ --data-binary 'size(5cm); draw(unitcircle);' \
30
+ -H 'Content-Type: text/plain' \
31
+ -H 'X-Asy-Format: png' \
32
+ -o circle.png
33
+
34
+ # Return base64 JSON
35
+ curl -s http://127.0.0.1:8787/v1/render \
36
+ -d '{"source": "size(5cm); draw(unitcircle);"}' \
37
+ -H 'Content-Type: application/json' \
38
+ -H 'X-Asy-Encoding: base64'
39
+
40
+ # Upload to storage, return URL
41
+ curl -s http://127.0.0.1:8787/v1/render \
42
+ -d '{"source": "size(5cm); draw(unitcircle);"}' \
43
+ -H 'Content-Type: application/json' \
44
+ -H 'X-Asy-Mode: url'
45
+ ```
46
+
47
+ ## API
48
+
49
+ ### `POST /v1/render`
50
+
51
+ Accepts an Asymptote source and returns the compiled output.
52
+
53
+ **Request body** (one of):
54
+
55
+ | Body type | Content-Type | Description |
56
+ |-----------|-------------|-------------|
57
+ | Raw source text | `text/plain` (or any non-JSON) | The `.asy` source code directly. Auto-detection: if the body is a single line starting with `http://` or `https://`, it's treated as a URL. |
58
+ | JSON object | `application/json` | `{"source": "..."}` or `{"url": "https://..."}` |
59
+
60
+ **Control headers:**
61
+
62
+ | Header | Values | Default | Description |
63
+ |--------|--------|---------|-------------|
64
+ | `X-Asy-Format` | `pdf` `svg` `eps` `png` `jpg` `jpeg` | `pdf` (or inferred from `Accept`) | Output format |
65
+ | `X-Asy-Mode` | `inline` `url` | `inline` | `inline` = return binary/base64; `url` = upload to storage, return URL |
66
+ | `X-Asy-Encoding` | `binary` `base64` | `binary` | Only for inline mode. `binary` returns raw bytes with proper Content-Type; `base64` returns JSON with base64-encoded data |
67
+ | `X-Asy-Input` | `auto` `source` `url` | `auto` | How to interpret a non-JSON body |
68
+ | `X-Asy-Dpi` | `1`–`4096` | `150` | DPI for raster formats (png/jpg) |
69
+ | `X-Asy-Timeout` | seconds | `60` | Compile timeout (capped by `ASYAGENT_MAX_TIMEOUT`) |
70
+ | `X-Asy-Filename` | string | — | Suggested filename for Content-Disposition / storage key |
71
+ | `X-Asy-Disposition` | `inline` `attachment` | `inline` | Content-Disposition value |
72
+ | `X-Asy-Storage-Prefix` | string | env `S3_PREFIX` | Override storage key prefix for this request |
73
+ | `X-Asy-Storage-Bucket` | string | env `S3_BUCKET` | Override S3 bucket for this request |
74
+ | `Accept` | MIME type | — | Alternative to `X-Asy-Format` (e.g. `Accept: image/png`) |
75
+
76
+ **Response (inline/binary):** Raw bytes with `Content-Type` matching the format (e.g. `application/pdf`, `image/png`).
77
+
78
+ **Response (inline/base64):**
79
+ ```json
80
+ {
81
+ "ok": true,
82
+ "mode": "inline",
83
+ "encoding": "base64",
84
+ "format": "pdf",
85
+ "mime": "application/pdf",
86
+ "size": 5989,
87
+ "data": "JVBERi0xLjU..."
88
+ }
89
+ ```
90
+
91
+ **Response (url mode):**
92
+ ```json
93
+ {
94
+ "ok": true,
95
+ "mode": "url",
96
+ "format": "pdf",
97
+ "mime": "application/pdf",
98
+ "size": 5989,
99
+ "url": "http://host/files/...",
100
+ "key": "files/abc123.pdf",
101
+ "urls": [{"url": "...", "key": "...", "mime": "...", "size": 5989}]
102
+ }
103
+ ```
104
+
105
+ ### `GET /`
106
+
107
+ Service info (version, formats, storage health, defaults).
108
+
109
+ ### `GET /healthz`
110
+
111
+ Health check — returns 200 if storage is writable, 503 otherwise.
112
+
113
+ ### `GET /files/{key}`
114
+
115
+ Serves files from local storage (only available when `ASYAGENT_STORAGE=local`).
116
+
117
+ ## Configuration
118
+
119
+ All configuration via environment variables:
120
+
121
+ ### Server
122
+
123
+ | Variable | Default | Description |
124
+ |----------|---------|-------------|
125
+ | `ASYAGENT_HOST` | `0.0.0.0` | Listen address |
126
+ | `ASYAGENT_PORT` | `8787` | Listen port |
127
+ | `ASYAGENT_MAX_WORKERS` | `16` | Max concurrent compiles (semaphore) |
128
+ | `ASYAGENT_COMPILE_TIMEOUT` | `60` | Default compile timeout (s) |
129
+ | `ASYAGENT_MAX_TIMEOUT` | `300` | Max allowed client-requested timeout |
130
+ | `ASYAGENT_FETCH_TIMEOUT` | `20` | URL fetch timeout (s) |
131
+ | `ASYAGENT_MAX_SOURCE_BYTES` | `1048576` | Max request body size |
132
+ | `ASYAGENT_MAX_FETCH_BYTES` | `5242880` | Max remote file size for URL input |
133
+ | `ASYAGENT_ASY_BIN` | `asy` | Path to asy binary |
134
+ | `ASYAGENT_GS_BIN` | `gs` | Path to ghostscript binary |
135
+ | `ASYAGENT_DEFAULT_FORMAT` | `pdf` | Default output format |
136
+ | `ASYAGENT_DEFAULT_MODE` | `inline` | Default response mode |
137
+ | `ASYAGENT_DEFAULT_ENCODING` | `binary` | Default inline encoding |
138
+ | `ASYAGENT_DEFAULT_DPI` | `150` | Default raster DPI |
139
+ | `ASYAGENT_TMP_DIR` | — | Override temp directory for compile working dirs |
140
+
141
+ ### Storage
142
+
143
+ | Variable | Default | Description |
144
+ |----------|---------|-------------|
145
+ | `ASYAGENT_STORAGE` | `local` | Backend: `local`, `s3`, or `none` |
146
+ | `ASYAGENT_LOCAL_DIR` | `./storage` | Local storage directory |
147
+ | `ASYAGENT_LOCAL_BASE_URL` | auto | Base URL for local file serving |
148
+
149
+ ### S3 / Object Storage
150
+
151
+ | Variable | Default | Description |
152
+ |----------|---------|-------------|
153
+ | `S3_BUCKET` | — | Bucket name (required for S3) |
154
+ | `S3_PREFIX` | `asyagent/` | Key prefix |
155
+ | `S3_ENDPOINT` | auto | Custom endpoint (e.g. `http://minio:9000`) |
156
+ | `S3_URL_STYLE` | `path` | `path` or `virtual` (virtual-hosted) |
157
+ | `S3_PUBLIC_BASE_URL` | — | Override the public URL base (e.g. CDN domain) |
158
+ | `S3_PRESIGN` | `false` | Return presigned URLs instead of public URLs |
159
+ | `S3_PRESIGN_EXPIRES` | `3600` | Presigned URL expiry (s) |
160
+ | `S3_USE_TLS` | `true` | Use HTTPS for S3 API calls |
161
+ | `AWS_ACCESS_KEY_ID` | — | Access key |
162
+ | `AWS_SECRET_ACCESS_KEY` | — | Secret key |
163
+ | `AWS_SESSION_TOKEN` | — | STS session token (optional) |
164
+ | `AWS_REGION` | `us-east-1` | Region |
165
+
166
+ ## Architecture
167
+
168
+ ```
169
+ asyagent/
170
+ __init__.py # package metadata
171
+ __main__.py # python -m asyagent entry point
172
+ config.py # Settings dataclass, env-driven
173
+ errors.py # typed exception hierarchy
174
+ sigv4.py # AWS Signature V4 (sign + presign), zero-dep
175
+ fetcher.py # URL -> source text fetcher
176
+ compiler.py # asy invocation + gs rasterization + ZIP bundling
177
+ storage.py # Local / S3 / None storage backends
178
+ server.py # ThreadingHTTPServer, header-driven rendering
179
+ tests/
180
+ test_sigv4.py # KAT against AWS test vector (AKIDEXAMPLE)
181
+ test_compiler.py # all formats, multi-shipout, errors
182
+ test_server.py # end-to-end integration tests
183
+ examples/
184
+ unit_circle.asy # example source
185
+ function_plot.asy # example source
186
+ multi_page.asy # multi-shipout example
187
+ client.py # example client script
188
+ ```
189
+
190
+ ### Request flow
191
+
192
+ ```
193
+ Client ──POST /v1/render──▶ server.py
194
+ │ │
195
+ │ Headers: X-Asy-Format, │ RenderContext (parses all X-Asy-* headers)
196
+ │ X-Asy-Mode, etc. │
197
+ │ ├── text/plain body ──▶ _resolve_source (auto-detect url/source)
198
+ │ ├── application/json ──▶ _resolve_source (json.source or json.url)
199
+ │ │ └── fetcher.py (if url)
200
+ │ │
201
+ │ ├── compile_source (semaphore-gated)
202
+ │ │ ├── asy -f pdf -o out input.asy (native: pdf/svg/eps)
203
+ │ │ ├── gs -sDEVICE=pngalpha ... (raster: png/jpg)
204
+ │ │ └── select_or_bundle (ZIP if multiple outputs)
205
+ │ │
206
+ │ ├── mode=inline,encoding=binary ──▶ raw bytes + Content-Type
207
+ │ ├── mode=inline,encoding=base64 ──▶ JSON {data: base64...}
208
+ │ └── mode=url ──▶ storage.upload ──▶ JSON {url: ...}
209
+
210
+ ◀── response (binary / JSON) ─┘
211
+ ```
212
+
213
+ ### Response mode comparison
214
+
215
+ | Mode | Encoding | Response body | Content-Type | Use case |
216
+ |------|----------|---------------|-------------|----------|
217
+ | `inline` | `binary` | Raw compiled bytes | matches format | Direct download, browser display |
218
+ | `inline` | `base64` | JSON `{data: "..."}` | `application/json` | API compositing, embedding in JSON workflows |
219
+ | `url` | — | JSON `{url: "..."}` | `application/json` | Large files, CDN delivery, async workflows |
220
+
221
+ ## Running tests
222
+
223
+ ```bash
224
+ python3 -m unittest discover -s tests -v
225
+ ```
226
+
227
+ ## Docker
228
+
229
+ ```bash
230
+ docker build -t asyagent .
231
+ docker run -p 8787:8787 asyagent
232
+ ```
233
+
234
+ ## License
235
+
236
+ MIT
@@ -0,0 +1,10 @@
1
+ """asyagent — an HTTP service that compiles Asymptote (*.asy) sources and
2
+ returns the rendered output either inline (binary/base64) or as an
3
+ object-storage URL.
4
+
5
+ Pure Python 3.10+ standard library. No third-party dependencies.
6
+ """
7
+
8
+ __version__ = "0.1.0"
9
+
10
+ __all__ = ["__version__"]
@@ -0,0 +1,20 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+
5
+ from .config import Settings
6
+ from .server import run
7
+
8
+
9
+ def main() -> int:
10
+ settings = Settings.from_env()
11
+ try:
12
+ run(settings)
13
+ except OSError as e:
14
+ print(f"asyagent: failed to start server: {e}", file=sys.stderr)
15
+ return 1
16
+ return 0
17
+
18
+
19
+ if __name__ == "__main__":
20
+ raise SystemExit(main())