606 lines
20 KiB
Python
606 lines
20 KiB
Python
import json
|
|
import pathlib
|
|
import re
|
|
from collections.abc import Callable
|
|
from enum import Enum
|
|
from typing import Any, TypeVar
|
|
|
|
import click
|
|
|
|
from .constants import HTTPModes, Interfaces, Loops, RuntimeModes, SSLProtocols, TaskImpl
|
|
from .errors import FatalError
|
|
from .http import HTTP1Settings, HTTP2Settings
|
|
from .log import LogLevels
|
|
from .server import Server
|
|
|
|
|
|
_AnyCallable = Callable[..., Any]
|
|
FC = TypeVar('FC', bound=_AnyCallable | click.Command)
|
|
|
|
|
|
class Duration(click.IntRange):
|
|
"""Custom parameter type for duration strings like '24h', '6m', '2s', '1h30m', etc.
|
|
|
|
If the value is a plain number, it will be treated as seconds.
|
|
"""
|
|
|
|
name = 'duration'
|
|
_multipliers = {'s': 1, 'm': 60, 'h': 60 * 60, 'd': 60 * 60 * 24}
|
|
_pattern = re.compile(r'^(?:(?P<d>\d+)d)?(?:(?P<h>\d+)h)?(?:(?P<m>\d+)m)?(?:(?P<s>\d+)s)?$')
|
|
|
|
def __init__(self, min: int | None = None, max: int | None = None) -> None:
|
|
super().__init__(min, max, min_open=False, max_open=False, clamp=False)
|
|
|
|
def convert(self, value: Any, param: click.Parameter | None, ctx: click.Context | None) -> Any:
|
|
if value is None:
|
|
return value
|
|
|
|
if isinstance(value, int):
|
|
seconds = value
|
|
elif isinstance(value, str):
|
|
if value.isnumeric():
|
|
seconds = int(value)
|
|
elif (match := self._pattern.fullmatch(value)) is not None:
|
|
seconds = (
|
|
int(match.group('d') or 0) * self._multipliers['d']
|
|
+ int(match.group('h') or 0) * self._multipliers['h']
|
|
+ int(match.group('m') or 0) * self._multipliers['m']
|
|
+ int(match.group('s') or 0) * self._multipliers['s']
|
|
)
|
|
else:
|
|
self.fail(f'{value!r} is not a valid duration', param, ctx)
|
|
else:
|
|
self.fail(f'{value!r} is not a valid duration', param, ctx)
|
|
|
|
if self.min is not None and seconds < self.min:
|
|
self.fail(f'{value!r} is less than the minimum allowed value of {self.min} seconds', param, ctx)
|
|
|
|
if self.max is not None and seconds > self.max:
|
|
self.fail(f'{value!r} is greater than the maximum allowed value of {self.max} seconds', param, ctx)
|
|
|
|
return seconds
|
|
|
|
|
|
class EnumType(click.Choice):
|
|
def __init__(self, enum: Enum, case_sensitive=False) -> None:
|
|
self.__enum = enum
|
|
super().__init__(choices=[item.value for item in enum], case_sensitive=case_sensitive)
|
|
|
|
def convert(self, value: Any, param: click.Parameter | None, ctx: click.Context | None) -> Enum:
|
|
if value is None or isinstance(value, Enum):
|
|
return value
|
|
|
|
converted_str = super().convert(value, param, ctx)
|
|
return self.__enum(converted_str)
|
|
|
|
|
|
class OctalIntType(click.ParamType):
|
|
name = 'Octal integer'
|
|
|
|
def convert(self, value, param, ctx):
|
|
if value is None or isinstance(value, int):
|
|
return value
|
|
|
|
if not isinstance(value, str):
|
|
self.fail(f'{value!r} is not a valid integer')
|
|
|
|
base = 8 if len(value) > 0 and value[0] == '0' else 10
|
|
try:
|
|
return int(value, base)
|
|
except ValueError as e:
|
|
self.fail(str(e))
|
|
|
|
|
|
def _pretty_print_default(value: bool | None) -> str | None:
|
|
if isinstance(value, bool):
|
|
return 'enabled' if value else 'disabled'
|
|
if isinstance(value, Enum):
|
|
return value.value
|
|
return value
|
|
|
|
|
|
def option(*param_decls: str, cls: type[click.Option] | None = None, **attrs: Any) -> Callable[[FC], FC]:
|
|
attrs['show_envvar'] = True
|
|
if 'default' in attrs:
|
|
attrs['show_default'] = _pretty_print_default(attrs['default'])
|
|
return click.option(*param_decls, cls=cls, **attrs)
|
|
|
|
|
|
@click.command(
|
|
context_settings={'show_default': True},
|
|
help='APP Application target to serve. [required]',
|
|
)
|
|
@click.argument('app', required=True)
|
|
@option(
|
|
'--host',
|
|
default='127.0.0.1',
|
|
help='Host address to bind to',
|
|
)
|
|
@option('--port', type=int, default=8000, help='Port to bind to.')
|
|
@option(
|
|
'--uds', type=click.Path(exists=False, writable=True, path_type=pathlib.Path), help='Unix Domain Socket to bind to.'
|
|
)
|
|
@option('--uds-permissions', type=OctalIntType(), default=None, help='Unix Domain Socket file permissions')
|
|
@option(
|
|
'--interface',
|
|
type=EnumType(Interfaces),
|
|
default=Interfaces.RSGI,
|
|
help='Application interface type',
|
|
)
|
|
@option('--http', type=EnumType(HTTPModes), default=HTTPModes.auto, help='HTTP version')
|
|
@option('--ws/--no-ws', 'websockets', default=True, help='Enable websockets handling')
|
|
@option('--workers', type=click.IntRange(1), default=1, help='Number of worker processes')
|
|
@option(
|
|
'--blocking-threads',
|
|
type=click.IntRange(1),
|
|
help='Number of blocking threads (per worker)',
|
|
)
|
|
@option(
|
|
'--blocking-threads-idle-timeout',
|
|
type=Duration(5, 600),
|
|
default=30,
|
|
help='The maximum amount of time in seconds (or a human-readable duration) an idle blocking thread will be kept alive',
|
|
)
|
|
@option('--runtime-threads', type=click.IntRange(1), default=1, help='Number of runtime threads (per worker)')
|
|
@option(
|
|
'--runtime-blocking-threads',
|
|
type=click.IntRange(1),
|
|
help='Number of runtime I/O blocking threads (per worker)',
|
|
)
|
|
@option(
|
|
'--runtime-mode',
|
|
type=EnumType(RuntimeModes),
|
|
default=RuntimeModes.auto,
|
|
help='Runtime mode to use (single/multi threaded)',
|
|
)
|
|
@option('--loop', type=EnumType(Loops), default=Loops.auto, help='Event loop implementation')
|
|
@option(
|
|
'--task-impl',
|
|
type=EnumType(TaskImpl),
|
|
default=TaskImpl.asyncio,
|
|
help='Async task implementation to use',
|
|
)
|
|
@option(
|
|
'--backlog',
|
|
type=click.IntRange(128),
|
|
default=1024,
|
|
help='Maximum number of connections to hold in backlog (globally)',
|
|
)
|
|
@option(
|
|
'--backpressure',
|
|
type=click.IntRange(1),
|
|
show_default='backlog/workers',
|
|
help='Maximum number of requests to process concurrently (per worker)',
|
|
)
|
|
@option(
|
|
'--http1-buffer-size',
|
|
type=click.IntRange(8192),
|
|
default=HTTP1Settings.max_buffer_size,
|
|
help='Sets the maximum buffer size for HTTP/1 connections',
|
|
)
|
|
@option(
|
|
'--http1-header-read-timeout',
|
|
type=click.IntRange(1, 60_000),
|
|
default=HTTP1Settings.header_read_timeout,
|
|
help='Sets a timeout (in milliseconds) to read headers',
|
|
)
|
|
@option(
|
|
'--http1-keep-alive/--no-http1-keep-alive',
|
|
default=HTTP1Settings.keep_alive,
|
|
help='Enables or disables HTTP/1 keep-alive',
|
|
)
|
|
@option(
|
|
'--http1-pipeline-flush/--no-http1-pipeline-flush',
|
|
default=HTTP1Settings.pipeline_flush,
|
|
help='Aggregates HTTP/1 flushes to better support pipelined responses (experimental)',
|
|
)
|
|
@option(
|
|
'--http2-adaptive-window/--no-http2-adaptive-window',
|
|
default=HTTP2Settings.adaptive_window,
|
|
help='Sets whether to use an adaptive flow control for HTTP2',
|
|
)
|
|
@option(
|
|
'--http2-initial-connection-window-size',
|
|
type=click.IntRange(1024),
|
|
default=HTTP2Settings.initial_connection_window_size,
|
|
help='Sets the max connection-level flow control for HTTP2',
|
|
)
|
|
@option(
|
|
'--http2-initial-stream-window-size',
|
|
type=click.IntRange(1024),
|
|
default=HTTP2Settings.initial_stream_window_size,
|
|
help='Sets the `SETTINGS_INITIAL_WINDOW_SIZE` option for HTTP2 stream-level flow control',
|
|
)
|
|
@option(
|
|
'--http2-keep-alive-interval',
|
|
type=click.IntRange(1, 60_000),
|
|
default=HTTP2Settings.keep_alive_interval,
|
|
help='Sets an interval (in milliseconds) for HTTP2 Ping frames should be sent to keep a connection alive',
|
|
)
|
|
@option(
|
|
'--http2-keep-alive-timeout',
|
|
type=Duration(1),
|
|
default=HTTP2Settings.keep_alive_timeout,
|
|
help='Sets a timeout (in seconds or a human-readable duration) for receiving an acknowledgement of the HTTP2 keep-alive ping',
|
|
)
|
|
@option(
|
|
'--http2-max-concurrent-streams',
|
|
type=click.IntRange(10),
|
|
default=HTTP2Settings.max_concurrent_streams,
|
|
help='Sets the SETTINGS_MAX_CONCURRENT_STREAMS option for HTTP2 connections',
|
|
)
|
|
@option(
|
|
'--http2-max-frame-size',
|
|
type=click.IntRange(1024),
|
|
default=HTTP2Settings.max_frame_size,
|
|
help='Sets the maximum frame size to use for HTTP2',
|
|
)
|
|
@option(
|
|
'--http2-max-headers-size',
|
|
type=click.IntRange(1),
|
|
default=HTTP2Settings.max_headers_size,
|
|
help='Sets the max size of received header frames',
|
|
)
|
|
@option(
|
|
'--http2-max-send-buffer-size',
|
|
type=click.IntRange(1024),
|
|
default=HTTP2Settings.max_send_buffer_size,
|
|
help='Set the maximum write buffer size for each HTTP/2 stream',
|
|
)
|
|
@option('--log/--no-log', 'log_enabled', default=True, help='Enable logging')
|
|
@option('--log-level', type=EnumType(LogLevels), default=LogLevels.info, help='Log level')
|
|
@option(
|
|
'--log-config',
|
|
type=click.Path(exists=True, file_okay=True, dir_okay=False, readable=True, path_type=pathlib.Path),
|
|
help='Logging configuration file (json)',
|
|
)
|
|
@option('--access-log/--no-access-log', 'log_access_enabled', default=False, help='Enable access log')
|
|
@option('--access-log-fmt', 'log_access_fmt', help='Access log format')
|
|
@option(
|
|
'--ssl-certificate',
|
|
type=click.Path(exists=True, file_okay=True, dir_okay=False, readable=True, path_type=pathlib.Path),
|
|
help='SSL certificate file',
|
|
)
|
|
@option(
|
|
'--ssl-keyfile',
|
|
type=click.Path(exists=True, file_okay=True, dir_okay=False, readable=True, path_type=pathlib.Path),
|
|
help='SSL key file (PKCS#8 format only)',
|
|
)
|
|
@option('--ssl-keyfile-password', help='SSL key password')
|
|
@option(
|
|
'--ssl-protocol-min',
|
|
type=EnumType(SSLProtocols),
|
|
default=SSLProtocols.tls13,
|
|
help='Set the minimum supported protocol for SSL connections.',
|
|
)
|
|
@option(
|
|
'--ssl-ca',
|
|
type=click.Path(exists=True, file_okay=True, dir_okay=False, readable=True, path_type=pathlib.Path),
|
|
help='Root SSL cerificate file for client verification',
|
|
)
|
|
@option(
|
|
'--ssl-crl',
|
|
type=click.Path(exists=True, file_okay=True, dir_okay=False, readable=True, path_type=pathlib.Path),
|
|
help='SSL CRL file(s)',
|
|
multiple=True,
|
|
)
|
|
@option(
|
|
'--ssl-client-verify/--no-ssl-client-verify',
|
|
default=False,
|
|
help='Verify clients SSL certificates',
|
|
)
|
|
@option('--url-path-prefix', help='URL path prefix the app is mounted on')
|
|
@option(
|
|
'--respawn-failed-workers/--no-respawn-failed-workers',
|
|
default=False,
|
|
help='Enable workers respawn on unexpected exit',
|
|
)
|
|
@option(
|
|
'--respawn-interval',
|
|
default=3.5,
|
|
help='The number of seconds to sleep between workers respawn',
|
|
)
|
|
@option(
|
|
'--rss-sample-interval',
|
|
type=Duration(1, 300),
|
|
default=30,
|
|
help='The sample rate in seconds (or a human-readable duration) for the resource monitor',
|
|
)
|
|
@option(
|
|
'--rss-samples',
|
|
type=click.IntRange(1),
|
|
default=1,
|
|
help='The number of consecutive samples to consider a worker over resource limit',
|
|
)
|
|
@option(
|
|
'--workers-lifetime',
|
|
type=Duration(60),
|
|
help='The maximum amount of time in seconds (or a human-readable duration) a worker will be kept alive before respawn',
|
|
)
|
|
@option(
|
|
'--workers-max-rss',
|
|
type=click.IntRange(1),
|
|
help='The maximum amount of memory (in MiB) a worker can consume before respawn',
|
|
)
|
|
@option(
|
|
'--workers-kill-timeout',
|
|
type=Duration(1, 1800),
|
|
help='The amount of time in seconds (or a human-readable duration) to wait for killing workers that refused to gracefully stop',
|
|
show_default='disabled',
|
|
)
|
|
@option(
|
|
'--factory/--no-factory',
|
|
default=False,
|
|
help='Treat target as a factory function, that should be invoked to build the actual target',
|
|
)
|
|
@option(
|
|
'--working-dir',
|
|
type=click.Path(exists=True, file_okay=False, dir_okay=True, readable=True, path_type=pathlib.Path),
|
|
help='Set the working directory',
|
|
)
|
|
@option(
|
|
'--env-files',
|
|
type=click.Path(exists=True, file_okay=True, dir_okay=False, readable=True, path_type=pathlib.Path),
|
|
help='Environment file(s) to load (requires granian[dotenv] extra)',
|
|
multiple=True,
|
|
)
|
|
@option(
|
|
'--static-path-route',
|
|
multiple=True,
|
|
show_default='/static',
|
|
help='Route(s) for static file serving',
|
|
)
|
|
@option(
|
|
'--static-path-mount',
|
|
type=click.Path(exists=True, file_okay=False, dir_okay=True, readable=True, path_type=pathlib.Path),
|
|
multiple=True,
|
|
help='Path(s) to mount for static file serving',
|
|
)
|
|
@option(
|
|
'--static-path-dir-to-file',
|
|
default=None,
|
|
help='Serve the specified file as the index for directory listings',
|
|
)
|
|
@option(
|
|
'--static-path-expires',
|
|
type=Duration(0),
|
|
default=86400,
|
|
help='Cache headers expiration (in seconds or a human-readable duration) for static file serving. 0 to disable.',
|
|
)
|
|
@option('--metrics/--no-metrics', 'metrics_enabled', default=False, help='Enable the prometheus metrics exporter.')
|
|
@option(
|
|
'--metrics-scrape-interval', default=15, type=Duration(1, 60), help='Configure the interval for metrics collection.'
|
|
)
|
|
@option(
|
|
'--metrics-address',
|
|
default='127.0.0.1',
|
|
help='Metrics exporter host address to bind to',
|
|
)
|
|
@option('--metrics-port', type=int, default=9090, help='Metrics exporter port to bind to.')
|
|
@option(
|
|
'--reload/--no-reload',
|
|
default=False,
|
|
help="Enable auto reload on application's files changes (requires granian[reload] extra)",
|
|
)
|
|
@option(
|
|
'--reload-paths',
|
|
type=click.Path(exists=True, file_okay=True, dir_okay=True, readable=True, path_type=pathlib.Path),
|
|
help='Paths to watch for changes',
|
|
show_default='Working directory',
|
|
multiple=True,
|
|
)
|
|
@option(
|
|
'--reload-ignore-dirs',
|
|
help=(
|
|
'Names of directories to ignore changes for. '
|
|
"Extends the default list of directories to ignore in watchfiles' default filter"
|
|
),
|
|
multiple=True,
|
|
)
|
|
@option(
|
|
'--reload-ignore-patterns',
|
|
help=(
|
|
'File/directory name patterns (regex) to ignore changes for. '
|
|
"Extends the default list of patterns to ignore in watchfiles' default filter"
|
|
),
|
|
multiple=True,
|
|
)
|
|
@option(
|
|
'--reload-ignore-paths',
|
|
type=click.Path(exists=False, path_type=pathlib.Path),
|
|
help='Absolute paths to ignore changes for',
|
|
multiple=True,
|
|
)
|
|
@option(
|
|
'--reload-tick',
|
|
type=click.IntRange(50, 5000),
|
|
help='The tick frequency (in milliseconds) the reloader watch for changes',
|
|
default=50,
|
|
)
|
|
@option(
|
|
'--reload-ignore-worker-failure/--no-reload-ignore-worker-failure',
|
|
default=False,
|
|
help='Ignore worker failures when auto reload is enabled',
|
|
)
|
|
@option(
|
|
'--process-name',
|
|
help='Set a custom name for processes (requires granian[pname] extra)',
|
|
)
|
|
@option(
|
|
'--pid-file',
|
|
type=click.Path(exists=False, file_okay=True, dir_okay=False, writable=True, path_type=pathlib.Path),
|
|
help='A path to write the PID file to',
|
|
)
|
|
@click.version_option(message='%(prog)s %(version)s')
|
|
def cli(
|
|
app: str,
|
|
host: str,
|
|
port: int,
|
|
uds: pathlib.Path | None,
|
|
uds_permissions: int | None,
|
|
interface: Interfaces,
|
|
http: HTTPModes,
|
|
websockets: bool,
|
|
workers: int,
|
|
blocking_threads: int | None,
|
|
blocking_threads_idle_timeout: int,
|
|
runtime_threads: int,
|
|
runtime_blocking_threads: int | None,
|
|
runtime_mode: RuntimeModes,
|
|
loop: Loops,
|
|
task_impl: TaskImpl,
|
|
backlog: int,
|
|
backpressure: int | None,
|
|
http1_buffer_size: int,
|
|
http1_header_read_timeout: int,
|
|
http1_keep_alive: bool,
|
|
http1_pipeline_flush: bool,
|
|
http2_adaptive_window: bool,
|
|
http2_initial_connection_window_size: int,
|
|
http2_initial_stream_window_size: int,
|
|
http2_keep_alive_interval: int | None,
|
|
http2_keep_alive_timeout: int,
|
|
http2_max_concurrent_streams: int,
|
|
http2_max_frame_size: int,
|
|
http2_max_headers_size: int,
|
|
http2_max_send_buffer_size: int,
|
|
log_enabled: bool,
|
|
log_access_enabled: bool,
|
|
log_access_fmt: str | None,
|
|
log_level: LogLevels,
|
|
log_config: pathlib.Path | None,
|
|
ssl_certificate: pathlib.Path | None,
|
|
ssl_keyfile: pathlib.Path | None,
|
|
ssl_keyfile_password: str | None,
|
|
ssl_protocol_min: SSLProtocols,
|
|
ssl_ca: pathlib.Path | None,
|
|
ssl_crl: list[pathlib.Path] | None,
|
|
ssl_client_verify: bool,
|
|
url_path_prefix: str | None,
|
|
respawn_failed_workers: bool,
|
|
respawn_interval: float,
|
|
rss_sample_interval: int,
|
|
rss_samples: int,
|
|
workers_lifetime: int | None,
|
|
workers_max_rss: int | None,
|
|
workers_kill_timeout: int | None,
|
|
factory: bool,
|
|
working_dir: pathlib.Path | None,
|
|
env_files: list[pathlib.Path] | None,
|
|
static_path_route: list[str],
|
|
static_path_mount: list[pathlib.Path],
|
|
static_path_dir_to_file: str | None,
|
|
static_path_expires: int,
|
|
metrics_enabled: bool,
|
|
metrics_scrape_interval: int,
|
|
metrics_address: str,
|
|
metrics_port: int,
|
|
reload: bool,
|
|
reload_paths: list[pathlib.Path] | None,
|
|
reload_ignore_dirs: list[str] | None,
|
|
reload_ignore_patterns: list[str] | None,
|
|
reload_ignore_paths: list[pathlib.Path] | None,
|
|
reload_tick: int,
|
|
reload_ignore_worker_failure: bool,
|
|
process_name: str | None,
|
|
pid_file: pathlib.Path | None,
|
|
) -> None:
|
|
log_dictconfig = None
|
|
if log_config:
|
|
with log_config.open() as log_config_file:
|
|
try:
|
|
log_dictconfig = json.loads(log_config_file.read())
|
|
except Exception:
|
|
print('Unable to parse provided logging config.')
|
|
raise click.exceptions.Exit(1)
|
|
|
|
from ._internal import patch_pypath
|
|
|
|
patch_pypath(working_dir)
|
|
|
|
server = Server(
|
|
app,
|
|
address=host,
|
|
port=port,
|
|
uds=uds,
|
|
uds_permissions=uds_permissions,
|
|
interface=interface,
|
|
workers=workers,
|
|
blocking_threads=blocking_threads,
|
|
blocking_threads_idle_timeout=blocking_threads_idle_timeout,
|
|
runtime_threads=runtime_threads,
|
|
runtime_blocking_threads=runtime_blocking_threads,
|
|
runtime_mode=runtime_mode,
|
|
loop=loop,
|
|
task_impl=task_impl,
|
|
http=http,
|
|
websockets=websockets,
|
|
backlog=backlog,
|
|
backpressure=backpressure,
|
|
http1_settings=HTTP1Settings(
|
|
header_read_timeout=http1_header_read_timeout,
|
|
keep_alive=http1_keep_alive,
|
|
max_buffer_size=http1_buffer_size,
|
|
pipeline_flush=http1_pipeline_flush,
|
|
),
|
|
http2_settings=HTTP2Settings(
|
|
adaptive_window=http2_adaptive_window,
|
|
initial_connection_window_size=http2_initial_connection_window_size,
|
|
initial_stream_window_size=http2_initial_stream_window_size,
|
|
keep_alive_interval=http2_keep_alive_interval,
|
|
keep_alive_timeout=http2_keep_alive_timeout,
|
|
max_concurrent_streams=http2_max_concurrent_streams,
|
|
max_frame_size=http2_max_frame_size,
|
|
max_headers_size=http2_max_headers_size,
|
|
max_send_buffer_size=http2_max_send_buffer_size,
|
|
),
|
|
log_enabled=log_enabled,
|
|
log_level=log_level,
|
|
log_dictconfig=log_dictconfig,
|
|
log_access=log_access_enabled,
|
|
log_access_format=log_access_fmt,
|
|
ssl_cert=ssl_certificate,
|
|
ssl_key=ssl_keyfile,
|
|
ssl_key_password=ssl_keyfile_password,
|
|
ssl_protocol_min=ssl_protocol_min,
|
|
ssl_ca=ssl_ca,
|
|
ssl_crl=ssl_crl,
|
|
ssl_client_verify=ssl_client_verify,
|
|
url_path_prefix=url_path_prefix,
|
|
respawn_failed_workers=respawn_failed_workers,
|
|
respawn_interval=respawn_interval,
|
|
rss_sample_interval=rss_sample_interval,
|
|
rss_samples=rss_samples,
|
|
workers_lifetime=workers_lifetime,
|
|
workers_max_rss=workers_max_rss,
|
|
workers_kill_timeout=workers_kill_timeout,
|
|
factory=factory,
|
|
working_dir=working_dir,
|
|
env_files=env_files,
|
|
static_path_route=static_path_route,
|
|
static_path_mount=static_path_mount,
|
|
static_path_dir_to_file=static_path_dir_to_file,
|
|
static_path_expires=static_path_expires,
|
|
metrics_enabled=metrics_enabled,
|
|
metrics_scrape_interval=metrics_scrape_interval,
|
|
metrics_address=metrics_address,
|
|
metrics_port=metrics_port,
|
|
reload=reload,
|
|
reload_paths=reload_paths,
|
|
reload_ignore_paths=reload_ignore_paths,
|
|
reload_ignore_dirs=reload_ignore_dirs,
|
|
reload_ignore_patterns=reload_ignore_patterns,
|
|
reload_tick=reload_tick,
|
|
reload_ignore_worker_failure=reload_ignore_worker_failure,
|
|
process_name=process_name,
|
|
pid_file=pid_file,
|
|
)
|
|
|
|
try:
|
|
server.serve()
|
|
except FatalError:
|
|
raise click.exceptions.Exit(1)
|
|
|
|
|
|
def entrypoint():
|
|
cli(auto_envvar_prefix='GRANIAN')
|