functualize-http 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.
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
"""Functualize HTTP Plugin - HTTP delivery adapter using asyncio.
|
|
2
|
+
|
|
3
|
+
Provides HTTP serving support both as a standalone adapter (HttpAdapter)
|
|
4
|
+
and as a CLI command plugin (HttpServerPlugin). Both share a common
|
|
5
|
+
HttpServerCore class containing route building, request handling, and
|
|
6
|
+
async-to-sync bridging logic.
|
|
7
|
+
|
|
8
|
+
Uses Python's stdlib asyncio for a lightweight HTTP server without
|
|
9
|
+
heavy dependencies (no uvicorn/starlette required).
|
|
10
|
+
|
|
11
|
+
Key design:
|
|
12
|
+
- HttpServerCore: shared class with route building, request handling,
|
|
13
|
+
async-to-sync bridging via asyncio.to_thread()
|
|
14
|
+
- HttpAdapter: satisfies AdapterPlugin Protocol, adapter_type="http"
|
|
15
|
+
- HttpServerPlugin: capability plugin registering a "serve" command
|
|
16
|
+
|
|
17
|
+
The kernel stays synchronous — the adapter owns the event loop internally
|
|
18
|
+
via asyncio.run().
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import asyncio
|
|
24
|
+
import contextlib
|
|
25
|
+
import json
|
|
26
|
+
import logging
|
|
27
|
+
from dataclasses import dataclass
|
|
28
|
+
from typing import TYPE_CHECKING, Any
|
|
29
|
+
|
|
30
|
+
if TYPE_CHECKING:
|
|
31
|
+
from asyncio import AbstractEventLoop
|
|
32
|
+
|
|
33
|
+
from functualize.app.core import FunctualizeApp
|
|
34
|
+
|
|
35
|
+
logger = logging.getLogger(__name__)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True)
|
|
39
|
+
class PluginMetadata:
|
|
40
|
+
"""Metadata for the functualize-http plugin package."""
|
|
41
|
+
|
|
42
|
+
name: str = "functualize-http"
|
|
43
|
+
version: str = "0.1.0"
|
|
44
|
+
description: str = "HTTP delivery adapter plugin for functualize using asyncio"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class HttpServerCore:
|
|
48
|
+
"""Shared HTTP server logic for route building and request handling.
|
|
49
|
+
|
|
50
|
+
Contains:
|
|
51
|
+
- Route building: maps each job to POST /jobs/{job_name}/execute
|
|
52
|
+
- Request handling: parse JSON body as kwargs, call app.execute()
|
|
53
|
+
- Async-to-sync bridging: asyncio.to_thread() for kernel execution
|
|
54
|
+
- Response formatting: JSON response with status and result
|
|
55
|
+
- Health endpoint: GET /health returns 200
|
|
56
|
+
- Job listing: GET /jobs returns available jobs
|
|
57
|
+
|
|
58
|
+
Both HttpAdapter and HttpServerPlugin use this class internally
|
|
59
|
+
to avoid code duplication.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
def __init__(self, app: FunctualizeApp) -> None:
|
|
63
|
+
self._app = app
|
|
64
|
+
self._server: asyncio.Server | None = None
|
|
65
|
+
|
|
66
|
+
async def start(self, host: str, port: int) -> None:
|
|
67
|
+
"""Start the HTTP server (async).
|
|
68
|
+
|
|
69
|
+
Blocks until the server is shut down via stop().
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
host: Host address to bind to.
|
|
73
|
+
port: Port number to listen on.
|
|
74
|
+
"""
|
|
75
|
+
self._server = await asyncio.start_server(self._handle_connection, host, port)
|
|
76
|
+
addrs = ", ".join(str(s.getsockname()) for s in self._server.sockets)
|
|
77
|
+
logger.info(f"HTTP server listening on {addrs}")
|
|
78
|
+
async with self._server:
|
|
79
|
+
await self._server.serve_forever()
|
|
80
|
+
|
|
81
|
+
async def stop(self) -> None:
|
|
82
|
+
"""Stop the HTTP server gracefully."""
|
|
83
|
+
if self._server is not None:
|
|
84
|
+
self._server.close()
|
|
85
|
+
await self._server.wait_closed()
|
|
86
|
+
self._server = None
|
|
87
|
+
|
|
88
|
+
def build_routes(self) -> dict[str, dict[str, Any]]:
|
|
89
|
+
"""Build route map from registered jobs.
|
|
90
|
+
|
|
91
|
+
Returns a dict mapping (method, path) tuples conceptually:
|
|
92
|
+
- GET /health
|
|
93
|
+
- GET /jobs
|
|
94
|
+
- POST /jobs/{job_name}/execute for each job
|
|
95
|
+
|
|
96
|
+
This is used internally for request routing.
|
|
97
|
+
"""
|
|
98
|
+
routes: dict[str, dict[str, Any]] = {}
|
|
99
|
+
for descriptor in self._app.get_jobs():
|
|
100
|
+
route_path = f"/jobs/{descriptor.name}/execute"
|
|
101
|
+
routes[route_path] = {
|
|
102
|
+
"method": "POST",
|
|
103
|
+
"job_name": descriptor.name,
|
|
104
|
+
}
|
|
105
|
+
return routes
|
|
106
|
+
|
|
107
|
+
async def handle_request(
|
|
108
|
+
self, method: str, path: str, body: bytes
|
|
109
|
+
) -> tuple[int, dict[str, Any]]:
|
|
110
|
+
"""Route and handle a single HTTP request.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
method: HTTP method (GET, POST, etc.)
|
|
114
|
+
path: Request path.
|
|
115
|
+
body: Request body bytes.
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
Tuple of (status_code, response_dict).
|
|
119
|
+
"""
|
|
120
|
+
# Health check
|
|
121
|
+
if method == "GET" and path == "/health":
|
|
122
|
+
return 200, {"status": "healthy"}
|
|
123
|
+
|
|
124
|
+
# List jobs
|
|
125
|
+
if method == "GET" and path == "/jobs":
|
|
126
|
+
jobs = self._app.get_jobs()
|
|
127
|
+
job_list = [
|
|
128
|
+
{
|
|
129
|
+
"name": d.name,
|
|
130
|
+
"group": d.group,
|
|
131
|
+
"docstring": d.docstring,
|
|
132
|
+
}
|
|
133
|
+
for d in jobs
|
|
134
|
+
]
|
|
135
|
+
return 200, {"jobs": job_list}
|
|
136
|
+
|
|
137
|
+
# Execute job: POST /jobs/{job_name}/execute
|
|
138
|
+
if method == "POST" and path.startswith("/jobs/") and path.endswith("/execute"):
|
|
139
|
+
# Extract job name from path
|
|
140
|
+
parts = path.split("/")
|
|
141
|
+
# Expected: ["", "jobs", "<job_name>", "execute"]
|
|
142
|
+
if len(parts) == 4:
|
|
143
|
+
job_name = parts[2]
|
|
144
|
+
return await self._execute_job(job_name, body)
|
|
145
|
+
|
|
146
|
+
# Not found
|
|
147
|
+
return 404, {"error": "Not found", "path": path}
|
|
148
|
+
|
|
149
|
+
async def _execute_job(
|
|
150
|
+
self, job_name: str, body: bytes
|
|
151
|
+
) -> tuple[int, dict[str, Any]]:
|
|
152
|
+
"""Execute a job with kwargs from the request body.
|
|
153
|
+
|
|
154
|
+
Uses asyncio.to_thread() to bridge the synchronous kernel
|
|
155
|
+
execution into the async server context.
|
|
156
|
+
"""
|
|
157
|
+
# Parse body
|
|
158
|
+
kwargs: dict[str, Any] = {}
|
|
159
|
+
if body:
|
|
160
|
+
try:
|
|
161
|
+
kwargs = json.loads(body)
|
|
162
|
+
except (json.JSONDecodeError, UnicodeDecodeError) as e:
|
|
163
|
+
return 400, {"error": f"Invalid JSON body: {e}"}
|
|
164
|
+
|
|
165
|
+
if not isinstance(kwargs, dict):
|
|
166
|
+
return 400, {"error": "Request body must be a JSON object"}
|
|
167
|
+
|
|
168
|
+
# Check job exists
|
|
169
|
+
job = self._app.get_job(job_name)
|
|
170
|
+
if job is None:
|
|
171
|
+
return 404, {"error": f"Job '{job_name}' not found"}
|
|
172
|
+
|
|
173
|
+
# Execute via asyncio.to_thread (async-to-sync bridge)
|
|
174
|
+
try:
|
|
175
|
+
result = await asyncio.to_thread(self._app.execute, job_name, **kwargs)
|
|
176
|
+
return 200, {
|
|
177
|
+
"status": result.status.value
|
|
178
|
+
if hasattr(result.status, "value")
|
|
179
|
+
else str(result.status),
|
|
180
|
+
"duration_ms": result.duration_ms,
|
|
181
|
+
"return_value": self._serialize_return_value(result.return_value),
|
|
182
|
+
}
|
|
183
|
+
except Exception as e:
|
|
184
|
+
logger.exception(f"Error executing job '{job_name}'")
|
|
185
|
+
return 500, {"error": str(e)}
|
|
186
|
+
|
|
187
|
+
@staticmethod
|
|
188
|
+
def _serialize_return_value(value: Any) -> Any:
|
|
189
|
+
"""Attempt to serialize a return value to JSON-compatible form."""
|
|
190
|
+
if value is None:
|
|
191
|
+
return None
|
|
192
|
+
if isinstance(value, str | int | float | bool):
|
|
193
|
+
return value
|
|
194
|
+
if isinstance(value, list | tuple):
|
|
195
|
+
return [HttpServerCore._serialize_return_value(v) for v in value]
|
|
196
|
+
if isinstance(value, dict):
|
|
197
|
+
return {
|
|
198
|
+
str(k): HttpServerCore._serialize_return_value(v)
|
|
199
|
+
for k, v in value.items()
|
|
200
|
+
}
|
|
201
|
+
# Fall back to string representation
|
|
202
|
+
return str(value)
|
|
203
|
+
|
|
204
|
+
async def _handle_connection(
|
|
205
|
+
self,
|
|
206
|
+
reader: asyncio.StreamReader,
|
|
207
|
+
writer: asyncio.StreamWriter,
|
|
208
|
+
) -> None:
|
|
209
|
+
"""Handle a single TCP connection (HTTP/1.1 basic parsing)."""
|
|
210
|
+
try:
|
|
211
|
+
# Read request line
|
|
212
|
+
request_line = await reader.readline()
|
|
213
|
+
if not request_line:
|
|
214
|
+
writer.close()
|
|
215
|
+
await writer.wait_closed()
|
|
216
|
+
return
|
|
217
|
+
|
|
218
|
+
request_str = request_line.decode("utf-8", errors="replace").strip()
|
|
219
|
+
parts = request_str.split(" ")
|
|
220
|
+
if len(parts) < 2:
|
|
221
|
+
await self._send_response(writer, 400, {"error": "Bad request"})
|
|
222
|
+
return
|
|
223
|
+
|
|
224
|
+
method = parts[0].upper()
|
|
225
|
+
path = parts[1].split("?")[0] # Strip query params
|
|
226
|
+
|
|
227
|
+
# Read headers
|
|
228
|
+
content_length = 0
|
|
229
|
+
while True:
|
|
230
|
+
header_line = await reader.readline()
|
|
231
|
+
if header_line in (b"\r\n", b"\n", b""):
|
|
232
|
+
break
|
|
233
|
+
header_str = header_line.decode("utf-8", errors="replace").strip()
|
|
234
|
+
if header_str.lower().startswith("content-length:"):
|
|
235
|
+
with contextlib.suppress(ValueError):
|
|
236
|
+
content_length = int(header_str.split(":", 1)[1].strip())
|
|
237
|
+
|
|
238
|
+
# Read body
|
|
239
|
+
body = b""
|
|
240
|
+
if content_length > 0:
|
|
241
|
+
body = await reader.readexactly(content_length)
|
|
242
|
+
|
|
243
|
+
# Handle request
|
|
244
|
+
status_code, response_body = await self.handle_request(method, path, body)
|
|
245
|
+
await self._send_response(writer, status_code, response_body)
|
|
246
|
+
|
|
247
|
+
except (ConnectionResetError, asyncio.IncompleteReadError):
|
|
248
|
+
pass
|
|
249
|
+
except Exception:
|
|
250
|
+
logger.exception("Error handling HTTP connection")
|
|
251
|
+
with contextlib.suppress(Exception):
|
|
252
|
+
await self._send_response(
|
|
253
|
+
writer, 500, {"error": "Internal server error"}
|
|
254
|
+
)
|
|
255
|
+
finally:
|
|
256
|
+
try:
|
|
257
|
+
writer.close()
|
|
258
|
+
await writer.wait_closed()
|
|
259
|
+
except Exception:
|
|
260
|
+
pass
|
|
261
|
+
|
|
262
|
+
@staticmethod
|
|
263
|
+
async def _send_response(
|
|
264
|
+
writer: asyncio.StreamWriter,
|
|
265
|
+
status_code: int,
|
|
266
|
+
body: dict[str, Any],
|
|
267
|
+
) -> None:
|
|
268
|
+
"""Send an HTTP response with JSON body."""
|
|
269
|
+
status_messages = {
|
|
270
|
+
200: "OK",
|
|
271
|
+
400: "Bad Request",
|
|
272
|
+
404: "Not Found",
|
|
273
|
+
500: "Internal Server Error",
|
|
274
|
+
}
|
|
275
|
+
status_text = status_messages.get(status_code, "Unknown")
|
|
276
|
+
body_bytes = json.dumps(body).encode("utf-8")
|
|
277
|
+
|
|
278
|
+
response = (
|
|
279
|
+
f"HTTP/1.1 {status_code} {status_text}\r\n"
|
|
280
|
+
f"Content-Type: application/json\r\n"
|
|
281
|
+
f"Content-Length: {len(body_bytes)}\r\n"
|
|
282
|
+
f"Connection: close\r\n"
|
|
283
|
+
f"\r\n"
|
|
284
|
+
).encode() + body_bytes
|
|
285
|
+
|
|
286
|
+
writer.write(response)
|
|
287
|
+
await writer.drain()
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
class HttpAdapter:
|
|
291
|
+
"""HTTP delivery adapter — satisfies AdapterPlugin Protocol.
|
|
292
|
+
|
|
293
|
+
Starts an async HTTP server that blocks until shutdown, exposing
|
|
294
|
+
all registered jobs as HTTP endpoints.
|
|
295
|
+
|
|
296
|
+
The adapter owns the event loop internally via asyncio.run(),
|
|
297
|
+
keeping the kernel synchronous.
|
|
298
|
+
|
|
299
|
+
Usage:
|
|
300
|
+
app = FunctualizeApp("myapp", job_sources=...)
|
|
301
|
+
adapter = HttpAdapter()
|
|
302
|
+
adapter(app)
|
|
303
|
+
adapter.run(host="0.0.0.0", port=8000)
|
|
304
|
+
"""
|
|
305
|
+
|
|
306
|
+
name: str = "functualize-http"
|
|
307
|
+
version: str = "1.0.0"
|
|
308
|
+
description: str = "HTTP delivery adapter plugin for functualize using asyncio"
|
|
309
|
+
adapter_type: str = "http"
|
|
310
|
+
|
|
311
|
+
def __init__(self) -> None:
|
|
312
|
+
self._app: FunctualizeApp | None = None
|
|
313
|
+
self._core: HttpServerCore | None = None
|
|
314
|
+
|
|
315
|
+
def __call__(self, app: FunctualizeApp) -> None:
|
|
316
|
+
"""Setup phase — store app reference and create server core.
|
|
317
|
+
|
|
318
|
+
Args:
|
|
319
|
+
app: The FunctualizeApp kernel instance.
|
|
320
|
+
"""
|
|
321
|
+
self._app = app
|
|
322
|
+
self._core = HttpServerCore(app)
|
|
323
|
+
|
|
324
|
+
def run(self, *args: Any, **kwargs: Any) -> Any:
|
|
325
|
+
"""Start the HTTP server (blocking).
|
|
326
|
+
|
|
327
|
+
Accepts keyword arguments:
|
|
328
|
+
host: Host address to bind to (default: "0.0.0.0").
|
|
329
|
+
port: Port number to listen on (default: 8000).
|
|
330
|
+
|
|
331
|
+
The adapter creates and runs an asyncio event loop internally.
|
|
332
|
+
This method blocks until shutdown() is called or the server
|
|
333
|
+
is interrupted.
|
|
334
|
+
"""
|
|
335
|
+
if self._core is None:
|
|
336
|
+
raise RuntimeError("HttpAdapter.run() called before __call__(app)")
|
|
337
|
+
|
|
338
|
+
host = kwargs.get("host", "0.0.0.0")
|
|
339
|
+
port = kwargs.get("port", 8000)
|
|
340
|
+
|
|
341
|
+
asyncio.run(self._core.start(host, port))
|
|
342
|
+
|
|
343
|
+
def shutdown(self) -> None:
|
|
344
|
+
"""Graceful shutdown — stops the HTTP server."""
|
|
345
|
+
if self._core is not None and self._core._server is not None:
|
|
346
|
+
# Schedule the stop coroutine on the running loop
|
|
347
|
+
loop = self._get_running_loop()
|
|
348
|
+
if loop is not None and loop.is_running():
|
|
349
|
+
loop.call_soon_threadsafe(
|
|
350
|
+
lambda: asyncio.ensure_future(self._core.stop()) # type: ignore[union-attr]
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
@staticmethod
|
|
354
|
+
def _get_running_loop() -> AbstractEventLoop | None:
|
|
355
|
+
"""Get the currently running event loop, if any."""
|
|
356
|
+
try:
|
|
357
|
+
return asyncio.get_running_loop()
|
|
358
|
+
except RuntimeError:
|
|
359
|
+
return None
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
class HttpServerPlugin:
|
|
363
|
+
"""Capability plugin that registers a 'serve' CLI command.
|
|
364
|
+
|
|
365
|
+
This plugin registers a `serve` command via
|
|
366
|
+
`app.register_plugin_command()`. When invoked, the serve command
|
|
367
|
+
starts an HTTP server using the shared HttpServerCore.
|
|
368
|
+
|
|
369
|
+
Usage:
|
|
370
|
+
app = FunctualizeApp("myapp", ...)
|
|
371
|
+
plugin = HttpServerPlugin()
|
|
372
|
+
plugin(app)
|
|
373
|
+
# The 'serve' command is now available in the CLI
|
|
374
|
+
|
|
375
|
+
The plugin is NOT an adapter — it augments the CLI adapter with
|
|
376
|
+
an HTTP serving command.
|
|
377
|
+
"""
|
|
378
|
+
|
|
379
|
+
name: str = "functualize-http-server"
|
|
380
|
+
version: str = "1.0.0"
|
|
381
|
+
description: str = "Registers a 'serve' command for HTTP serving"
|
|
382
|
+
|
|
383
|
+
def __init__(self) -> None:
|
|
384
|
+
self._app: FunctualizeApp | None = None
|
|
385
|
+
self._core: HttpServerCore | None = None
|
|
386
|
+
|
|
387
|
+
def __call__(self, app: FunctualizeApp) -> None:
|
|
388
|
+
"""Register the 'serve' command on the app.
|
|
389
|
+
|
|
390
|
+
Args:
|
|
391
|
+
app: The FunctualizeApp kernel instance.
|
|
392
|
+
"""
|
|
393
|
+
self._app = app
|
|
394
|
+
self._core = HttpServerCore(app)
|
|
395
|
+
|
|
396
|
+
app.register_plugin_command(
|
|
397
|
+
name="serve",
|
|
398
|
+
callback=self._serve_command,
|
|
399
|
+
help_text="Start an HTTP server exposing all jobs as endpoints",
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
def _serve_command(
|
|
403
|
+
self,
|
|
404
|
+
host: str = "0.0.0.0",
|
|
405
|
+
port: int = 8000,
|
|
406
|
+
) -> None:
|
|
407
|
+
"""Start the HTTP server (CLI command handler).
|
|
408
|
+
|
|
409
|
+
Args:
|
|
410
|
+
host: Host address to bind to.
|
|
411
|
+
port: Port number to listen on.
|
|
412
|
+
"""
|
|
413
|
+
if self._core is None:
|
|
414
|
+
raise RuntimeError("HttpServerPlugin not initialized")
|
|
415
|
+
asyncio.run(self._core.start(host, port))
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
__all__ = [
|
|
419
|
+
"HttpAdapter",
|
|
420
|
+
"HttpServerCore",
|
|
421
|
+
"HttpServerPlugin",
|
|
422
|
+
"PluginMetadata",
|
|
423
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: functualize-http
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: HTTP delivery adapter plugin for functualize using asyncio
|
|
5
|
+
Author-email: Mohammad Hakim Adiprasetya <viltohmyst@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Classifier: Development Status :: 3 - Alpha
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
12
|
+
Classifier: Typing :: Typed
|
|
13
|
+
Requires-Python: >=3.11
|
|
14
|
+
Requires-Dist: functualize<1.0.0,>=0.1.0
|
|
15
|
+
Provides-Extra: dev
|
|
16
|
+
Requires-Dist: pytest-cov>=4.1.0; extra == 'dev'
|
|
17
|
+
Requires-Dist: pytest>=7.4.0; extra == 'dev'
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# functualize-http
|
|
21
|
+
|
|
22
|
+
> **Status: Published** — Independently installable from PyPI.
|
|
23
|
+
|
|
24
|
+
HTTP delivery adapter plugin for functualize using Python's stdlib asyncio.
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install functualize-http
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Usage
|
|
33
|
+
|
|
34
|
+
### As an Adapter
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from functualize.app import FunctualizeApp
|
|
38
|
+
from functualize_http import HttpAdapter
|
|
39
|
+
|
|
40
|
+
app = FunctualizeApp("myapp", job_sources=...)
|
|
41
|
+
adapter = HttpAdapter()
|
|
42
|
+
adapter(app)
|
|
43
|
+
adapter.run(host="0.0.0.0", port=8000)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### As a CLI Plugin
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
from functualize.app import FunctualizeApp
|
|
50
|
+
from functualize_http import HttpServerPlugin
|
|
51
|
+
|
|
52
|
+
app = FunctualizeApp("myapp", job_sources=...)
|
|
53
|
+
plugin = HttpServerPlugin()
|
|
54
|
+
plugin(app)
|
|
55
|
+
# The 'serve' command is now available in the CLI
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Endpoints
|
|
59
|
+
|
|
60
|
+
- `GET /health` — Health check
|
|
61
|
+
- `GET /jobs` — List available jobs
|
|
62
|
+
- `POST /jobs/{job_name}/execute` — Execute a job with JSON body as kwargs
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
functualize_http/__init__.py,sha256=v15kr5NP5GTO7S2vVsaW0LvPixkIoMyTliCQ3xzfhz4,14260
|
|
2
|
+
functualize_http/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
+
functualize_http-0.1.0.dist-info/METADATA,sha256=niFzZMgmCht4vqoDGRw40rDaZR4Bm1_SBLg6E94IBcw,1608
|
|
4
|
+
functualize_http-0.1.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
5
|
+
functualize_http-0.1.0.dist-info/entry_points.txt,sha256=yjU7i50VJJ3eyZS9C68CC6hptTp3IZmeeNYC8qfdeCQ,58
|
|
6
|
+
functualize_http-0.1.0.dist-info/RECORD,,
|