apprc 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
apprc/__init__.py ADDED
@@ -0,0 +1,19 @@
1
+ """Reusable application runtime configuration and logging helpers."""
2
+
3
+ # ruff: noqa: F401
4
+
5
+ from apprc.config import (
6
+ AppConfigKit,
7
+ AppConfigSpec,
8
+ BaseConfig,
9
+ BaseEnv,
10
+ ConfigField,
11
+ ConfigOwner,
12
+ EnvBootstrapSpec,
13
+ )
14
+ from apprc.logging import (
15
+ AppLogger,
16
+ LoggingConfig,
17
+ get_logger,
18
+ setup_logging,
19
+ )
apprc/_dotenv_guard.py ADDED
@@ -0,0 +1,29 @@
1
+ """Stdlib-only helpers for guarding dotenv autoload side effects."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from collections.abc import Iterator
7
+ from contextlib import contextmanager
8
+
9
+
10
+ @contextmanager
11
+ def _disable_dotenv_autoload() -> Iterator[None]:
12
+ """Temporarily disable libraries that auto-load ``.env`` files.
13
+
14
+ Some libraries import ``python-dotenv`` through their dependency graph.
15
+ Setting ``PYTHON_DOTENV_DISABLED`` around those imports preserves the
16
+ application's explicit environment loading policy while restoring the
17
+ user's original process environment afterward.
18
+
19
+ :return: Context manager that restores ``PYTHON_DOTENV_DISABLED``.
20
+ """
21
+ prev = os.environ.get("PYTHON_DOTENV_DISABLED")
22
+ os.environ["PYTHON_DOTENV_DISABLED"] = "1"
23
+ try:
24
+ yield
25
+ finally:
26
+ if prev is None:
27
+ os.environ.pop("PYTHON_DOTENV_DISABLED", None)
28
+ else:
29
+ os.environ["PYTHON_DOTENV_DISABLED"] = prev
apprc/_lazy.py ADDED
@@ -0,0 +1,62 @@
1
+ """Small helpers for PEP 562 package facades. This speeds up import time a lot
2
+
3
+ Once Python 3.15 hits, replace all this logic with `lazy import`
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import sys
9
+ from collections.abc import Callable, Mapping, Sequence
10
+ from importlib import import_module
11
+
12
+
13
+ type LazyGetattr = Callable[[str], object]
14
+ type LazyDir = Callable[[], list[str]]
15
+
16
+
17
+ def build_lazy_facade(
18
+ *,
19
+ public_module: str,
20
+ all_exports: Sequence[str],
21
+ module_exports: Mapping[str, str],
22
+ symbol_exports: Mapping[str, str],
23
+ ) -> tuple[list[str], LazyGetattr, LazyDir]:
24
+ """Build module-level hooks for one lazy public facade.
25
+
26
+ The returned ``__getattr__`` resolves modules and symbols only when a caller
27
+ first touches them. Resolved values are stored on the public module, not on
28
+ the private ``_facade`` module, so repeated access is the same cheap global
29
+ lookup that an eager facade would provide.
30
+
31
+ :param public_module: Public module name that owns the lazy attributes.
32
+ :param all_exports: Stable ``__all__`` values for the facade.
33
+ :param module_exports: Exported submodule names to import paths.
34
+ :param symbol_exports: Exported symbol names to the module that defines them.
35
+ :return: ``(__all__, __getattr__, __dir__)`` for re-export by ``__init__``.
36
+ """
37
+ all_names = list(all_exports)
38
+ module_map = dict(module_exports)
39
+ symbol_map = dict(symbol_exports)
40
+ known_names = frozenset((*module_map, *symbol_map))
41
+
42
+ def __getattr__(name: str) -> object:
43
+ namespace = sys.modules[public_module].__dict__
44
+ if name in module_map:
45
+ value = import_module(module_map[name])
46
+ else:
47
+ try:
48
+ module_name = symbol_map[name]
49
+ except KeyError as exc:
50
+ raise AttributeError(
51
+ f"module {public_module!r} has no attribute {name!r}"
52
+ ) from exc
53
+ module = import_module(module_name)
54
+ value = getattr(module, name)
55
+ namespace[name] = value
56
+ return value
57
+
58
+ def __dir__() -> list[str]:
59
+ namespace = sys.modules[public_module].__dict__
60
+ return sorted(set(namespace) | known_names | set(all_names))
61
+
62
+ return all_names, __getattr__, __dir__
apprc/cli/__init__.py ADDED
@@ -0,0 +1,31 @@
1
+ """Reusable CLI helpers for application config commands."""
2
+
3
+ # ruff: noqa: F401
4
+
5
+ from apprc.cli.bootstrap import bootstrap_cli_env, parse_log_level
6
+ from apprc.cli.config_app import (
7
+ ConfigCliState,
8
+ active_storage_root_from_state,
9
+ build_config_typer_app,
10
+ config_request_skips_bootstrap,
11
+ initial_storage_from_state,
12
+ )
13
+ from apprc.cli.doctor import (
14
+ build_config_doctor_payload,
15
+ config_command_text,
16
+ config_setup_message,
17
+ print_config_doctor,
18
+ )
19
+ from apprc.cli.options import (
20
+ COMMON_ROOT_FLAG_OPTIONS,
21
+ COMMON_ROOT_VALUE_OPTIONS,
22
+ )
23
+ from apprc.cli.typer_utils import (
24
+ MISSING_ACTION_MESSAGE,
25
+ args_after_command,
26
+ dump_json,
27
+ exit_missing_action,
28
+ run_typer_app,
29
+ state_from,
30
+ strip_leading_options,
31
+ )
apprc/cli/bootstrap.py ADDED
@@ -0,0 +1,66 @@
1
+ """Root CLI bootstrap helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ # == Standard Library ========================
6
+ from collections.abc import Callable
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ # == 3rd Party ===============================
11
+ import typer
12
+
13
+ # == Internal ================================
14
+ from apprc.config.environment import BootstrapLogger, EnvBootstrapResult
15
+ from apprc.config.kit import AppConfigKit
16
+
17
+
18
+ def parse_log_level(log_level: str) -> str | int:
19
+ """Convert a CLI log-level token into the logging backend value.
20
+
21
+ :param log_level: User-provided Typer option value.
22
+ :return: Integer level for decimal strings, otherwise the original token.
23
+ """
24
+ return int(log_level) if log_level.isdecimal() else log_level
25
+
26
+
27
+ def bootstrap_cli_env(
28
+ kit: AppConfigKit,
29
+ *,
30
+ env_file: Path | None,
31
+ env_file_overrides_shell: bool,
32
+ no_dotenv: bool,
33
+ storage_name: str | None,
34
+ log_level: str | None = None,
35
+ setup_logging: Callable[..., Any] | None = None,
36
+ logger: BootstrapLogger | None = None,
37
+ ) -> EnvBootstrapResult:
38
+ """Initialize logging and dotenv layers for one CLI process.
39
+
40
+ :param kit: Application config facade.
41
+ :param env_file: Optional explicit dotenv file.
42
+ :param env_file_overrides_shell: Whether explicit dotenv values beat
43
+ already exported variables inside this process.
44
+ :param no_dotenv: Disable dotenv layer loading.
45
+ :param storage_name: Optional named storage selector.
46
+ :param log_level: Optional CLI log-level token.
47
+ :param setup_logging: Optional application logging setup callable.
48
+ :param logger: Optional application logger for bootstrap status messages.
49
+ :return: Bootstrap summary for diagnostics and tests.
50
+ :raises typer.BadParameter: If the explicit env file or storage selector
51
+ is invalid.
52
+ """
53
+ if setup_logging is not None and log_level is not None:
54
+ setup_logging(level=parse_log_level(log_level))
55
+ try:
56
+ return kit.bootstrap(
57
+ env_file=env_file,
58
+ env_file_overrides_shell=env_file_overrides_shell,
59
+ no_dotenv=no_dotenv,
60
+ storage_name=storage_name,
61
+ logger=logger,
62
+ )
63
+ except FileNotFoundError as exc:
64
+ raise typer.BadParameter(str(exc), param_hint="--env-file") from exc
65
+ except ValueError as exc:
66
+ raise typer.BadParameter(str(exc), param_hint="--storage") from exc