Skip to content

Configuration

palfrey.config

Configuration management and CLI option parsing for Palfrey.

This module provides the central configuration system for Palfrey, integrating environment variables, CLI arguments (via Click), and configuration files into a unified Config dataclass. It defines type aliases and validation sets for all configurable parameters (loop type, HTTP backend, WebSocket mode, etc.) and includes helper functions for path resolution, SSL setup, and application loading. The module bridges Palfrey's command-line interface with its runtime behavior.

PalfreyConfig dataclass

The central configuration model for the Palfrey server runtime.

This class encapsulates all settings related to networking, protocol handling, worker management, and security. It mirrors Uvicorn's configuration structure to maintain a familiar interface for users.

Source code in palfrey/config.py
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
@dataclass(slots=True)
class PalfreyConfig:
    """
    The central configuration model for the Palfrey server runtime.

    This class encapsulates all settings related to networking, protocol handling,
    worker management, and security. It mirrors Uvicorn's configuration structure
    to maintain a familiar interface for users.
    """

    app: AppType
    host: str = "127.0.0.1"
    port: int = 8000
    uds: str | None = None
    fd: int | None = None
    loop: LoopType = "auto"
    http: HTTPType = "auto"
    ws: WSType = "auto"
    ws_max_size: int = 16_777_216
    ws_max_queue: int = 32
    ws_ping_interval: float | None = 20.0
    ws_ping_timeout: float | None = 20.0
    ws_per_message_deflate: bool = True
    lifespan: LifespanMode = "auto"
    interface: InterfaceType = "auto"
    reload: bool = False
    reload_dirs: list[str] = field(default_factory=list)
    reload_includes: list[str] = field(default_factory=list)
    reload_excludes: list[str] = field(default_factory=list)
    reload_delay: float = 0.25
    workers: int | None = None
    env_file: str | os.PathLike[str] | None = None
    log_config: dict[str, Any] | str | RawConfigParser | IO[Any] | None = field(
        default_factory=lambda: deepcopy(LOGGING_CONFIG)
    )
    log_level: LogLevel | int | str | None = None
    access_log: bool = True
    proxy_headers: bool = True
    server_header: bool = True
    date_header: bool = True
    forwarded_allow_ips: list[str] | str | None = None
    root_path: str = ""
    limit_concurrency: int | None = None
    backlog: int = 2048
    limit_max_requests: int | None = None
    limit_max_requests_jitter: int = 0
    timeout_keep_alive: int = 5
    timeout_notify: int = 30
    timeout_graceful_shutdown: int | None = None
    timeout_worker_healthcheck: int = 5
    callback_notify: Callable[..., Awaitable[None]] | None = None
    ssl_keyfile: str | None = None
    ssl_certfile: str | None = None
    ssl_keyfile_password: str | None = None
    ssl_version: int = int(ssl.PROTOCOL_TLS_SERVER)
    ssl_cert_reqs: int = int(ssl.CERT_NONE)
    ssl_ca_certs: str | None = None
    ssl_ciphers: str = "TLSv1"
    headers: list[tuple[str, str]] | list[str] | None = field(default_factory=list)
    use_colors: bool | None = None
    app_dir: str | None = ""
    factory: bool = False
    h11_max_incomplete_event_size: int | None = None
    loaded: bool = field(default=False, init=False)
    loaded_app: Any = field(default=None, init=False, repr=False)
    encoded_headers: list[tuple[bytes, bytes]] = field(default_factory=list, init=False, repr=False)
    ssl_context: ssl.SSLContext | None = field(default=None, init=False, repr=False)
    http_protocol_class: Any = field(default=None, init=False, repr=False)
    ws_protocol_class: Any = field(default=None, init=False, repr=False)
    lifespan_class: Any = field(default=None, init=False, repr=False)
    _normalized_headers_cache: list[tuple[str, str]] = field(
        default_factory=list,
        init=False,
        repr=False,
    )

    def __post_init__(self) -> None:
        """
        Normalizes and validates configuration settings after initialization.

        This method performs input sanitization, resolves environmental defaults
        (like worker count), and sets up directory watching parameters for the reloader.
        """
        if ":" not in self.loop:
            self.loop = self.loop.lower()
        if isinstance(self.http, str) and ":" not in self.http:
            self.http = self.http.lower()
        if isinstance(self.ws, str) and ":" not in self.ws:
            self.ws = self.ws.lower()
        self.lifespan = self.lifespan.lower()
        self.interface = self.interface.lower()

        if isinstance(self.log_level, str):
            lowered = self.log_level.lower()
            if lowered not in KNOWN_LOG_LEVELS:
                raise ValueError(f"Unsupported log level: {self.log_level}")
            self.log_level = lowered

        # Validate implementation modes against known allowed sets
        if self.loop not in KNOWN_LOOP_TYPES and ":" not in self.loop:
            raise ValueError(f"Unsupported loop mode: {self.loop}")
        if isinstance(self.http, str):
            if self.http not in KNOWN_HTTP_TYPES and ":" not in self.http:
                raise ValueError(f"Unsupported HTTP mode: {self.http}")
        if isinstance(self.ws, str):
            if self.ws not in KNOWN_WS_TYPES and ":" not in self.ws:
                raise ValueError(f"Unsupported WebSocket mode: {self.ws}")
        if self.lifespan not in KNOWN_LIFESPAN_MODES:
            raise ValueError(f"Unsupported lifespan mode: {self.lifespan}")
        if self.interface not in KNOWN_INTERFACE_TYPES:
            raise ValueError(f"Unsupported interface mode: {self.interface}")

        # Resolve worker count from environment if not explicitly provided
        if self.workers is None:
            web_concurrency = os.getenv("WEB_CONCURRENCY")
            self.workers = int(web_concurrency) if web_concurrency else 1
        elif self.workers < 1:
            raise ValueError("workers must be >= 1")

        if self.limit_max_requests_jitter < 0:
            raise ValueError("limit_max_requests_jitter must be >= 0")

        if self.forwarded_allow_ips is None:
            self.forwarded_allow_ips = os.getenv("FORWARDED_ALLOW_IPS", "127.0.0.1")

        # Handle reload logic and directory normalization
        if (
            self.reload_dirs or self.reload_includes or self.reload_excludes
        ) and not self.should_reload:
            logger.warning(
                "Current configuration will not reload as not all conditions are met, "
                "please refer to documentation."
            )

        if self.should_reload:
            original_reload_dirs = _normalize_dirs(self.reload_dirs)
            normalized_reload_includes = _normalize_dirs(self.reload_includes)
            normalized_reload_excludes = _normalize_dirs(self.reload_excludes)

            self.reload_includes, resolved_reload_dirs = resolve_reload_patterns(
                normalized_reload_includes,
                original_reload_dirs,
            )
            self.reload_excludes, resolved_reload_dirs_excludes = resolve_reload_patterns(
                normalized_reload_excludes,
                [],
            )

            # Exclude directories as requested
            reload_dirs_tmp = resolved_reload_dirs.copy()
            for excluded_dir in resolved_reload_dirs_excludes:
                for reload_dir in reload_dirs_tmp:
                    if excluded_dir == reload_dir or excluded_dir in reload_dir.parents:
                        with suppress(ValueError):
                            resolved_reload_dirs.remove(reload_dir)

            for pattern in self.reload_excludes:
                if pattern in self.reload_includes:
                    self.reload_includes.remove(pattern)

            if not resolved_reload_dirs:
                if original_reload_dirs:
                    logger.warning(
                        "Provided reload directories %s did not contain valid directories, "
                        "watching current working directory.",
                        original_reload_dirs,
                    )
                resolved_reload_dirs = [Path.cwd()]

            self.reload_dirs = sorted(str(path) for path in resolved_reload_dirs)
            logger.info("Will watch for changes in these directories: %s", self.reload_dirs)
        else:
            self.reload_dirs = _normalize_dirs(self.reload_dirs)
            self.reload_includes = _normalize_dirs(self.reload_includes)
            self.reload_excludes = _normalize_dirs(self.reload_excludes)

        if self.app_dir is not None:
            self.app_dir = str(Path(self.app_dir).resolve())

        # Normalize header cache
        if not self.headers:
            self._normalized_headers_cache = []
        else:
            first_item = self.headers[0]
            if isinstance(first_item, tuple):
                self._normalized_headers_cache = [
                    (str(name), str(value)) for name, value in self.headers
                ]
            else:
                self._normalized_headers_cache = parse_header_items(
                    [str(item) for item in self.headers]
                )

    @property
    def normalized_headers(self) -> list[tuple[str, str]]:
        """
        Returns the parsed and normalized list of custom HTTP headers.

        Returns:
            list[tuple[str, str]]: A list of (name, value) string pairs.
        """
        return self._normalized_headers_cache

    @property
    def workers_count(self) -> int:
        """
        Provides the effective number of worker processes to spawn.

        Returns:
            int: The worker count, defaulting to 1.
        """
        return self.workers or 1

    @property
    def effective_http(self) -> KnownHTTPType:
        """
        Resolves the concrete HTTP implementation to use when 'auto' is selected.

        Returns:
            KnownHTTPType: Either 'httptools' if available, or a configured explicit mode.
        """
        if not isinstance(self.http, str):
            return "h11"
        if self.http == "auto":
            return "httptools" if _module_available("httptools") else "h11"
        if self.http in KNOWN_HTTP_TYPES:
            return cast(KnownHTTPType, self.http)
        return "h11"

    @property
    def effective_ws(self) -> KnownWSType:
        """
        Resolves the concrete WebSocket implementation to use.

        Returns:
            KnownWSType: The resolved WebSocket backend or 'none' if inapplicable.
        """
        if self.effective_http == "h3":
            return "none"
        if self.interface == "wsgi":
            return "none"
        if not isinstance(self.ws, str):
            if _module_available("websockets"):
                return "websockets"
            if _module_available("wsproto"):
                return "wsproto"
            return "none"
        if self.ws == "auto":
            if _module_available("websockets"):
                return "websockets"
            if _module_available("wsproto"):
                return "wsproto"
            return "none"
        if self.ws in KNOWN_WS_TYPES:
            return cast(KnownWSType, self.ws)
        if _module_available("websockets"):
            return "websockets"
        if _module_available("wsproto"):
            return "wsproto"
        return "none"

    @property
    def is_ssl(self) -> bool:
        """
        Indicates whether the configuration includes SSL/TLS parameters.

        Returns:
            bool: True if cert or key files are provided.
        """
        return bool(self.ssl_keyfile or self.ssl_certfile)

    @property
    def should_reload(self) -> bool:
        """
        Determines if the server should run in auto-reload mode.

        Returns:
            bool: True if reload is enabled and the app is provided as a string.
        """
        return isinstance(self.app, str) and self.reload

    @property
    def use_subprocess(self) -> bool:
        """
        Checks if the configuration requires spawning subprocesses.

        Returns:
            bool: True for reload mode or multiple workers.
        """
        return bool(self.reload or self.workers_count > 1)

    @property
    def asgi_version(self) -> Literal["2.0", "3.0"]:
        """
        Determines the ASGI version based on the selected interface.

        Returns:
            Literal["2.0", "3.0"]: The version string.
        """
        mapping: dict[str, Literal["2.0", "3.0"]] = {
            "asgi2": "2.0",
            "asgi3": "3.0",
            "wsgi": "3.0",
        }
        return mapping[self.interface]

    def bind_socket(self) -> socket.socket:
        """
        Creates and binds a server socket based on UDS, FD, or TCP configuration.

        Returns:
            socket.socket: A bound and inheritable socket object.

        Raises:
            SystemExit: If the socket cannot be bound.
        """
        logger_args: list[Any]
        if self.uds:
            sock = socket.socket(SOCKET_AF_UNIX, socket.SOCK_STREAM)
            try:
                sock.bind(self.uds)
                os.chmod(self.uds, 0o666)
            except OSError as exc:
                logger.error("%s", exc)
                raise SystemExit(1) from exc

            message = "Palfrey running on unix socket %s (Press CTRL+C to quit)"
            socket_name_format = "%s"
            color_message = (
                "Palfrey running on "
                + _CLICK_MODULE.style(socket_name_format, bold=True)
                + " (Press CTRL+C to quit)"
            )
            logger_args = [self.uds]
        elif self.fd is not None:
            sock = socket.fromfd(self.fd, SOCKET_AF_UNIX, socket.SOCK_STREAM)
            message = "Palfrey running on socket %s (Press CTRL+C to quit)"
            socket_name_format = "%s"
            color_message = (
                "Palfrey running on "
                + _CLICK_MODULE.style(socket_name_format, bold=True)
                + " (Press CTRL+C to quit)"
            )
            logger_args = [sock.getsockname()]
        else:
            family = socket.AF_INET6 if self.host and ":" in self.host else socket.AF_INET
            sock = socket.socket(family=family)
            sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
            try:
                sock.bind((self.host, self.port))
            except OSError as exc:
                logger.error("%s", exc)
                raise SystemExit(1) from exc

            protocol_name = "https" if self.is_ssl else "http"
            bound_port = int(sock.getsockname()[1])
            if family == socket.AF_INET6:
                address_format = "%s://[%s]:%d"
            else:
                address_format = "%s://%s:%d"
            message = f"Palfrey running on {address_format} (Press CTRL+C to quit)"
            color_message = (
                "Palfrey running on "
                + _CLICK_MODULE.style(address_format, bold=True)
                + " (Press CTRL+C to quit)"
            )
            logger_args = [protocol_name, self.host, bound_port]

        logger.info(message, *logger_args, extra={"color_message": color_message})

        sock.set_inheritable(True)
        return sock

    def load(self) -> None:
        """
        Loads the application, protocol classes, and initializes the SSL context.

        This method triggers the actual importing of the application and the setup
        of the internal protocol classes used by the Palfrey worker.
        """
        assert not self.loaded

        if self.is_ssl:
            assert self.ssl_certfile
            self.ssl_context = create_ssl_context(
                certfile=self.ssl_certfile,
                keyfile=self.ssl_keyfile,
                password=self.ssl_keyfile_password,
                ssl_version=self.ssl_version,
                cert_reqs=self.ssl_cert_reqs,
                ca_certs=self.ssl_ca_certs,
                ciphers=self.ssl_ciphers,
            )
        else:
            self.ssl_context = None

        encoded = [
            (name.lower().encode("latin-1"), value.encode("latin-1"))
            for name, value in self.normalized_headers
        ]
        if self.server_header and b"server" not in dict(encoded):
            self.encoded_headers = [(b"server", b"palfrey")] + encoded
        else:
            self.encoded_headers = encoded

        # Deferred imports for runtime dependencies
        from palfrey.importer import (
            AppFactoryError,
            AppImportError,
            ImportFromStringError,
            _import_from_string,
            resolve_application,
        )
        from palfrey.lifespan import LifespanManager

        # Resolve HTTP protocol class
        if isinstance(self.http, str):
            if self.http in KNOWN_HTTP_TYPES:
                self.http_protocol_class = self.effective_http
            else:
                try:
                    self.http_protocol_class = _import_from_string(self.http)
                except ImportFromStringError as exc:
                    logger.error("Error loading HTTP protocol class. %s", exc)
                    raise SystemExit(1) from exc
        else:
            self.http_protocol_class = self.http

        # Resolve WebSocket protocol class
        if self.interface == "wsgi" or self.ws == "none":
            self.ws_protocol_class = None
        elif isinstance(self.ws, str):
            if self.ws in KNOWN_WS_TYPES:
                self.ws_protocol_class = self.effective_ws
            else:
                try:
                    self.ws_protocol_class = _import_from_string(self.ws)
                except ImportFromStringError as exc:
                    logger.error("Error loading WebSocket protocol class. %s", exc)
                    raise SystemExit(1) from exc
        else:
            self.ws_protocol_class = self.ws

        self.lifespan_class = None if self.lifespan == "off" else LifespanManager

        try:
            resolved = resolve_application(self)
        except AppFactoryError as exc:
            logger.error("Error loading ASGI app factory: %s", exc)
            raise SystemExit(1) from exc
        except AppImportError as exc:
            logger.error("Error loading ASGI app. %s", exc)
            raise SystemExit(1) from exc

        self.loaded_app = resolved.app
        self.interface = resolved.interface
        self.loaded = True

    def setup_event_loop(self) -> None:
        """
        A deprecated method placeholder to match historical Uvicorn interface parity.

        Raises:
            AttributeError: Instructs user to use get_loop_factory.
        """
        raise AttributeError(
            "The `setup_event_loop` method was replaced by `get_loop_factory` in uvicorn 0.36.0.\n"
            "None of those methods are supposed to be used directly. If you are doing it, "
            "please let me know here: https://github.com/Kludex/uvicorn/discussions/2706."
        )

    def get_loop_factory(self) -> Callable[[], asyncio.AbstractEventLoop] | None:
        """
        Resolves the configured event loop string into a concrete factory function.

        Returns:
            Callable[[], asyncio.AbstractEventLoop] | None: A factory function or None if no
                loop is needed.

        Raises:
            SystemExit: If a custom loop factory cannot be imported or is not callable.
        """
        from palfrey.importer import ImportFromStringError, _import_from_string

        if self.loop == "none":
            return None
        if self.loop == "auto":
            return _auto_loop_factory(use_subprocess=self.use_subprocess)
        if self.loop == "asyncio":
            return _asyncio_loop_factory(use_subprocess=self.use_subprocess)
        if self.loop == "uvloop":
            return _uvloop_loop_factory(use_subprocess=self.use_subprocess)

        try:
            loop_factory = _import_from_string(self.loop)
        except ImportFromStringError as exc:
            logger.error("Error loading custom loop setup function. %s", exc)
            raise SystemExit(1) from exc
        if not callable(loop_factory):
            logger.error("Error loading custom loop setup function. Import target not callable.")
            raise SystemExit(1)
        return cast("Callable[[], asyncio.AbstractEventLoop]", loop_factory)

    @classmethod
    def from_import_string(
        cls,
        app: str,
        *,
        host: str = "127.0.0.1",
        port: int = 8000,
        app_dir: str | Path | None = None,
        **kwargs: Any,
    ) -> PalfreyConfig:
        """
        Factory method to create a PalfreyConfig from an application import string.

        Args:
            app (str): The import string (e.g. 'my_module:app').
            host (str): Bind address.
            port (int): Bind port.
            app_dir (str | Path | None): Directory containing the app.
            **kwargs: Arbitrary configuration overrides.

        Returns:
            PalfreyConfig: An initialized configuration instance.
        """
        options: dict[str, Any] = {
            "app": app,
            "host": host,
            "port": port,
            "app_dir": str(app_dir) if app_dir is not None else None,
        }
        options.update(kwargs)
        return cls(**options)

normalized_headers property

Returns the parsed and normalized list of custom HTTP headers.

RETURNS DESCRIPTION
list[tuple[str, str]]

list[tuple[str, str]]: A list of (name, value) string pairs.

workers_count property

Provides the effective number of worker processes to spawn.

RETURNS DESCRIPTION
int

The worker count, defaulting to 1.

TYPE: int

effective_http property

Resolves the concrete HTTP implementation to use when 'auto' is selected.

RETURNS DESCRIPTION
KnownHTTPType

Either 'httptools' if available, or a configured explicit mode.

TYPE: KnownHTTPType

effective_ws property

Resolves the concrete WebSocket implementation to use.

RETURNS DESCRIPTION
KnownWSType

The resolved WebSocket backend or 'none' if inapplicable.

TYPE: KnownWSType

is_ssl property

Indicates whether the configuration includes SSL/TLS parameters.

RETURNS DESCRIPTION
bool

True if cert or key files are provided.

TYPE: bool

should_reload property

Determines if the server should run in auto-reload mode.

RETURNS DESCRIPTION
bool

True if reload is enabled and the app is provided as a string.

TYPE: bool

use_subprocess property

Checks if the configuration requires spawning subprocesses.

RETURNS DESCRIPTION
bool

True for reload mode or multiple workers.

TYPE: bool

asgi_version property

Determines the ASGI version based on the selected interface.

RETURNS DESCRIPTION
Literal['2.0', '3.0']

Literal["2.0", "3.0"]: The version string.

__post_init__()

Normalizes and validates configuration settings after initialization.

This method performs input sanitization, resolves environmental defaults (like worker count), and sets up directory watching parameters for the reloader.

Source code in palfrey/config.py
def __post_init__(self) -> None:
    """
    Normalizes and validates configuration settings after initialization.

    This method performs input sanitization, resolves environmental defaults
    (like worker count), and sets up directory watching parameters for the reloader.
    """
    if ":" not in self.loop:
        self.loop = self.loop.lower()
    if isinstance(self.http, str) and ":" not in self.http:
        self.http = self.http.lower()
    if isinstance(self.ws, str) and ":" not in self.ws:
        self.ws = self.ws.lower()
    self.lifespan = self.lifespan.lower()
    self.interface = self.interface.lower()

    if isinstance(self.log_level, str):
        lowered = self.log_level.lower()
        if lowered not in KNOWN_LOG_LEVELS:
            raise ValueError(f"Unsupported log level: {self.log_level}")
        self.log_level = lowered

    # Validate implementation modes against known allowed sets
    if self.loop not in KNOWN_LOOP_TYPES and ":" not in self.loop:
        raise ValueError(f"Unsupported loop mode: {self.loop}")
    if isinstance(self.http, str):
        if self.http not in KNOWN_HTTP_TYPES and ":" not in self.http:
            raise ValueError(f"Unsupported HTTP mode: {self.http}")
    if isinstance(self.ws, str):
        if self.ws not in KNOWN_WS_TYPES and ":" not in self.ws:
            raise ValueError(f"Unsupported WebSocket mode: {self.ws}")
    if self.lifespan not in KNOWN_LIFESPAN_MODES:
        raise ValueError(f"Unsupported lifespan mode: {self.lifespan}")
    if self.interface not in KNOWN_INTERFACE_TYPES:
        raise ValueError(f"Unsupported interface mode: {self.interface}")

    # Resolve worker count from environment if not explicitly provided
    if self.workers is None:
        web_concurrency = os.getenv("WEB_CONCURRENCY")
        self.workers = int(web_concurrency) if web_concurrency else 1
    elif self.workers < 1:
        raise ValueError("workers must be >= 1")

    if self.limit_max_requests_jitter < 0:
        raise ValueError("limit_max_requests_jitter must be >= 0")

    if self.forwarded_allow_ips is None:
        self.forwarded_allow_ips = os.getenv("FORWARDED_ALLOW_IPS", "127.0.0.1")

    # Handle reload logic and directory normalization
    if (
        self.reload_dirs or self.reload_includes or self.reload_excludes
    ) and not self.should_reload:
        logger.warning(
            "Current configuration will not reload as not all conditions are met, "
            "please refer to documentation."
        )

    if self.should_reload:
        original_reload_dirs = _normalize_dirs(self.reload_dirs)
        normalized_reload_includes = _normalize_dirs(self.reload_includes)
        normalized_reload_excludes = _normalize_dirs(self.reload_excludes)

        self.reload_includes, resolved_reload_dirs = resolve_reload_patterns(
            normalized_reload_includes,
            original_reload_dirs,
        )
        self.reload_excludes, resolved_reload_dirs_excludes = resolve_reload_patterns(
            normalized_reload_excludes,
            [],
        )

        # Exclude directories as requested
        reload_dirs_tmp = resolved_reload_dirs.copy()
        for excluded_dir in resolved_reload_dirs_excludes:
            for reload_dir in reload_dirs_tmp:
                if excluded_dir == reload_dir or excluded_dir in reload_dir.parents:
                    with suppress(ValueError):
                        resolved_reload_dirs.remove(reload_dir)

        for pattern in self.reload_excludes:
            if pattern in self.reload_includes:
                self.reload_includes.remove(pattern)

        if not resolved_reload_dirs:
            if original_reload_dirs:
                logger.warning(
                    "Provided reload directories %s did not contain valid directories, "
                    "watching current working directory.",
                    original_reload_dirs,
                )
            resolved_reload_dirs = [Path.cwd()]

        self.reload_dirs = sorted(str(path) for path in resolved_reload_dirs)
        logger.info("Will watch for changes in these directories: %s", self.reload_dirs)
    else:
        self.reload_dirs = _normalize_dirs(self.reload_dirs)
        self.reload_includes = _normalize_dirs(self.reload_includes)
        self.reload_excludes = _normalize_dirs(self.reload_excludes)

    if self.app_dir is not None:
        self.app_dir = str(Path(self.app_dir).resolve())

    # Normalize header cache
    if not self.headers:
        self._normalized_headers_cache = []
    else:
        first_item = self.headers[0]
        if isinstance(first_item, tuple):
            self._normalized_headers_cache = [
                (str(name), str(value)) for name, value in self.headers
            ]
        else:
            self._normalized_headers_cache = parse_header_items(
                [str(item) for item in self.headers]
            )

bind_socket()

Creates and binds a server socket based on UDS, FD, or TCP configuration.

RETURNS DESCRIPTION
socket

socket.socket: A bound and inheritable socket object.

RAISES DESCRIPTION
SystemExit

If the socket cannot be bound.

Source code in palfrey/config.py
def bind_socket(self) -> socket.socket:
    """
    Creates and binds a server socket based on UDS, FD, or TCP configuration.

    Returns:
        socket.socket: A bound and inheritable socket object.

    Raises:
        SystemExit: If the socket cannot be bound.
    """
    logger_args: list[Any]
    if self.uds:
        sock = socket.socket(SOCKET_AF_UNIX, socket.SOCK_STREAM)
        try:
            sock.bind(self.uds)
            os.chmod(self.uds, 0o666)
        except OSError as exc:
            logger.error("%s", exc)
            raise SystemExit(1) from exc

        message = "Palfrey running on unix socket %s (Press CTRL+C to quit)"
        socket_name_format = "%s"
        color_message = (
            "Palfrey running on "
            + _CLICK_MODULE.style(socket_name_format, bold=True)
            + " (Press CTRL+C to quit)"
        )
        logger_args = [self.uds]
    elif self.fd is not None:
        sock = socket.fromfd(self.fd, SOCKET_AF_UNIX, socket.SOCK_STREAM)
        message = "Palfrey running on socket %s (Press CTRL+C to quit)"
        socket_name_format = "%s"
        color_message = (
            "Palfrey running on "
            + _CLICK_MODULE.style(socket_name_format, bold=True)
            + " (Press CTRL+C to quit)"
        )
        logger_args = [sock.getsockname()]
    else:
        family = socket.AF_INET6 if self.host and ":" in self.host else socket.AF_INET
        sock = socket.socket(family=family)
        sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        try:
            sock.bind((self.host, self.port))
        except OSError as exc:
            logger.error("%s", exc)
            raise SystemExit(1) from exc

        protocol_name = "https" if self.is_ssl else "http"
        bound_port = int(sock.getsockname()[1])
        if family == socket.AF_INET6:
            address_format = "%s://[%s]:%d"
        else:
            address_format = "%s://%s:%d"
        message = f"Palfrey running on {address_format} (Press CTRL+C to quit)"
        color_message = (
            "Palfrey running on "
            + _CLICK_MODULE.style(address_format, bold=True)
            + " (Press CTRL+C to quit)"
        )
        logger_args = [protocol_name, self.host, bound_port]

    logger.info(message, *logger_args, extra={"color_message": color_message})

    sock.set_inheritable(True)
    return sock

load()

Loads the application, protocol classes, and initializes the SSL context.

This method triggers the actual importing of the application and the setup of the internal protocol classes used by the Palfrey worker.

Source code in palfrey/config.py
def load(self) -> None:
    """
    Loads the application, protocol classes, and initializes the SSL context.

    This method triggers the actual importing of the application and the setup
    of the internal protocol classes used by the Palfrey worker.
    """
    assert not self.loaded

    if self.is_ssl:
        assert self.ssl_certfile
        self.ssl_context = create_ssl_context(
            certfile=self.ssl_certfile,
            keyfile=self.ssl_keyfile,
            password=self.ssl_keyfile_password,
            ssl_version=self.ssl_version,
            cert_reqs=self.ssl_cert_reqs,
            ca_certs=self.ssl_ca_certs,
            ciphers=self.ssl_ciphers,
        )
    else:
        self.ssl_context = None

    encoded = [
        (name.lower().encode("latin-1"), value.encode("latin-1"))
        for name, value in self.normalized_headers
    ]
    if self.server_header and b"server" not in dict(encoded):
        self.encoded_headers = [(b"server", b"palfrey")] + encoded
    else:
        self.encoded_headers = encoded

    # Deferred imports for runtime dependencies
    from palfrey.importer import (
        AppFactoryError,
        AppImportError,
        ImportFromStringError,
        _import_from_string,
        resolve_application,
    )
    from palfrey.lifespan import LifespanManager

    # Resolve HTTP protocol class
    if isinstance(self.http, str):
        if self.http in KNOWN_HTTP_TYPES:
            self.http_protocol_class = self.effective_http
        else:
            try:
                self.http_protocol_class = _import_from_string(self.http)
            except ImportFromStringError as exc:
                logger.error("Error loading HTTP protocol class. %s", exc)
                raise SystemExit(1) from exc
    else:
        self.http_protocol_class = self.http

    # Resolve WebSocket protocol class
    if self.interface == "wsgi" or self.ws == "none":
        self.ws_protocol_class = None
    elif isinstance(self.ws, str):
        if self.ws in KNOWN_WS_TYPES:
            self.ws_protocol_class = self.effective_ws
        else:
            try:
                self.ws_protocol_class = _import_from_string(self.ws)
            except ImportFromStringError as exc:
                logger.error("Error loading WebSocket protocol class. %s", exc)
                raise SystemExit(1) from exc
    else:
        self.ws_protocol_class = self.ws

    self.lifespan_class = None if self.lifespan == "off" else LifespanManager

    try:
        resolved = resolve_application(self)
    except AppFactoryError as exc:
        logger.error("Error loading ASGI app factory: %s", exc)
        raise SystemExit(1) from exc
    except AppImportError as exc:
        logger.error("Error loading ASGI app. %s", exc)
        raise SystemExit(1) from exc

    self.loaded_app = resolved.app
    self.interface = resolved.interface
    self.loaded = True

setup_event_loop()

A deprecated method placeholder to match historical Uvicorn interface parity.

RAISES DESCRIPTION
AttributeError

Instructs user to use get_loop_factory.

Source code in palfrey/config.py
def setup_event_loop(self) -> None:
    """
    A deprecated method placeholder to match historical Uvicorn interface parity.

    Raises:
        AttributeError: Instructs user to use get_loop_factory.
    """
    raise AttributeError(
        "The `setup_event_loop` method was replaced by `get_loop_factory` in uvicorn 0.36.0.\n"
        "None of those methods are supposed to be used directly. If you are doing it, "
        "please let me know here: https://github.com/Kludex/uvicorn/discussions/2706."
    )

get_loop_factory()

Resolves the configured event loop string into a concrete factory function.

RETURNS DESCRIPTION
Callable[[], AbstractEventLoop] | None

Callable[[], asyncio.AbstractEventLoop] | None: A factory function or None if no loop is needed.

RAISES DESCRIPTION
SystemExit

If a custom loop factory cannot be imported or is not callable.

Source code in palfrey/config.py
def get_loop_factory(self) -> Callable[[], asyncio.AbstractEventLoop] | None:
    """
    Resolves the configured event loop string into a concrete factory function.

    Returns:
        Callable[[], asyncio.AbstractEventLoop] | None: A factory function or None if no
            loop is needed.

    Raises:
        SystemExit: If a custom loop factory cannot be imported or is not callable.
    """
    from palfrey.importer import ImportFromStringError, _import_from_string

    if self.loop == "none":
        return None
    if self.loop == "auto":
        return _auto_loop_factory(use_subprocess=self.use_subprocess)
    if self.loop == "asyncio":
        return _asyncio_loop_factory(use_subprocess=self.use_subprocess)
    if self.loop == "uvloop":
        return _uvloop_loop_factory(use_subprocess=self.use_subprocess)

    try:
        loop_factory = _import_from_string(self.loop)
    except ImportFromStringError as exc:
        logger.error("Error loading custom loop setup function. %s", exc)
        raise SystemExit(1) from exc
    if not callable(loop_factory):
        logger.error("Error loading custom loop setup function. Import target not callable.")
        raise SystemExit(1)
    return cast("Callable[[], asyncio.AbstractEventLoop]", loop_factory)

from_import_string(app, *, host='127.0.0.1', port=8000, app_dir=None, **kwargs) classmethod

Factory method to create a PalfreyConfig from an application import string.

PARAMETER DESCRIPTION
app

The import string (e.g. 'my_module:app').

TYPE: str

host

Bind address.

TYPE: str DEFAULT: '127.0.0.1'

port

Bind port.

TYPE: int DEFAULT: 8000

app_dir

Directory containing the app.

TYPE: str | Path | None DEFAULT: None

**kwargs

Arbitrary configuration overrides.

TYPE: Any DEFAULT: {}

RETURNS DESCRIPTION
PalfreyConfig

An initialized configuration instance.

TYPE: PalfreyConfig

Source code in palfrey/config.py
@classmethod
def from_import_string(
    cls,
    app: str,
    *,
    host: str = "127.0.0.1",
    port: int = 8000,
    app_dir: str | Path | None = None,
    **kwargs: Any,
) -> PalfreyConfig:
    """
    Factory method to create a PalfreyConfig from an application import string.

    Args:
        app (str): The import string (e.g. 'my_module:app').
        host (str): Bind address.
        port (int): Bind port.
        app_dir (str | Path | None): Directory containing the app.
        **kwargs: Arbitrary configuration overrides.

    Returns:
        PalfreyConfig: An initialized configuration instance.
    """
    options: dict[str, Any] = {
        "app": app,
        "host": host,
        "port": port,
        "app_dir": str(app_dir) if app_dir is not None else None,
    }
    options.update(kwargs)
    return cls(**options)

is_dir(path)

Checks if a path object resolves to a valid directory on the filesystem.

PARAMETER DESCRIPTION
path

The filesystem path to investigate.

TYPE: Path

RETURNS DESCRIPTION
bool

True if the path exists and is a directory; False otherwise.

TYPE: bool

Source code in palfrey/config.py
def is_dir(path: Path) -> bool:
    """
    Checks if a path object resolves to a valid directory on the filesystem.

    Args:
        path (Path): The filesystem path to investigate.

    Returns:
        bool: True if the path exists and is a directory; False otherwise.
    """
    try:
        if not path.is_absolute():
            path = path.resolve()
        return path.is_dir()
    except OSError:
        return False

resolve_reload_patterns(patterns_list, directories_list)

Resolves and normalizes file glob patterns and directory roots for the reload supervisor.

PARAMETER DESCRIPTION
patterns_list

Glob patterns provided by the user for file watching.

TYPE: list[str]

directories_list

Explicit directory paths to be included in the watch.

TYPE: list[str]

RETURNS DESCRIPTION
tuple[list[str], list[Path]]

tuple[list[str], list[Path]]: A tuple containing a unique list of glob patterns and a list of resolved directory Path objects.

Source code in palfrey/config.py
def resolve_reload_patterns(
    patterns_list: list[str],
    directories_list: list[str],
) -> tuple[list[str], list[Path]]:
    """
    Resolves and normalizes file glob patterns and directory roots for the reload supervisor.

    Args:
        patterns_list (list[str]): Glob patterns provided by the user for file watching.
        directories_list (list[str]): Explicit directory paths to be included in the watch.

    Returns:
        tuple[list[str], list[Path]]: A tuple containing a unique list of glob patterns and a
            list of resolved directory Path objects.
    """
    directories: list[Path] = list(set(map(Path, directories_list.copy())))
    patterns: list[str] = patterns_list.copy()

    current_working_directory = Path.cwd()
    for pattern in patterns_list:
        if pattern == ".*":
            continue
        patterns.append(pattern)
        if is_dir(Path(pattern)):
            directories.append(Path(pattern))
        else:
            for match in current_working_directory.glob(pattern):
                if is_dir(match):
                    directories.append(match)

    directories = list(set(directories))
    directories = [Path(path).resolve() for path in directories]
    directories = list({path for path in directories if is_dir(path)})

    # Eliminate child directories if their parent is already being watched
    nested_children: list[Path] = []
    for index, first in enumerate(directories):
        for second in directories[index + 1 :]:
            if first in second.parents:
                nested_children.append(second)
            elif second in first.parents:
                nested_children.append(first)

    directories = list(set(directories).difference(set(nested_children)))
    return list(set(patterns)), directories

create_ssl_context(certfile, keyfile, password, ssl_version, cert_reqs, ca_certs, ciphers)

Constructs an SSLContext using standard Uvicorn/Palfrey configuration parameters.

PARAMETER DESCRIPTION
certfile

Path to the certificate file.

TYPE: str | PathLike[str]

keyfile

Path to the private key file.

TYPE: str | PathLike[str] | None

password

Password for the key file, if applicable.

TYPE: str | None

ssl_version

The protocol version (e.g., ssl.PROTOCOL_TLS_SERVER).

TYPE: int

cert_reqs

Verify mode (e.g., ssl.CERT_REQUIRED).

TYPE: int

ca_certs

Path to CA bundle.

TYPE: str | PathLike[str] | None

ciphers

OpenSSL cipher suite string.

TYPE: str | None

RETURNS DESCRIPTION
SSLContext

ssl.SSLContext: A configured SSL context object ready for socket wrapping.

Source code in palfrey/config.py
def create_ssl_context(
    certfile: str | os.PathLike[str],
    keyfile: str | os.PathLike[str] | None,
    password: str | None,
    ssl_version: int,
    cert_reqs: int,
    ca_certs: str | os.PathLike[str] | None,
    ciphers: str | None,
) -> ssl.SSLContext:
    """
    Constructs an SSLContext using standard Uvicorn/Palfrey configuration parameters.

    Args:
        certfile (str | os.PathLike[str]): Path to the certificate file.
        keyfile (str | os.PathLike[str] | None): Path to the private key file.
        password (str | None): Password for the key file, if applicable.
        ssl_version (int): The protocol version (e.g., ssl.PROTOCOL_TLS_SERVER).
        cert_reqs (int): Verify mode (e.g., ssl.CERT_REQUIRED).
        ca_certs (str | os.PathLike[str] | None): Path to CA bundle.
        ciphers (str | None): OpenSSL cipher suite string.

    Returns:
        ssl.SSLContext: A configured SSL context object ready for socket wrapping.
    """
    context = ssl.SSLContext(ssl_version)
    get_password = (lambda: password) if password else None
    context.load_cert_chain(certfile, keyfile, get_password)
    context.verify_mode = ssl.VerifyMode(cert_reqs)
    if ca_certs:
        context.load_verify_locations(ca_certs)
    if ciphers:
        context.set_ciphers(ciphers)
    return context