vgi-python 0.8.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.
Files changed (124) hide show
  1. vgi/__init__.py +152 -0
  2. vgi/_duckdb.py +62 -0
  3. vgi/_storage_profile.py +132 -0
  4. vgi/_test_fixtures/__init__.py +20 -0
  5. vgi/_test_fixtures/accumulate/__init__.py +19 -0
  6. vgi/_test_fixtures/accumulate/worker.py +762 -0
  7. vgi/_test_fixtures/aggregate/__init__.py +62 -0
  8. vgi/_test_fixtures/aggregate/_common.py +21 -0
  9. vgi/_test_fixtures/aggregate/basic.py +232 -0
  10. vgi/_test_fixtures/aggregate/dynamic.py +409 -0
  11. vgi/_test_fixtures/aggregate/generic.py +86 -0
  12. vgi/_test_fixtures/aggregate/listagg.py +71 -0
  13. vgi/_test_fixtures/aggregate/percentile.py +107 -0
  14. vgi/_test_fixtures/aggregate/streaming.py +192 -0
  15. vgi/_test_fixtures/aggregate/varargs.py +75 -0
  16. vgi/_test_fixtures/aggregate/window.py +380 -0
  17. vgi/_test_fixtures/attach_options.py +308 -0
  18. vgi/_test_fixtures/bad_protocol.py +62 -0
  19. vgi/_test_fixtures/cancellable.py +336 -0
  20. vgi/_test_fixtures/catalog.py +813 -0
  21. vgi/_test_fixtures/http_server.py +394 -0
  22. vgi/_test_fixtures/nest_tensor.py +614 -0
  23. vgi/_test_fixtures/orchard_catalog.py +47 -0
  24. vgi/_test_fixtures/projection_repro/__init__.py +6 -0
  25. vgi/_test_fixtures/projection_repro/worker.py +454 -0
  26. vgi/_test_fixtures/scalar/__init__.py +116 -0
  27. vgi/_test_fixtures/scalar/_common.py +69 -0
  28. vgi/_test_fixtures/scalar/arithmetic.py +321 -0
  29. vgi/_test_fixtures/scalar/binary.py +120 -0
  30. vgi/_test_fixtures/scalar/formatting.py +176 -0
  31. vgi/_test_fixtures/scalar/geo.py +300 -0
  32. vgi/_test_fixtures/scalar/null_handling.py +107 -0
  33. vgi/_test_fixtures/scalar/random_demo.py +171 -0
  34. vgi/_test_fixtures/scalar/settings_secrets.py +102 -0
  35. vgi/_test_fixtures/scalar/type_info.py +219 -0
  36. vgi/_test_fixtures/schema_reconcile/__init__.py +29 -0
  37. vgi/_test_fixtures/schema_reconcile/worker.py +653 -0
  38. vgi/_test_fixtures/simple_writable.py +793 -0
  39. vgi/_test_fixtures/table/__init__.py +221 -0
  40. vgi/_test_fixtures/table/_common.py +162 -0
  41. vgi/_test_fixtures/table/batch_index.py +283 -0
  42. vgi/_test_fixtures/table/batch_index_broken.py +200 -0
  43. vgi/_test_fixtures/table/catalog_scans.py +162 -0
  44. vgi/_test_fixtures/table/filters.py +1005 -0
  45. vgi/_test_fixtures/table/late_materialization.py +249 -0
  46. vgi/_test_fixtures/table/make_series.py +273 -0
  47. vgi/_test_fixtures/table/misc.py +499 -0
  48. vgi/_test_fixtures/table/order_modes.py +164 -0
  49. vgi/_test_fixtures/table/pairs.py +437 -0
  50. vgi/_test_fixtures/table/partition_columns.py +472 -0
  51. vgi/_test_fixtures/table/partition_columns_broken.py +304 -0
  52. vgi/_test_fixtures/table/profiling_example.py +195 -0
  53. vgi/_test_fixtures/table/required_filters.py +234 -0
  54. vgi/_test_fixtures/table/sequence.py +710 -0
  55. vgi/_test_fixtures/table/settings.py +426 -0
  56. vgi/_test_fixtures/table/transaction_storage.py +162 -0
  57. vgi/_test_fixtures/table/tt_pushdown.py +191 -0
  58. vgi/_test_fixtures/table/versioned.py +230 -0
  59. vgi/_test_fixtures/table_in_out.py +1392 -0
  60. vgi/_test_fixtures/versioned.py +155 -0
  61. vgi/_test_fixtures/versioned_tables.py +595 -0
  62. vgi/_test_fixtures/worker.py +1631 -0
  63. vgi/_test_fixtures/writable/__init__.py +8 -0
  64. vgi/_test_fixtures/writable/generic.py +236 -0
  65. vgi/_test_fixtures/writable/table.py +149 -0
  66. vgi/_test_fixtures/writable/worker.py +1148 -0
  67. vgi/aggregate_function.py +607 -0
  68. vgi/argument_spec.py +472 -0
  69. vgi/arguments.py +1747 -0
  70. vgi/auth.py +55 -0
  71. vgi/catalog/__init__.py +88 -0
  72. vgi/catalog/attach_option.py +206 -0
  73. vgi/catalog/catalog_interface.py +2767 -0
  74. vgi/catalog/descriptors.py +870 -0
  75. vgi/catalog/duckdb_statistics.py +377 -0
  76. vgi/catalog/secret_type.py +96 -0
  77. vgi/catalog/setting.py +253 -0
  78. vgi/catalog/storage.py +372 -0
  79. vgi/client/__init__.py +67 -0
  80. vgi/client/catalog_mixin.py +1251 -0
  81. vgi/client/cli.py +582 -0
  82. vgi/client/cli_catalog.py +182 -0
  83. vgi/client/cli_schema.py +270 -0
  84. vgi/client/cli_table.py +907 -0
  85. vgi/client/cli_transaction.py +97 -0
  86. vgi/client/cli_utils.py +441 -0
  87. vgi/client/cli_view.py +303 -0
  88. vgi/client/client.py +2183 -0
  89. vgi/exceptions.py +205 -0
  90. vgi/function.py +245 -0
  91. vgi/function_storage.py +1636 -0
  92. vgi/function_storage_azure_sql.py +922 -0
  93. vgi/function_storage_cf_do.py +740 -0
  94. vgi/http/__init__.py +25 -0
  95. vgi/http/demo_storage.py +212 -0
  96. vgi/http/worker_page.py +1252 -0
  97. vgi/invocation.py +154 -0
  98. vgi/logging_config.py +93 -0
  99. vgi/meta_worker.py +661 -0
  100. vgi/metadata.py +1403 -0
  101. vgi/otel.py +406 -0
  102. vgi/protocol.py +2418 -0
  103. vgi/protocol_version.txt +1 -0
  104. vgi/py.typed +0 -0
  105. vgi/scalar_function.py +1211 -0
  106. vgi/schema_utils.py +234 -0
  107. vgi/secret_protocol.py +124 -0
  108. vgi/secret_service.py +238 -0
  109. vgi/serve.py +769 -0
  110. vgi/table_buffering_function.py +443 -0
  111. vgi/table_filter_pushdown.py +1528 -0
  112. vgi/table_function.py +1130 -0
  113. vgi/table_in_out_function.py +383 -0
  114. vgi/transactor/__init__.py +24 -0
  115. vgi/transactor/_duckdb_compat.py +27 -0
  116. vgi/transactor/client.py +137 -0
  117. vgi/transactor/protocol.py +149 -0
  118. vgi/transactor/server.py +740 -0
  119. vgi/worker.py +4761 -0
  120. vgi_python-0.8.0.dist-info/METADATA +735 -0
  121. vgi_python-0.8.0.dist-info/RECORD +124 -0
  122. vgi_python-0.8.0.dist-info/WHEEL +4 -0
  123. vgi_python-0.8.0.dist-info/entry_points.txt +5 -0
  124. vgi_python-0.8.0.dist-info/licenses/LICENSE +134 -0
@@ -0,0 +1,394 @@
1
+ # Copyright 2025, 2026 Query Farm LLC - https://query.farm
2
+
3
+ """Run the example worker as an HTTP server.
4
+
5
+ Usage::
6
+
7
+ vgi-fixture-http
8
+ vgi-fixture-http --port 9000
9
+ vgi-fixture-http --host 0.0.0.0 --port 8080 --debug
10
+ vgi-fixture-http --s3-bucket rusty-vgi-test
11
+ vgi-fixture-http --demo-storage
12
+
13
+ Requires the ``http`` extra: ``pip install vgi-python[http]``
14
+ For S3 offload support, also install: ``pip install vgi-rpc[s3]``
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import os
20
+ import sys
21
+ from datetime import UTC, datetime, timedelta
22
+ from typing import TYPE_CHECKING, Any
23
+
24
+ from vgi._test_fixtures.accumulate.worker import AccumulateWorker
25
+ from vgi._test_fixtures.projection_repro.worker import ProjReproWorker
26
+ from vgi._test_fixtures.schema_reconcile.worker import SchemaReconcileWorker
27
+ from vgi._test_fixtures.worker import ExampleWorker
28
+ from vgi.logging_config import LogFormat, LogLevel, configure_worker_logging
29
+ from vgi.meta_worker import MetaWorker
30
+
31
+ if TYPE_CHECKING:
32
+ from vgi_rpc.external import UploadUrl
33
+
34
+
35
+ class _SigV4S3Storage:
36
+ """S3 external storage that forces SigV4 presigned URLs."""
37
+
38
+ def __init__(
39
+ self,
40
+ *,
41
+ bucket: str,
42
+ prefix: str,
43
+ region_name: str | None,
44
+ endpoint_url: str | None,
45
+ presign_expiry_seconds: int = 3600,
46
+ ) -> None:
47
+ self.bucket = bucket
48
+ self.prefix = prefix
49
+ self.region_name = region_name
50
+ self.endpoint_url = endpoint_url
51
+ self.presign_expiry_seconds = presign_expiry_seconds
52
+ self._client = None
53
+
54
+ def _get_client(self) -> Any:
55
+ if self._client is None:
56
+ import boto3 # type: ignore[import-not-found]
57
+ from botocore.config import Config # type: ignore[import-not-found]
58
+
59
+ self._client = boto3.client(
60
+ "s3",
61
+ region_name=self.region_name,
62
+ endpoint_url=self.endpoint_url,
63
+ config=Config(signature_version="s3v4"),
64
+ )
65
+ return self._client
66
+
67
+ def upload(self, data: bytes, schema: Any, *, content_encoding: str | None = None) -> str:
68
+ import uuid
69
+
70
+ client = self._get_client()
71
+ ext = ".arrow.zst" if content_encoding == "zstd" else ".arrow"
72
+ key = f"{self.prefix}{uuid.uuid4().hex}{ext}"
73
+
74
+ put_kwargs = {
75
+ "Bucket": self.bucket,
76
+ "Key": key,
77
+ "Body": data,
78
+ "ContentType": "application/octet-stream",
79
+ }
80
+ if content_encoding is not None:
81
+ put_kwargs["ContentEncoding"] = content_encoding
82
+ client.put_object(**put_kwargs)
83
+
84
+ url: str = client.generate_presigned_url(
85
+ "get_object",
86
+ Params={"Bucket": self.bucket, "Key": key},
87
+ ExpiresIn=self.presign_expiry_seconds,
88
+ )
89
+ return url
90
+
91
+ def generate_upload_url(self, schema: Any) -> UploadUrl:
92
+ import uuid
93
+
94
+ from vgi_rpc.external import UploadUrl
95
+
96
+ client = self._get_client()
97
+ key = f"{self.prefix}{uuid.uuid4().hex}.arrow"
98
+ params = {"Bucket": self.bucket, "Key": key}
99
+ expires_at = datetime.now(UTC) + timedelta(seconds=self.presign_expiry_seconds)
100
+
101
+ put_url = client.generate_presigned_url(
102
+ "put_object",
103
+ Params=params,
104
+ ExpiresIn=self.presign_expiry_seconds,
105
+ )
106
+ get_url = client.generate_presigned_url(
107
+ "get_object",
108
+ Params=params,
109
+ ExpiresIn=self.presign_expiry_seconds,
110
+ )
111
+ return UploadUrl(upload_url=put_url, download_url=get_url, expires_at=expires_at)
112
+
113
+
114
+ def main() -> None:
115
+ """Run the fixture worker as an HTTP server.
116
+
117
+ When ``--s3-bucket`` is provided (or ``VGI_HTTP_S3_BUCKET`` is set),
118
+ response batches larger than ``--externalize-threshold-bytes`` are uploaded
119
+ to S3 and replaced by signed URLs on the wire.
120
+ """
121
+ # The test fixture server is single-process and ephemeral — there's no
122
+ # value in fsyncing every commit through the WAL. Default to in-memory
123
+ # storage unless the caller explicitly picked a path. Must run before
124
+ # any Function.storage access so _DefaultStorageDescriptor caches the
125
+ # right backend; main() runs before the WSGI app starts handling RPCs.
126
+ os.environ.setdefault("VGI_WORKER_SQLITE_PATH", ":memory:")
127
+
128
+ import typer
129
+
130
+ app = typer.Typer(add_completion=False)
131
+
132
+ @app.command()
133
+ def _run(
134
+ host: str = typer.Option("127.0.0.1", "--host", "-h", help="Bind address"),
135
+ port: int = typer.Option(0, "--port", "-p", help="Bind port (0 = auto-select)"),
136
+ prefix: str = typer.Option("", "--prefix", help="URL prefix for RPC endpoints"),
137
+ cors_origins: str = typer.Option("*", "--cors-origins", help="Allowed CORS origins"),
138
+ describe: bool = typer.Option( # noqa: B008
139
+ True, "--describe/--no-describe", help="Enable description pages (worker + RPC API)"
140
+ ),
141
+ debug: bool = typer.Option(False, "--debug", help="Enable DEBUG on all vgi + vgi_rpc loggers"),
142
+ log_level: LogLevel = typer.Option(LogLevel.INFO, "--log-level", help="Set log level"), # noqa: B008
143
+ log_logger: list[str] | None = typer.Option(None, "--log-logger", help="Target specific logger(s)"), # noqa: B008
144
+ log_format: LogFormat = typer.Option(LogFormat.text, "--log-format", help="Stderr log format"), # noqa: B008
145
+ s3_bucket: str | None = typer.Option(
146
+ None,
147
+ "--s3-bucket",
148
+ help="S3 bucket for externalized payloads (or env VGI_HTTP_S3_BUCKET)",
149
+ ),
150
+ s3_prefix: str = typer.Option(
151
+ "vgi-http/",
152
+ "--s3-prefix",
153
+ help="S3 key prefix for externalized payloads",
154
+ ),
155
+ s3_region: str | None = typer.Option(None, "--s3-region", help="AWS region for S3 client"),
156
+ s3_endpoint_url: str | None = typer.Option(
157
+ None,
158
+ "--s3-endpoint-url",
159
+ help="Custom S3 endpoint URL (for MinIO/LocalStack)",
160
+ ),
161
+ externalize_threshold_bytes: int = typer.Option(
162
+ 4 * 1024 * 1024,
163
+ "--externalize-threshold-bytes",
164
+ help="Externalize batches larger than this many bytes (default: 4 MiB)",
165
+ ),
166
+ max_upload_bytes: int = typer.Option(
167
+ 4 * 1024 * 1024,
168
+ "--max-upload-bytes",
169
+ help="Advertise max direct HTTP upload bytes for server-vended upload URLs",
170
+ ),
171
+ externalize_compression: str = typer.Option(
172
+ "none",
173
+ "--externalize-compression",
174
+ help="Compression for externalized batches: none, zstd, or gzip",
175
+ ),
176
+ demo_storage: bool = typer.Option(
177
+ False,
178
+ "--demo-storage",
179
+ help="Enable in-process blob storage for externalized payloads (no S3 required)",
180
+ ),
181
+ port_file: str | None = typer.Option(
182
+ None,
183
+ "--port-file",
184
+ help=(
185
+ "Write the bound port number (one line, no prefix) to this file before starting "
186
+ "to serve. For test harnesses / process managers that need the port side-channel "
187
+ "without parsing stdout."
188
+ ),
189
+ ),
190
+ ) -> None:
191
+ try:
192
+ from vgi_rpc import Compression, ExternalLocationConfig, RpcServer
193
+ from vgi_rpc.http import make_wsgi_app
194
+ except ImportError:
195
+ sys.stderr.write(
196
+ "Error: HTTP dependencies not installed.\n"
197
+ "Install with: pip install vgi-python[http] (or: uv sync --extra http)\n"
198
+ )
199
+ sys.exit(1)
200
+
201
+ try:
202
+ import waitress # type: ignore[import-untyped]
203
+ except ImportError:
204
+ sys.stderr.write(
205
+ "Error: waitress not installed.\n"
206
+ "Install with: pip install vgi-python[http] (or: uv sync --extra http)\n"
207
+ )
208
+ sys.exit(1)
209
+
210
+ env_debug = os.environ.get("VGI_WORKER_DEBUG", "").lower() in ("1", "true", "yes")
211
+ effective_debug = debug or env_debug
212
+ effective_level = configure_worker_logging(
213
+ debug=effective_debug,
214
+ log_level=log_level,
215
+ log_loggers=log_logger,
216
+ log_format=log_format,
217
+ )
218
+
219
+ bucket = s3_bucket or os.environ.get("VGI_HTTP_S3_BUCKET")
220
+ external_location = None
221
+ upload_url_provider: Any = None
222
+ max_request_bytes: int | None = None
223
+ compression_choice = externalize_compression.lower()
224
+ if compression_choice not in {"none", "zstd", "gzip"}:
225
+ raise typer.BadParameter("externalize-compression must be one of: none, zstd, gzip")
226
+
227
+ def _make_compression() -> Compression | None:
228
+ if compression_choice == "none":
229
+ return None
230
+ # ``Compression.level`` historically defaults to 3 (zstd-tuned);
231
+ # leave it at the dataclass default for zstd, but use a
232
+ # gzip-appropriate 6 when the operator picks gzip.
233
+ if compression_choice == "gzip":
234
+ return Compression(algorithm="gzip", level=6)
235
+ return Compression(algorithm="zstd")
236
+
237
+ if bucket and demo_storage:
238
+ raise typer.BadParameter("--s3-bucket and --demo-storage are mutually exclusive")
239
+ if bucket:
240
+ storage = _SigV4S3Storage(
241
+ bucket=bucket,
242
+ prefix=s3_prefix,
243
+ region_name=s3_region,
244
+ endpoint_url=s3_endpoint_url,
245
+ )
246
+ compression = _make_compression()
247
+ _s3_kwargs: dict[str, Any] = {}
248
+ if s3_endpoint_url:
249
+ from urllib.parse import urlparse as _urlparse
250
+
251
+ if _urlparse(s3_endpoint_url).hostname in ("127.0.0.1", "localhost"):
252
+ # Local S3 stand-ins (LocalStack/MinIO) presign http://
253
+ # URLs that the https-only default validator rejects.
254
+ from vgi.http.demo_storage import localhost_only_validator
255
+
256
+ _s3_kwargs["url_validator"] = localhost_only_validator
257
+ external_location = ExternalLocationConfig(
258
+ storage=storage,
259
+ externalize_threshold_bytes=externalize_threshold_bytes,
260
+ compression=compression,
261
+ **_s3_kwargs,
262
+ )
263
+ upload_url_provider = storage
264
+ sys.stderr.write(
265
+ "S3 offload enabled: "
266
+ f"bucket={bucket} prefix={s3_prefix} threshold={externalize_threshold_bytes} "
267
+ f"bytes compression={compression_choice}\n"
268
+ )
269
+ sys.stderr.flush()
270
+ elif demo_storage:
271
+ from vgi.http.demo_storage import DemoBlobStorage, localhost_only_validator
272
+
273
+ demo_blob_storage = DemoBlobStorage()
274
+ compression = _make_compression()
275
+ external_location = ExternalLocationConfig(
276
+ storage=demo_blob_storage,
277
+ externalize_threshold_bytes=externalize_threshold_bytes,
278
+ compression=compression,
279
+ url_validator=localhost_only_validator,
280
+ )
281
+ upload_url_provider = demo_blob_storage
282
+ max_request_bytes = max_upload_bytes
283
+ sys.stderr.write(
284
+ "Demo blob storage enabled: "
285
+ f"threshold={externalize_threshold_bytes} bytes "
286
+ f"compression={compression_choice}\n"
287
+ )
288
+ sys.stderr.flush()
289
+
290
+ import socket
291
+
292
+ from vgi.protocol import VgiProtocol
293
+
294
+ if port == 0:
295
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
296
+ s.bind((host, 0))
297
+ port = int(s.getsockname()[1])
298
+
299
+ if demo_storage:
300
+ demo_blob_storage.set_base_url(f"http://{host}:{port}")
301
+
302
+ from vgi.serve import _resolve_authenticate, _resolve_oauth_resource_metadata
303
+
304
+ authenticate = _resolve_authenticate()
305
+ oauth_metadata = _resolve_oauth_resource_metadata()
306
+
307
+ from vgi.worker import _get_vgi_version
308
+
309
+ # Match vgi-fixture-worker (subprocess transport): always serve the
310
+ # base ExampleWorker plus the projection_repro, schema_reconcile,
311
+ # and accumulate fixture catalogs. Add the writable catalog when its
312
+ # extra is installed.
313
+ worker_classes: list[type] = [ExampleWorker, ProjReproWorker, SchemaReconcileWorker, AccumulateWorker]
314
+ try:
315
+ from vgi._test_fixtures.writable.worker import WritableWorker
316
+ except ImportError:
317
+ pass
318
+ else:
319
+ worker_classes.append(WritableWorker)
320
+ workers = [wc(quiet=True, log_level=effective_level) for wc in worker_classes]
321
+ # One signing key shared by every sub-worker (which seal catalog
322
+ # opaque-data envelopes) and the HTTP state-token machinery.
323
+ signing_key = os.environ.get("VGI_SIGNING_KEY", "").encode() or os.urandom(32)
324
+ for w in workers:
325
+ w._signing_key = signing_key
326
+ worker: Any = workers[0] if len(workers) == 1 else MetaWorker(workers)
327
+ server = RpcServer(
328
+ VgiProtocol,
329
+ worker,
330
+ external_location=external_location,
331
+ enable_describe=describe,
332
+ server_version=_get_vgi_version(),
333
+ )
334
+ wsgi_app = make_wsgi_app(
335
+ server,
336
+ prefix=prefix,
337
+ cors_origins=cors_origins,
338
+ token_key=signing_key,
339
+ upload_url_provider=upload_url_provider,
340
+ max_upload_bytes=max_upload_bytes if upload_url_provider is not None else None,
341
+ max_request_bytes=max_request_bytes,
342
+ authenticate=authenticate,
343
+ oauth_resource_metadata=oauth_metadata,
344
+ )
345
+
346
+ if demo_storage:
347
+ from vgi.http.demo_storage import add_blob_routes
348
+
349
+ add_blob_routes(wsgi_app, demo_blob_storage, prefix=prefix)
350
+
351
+ # vgi_rpc's make_wsgi_app installs a Falcon middleware that 413s
352
+ # any request body over max_request_bytes, exempting only the
353
+ # capability/upload-URL routes. The demo blob endpoint
354
+ # (``/__blobs__/``) serves the *externalized* payloads — bodies
355
+ # that are intentionally larger than max_request_bytes — so it
356
+ # must be exempt too, or auto-externalization can never land its
357
+ # upload. Real deployments point upload URLs at S3, so vgi_rpc has
358
+ # no built-in exemption for it; we add one here for the in-process
359
+ # demo store.
360
+ for mw in getattr(wsgi_app, "_unprepared_middleware", []):
361
+ if type(mw).__name__ == "_MaxRequestBytesMiddleware":
362
+ mw._exempt_prefixes = (*mw._exempt_prefixes, f"{prefix}/__blobs__")
363
+
364
+ if describe:
365
+ from vgi.http.worker_page import WorkerPageResource
366
+
367
+ wsgi_app.add_route(f"{prefix}/worker", WorkerPageResource(ExampleWorker, prefix))
368
+
369
+ # vgi_rpc's make_wsgi_app already advertises VGI-Max-Request-Bytes and
370
+ # installs a Falcon middleware enforcing it with a structured 413 that
371
+ # the client recognizes as the externalize-and-retry signal (and which
372
+ # we exempted for /__blobs__/ above). A second WSGI-level wrapper here
373
+ # would only re-413 with a plain-text body the client can't parse, so
374
+ # we serve the Falcon app directly.
375
+ serving_app: Any = wsgi_app
376
+
377
+ if port_file is not None:
378
+ # Atomic side-channel publication so test harnesses can watch
379
+ # for the file without racing a partial write. Same helper the
380
+ # main Worker class uses via `--port-file`.
381
+ from vgi.worker import _write_port_file
382
+
383
+ _write_port_file(port_file, port)
384
+
385
+ print(f"PORT:{port}", flush=True)
386
+ sys.stderr.write(f"Serving ExampleWorker on http://{host}:{port}{prefix}\n")
387
+ sys.stderr.flush()
388
+ waitress.serve(serving_app, host=host, port=port, _quiet=True)
389
+
390
+ app()
391
+
392
+
393
+ if __name__ == "__main__":
394
+ main()