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.
- asyagent-0.1.0/PKG-INFO +262 -0
- asyagent-0.1.0/README.md +236 -0
- asyagent-0.1.0/asyagent/__init__.py +10 -0
- asyagent-0.1.0/asyagent/__main__.py +20 -0
- asyagent-0.1.0/asyagent/compiler.py +218 -0
- asyagent-0.1.0/asyagent/config.py +158 -0
- asyagent-0.1.0/asyagent/errors.py +73 -0
- asyagent-0.1.0/asyagent/fetcher.py +56 -0
- asyagent-0.1.0/asyagent/py.typed +0 -0
- asyagent-0.1.0/asyagent/server.py +362 -0
- asyagent-0.1.0/asyagent/sigv4.py +229 -0
- asyagent-0.1.0/asyagent/storage.py +252 -0
- asyagent-0.1.0/asyagent.egg-info/PKG-INFO +262 -0
- asyagent-0.1.0/asyagent.egg-info/SOURCES.txt +20 -0
- asyagent-0.1.0/asyagent.egg-info/dependency_links.txt +1 -0
- asyagent-0.1.0/asyagent.egg-info/entry_points.txt +2 -0
- asyagent-0.1.0/asyagent.egg-info/top_level.txt +1 -0
- asyagent-0.1.0/pyproject.toml +46 -0
- asyagent-0.1.0/setup.cfg +4 -0
- asyagent-0.1.0/tests/test_compiler.py +121 -0
- asyagent-0.1.0/tests/test_server.py +315 -0
- asyagent-0.1.0/tests/test_sigv4.py +211 -0
asyagent-0.1.0/PKG-INFO
ADDED
|
@@ -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
|
asyagent-0.1.0/README.md
ADDED
|
@@ -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())
|