pineforge-data 0.2.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,646 @@
1
+ """Bounded-concurrency FastAPI service backed by pineforge-release."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import hashlib
7
+ import json
8
+ import os
9
+ import platform
10
+ import re
11
+ import secrets
12
+ import shutil
13
+ import signal
14
+ import tempfile
15
+ from collections.abc import Awaitable, Callable, Mapping
16
+ from dataclasses import dataclass
17
+ from pathlib import Path
18
+ from typing import Annotated, Literal, TypeAlias
19
+ from uuid import uuid4
20
+
21
+ from fastapi import FastAPI, Header
22
+ from fastapi.responses import JSONResponse
23
+ from pydantic import BaseModel, ConfigDict, Field, model_validator
24
+
25
+ from .backtest import BacktestOptions
26
+ from .compile_cache import CompileCache
27
+ from .models import Bar, Instrument
28
+ from .release_contract import (
29
+ DEFAULT_RELEASE_IMAGE,
30
+ RELEASE_ENTRYPOINT,
31
+ ReleaseContractError,
32
+ parse_release_report,
33
+ release_environment,
34
+ release_response,
35
+ write_release_inputs,
36
+ )
37
+
38
+ InputValue: TypeAlias = str | int | float | bool
39
+ MagnifierName = Literal[
40
+ "uniform",
41
+ "cosine",
42
+ "triangle",
43
+ "endpoints",
44
+ "front_loaded",
45
+ "back_loaded",
46
+ ]
47
+ _REQUEST_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$")
48
+
49
+
50
+ class ApiInstrument(BaseModel):
51
+ model_config = ConfigDict(extra="forbid")
52
+
53
+ symbol: str = Field(min_length=1, max_length=256)
54
+ venue: str = Field(default="", max_length=128)
55
+ timezone: str = Field(default="UTC", min_length=1, max_length=128)
56
+ session: str = Field(default="24x7", min_length=1, max_length=256)
57
+ volume_unit: str = Field(default="base", min_length=1, max_length=64)
58
+
59
+
60
+ class ApiBar(BaseModel):
61
+ model_config = ConfigDict(extra="forbid", allow_inf_nan=False)
62
+
63
+ timestamp_ms: int = Field(ge=0, le=2**63 - 1)
64
+ open: float = Field(gt=0)
65
+ high: float = Field(gt=0)
66
+ low: float = Field(gt=0)
67
+ close: float = Field(gt=0)
68
+ volume: float = Field(ge=0)
69
+
70
+ @model_validator(mode="after")
71
+ def validate_ohlc(self) -> ApiBar:
72
+ if self.high < max(self.open, self.close, self.low):
73
+ raise ValueError("high must be greater than or equal to OHLC values")
74
+ if self.low > min(self.open, self.close, self.high):
75
+ raise ValueError("low must be less than or equal to OHLC values")
76
+ return self
77
+
78
+
79
+ class ApiBacktestOptions(BaseModel):
80
+ model_config = ConfigDict(extra="forbid")
81
+
82
+ input_timeframe: str = Field(default="", max_length=32)
83
+ script_timeframe: str = Field(default="", max_length=32)
84
+ bar_magnifier: bool = False
85
+ magnifier_samples: int = Field(default=4, ge=1, le=10_000)
86
+ magnifier_distribution: MagnifierName = "endpoints"
87
+ trace_enabled: bool = False
88
+ chart_timezone: str | None = Field(default=None, max_length=128)
89
+ trade_start_time_ms: int | None = Field(default=None, ge=0, le=2**63 - 1)
90
+
91
+
92
+ class BacktestApiRequest(BaseModel):
93
+ model_config = ConfigDict(extra="forbid", allow_inf_nan=False)
94
+
95
+ pine_source: str = Field(min_length=1, max_length=2_000_000)
96
+ bars: list[ApiBar] = Field(min_length=1, max_length=1_000_000)
97
+ instrument: ApiInstrument
98
+ source: str = Field(default="provider", min_length=1, max_length=256)
99
+ options: ApiBacktestOptions = Field(default_factory=ApiBacktestOptions)
100
+ strategy_params: dict[str, InputValue] = Field(default_factory=dict)
101
+ strategy_overrides: dict[str, InputValue] = Field(default_factory=dict)
102
+
103
+ @model_validator(mode="after")
104
+ def validate_timestamps(self) -> BacktestApiRequest:
105
+ if any(
106
+ left.timestamp_ms >= right.timestamp_ms
107
+ for left, right in zip(self.bars, self.bars[1:], strict=False)
108
+ ):
109
+ raise ValueError("bar timestamps must be strictly increasing")
110
+ return self
111
+
112
+ def domain_values(
113
+ self,
114
+ ) -> tuple[str, list[Bar], Instrument, BacktestOptions]:
115
+ instrument = Instrument(
116
+ self.instrument.symbol,
117
+ venue=self.instrument.venue,
118
+ timezone=self.instrument.timezone,
119
+ session=self.instrument.session,
120
+ volume_unit=self.instrument.volume_unit,
121
+ )
122
+ bars = [
123
+ Bar(
124
+ instrument,
125
+ value.timestamp_ms,
126
+ value.open,
127
+ value.high,
128
+ value.low,
129
+ value.close,
130
+ value.volume,
131
+ self.source,
132
+ )
133
+ for value in self.bars
134
+ ]
135
+ from .backtest import MagnifierDistribution
136
+
137
+ options = BacktestOptions(
138
+ input_timeframe=self.options.input_timeframe,
139
+ script_timeframe=self.options.script_timeframe,
140
+ bar_magnifier=self.options.bar_magnifier,
141
+ magnifier_samples=self.options.magnifier_samples,
142
+ magnifier_distribution=MagnifierDistribution[
143
+ self.options.magnifier_distribution.upper()
144
+ ],
145
+ trace_enabled=self.options.trace_enabled,
146
+ chart_timezone=self.options.chart_timezone,
147
+ trade_start_time_ms=self.options.trade_start_time_ms,
148
+ )
149
+ return self.pine_source, bars, instrument, options
150
+
151
+
152
+ class BacktestServiceError(RuntimeError):
153
+ def __init__(
154
+ self,
155
+ code: str,
156
+ message: str,
157
+ *,
158
+ status_code: int,
159
+ phase: str,
160
+ ) -> None:
161
+ super().__init__(message)
162
+ self.code = code
163
+ self.status_code = status_code
164
+ self.phase = phase
165
+
166
+
167
+ @dataclass(frozen=True, slots=True)
168
+ class ExecutionResult:
169
+ report: Mapping[str, object]
170
+ runtime_metadata: Mapping[str, object]
171
+
172
+
173
+ Executor = Callable[[BacktestApiRequest], Awaitable[ExecutionResult]]
174
+
175
+
176
+ def _positive_env(name: str, default: int) -> int:
177
+ raw = os.environ.get(name, str(default))
178
+ try:
179
+ value = int(raw)
180
+ except ValueError as exc:
181
+ raise RuntimeError(f"{name} must be an integer") from exc
182
+ if value <= 0:
183
+ raise RuntimeError(f"{name} must be positive")
184
+ return value
185
+
186
+
187
+ def _non_negative_env(name: str, default: int) -> int:
188
+ raw = os.environ.get(name, str(default))
189
+ try:
190
+ value = int(raw)
191
+ except ValueError as exc:
192
+ raise RuntimeError(f"{name} must be an integer") from exc
193
+ if value < 0:
194
+ raise RuntimeError(f"{name} must be non-negative")
195
+ return value
196
+
197
+
198
+ class BacktestService:
199
+ """Admit a bounded number of jobs and execute them in isolated directories."""
200
+
201
+ def __init__(
202
+ self,
203
+ *,
204
+ max_concurrency: int | None = None,
205
+ max_queue: int | None = None,
206
+ queue_timeout_seconds: float | None = None,
207
+ execution_timeout_seconds: float | None = None,
208
+ release_image: str | None = None,
209
+ entrypoint: Path | None = None,
210
+ run_json_path: Path | None = None,
211
+ cache: CompileCache | None = None,
212
+ executor: Executor | None = None,
213
+ ) -> None:
214
+ cpu_default = max(1, min(4, os.cpu_count() or 1))
215
+ self.max_concurrency = (
216
+ max_concurrency
217
+ if max_concurrency is not None
218
+ else _positive_env("PINEFORGE_SERVER_CONCURRENCY", cpu_default)
219
+ )
220
+ self.max_queue = (
221
+ max_queue
222
+ if max_queue is not None
223
+ else _non_negative_env("PINEFORGE_SERVER_MAX_QUEUE", self.max_concurrency * 2)
224
+ )
225
+ self.queue_timeout_seconds = (
226
+ queue_timeout_seconds
227
+ if queue_timeout_seconds is not None
228
+ else float(os.environ.get("PINEFORGE_SERVER_QUEUE_TIMEOUT", "30"))
229
+ )
230
+ self.execution_timeout_seconds = (
231
+ execution_timeout_seconds
232
+ if execution_timeout_seconds is not None
233
+ else float(os.environ.get("PINEFORGE_SERVER_EXECUTION_TIMEOUT", "300"))
234
+ )
235
+ if self.max_concurrency <= 0 or self.max_queue < 0:
236
+ raise ValueError("concurrency must be positive and max_queue non-negative")
237
+ if self.queue_timeout_seconds <= 0 or self.execution_timeout_seconds <= 0:
238
+ raise ValueError("queue and execution timeouts must be positive")
239
+ self.release_image = release_image or os.environ.get(
240
+ "PINEFORGE_RELEASE_IMAGE", DEFAULT_RELEASE_IMAGE
241
+ )
242
+ self.entrypoint = entrypoint or Path(
243
+ os.environ.get("PINEFORGE_RELEASE_ENTRYPOINT", RELEASE_ENTRYPOINT)
244
+ )
245
+ self.run_json_path = run_json_path or Path(
246
+ os.environ.get("PINEFORGE_RELEASE_RUN_JSON", "/opt/pineforge/bin/run_json.py")
247
+ )
248
+ self.cache = cache or CompileCache(
249
+ Path(os.environ.get("PINEFORGE_SERVER_CACHE_DIR", "/tmp/pineforge-compile-cache")),
250
+ max_entries=_positive_env("PINEFORGE_SERVER_CACHE_MAX_ENTRIES", 1_024),
251
+ max_bytes=_positive_env("PINEFORGE_SERVER_CACHE_MAX_BYTES", 2 * 1_024 * 1_024 * 1_024),
252
+ )
253
+ self._executor = executor or self._execute_release
254
+ self._requires_entrypoint = executor is None
255
+ self._semaphore = asyncio.Semaphore(self.max_concurrency)
256
+ self._state_lock = asyncio.Lock()
257
+ self._admitted = 0
258
+ self._running = 0
259
+
260
+ def ready(self) -> bool:
261
+ return not self._requires_entrypoint or (
262
+ self.entrypoint.is_file()
263
+ and os.access(self.entrypoint, os.X_OK)
264
+ and self.run_json_path.is_file()
265
+ and shutil.which("g++") is not None
266
+ )
267
+
268
+ async def status(self) -> dict[str, object]:
269
+ cache_status = await self.cache.status()
270
+ async with self._state_lock:
271
+ return {
272
+ "ready": self.ready(),
273
+ "running": self._running,
274
+ "queued": self._admitted - self._running,
275
+ "max_concurrency": self.max_concurrency,
276
+ "max_queue": self.max_queue,
277
+ "release_image": self.release_image,
278
+ "compile_cache": cache_status,
279
+ }
280
+
281
+ async def _admit(self) -> None:
282
+ async with self._state_lock:
283
+ capacity = self.max_concurrency + self.max_queue
284
+ if self._admitted >= capacity:
285
+ raise BacktestServiceError(
286
+ "server_overloaded",
287
+ "backtest capacity is full; retry later",
288
+ status_code=429,
289
+ phase="queue",
290
+ )
291
+ self._admitted += 1
292
+
293
+ async def _release_admission(self) -> None:
294
+ async with self._state_lock:
295
+ self._admitted -= 1
296
+
297
+ async def run(self, request: BacktestApiRequest, request_id: str) -> dict[str, object]:
298
+ await self._admit()
299
+ acquired = False
300
+ try:
301
+ try:
302
+ await asyncio.wait_for(
303
+ self._semaphore.acquire(), timeout=self.queue_timeout_seconds
304
+ )
305
+ acquired = True
306
+ except TimeoutError as exc:
307
+ raise BacktestServiceError(
308
+ "queue_timeout",
309
+ "backtest did not reach an execution slot before the queue timeout",
310
+ status_code=503,
311
+ phase="queue",
312
+ ) from exc
313
+ async with self._state_lock:
314
+ self._running += 1
315
+ try:
316
+ result = await self._executor(request)
317
+ finally:
318
+ async with self._state_lock:
319
+ self._running -= 1
320
+ return release_response(
321
+ result.report,
322
+ release_image=self.release_image,
323
+ mode="fastapi-server",
324
+ request_id=request_id,
325
+ runtime_metadata=result.runtime_metadata,
326
+ )
327
+ finally:
328
+ if acquired:
329
+ self._semaphore.release()
330
+ await self._release_admission()
331
+
332
+ async def _run_command(
333
+ self,
334
+ command: list[str],
335
+ *,
336
+ environment: Mapping[str, str],
337
+ phase: str,
338
+ deadline: float,
339
+ ) -> bytes:
340
+ remaining = deadline - asyncio.get_running_loop().time()
341
+ if remaining <= 0:
342
+ raise BacktestServiceError(
343
+ "execution_timeout",
344
+ f"backtest exceeded {self.execution_timeout_seconds:g} seconds",
345
+ status_code=504,
346
+ phase=phase,
347
+ )
348
+ try:
349
+ process = await asyncio.create_subprocess_exec(
350
+ *command,
351
+ stdout=asyncio.subprocess.PIPE,
352
+ stderr=asyncio.subprocess.PIPE,
353
+ env=environment,
354
+ start_new_session=True,
355
+ )
356
+ except OSError as exc:
357
+ raise BacktestServiceError(
358
+ "runtime_unavailable",
359
+ f"cannot start {phase} process: {exc}",
360
+ status_code=503,
361
+ phase=phase,
362
+ ) from exc
363
+ try:
364
+ stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=remaining)
365
+ except TimeoutError as exc:
366
+ try:
367
+ os.killpg(process.pid, signal.SIGKILL)
368
+ except (ProcessLookupError, PermissionError):
369
+ process.kill()
370
+ await process.communicate()
371
+ raise BacktestServiceError(
372
+ "execution_timeout",
373
+ f"backtest exceeded {self.execution_timeout_seconds:g} seconds",
374
+ status_code=504,
375
+ phase=phase,
376
+ ) from exc
377
+ if process.returncode != 0:
378
+ detail = stderr.decode("utf-8", "replace").strip() or "pineforge-release failed"
379
+ raise BacktestServiceError(
380
+ f"{phase}_failed",
381
+ detail,
382
+ status_code=422 if phase in ("input", "transpile", "compile", "backtest") else 500,
383
+ phase=phase,
384
+ )
385
+ return stdout
386
+
387
+ def _cache_key(self, generated_cpp: bytes) -> tuple[str, str]:
388
+ cpp_sha256 = hashlib.sha256(generated_cpp).hexdigest()
389
+ identity = "\0".join(
390
+ (
391
+ "pineforge-compiled-strategy-v1",
392
+ cpp_sha256,
393
+ self.release_image,
394
+ os.environ.get("PINEFORGE_ENGINE_VERSION", "unknown"),
395
+ os.environ.get("PINEFORGE_CODEGEN_VERSION", "unknown"),
396
+ os.environ.get("PINEFORGE_RELEASE_VERSION", "unknown"),
397
+ platform.machine(),
398
+ "g++-std=c++17-O2-ffp-contract=off-fPIC-shared-whole-archive",
399
+ )
400
+ )
401
+ return hashlib.sha256(identity.encode()).hexdigest(), cpp_sha256
402
+
403
+ async def _execute_release(self, request: BacktestApiRequest) -> ExecutionResult:
404
+ pine_source, bars, instrument, options = request.domain_values()
405
+ deadline = asyncio.get_running_loop().time() + self.execution_timeout_seconds
406
+ with tempfile.TemporaryDirectory(prefix="pineforge-server-") as temporary:
407
+ workspace = Path(temporary)
408
+ try:
409
+ await asyncio.to_thread(
410
+ write_release_inputs,
411
+ workspace,
412
+ pine_source,
413
+ bars,
414
+ instrument,
415
+ )
416
+ release_environment(
417
+ str(workspace),
418
+ instrument,
419
+ options,
420
+ request.strategy_params,
421
+ request.strategy_overrides,
422
+ )
423
+ except (ReleaseContractError, ValueError) as exc:
424
+ raise BacktestServiceError(
425
+ "invalid_request",
426
+ str(exc),
427
+ status_code=400,
428
+ phase="input",
429
+ ) from exc
430
+ environment = os.environ.copy()
431
+ environment.update(
432
+ {
433
+ "PINEFORGE_IN_DIR": str(workspace),
434
+ "PINEFORGE_TRANSPILE_ONLY": "1",
435
+ }
436
+ )
437
+ generated_cpp = await self._run_command(
438
+ [str(self.entrypoint)],
439
+ environment=environment,
440
+ phase="transpile",
441
+ deadline=deadline,
442
+ )
443
+ generated_path = workspace / "strategy.cpp"
444
+ await asyncio.to_thread(generated_path.write_bytes, generated_cpp)
445
+ cache_key, cpp_sha256 = self._cache_key(generated_cpp)
446
+ inputs = json.dumps(request.strategy_params, separators=(",", ":"), allow_nan=False)
447
+ overrides = json.dumps(
448
+ request.strategy_overrides, separators=(",", ":"), allow_nan=False
449
+ )
450
+ cache_hit = False
451
+ async with self.cache.compile_lock(cache_key):
452
+ strategy_library = await self.cache.acquire(cache_key)
453
+ if strategy_library is None:
454
+ temporary_library = self.cache.temporary_path(cache_key)
455
+ prefix = os.environ.get("PINEFORGE_PREFIX", "/opt/pineforge")
456
+ compile_command = [
457
+ "g++",
458
+ "-std=c++17",
459
+ "-O2",
460
+ "-ffp-contract=off",
461
+ "-fPIC",
462
+ "-shared",
463
+ f"-I{prefix}/include",
464
+ "-I/usr/include/eigen3",
465
+ str(generated_path),
466
+ "-Wl,--whole-archive",
467
+ f"{prefix}/lib/libpineforge.a",
468
+ "-Wl,--no-whole-archive",
469
+ "-o",
470
+ str(temporary_library),
471
+ ]
472
+ try:
473
+ await self._run_command(
474
+ compile_command,
475
+ environment=os.environ.copy(),
476
+ phase="compile",
477
+ deadline=deadline,
478
+ )
479
+ strategy_library = await self.cache.commit_and_acquire(
480
+ cache_key, temporary_library
481
+ )
482
+ finally:
483
+ if temporary_library.exists():
484
+ temporary_library.unlink()
485
+ else:
486
+ cache_hit = True
487
+ if strategy_library is None:
488
+ raise BacktestServiceError(
489
+ "compile_failed",
490
+ "compiled strategy cache artifact was not created",
491
+ status_code=500,
492
+ phase="compile",
493
+ )
494
+ command = [
495
+ "python3",
496
+ str(self.run_json_path),
497
+ "--so",
498
+ str(strategy_library),
499
+ "--ohlcv",
500
+ str(workspace / "ohlcv.csv"),
501
+ "--inputs",
502
+ inputs,
503
+ "--overrides",
504
+ overrides,
505
+ "--input-tf",
506
+ options.input_timeframe,
507
+ "--script-tf",
508
+ options.script_timeframe,
509
+ "--bar-magnifier",
510
+ "true" if options.bar_magnifier else "false",
511
+ "--magnifier-samples",
512
+ str(options.magnifier_samples),
513
+ "--magnifier-dist",
514
+ options.magnifier_distribution.name.lower(),
515
+ "--generated-cpp",
516
+ str(generated_path),
517
+ "--transpiled",
518
+ "true",
519
+ "--syminfo",
520
+ str(workspace / "syminfo.json"),
521
+ "--chart-tz",
522
+ options.chart_timezone or "",
523
+ ]
524
+ if options.trade_start_time_ms is not None:
525
+ command.extend(("--trade-start-ms", str(options.trade_start_time_ms)))
526
+ try:
527
+ stdout = await self._run_command(
528
+ command,
529
+ environment=os.environ.copy(),
530
+ phase="backtest",
531
+ deadline=deadline,
532
+ )
533
+ finally:
534
+ await self.cache.release(cache_key)
535
+ await self.cache.trim()
536
+ try:
537
+ report = parse_release_report(stdout.decode("utf-8", "replace"))
538
+ except ReleaseContractError as exc:
539
+ raise BacktestServiceError(
540
+ "invalid_release_response",
541
+ str(exc),
542
+ status_code=502,
543
+ phase="response",
544
+ ) from exc
545
+ return ExecutionResult(
546
+ report,
547
+ {
548
+ "release_version": os.environ.get("PINEFORGE_RELEASE_VERSION", "unknown"),
549
+ "engine_version": os.environ.get("PINEFORGE_ENGINE_VERSION", "unknown"),
550
+ "codegen_version": os.environ.get("PINEFORGE_CODEGEN_VERSION", "unknown"),
551
+ "compile_cache": {
552
+ "key": cache_key,
553
+ "hit": cache_hit,
554
+ "generated_cpp_sha256": cpp_sha256,
555
+ },
556
+ },
557
+ )
558
+
559
+
560
+ def _request_id(value: str | None) -> str:
561
+ if value is None:
562
+ return uuid4().hex
563
+ if not _REQUEST_ID.fullmatch(value):
564
+ raise BacktestServiceError(
565
+ "invalid_request_id",
566
+ "X-Request-ID must contain 1-128 safe identifier characters",
567
+ status_code=400,
568
+ phase="request",
569
+ )
570
+ return value
571
+
572
+
573
+ def create_app(
574
+ service: BacktestService | None = None,
575
+ *,
576
+ api_key: str | None = None,
577
+ ) -> FastAPI:
578
+ runtime = service or BacktestService()
579
+ expected_api_key = (
580
+ api_key if api_key is not None else os.environ.get("PINEFORGE_SERVER_API_KEY", "")
581
+ )
582
+ application = FastAPI(
583
+ title="PineForge Backtest Server",
584
+ version="1",
585
+ docs_url="/docs",
586
+ redoc_url=None,
587
+ )
588
+
589
+ @application.get("/healthz")
590
+ async def healthz() -> dict[str, str]:
591
+ return {"status": "ok"}
592
+
593
+ @application.get("/readyz")
594
+ async def readyz() -> JSONResponse:
595
+ status = await runtime.status()
596
+ return JSONResponse(status_code=200 if status["ready"] else 503, content=status)
597
+
598
+ @application.post("/v1/backtests")
599
+ async def backtest(
600
+ request: BacktestApiRequest,
601
+ authorization: Annotated[str | None, Header()] = None,
602
+ x_request_id: Annotated[str | None, Header()] = None,
603
+ ) -> JSONResponse:
604
+ request_id = ""
605
+ try:
606
+ request_id = _request_id(x_request_id)
607
+ if expected_api_key:
608
+ scheme, _, credential = (authorization or "").partition(" ")
609
+ supplied = credential if scheme.casefold() == "bearer" else ""
610
+ if not secrets.compare_digest(supplied, expected_api_key):
611
+ raise BacktestServiceError(
612
+ "unauthorized",
613
+ "a valid bearer token is required",
614
+ status_code=401,
615
+ phase="authorization",
616
+ )
617
+ result = await runtime.run(request, request_id)
618
+ return JSONResponse(status_code=200, content=result)
619
+ except BacktestServiceError as exc:
620
+ return JSONResponse(
621
+ status_code=exc.status_code,
622
+ content={
623
+ "error": {
624
+ "code": exc.code,
625
+ "phase": exc.phase,
626
+ "message": str(exc),
627
+ "request_id": request_id or None,
628
+ }
629
+ },
630
+ )
631
+
632
+ return application
633
+
634
+
635
+ app = create_app()
636
+
637
+
638
+ def main() -> None:
639
+ import uvicorn
640
+
641
+ uvicorn.run(
642
+ "pineforge_data.server:app",
643
+ host=os.environ.get("PINEFORGE_SERVER_HOST", "0.0.0.0"),
644
+ port=int(os.environ.get("PINEFORGE_SERVER_PORT", "8000")),
645
+ workers=1,
646
+ )