tigrcorn-runtime 0.3.16.dev5__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,191 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import os
5
+ import sys
6
+
7
+ from tigrcorn_config.policy_surface import flag_help
8
+ from tigrcorn_config.quic_surface import quic_flag_help
9
+ from tigrcorn_config.load import build_config_from_namespace
10
+ from tigrcorn_core.constants import SUPPORTED_RUNTIMES
11
+ from tigrcorn_runtime.server.bootstrap import run_config
12
+ from tigrcorn_runtime.server.reloader import PollingReloader
13
+ from tigrcorn_runtime.server.supervisor import ServerSupervisor
14
+
15
+
16
+ def _add_flag_pair(group: argparse._ArgumentGroup, positive: str, negative: str, *, dest: str, help_text: str) -> None:
17
+ group.add_argument(positive, action='store_true', dest=dest, default=None, help=help_text)
18
+ group.add_argument(negative, action='store_false', dest=dest, default=None, help=f"Disable {help_text.lower()}")
19
+
20
+
21
+ def build_parser() -> argparse.ArgumentParser:
22
+ parser = argparse.ArgumentParser(prog="tigrcorn", description="ASGI3-compatible transport server")
23
+ parser.add_argument("app", nargs="?", help="Application import string in module:attr form")
24
+
25
+ app_group = parser.add_argument_group("App / process / development")
26
+ app_group.add_argument("--app-interface", choices=["auto", "tigr-asgi-contract", "asgi3"], default=None, help="Application interface dispatch mode")
27
+ app_group.add_argument("--factory", action="store_true", default=None, help="Treat APP as an application factory")
28
+ app_group.add_argument("--app-dir", dest="app_dir", default=None, help="Add a directory to sys.path before loading the app")
29
+ app_group.add_argument("--reload", action="store_true", default=None, help="Enable development autoreload")
30
+ app_group.add_argument("--reload-dir", action="append", default=None, help="Directory to watch for reload")
31
+ app_group.add_argument("--reload-include", action="append", default=None, help="Glob to include in reload watch set")
32
+ app_group.add_argument("--reload-exclude", action="append", default=None, help="Glob to exclude from reload watch set")
33
+ app_group.add_argument("--workers", type=int, default=None, help="Worker process count")
34
+ app_group.add_argument("--worker-class", default=None, help="Worker implementation class")
35
+ app_group.add_argument("--runtime", choices=list(SUPPORTED_RUNTIMES), default=None, help="Runtime backend for sync entrypoints and worker processes")
36
+ app_group.add_argument("--pid", default=None, help="PID file path")
37
+ app_group.add_argument("--worker-healthcheck-timeout", type=float, default=None, help="Worker startup healthcheck timeout in seconds")
38
+ app_group.add_argument("--config", default=None, help="Config source: file path (.json, .toml, .yaml, .yml, .py), module:<module>, or object:<module>:<name>")
39
+ app_group.add_argument("--env-file", dest="env_file", default=None, help="Load additional prefixed config values from a dotenv file")
40
+ app_group.add_argument("--env-prefix", default=None, help="Environment variable prefix for config loading")
41
+ app_group.add_argument("--lifespan", choices=["auto", "on", "off"], default=None)
42
+ app_group.add_argument("--limit-max-requests", type=int, default=None, dest="limit_max_requests")
43
+ app_group.add_argument("--max-requests-jitter", type=int, default=None)
44
+
45
+ bind_group = parser.add_argument_group("Listener / binding")
46
+ bind_group.add_argument("--bind", action="append", default=None, help="Bind listener as host:port")
47
+ bind_group.add_argument("--host", default=None, help="Bind host for TCP/UDP listeners")
48
+ bind_group.add_argument("--port", default=None, type=int, help="Bind port for TCP/UDP listeners")
49
+ bind_group.add_argument("--uds", default=None, help="Bind Unix domain socket or pipe path")
50
+ bind_group.add_argument("--fd", action="append", default=None, help="Use an inherited file descriptor listener")
51
+ bind_group.add_argument("--endpoint", action="append", default=None, help="Endpoint / raw listener description")
52
+ bind_group.add_argument("--insecure-bind", action="append", default=None, help="Additional insecure bind alongside TLS listener(s)")
53
+ bind_group.add_argument("--quic-bind", action="append", default=None, help="Additional UDP/QUIC bind")
54
+ bind_group.add_argument("--transport", choices=["tcp", "udp", "unix", "pipe", "inproc"], default=None)
55
+ bind_group.add_argument("--reuse-port", action="store_true", default=None)
56
+ bind_group.add_argument("--reuse-address", action="store_true", default=None)
57
+ bind_group.add_argument("--backlog", type=int, default=None)
58
+ bind_group.add_argument("--user", default=None, help="User name or uid to own Unix sockets")
59
+ bind_group.add_argument("--group", default=None, help="Group name or gid to own Unix sockets")
60
+ bind_group.add_argument("--umask", default=None, help="Umask applied while creating Unix sockets (octal or integer)")
61
+
62
+ static_group = parser.add_argument_group("Static / delivery")
63
+ static_group.add_argument("--static-path-route", dest="static_path_route", default=None, help="HTTP route prefix served from the mounted static directory")
64
+ static_group.add_argument("--static-path-mount", dest="static_path_mount", default=None, help="Filesystem directory mounted at --static-path-route")
65
+ _add_flag_pair(static_group, "--static-path-dir-to-file", "--no-static-path-dir-to-file", dest="static_path_dir_to_file", help_text="directory index resolution for the mounted static path")
66
+ static_group.add_argument("--static-path-index-file", dest="static_path_index_file", default=None, help="Index file name served when directory index resolution is enabled")
67
+ static_group.add_argument("--static-path-expires", dest="static_path_expires", type=int, default=None, help="Static-response cache TTL in seconds; 0 disables caching headers")
68
+
69
+ tls_group = parser.add_argument_group("TLS / security")
70
+ tls_group.add_argument("--ssl-certfile", default=None, help="Certificate for TLS on TCP/Unix or QUIC-TLS on UDP")
71
+ tls_group.add_argument("--ssl-keyfile", default=None, help="Private key for TLS on TCP/Unix or QUIC-TLS on UDP")
72
+ tls_group.add_argument("--ssl-keyfile-password", default=None, help="Password for an encrypted private key PEM used by package-owned TLS/QUIC-TLS listeners")
73
+ tls_group.add_argument("--ssl-ca-certs", default=None, help="Trusted CA bundle for client-certificate verification")
74
+ tls_group.add_argument("--ssl-require-client-cert", action="store_true", default=None, help="Require peer client certificates")
75
+ tls_group.add_argument("--ssl-ciphers", default=None)
76
+ tls_group.add_argument("--ssl-alpn", action="append", default=None, help=flag_help("--ssl-alpn", "ALPN protocol(s); repeat or use comma-separated values"))
77
+ tls_group.add_argument("--ssl-ocsp-mode", choices=["off", "soft-fail", "require"], default=None, help=flag_help("--ssl-ocsp-mode"))
78
+ tls_group.add_argument("--ssl-ocsp-soft-fail", action="store_true", default=None, help=flag_help("--ssl-ocsp-soft-fail"))
79
+ tls_group.add_argument("--ssl-ocsp-cache-size", type=int, default=None, help=flag_help("--ssl-ocsp-cache-size"))
80
+ tls_group.add_argument("--ssl-ocsp-max-age", type=float, default=None, help=flag_help("--ssl-ocsp-max-age"))
81
+ tls_group.add_argument("--ssl-crl-mode", choices=["off", "soft-fail", "require"], default=None, help=flag_help("--ssl-crl-mode"))
82
+ tls_group.add_argument("--ssl-crl", default=None, help=flag_help("--ssl-crl", "Local CRL file (PEM or DER) loaded into the package-owned revocation material set"))
83
+ tls_group.add_argument("--ssl-revocation-fetch", choices=["off", "on"], default=None, help=flag_help("--ssl-revocation-fetch"))
84
+ tls_group.add_argument("--proxy-headers", action="store_true", default=None, help=flag_help("--proxy-headers"))
85
+ tls_group.add_argument("--forwarded-allow-ips", action="append", default=None, help=flag_help("--forwarded-allow-ips", "Trusted forwarded-header peers; repeat or use comma-separated values"))
86
+ tls_group.add_argument("--root-path", default=None, help=flag_help("--root-path", "ASGI root_path mount prefix"))
87
+ tls_group.add_argument("--server-header", nargs="?", const="tigrcorn", default=None, help="Enable or override the Server header value")
88
+ tls_group.add_argument("--no-server-header", action="store_true", default=False, help="Disable the Server header")
89
+ _add_flag_pair(tls_group, "--date-header", "--no-date-header", dest="date_header", help_text="Date header injection")
90
+ tls_group.add_argument("--header", dest="headers", action="append", default=None, help="Default response header in name:value form; repeat to add multiple headers")
91
+ tls_group.add_argument("--server-name", action="append", default=None, help="Allowed Host/:authority value; repeat or use comma-separated values")
92
+
93
+ log_group = parser.add_argument_group("Logging / observability")
94
+ log_group.add_argument("--log-level", default=None)
95
+ _add_flag_pair(log_group, "--access-log", "--no-access-log", dest="access_log", help_text="Access logging")
96
+ log_group.add_argument("--access-log-file", default=None)
97
+ log_group.add_argument("--access-log-format", default=None)
98
+ log_group.add_argument("--error-log-file", default=None)
99
+ log_group.add_argument("--log-config", default=None)
100
+ log_group.add_argument("--structured-log", action="store_true", default=None)
101
+ _add_flag_pair(log_group, "--use-colors", "--no-use-colors", dest="use_colors", help_text="Colorized logging")
102
+ log_group.add_argument("--metrics", action="store_true", default=None, help="Enable the package-owned metrics endpoint/export pipeline")
103
+ log_group.add_argument("--metrics-bind", default=None, help="Bind the in-process Prometheus-style metrics endpoint as host:port")
104
+ log_group.add_argument("--statsd-host", default=None, help="Export metrics to StatsD or DogStatsD using host:port, statsd://host:port, or dogstatsd://host:port")
105
+ log_group.add_argument("--otel-endpoint", default=None, help="Export metrics and spans to the package-owned OTLP-style HTTP collector endpoint")
106
+
107
+ limit_group = parser.add_argument_group("Resource / timeouts / concurrency")
108
+ limit_group.add_argument("--timeout-keep-alive", type=float, default=None, help=flag_help("--timeout-keep-alive"))
109
+ limit_group.add_argument("--read-timeout", type=float, default=None, help=flag_help("--read-timeout"))
110
+ limit_group.add_argument("--write-timeout", type=float, default=None, help=flag_help("--write-timeout"))
111
+ limit_group.add_argument("--timeout-graceful-shutdown", type=float, default=None, help=flag_help("--timeout-graceful-shutdown"))
112
+ limit_group.add_argument("--limit-concurrency", type=int, default=None, help=flag_help("--limit-concurrency"))
113
+ limit_group.add_argument("--max-connections", type=int, default=None, help=flag_help("--max-connections"))
114
+ limit_group.add_argument("--max-tasks", type=int, default=None, help=flag_help("--max-tasks"))
115
+ limit_group.add_argument("--max-streams", type=int, default=None, help=flag_help("--max-streams"))
116
+ limit_group.add_argument("--max-body-size", type=int, default=None, help=flag_help("--max-body-size"))
117
+ limit_group.add_argument("--max-header-size", type=int, default=None, help=flag_help("--max-header-size"))
118
+ limit_group.add_argument("--http1-max-incomplete-event-size", type=int, default=None, help=flag_help("--http1-max-incomplete-event-size", "Cap buffered incomplete HTTP/1.1 request-head bytes before the parser rejects the request"))
119
+ limit_group.add_argument("--http1-buffer-size", type=int, default=None, help=flag_help("--http1-buffer-size", "Read-buffer size used for HTTP/1.1 request-head/body incremental reads"))
120
+ limit_group.add_argument("--http1-header-read-timeout", type=float, default=None, help=flag_help("--http1-header-read-timeout", "HTTP/1.1 request-head read timeout in seconds; when set it tightens the generic read/keep-alive timeout"))
121
+ _add_flag_pair(limit_group, "--http1-keep-alive", "--no-http1-keep-alive", dest="http1_keep_alive", help_text=flag_help("--http1-keep-alive", "HTTP/1.1 connection persistence"))
122
+ limit_group.add_argument("--http2-max-concurrent-streams", type=int, default=None, help=flag_help("--http2-max-concurrent-streams", "Advertised HTTP/2 MAX_CONCURRENT_STREAMS value for inbound peer-created streams"))
123
+ limit_group.add_argument("--http2-max-headers-size", type=int, default=None, help=flag_help("--http2-max-headers-size", "HTTP/2-specific request-header and decoded header-list size cap"))
124
+ limit_group.add_argument("--http2-max-frame-size", type=int, default=None, help=flag_help("--http2-max-frame-size", "Advertised HTTP/2 MAX_FRAME_SIZE for inbound peer frames"))
125
+ _add_flag_pair(limit_group, "--http2-adaptive-window", "--no-http2-adaptive-window", dest="http2_adaptive_window", help_text=flag_help("--http2-adaptive-window", "HTTP/2 adaptive receive-window growth"))
126
+ limit_group.add_argument("--http2-initial-connection-window-size", type=int, default=None, help=flag_help("--http2-initial-connection-window-size", "HTTP/2 connection-level receive window target; values below 65535 are clamped to the protocol default"))
127
+ limit_group.add_argument("--http2-initial-stream-window-size", type=int, default=None, help=flag_help("--http2-initial-stream-window-size", "Advertised HTTP/2 INITIAL_WINDOW_SIZE for peer-created streams"))
128
+ limit_group.add_argument("--http2-keep-alive-interval", type=float, default=None, help=flag_help("--http2-keep-alive-interval", "Idle interval before the server sends an HTTP/2 connection-level PING"))
129
+ limit_group.add_argument("--http2-keep-alive-timeout", type=float, default=None, help=flag_help("--http2-keep-alive-timeout", "HTTP/2 keep-alive PING acknowledgement timeout in seconds"))
130
+ limit_group.add_argument("--websocket-max-message-size", type=int, default=None, help=flag_help("--websocket-max-message-size"))
131
+ limit_group.add_argument("--websocket-max-queue", type=int, default=None, help=flag_help("--websocket-max-queue", "Maximum queued inbound WebSocket messages before transport backpressure is applied"))
132
+ limit_group.add_argument("--websocket-ping-interval", type=float, default=None, help=flag_help("--websocket-ping-interval"))
133
+ limit_group.add_argument("--websocket-ping-timeout", type=float, default=None, help=flag_help("--websocket-ping-timeout"))
134
+ limit_group.add_argument("--idle-timeout", type=float, default=None, help=flag_help("--idle-timeout"))
135
+
136
+ protocol_group = parser.add_argument_group("Protocol / transport")
137
+ protocol_group.add_argument("--http", dest="http_versions", action="append", choices=["1.1", "2", "3"], default=None, help="Enable an HTTP version")
138
+ protocol_group.add_argument("--protocol", dest="protocols", action="append", choices=["http1", "http2", "http3", "quic", "websocket", "webtransport", "rawframed", "custom"], default=None, help="Enable a listener protocol")
139
+ protocol_group.add_argument("--disable-websocket", action="store_true", default=None)
140
+ protocol_group.add_argument("--disable-h2c", action="store_true", default=None, help=flag_help("--disable-h2c"))
141
+ protocol_group.add_argument("--websocket-compression", choices=["off", "permessage-deflate"], default=None, help=flag_help("--websocket-compression"))
142
+ protocol_group.add_argument("--connect-policy", choices=["relay", "deny", "allowlist"], default=None, help=flag_help("--connect-policy"))
143
+ protocol_group.add_argument("--connect-allow", action="append", default=None, help=flag_help("--connect-allow", "Repeat or use comma-separated host:port, host, or CIDR entries"))
144
+ protocol_group.add_argument("--trailer-policy", choices=["pass", "drop", "strict"], default=None, help=flag_help("--trailer-policy"))
145
+ protocol_group.add_argument("--content-coding-policy", choices=["allowlist", "identity-only", "strict"], default=None, help=flag_help("--content-coding-policy"))
146
+ protocol_group.add_argument("--content-codings", action="append", default=None, help=flag_help("--content-codings", "Repeat or use comma-separated values"))
147
+ protocol_group.add_argument("--alt-svc", action="append", default=None, help="Advertise Alt-Svc values; repeat or use comma-separated values")
148
+ _add_flag_pair(protocol_group, "--alt-svc-auto", "--no-alt-svc-auto", dest="alt_svc_auto", help_text="automatic Alt-Svc advertisement for HTTP/3-capable UDP listeners")
149
+ protocol_group.add_argument("--alt-svc-ma", type=int, default=None, help="Alt-Svc max-age for automatic advertisement")
150
+ protocol_group.add_argument("--alt-svc-persist", action="store_true", default=None, help="Set persist=1 on automatic Alt-Svc advertisements")
151
+ protocol_group.add_argument("--quic-require-retry", action="store_true", default=None, help=quic_flag_help("--quic-require-retry"))
152
+ protocol_group.add_argument("--quic-max-datagram-size", type=int, default=None, help=quic_flag_help("--quic-max-datagram-size"))
153
+ protocol_group.add_argument("--quic-idle-timeout", type=float, default=None, help=quic_flag_help("--quic-idle-timeout"))
154
+ protocol_group.add_argument("--quic-early-data-policy", choices=["allow", "deny", "require"], default=None, help=quic_flag_help("--quic-early-data-policy"))
155
+ protocol_group.add_argument("--webtransport-max-sessions", type=int, default=None, help="Maximum concurrently active WebTransport sessions")
156
+ protocol_group.add_argument("--webtransport-max-streams", type=int, default=None, help="Maximum concurrently active WebTransport streams")
157
+ protocol_group.add_argument("--webtransport-max-datagram-size", type=int, default=None, help="Maximum WebTransport datagram payload size")
158
+ protocol_group.add_argument("--webtransport-origin", action="append", default=None, help="Allowed WebTransport Origin value; repeat or use comma-separated values")
159
+ protocol_group.add_argument("--webtransport-path", default=None, help="WebTransport CONNECT path prefix")
160
+ protocol_group.add_argument("--pipe-mode", choices=["rawframed", "stream"], default=None)
161
+ protocol_group.add_argument("--quic-secret", default=None, help=argparse.SUPPRESS)
162
+ return parser
163
+
164
+
165
+ def main(argv: list[str] | None = None) -> int:
166
+ """Parse CLI arguments, build a ServerConfig, and hand off to run_config.
167
+
168
+ The CLI entrypoint is intentionally config-driven. The stable import-string
169
+ convenience surface lives in :mod:`tigrcorn_runtime.api` as ``serve_import_string``;
170
+ the CLI does not re-expose that helper as a module-level patch seam.
171
+ """
172
+ parser = build_parser()
173
+ effective_argv = list(sys.argv[1:] if argv is None else argv)
174
+ ns = parser.parse_args(effective_argv)
175
+ config = build_config_from_namespace(ns)
176
+ app_target = config.app.target or ns.app
177
+ if not app_target and not config.static_mount_enabled:
178
+ parser.error("an application import string is required (either as APP or in the config file) unless a static mount is configured")
179
+
180
+ if config.app.reload and not PollingReloader.is_child_process():
181
+ reloader = PollingReloader(effective_argv, config=config)
182
+ return reloader.run()
183
+
184
+ if config.process.workers > 1 and os.environ.get('TIGRCORN_INTERNAL_RELOADER_CHILD') != '1':
185
+ supervisor = ServerSupervisor(app_target=app_target, config=config)
186
+ supervisor.run()
187
+ return 0
188
+
189
+ config.app.target = app_target
190
+ run_config(config)
191
+ return 0
@@ -0,0 +1,77 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import Any
5
+
6
+ from tigrcorn_runtime.app_interfaces import resolve_app_dispatch
7
+ from tigrcorn_config.model import ServerConfig
8
+ from tigrcorn_runtime.server.runner import TigrCornServer
9
+ from tigrcorn_core.types import ASGIApp
10
+
11
+
12
+ @dataclass(slots=True)
13
+ class EmbeddedServer:
14
+ """Small async helper for embedding tigrcorn inside a larger application.
15
+
16
+ Public contract:
17
+
18
+ - ``start()`` is idempotent and returns the underlying ``TigrCornServer``
19
+ - ``close()`` is a no-op before startup and closes the running server after
20
+ startup
21
+ - the async context-manager surface calls ``start()`` on entry and
22
+ ``close()`` on exit
23
+ - ``listeners`` and ``bound_endpoints()`` expose the currently bound
24
+ listener/runtime endpoints
25
+ """
26
+
27
+ app: ASGIApp
28
+ config: ServerConfig
29
+ server: TigrCornServer | None = field(default=None, init=False)
30
+
31
+ async def start(self) -> TigrCornServer:
32
+ resolve_app_dispatch(self.app, self.config.app.interface)
33
+ if self.server is None:
34
+ self.server = TigrCornServer(self.app, self.config)
35
+ await self.server.start()
36
+ return self.server
37
+
38
+ async def close(self) -> None:
39
+ if self.server is None:
40
+ return
41
+ await self.server.close()
42
+
43
+ async def __aenter__(self) -> 'EmbeddedServer':
44
+ await self.start()
45
+ return self
46
+
47
+ async def __aexit__(self, exc_type, exc, tb) -> None:
48
+ await self.close()
49
+
50
+ @property
51
+ def listeners(self) -> list[Any]:
52
+ if self.server is None:
53
+ return []
54
+ return list(self.server._listeners)
55
+
56
+ def bound_endpoints(self) -> list[Any]:
57
+ if self.server is None:
58
+ return []
59
+ endpoints: list[Any] = []
60
+ for listener in self.server._listeners:
61
+ server = getattr(listener, 'server', None)
62
+ if server is not None and getattr(server, 'sockets', None):
63
+ endpoints.extend(sock.getsockname() for sock in server.sockets)
64
+ continue
65
+ transport = getattr(listener, 'transport', None)
66
+ if transport is not None:
67
+ sockname = transport.get_extra_info('sockname')
68
+ if sockname is not None:
69
+ endpoints.append(sockname)
70
+ continue
71
+ path = getattr(listener, 'path', None)
72
+ if path:
73
+ endpoints.append(path)
74
+ return endpoints
75
+
76
+
77
+ __all__ = ['EmbeddedServer']
@@ -0,0 +1 @@
1
+
@@ -0,0 +1,9 @@
1
+ __all__ = ["TigrCornServer"]
2
+
3
+
4
+ def __getattr__(name: str):
5
+ if name == "TigrCornServer":
6
+ from .runner import TigrCornServer
7
+
8
+ return TigrCornServer
9
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@@ -0,0 +1,42 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import sys
5
+ from contextlib import contextmanager
6
+
7
+ from tigrcorn_core.errors import AppLoadError
8
+ from tigrcorn_core.types import ASGIApp
9
+ from tigrcorn_core.utils.imports import import_from_string
10
+
11
+
12
+ @contextmanager
13
+ def _temporary_app_dir(app_dir: str | None):
14
+ effective_app_dir = os.getcwd() if app_dir is None else app_dir
15
+ if not effective_app_dir:
16
+ yield
17
+ return
18
+ inserted = False
19
+ if effective_app_dir not in sys.path:
20
+ sys.path.insert(0, effective_app_dir)
21
+ inserted = True
22
+ try:
23
+ yield
24
+ finally:
25
+ if inserted:
26
+ try:
27
+ sys.path.remove(effective_app_dir)
28
+ except ValueError: # pragma: no cover
29
+ pass
30
+
31
+
32
+ def load_app(target: str, *, factory: bool = False, app_dir: str | None = None) -> ASGIApp:
33
+ try:
34
+ with _temporary_app_dir(app_dir):
35
+ loaded = import_from_string(target)
36
+ except Exception as exc: # pragma: no cover
37
+ raise AppLoadError(f"failed to load ASGI app {target!r}: {exc}") from exc
38
+ if factory:
39
+ loaded = loaded()
40
+ if not callable(loaded):
41
+ raise AppLoadError(f"loaded object is not callable: {target!r}")
42
+ return loaded # type: ignore[return-value]
@@ -0,0 +1,225 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import contextlib
5
+ import os
6
+ import socket
7
+ from copy import deepcopy
8
+ from pathlib import Path
9
+ from typing import Any, Mapping
10
+
11
+ from tigrcorn_config.load import build_config, config_from_mapping, config_to_dict
12
+ from tigrcorn_config.model import ListenerConfig, ServerConfig
13
+ from tigrcorn_core.constants import SUPPORTED_RUNTIMES
14
+ from tigrcorn_runtime.server.app_loader import load_app
15
+ from tigrcorn_runtime.server.runner import TigrCornServer
16
+ from tigrcorn_static.static import mount_static_app
17
+ from tigrcorn_core.types import ASGIApp
18
+ from tigrcorn_runtime.server.signals import install_signal_handlers
19
+ from tigrcorn_transports.tcp.socketopts import configure_socket
20
+ from tigrcorn_transports.udp.socketopts import configure_udp_socket
21
+
22
+
23
+ def bootstrap(app_target: str, **kwargs) -> TigrCornServer:
24
+ config = build_config(app=app_target, **kwargs)
25
+ app = load_app(app_target, factory=bool(kwargs.get('factory', False)))
26
+ if config.static.mount:
27
+ app = mount_static_app(
28
+ app,
29
+ route=config.static.route or '/',
30
+ directory=config.static.mount,
31
+ dir_to_file=config.static.dir_to_file,
32
+ index_file=config.static.index_file,
33
+ expires=config.static.expires,
34
+ apply_content_coding=True,
35
+ content_coding_policy=config.http.content_coding_policy,
36
+ content_codings=tuple(config.http.content_codings),
37
+ )
38
+ return TigrCornServer(app=app, config=config)
39
+
40
+
41
+ def load_configured_app(config: ServerConfig) -> ASGIApp | None:
42
+ app: ASGIApp | None = None
43
+ if config.app.target:
44
+ app = load_app(config.app.target, factory=config.app.factory, app_dir=config.app.app_dir)
45
+ if config.static.mount:
46
+ app = mount_static_app(
47
+ app,
48
+ route=config.static.route or '/',
49
+ directory=config.static.mount,
50
+ dir_to_file=config.static.dir_to_file,
51
+ index_file=config.static.index_file,
52
+ expires=config.static.expires,
53
+ apply_content_coding=True,
54
+ content_coding_policy=config.http.content_coding_policy,
55
+ content_codings=tuple(config.http.content_codings),
56
+ )
57
+ return app
58
+
59
+
60
+ def _resolve_unix_identity(value: str | int | None, *, group: bool) -> int | None:
61
+ if value is None:
62
+ return None
63
+ if isinstance(value, int):
64
+ return value
65
+ raw = value.strip()
66
+ if not raw:
67
+ return None
68
+ if raw.isdigit():
69
+ return int(raw)
70
+ if os.name != 'posix':
71
+ raise RuntimeError('user/group unix socket ownership controls require POSIX')
72
+ if group:
73
+ import grp
74
+
75
+ return grp.getgrnam(raw).gr_gid
76
+ import pwd
77
+
78
+ return pwd.getpwnam(raw).pw_uid
79
+
80
+
81
+ def _apply_unix_socket_metadata(listener: ListenerConfig) -> None:
82
+ if listener.kind != 'unix' or not listener.path or os.name != 'posix':
83
+ return
84
+ uid = _resolve_unix_identity(listener.user, group=False)
85
+ gid = _resolve_unix_identity(listener.group, group=True)
86
+ path = Path(listener.path)
87
+ if uid is not None or gid is not None:
88
+ os.chown(path, -1 if uid is None else uid, -1 if gid is None else gid)
89
+ if listener.umask is not None:
90
+ path.chmod(0o777 & ~listener.umask)
91
+
92
+
93
+ def _socket_for_listener(listener: ListenerConfig) -> socket.socket | None:
94
+ if listener.kind not in {'tcp', 'udp', 'unix'} or listener.fd is not None or listener.endpoint:
95
+ return None
96
+ if listener.kind == 'unix':
97
+ sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
98
+ path = Path(listener.path or '')
99
+ path.parent.mkdir(parents=True, exist_ok=True)
100
+ if path.exists():
101
+ path.unlink()
102
+ previous_umask = None
103
+ if listener.umask is not None and os.name == 'posix':
104
+ previous_umask = os.umask(listener.umask)
105
+ try:
106
+ sock.bind(str(path))
107
+ finally:
108
+ if previous_umask is not None:
109
+ os.umask(previous_umask)
110
+ _apply_unix_socket_metadata(listener)
111
+ sock.listen(listener.backlog)
112
+ sock.setblocking(False)
113
+ sock.set_inheritable(True)
114
+ return sock
115
+ family = socket.AF_INET6 if ':' in listener.host else socket.AF_INET
116
+ if listener.kind == 'tcp':
117
+ sock = socket.socket(family, socket.SOCK_STREAM)
118
+ if listener.reuse_address:
119
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
120
+ if listener.reuse_port and hasattr(socket, 'SO_REUSEPORT'):
121
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
122
+ sock.bind((listener.host, listener.port))
123
+ sock.listen(listener.backlog)
124
+ sock.setblocking(False)
125
+ sock.set_inheritable(True)
126
+ configure_socket(sock, nodelay=listener.nodelay)
127
+ return sock
128
+ sock = socket.socket(family, socket.SOCK_DGRAM)
129
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
130
+ if listener.reuse_port and hasattr(socket, 'SO_REUSEPORT'):
131
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
132
+ sock.bind((listener.host, listener.port))
133
+ sock.setblocking(False)
134
+ sock.set_inheritable(True)
135
+ configure_udp_socket(sock)
136
+ return sock
137
+
138
+
139
+ def prebind_listener_sockets(config: ServerConfig) -> list[socket.socket]:
140
+ bound: list[socket.socket] = []
141
+ for listener in config.listeners:
142
+ sock = _socket_for_listener(listener)
143
+ if sock is None:
144
+ continue
145
+ listener.fd = sock.fileno()
146
+ bound.append(sock)
147
+ return bound
148
+
149
+
150
+ def config_payload(config: ServerConfig) -> dict[str, Any]:
151
+ return deepcopy(config_to_dict(config))
152
+
153
+
154
+ async def serve_from_config(config: ServerConfig, *, ready_pipe: Any | None = None) -> None:
155
+ app = load_configured_app(config)
156
+ if app is None:
157
+ raise ValueError('config.app.target or config.static.mount is required')
158
+ server = TigrCornServer(app=app, config=config)
159
+ install_signal_handlers(asyncio.get_running_loop(), server.request_shutdown)
160
+ await server.start()
161
+ if ready_pipe is not None:
162
+ with contextlib.suppress(Exception):
163
+ ready_pipe.send('ready')
164
+ ready_pipe.close()
165
+ try:
166
+ await server._should_exit.wait()
167
+ finally:
168
+ await server.close()
169
+
170
+
171
+
172
+ def runtime_compatibility_matrix() -> dict[str, dict[str, object]]:
173
+ """Return the public runtime compatibility contract for the supported runtime surface."""
174
+ matrix = {
175
+ 'auto': {
176
+ 'implemented': True,
177
+ 'strategy': 'prefers uvloop when installed, otherwise asyncio',
178
+ 'requires': [],
179
+ },
180
+ 'asyncio': {
181
+ 'implemented': True,
182
+ 'strategy': 'native asyncio event loop',
183
+ 'requires': [],
184
+ },
185
+ 'uvloop': {
186
+ 'implemented': True,
187
+ 'strategy': 'uvloop event loop',
188
+ 'requires': ['uvloop'],
189
+ },
190
+ }
191
+ return {name: matrix[name] for name in SUPPORTED_RUNTIMES}
192
+
193
+ def run_coro_with_runtime(factory, *, runtime: str) -> None:
194
+ selected = runtime
195
+ if selected == 'auto':
196
+ try:
197
+ import uvloop # type: ignore[import-not-found]
198
+ except Exception:
199
+ selected = 'asyncio'
200
+ else:
201
+ selected = 'uvloop'
202
+ uvloop.run(factory())
203
+ return
204
+ if selected == 'asyncio':
205
+ asyncio.run(factory())
206
+ return
207
+ if selected == 'uvloop':
208
+ try:
209
+ import uvloop # type: ignore[import-not-found]
210
+ except Exception as exc: # pragma: no cover - depends on optional dep
211
+ raise RuntimeError(
212
+ "runtime 'uvloop' requires the uvloop package; install tigrcorn[runtime-uvloop]"
213
+ ) from exc
214
+ uvloop.run(factory())
215
+ return
216
+ raise RuntimeError(f'unsupported runtime: {runtime!r}')
217
+
218
+
219
+ def run_config(config: ServerConfig, *, ready_pipe: Any | None = None) -> None:
220
+ run_coro_with_runtime(lambda: serve_from_config(config, ready_pipe=ready_pipe), runtime=config.process.runtime)
221
+
222
+
223
+ def run_worker_from_config_payload(payload: Mapping[str, Any], ready_pipe: Any | None = None) -> None:
224
+ config = config_from_mapping(payload)
225
+ run_config(config, ready_pipe=ready_pipe)
@@ -0,0 +1,24 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import inspect
5
+ from collections.abc import Iterable
6
+ from typing import Any
7
+
8
+
9
+ async def _run_one_async(hook: Any, *args: Any, **kwargs: Any) -> None:
10
+ result = hook(*args, **kwargs)
11
+ if inspect.isawaitable(result):
12
+ await result
13
+
14
+
15
+ async def run_async_hooks(hooks: Iterable[Any], *args: Any, **kwargs: Any) -> None:
16
+ for hook in hooks:
17
+ await _run_one_async(hook, *args, **kwargs)
18
+
19
+
20
+ def run_sync_hooks(hooks: Iterable[Any], *args: Any, **kwargs: Any) -> None:
21
+ for hook in hooks:
22
+ result = hook(*args, **kwargs)
23
+ if inspect.isawaitable(result):
24
+ asyncio.run(result)