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.
- vgi/__init__.py +152 -0
- vgi/_duckdb.py +62 -0
- vgi/_storage_profile.py +132 -0
- vgi/_test_fixtures/__init__.py +20 -0
- vgi/_test_fixtures/accumulate/__init__.py +19 -0
- vgi/_test_fixtures/accumulate/worker.py +762 -0
- vgi/_test_fixtures/aggregate/__init__.py +62 -0
- vgi/_test_fixtures/aggregate/_common.py +21 -0
- vgi/_test_fixtures/aggregate/basic.py +232 -0
- vgi/_test_fixtures/aggregate/dynamic.py +409 -0
- vgi/_test_fixtures/aggregate/generic.py +86 -0
- vgi/_test_fixtures/aggregate/listagg.py +71 -0
- vgi/_test_fixtures/aggregate/percentile.py +107 -0
- vgi/_test_fixtures/aggregate/streaming.py +192 -0
- vgi/_test_fixtures/aggregate/varargs.py +75 -0
- vgi/_test_fixtures/aggregate/window.py +380 -0
- vgi/_test_fixtures/attach_options.py +308 -0
- vgi/_test_fixtures/bad_protocol.py +62 -0
- vgi/_test_fixtures/cancellable.py +336 -0
- vgi/_test_fixtures/catalog.py +813 -0
- vgi/_test_fixtures/http_server.py +394 -0
- vgi/_test_fixtures/nest_tensor.py +614 -0
- vgi/_test_fixtures/orchard_catalog.py +47 -0
- vgi/_test_fixtures/projection_repro/__init__.py +6 -0
- vgi/_test_fixtures/projection_repro/worker.py +454 -0
- vgi/_test_fixtures/scalar/__init__.py +116 -0
- vgi/_test_fixtures/scalar/_common.py +69 -0
- vgi/_test_fixtures/scalar/arithmetic.py +321 -0
- vgi/_test_fixtures/scalar/binary.py +120 -0
- vgi/_test_fixtures/scalar/formatting.py +176 -0
- vgi/_test_fixtures/scalar/geo.py +300 -0
- vgi/_test_fixtures/scalar/null_handling.py +107 -0
- vgi/_test_fixtures/scalar/random_demo.py +171 -0
- vgi/_test_fixtures/scalar/settings_secrets.py +102 -0
- vgi/_test_fixtures/scalar/type_info.py +219 -0
- vgi/_test_fixtures/schema_reconcile/__init__.py +29 -0
- vgi/_test_fixtures/schema_reconcile/worker.py +653 -0
- vgi/_test_fixtures/simple_writable.py +793 -0
- vgi/_test_fixtures/table/__init__.py +221 -0
- vgi/_test_fixtures/table/_common.py +162 -0
- vgi/_test_fixtures/table/batch_index.py +283 -0
- vgi/_test_fixtures/table/batch_index_broken.py +200 -0
- vgi/_test_fixtures/table/catalog_scans.py +162 -0
- vgi/_test_fixtures/table/filters.py +1005 -0
- vgi/_test_fixtures/table/late_materialization.py +249 -0
- vgi/_test_fixtures/table/make_series.py +273 -0
- vgi/_test_fixtures/table/misc.py +499 -0
- vgi/_test_fixtures/table/order_modes.py +164 -0
- vgi/_test_fixtures/table/pairs.py +437 -0
- vgi/_test_fixtures/table/partition_columns.py +472 -0
- vgi/_test_fixtures/table/partition_columns_broken.py +304 -0
- vgi/_test_fixtures/table/profiling_example.py +195 -0
- vgi/_test_fixtures/table/required_filters.py +234 -0
- vgi/_test_fixtures/table/sequence.py +710 -0
- vgi/_test_fixtures/table/settings.py +426 -0
- vgi/_test_fixtures/table/transaction_storage.py +162 -0
- vgi/_test_fixtures/table/tt_pushdown.py +191 -0
- vgi/_test_fixtures/table/versioned.py +230 -0
- vgi/_test_fixtures/table_in_out.py +1392 -0
- vgi/_test_fixtures/versioned.py +155 -0
- vgi/_test_fixtures/versioned_tables.py +595 -0
- vgi/_test_fixtures/worker.py +1631 -0
- vgi/_test_fixtures/writable/__init__.py +8 -0
- vgi/_test_fixtures/writable/generic.py +236 -0
- vgi/_test_fixtures/writable/table.py +149 -0
- vgi/_test_fixtures/writable/worker.py +1148 -0
- vgi/aggregate_function.py +607 -0
- vgi/argument_spec.py +472 -0
- vgi/arguments.py +1747 -0
- vgi/auth.py +55 -0
- vgi/catalog/__init__.py +88 -0
- vgi/catalog/attach_option.py +206 -0
- vgi/catalog/catalog_interface.py +2767 -0
- vgi/catalog/descriptors.py +870 -0
- vgi/catalog/duckdb_statistics.py +377 -0
- vgi/catalog/secret_type.py +96 -0
- vgi/catalog/setting.py +253 -0
- vgi/catalog/storage.py +372 -0
- vgi/client/__init__.py +67 -0
- vgi/client/catalog_mixin.py +1251 -0
- vgi/client/cli.py +582 -0
- vgi/client/cli_catalog.py +182 -0
- vgi/client/cli_schema.py +270 -0
- vgi/client/cli_table.py +907 -0
- vgi/client/cli_transaction.py +97 -0
- vgi/client/cli_utils.py +441 -0
- vgi/client/cli_view.py +303 -0
- vgi/client/client.py +2183 -0
- vgi/exceptions.py +205 -0
- vgi/function.py +245 -0
- vgi/function_storage.py +1636 -0
- vgi/function_storage_azure_sql.py +922 -0
- vgi/function_storage_cf_do.py +740 -0
- vgi/http/__init__.py +25 -0
- vgi/http/demo_storage.py +212 -0
- vgi/http/worker_page.py +1252 -0
- vgi/invocation.py +154 -0
- vgi/logging_config.py +93 -0
- vgi/meta_worker.py +661 -0
- vgi/metadata.py +1403 -0
- vgi/otel.py +406 -0
- vgi/protocol.py +2418 -0
- vgi/protocol_version.txt +1 -0
- vgi/py.typed +0 -0
- vgi/scalar_function.py +1211 -0
- vgi/schema_utils.py +234 -0
- vgi/secret_protocol.py +124 -0
- vgi/secret_service.py +238 -0
- vgi/serve.py +769 -0
- vgi/table_buffering_function.py +443 -0
- vgi/table_filter_pushdown.py +1528 -0
- vgi/table_function.py +1130 -0
- vgi/table_in_out_function.py +383 -0
- vgi/transactor/__init__.py +24 -0
- vgi/transactor/_duckdb_compat.py +27 -0
- vgi/transactor/client.py +137 -0
- vgi/transactor/protocol.py +149 -0
- vgi/transactor/server.py +740 -0
- vgi/worker.py +4761 -0
- vgi_python-0.8.0.dist-info/METADATA +735 -0
- vgi_python-0.8.0.dist-info/RECORD +124 -0
- vgi_python-0.8.0.dist-info/WHEEL +4 -0
- vgi_python-0.8.0.dist-info/entry_points.txt +5 -0
- 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()
|