Skip to content

indikit.settings and indikit.logging_config

The INDIKIT_* environment, and the logging INDIkit configures at an entrypoint.

Settings is the only reader of the INDIKIT_* environment inside the package: no command-line option carries its own lookup, so a variable has one meaning and one place documenting it. Where a setting also has a flag - --log-level, --wire, --token, --allow-origin, --allow-insecure-bind - the flag wins and its absence defers to the environment.

The one reader outside the package is the Docker image's entrypoint, which reads INDIKIT_TOKEN, INDIKIT_ALLOW_INSECURE_BIND and INDIKIT_ALLOWED_ORIGINS as fallbacks for its own WEB_* spellings, because it has to generate a token and print the panel's URL with it before serve starts. It passes what it resolved on as flags, so Settings still decides one value. See Docker for both spellings.

Nothing reads either module implicitly: IndiClient, Bridge and create_app take explicit parameters, and the entrypoints - the indikit callback and indikit.driver.run - read the settings and pass the values down. Logging goes to stderr, because a driver's stdout is the INDI wire.

log_level accepts CRITICAL, ERROR, WARNING, INFO or DEBUG, case-insensitive; anything else is a usage error rather than a traceback out of uvicorn.Config. Wire traffic goes to one logger for all four sites, named by WIRE_LOGGER ("indikit.wire").

indikit.settings

INDIKIT_*: the environment an operator configures INDIkit with.

A container is configured by environment, not by editing the command line inside it, so the knobs an operator actually reaches for - how chatty the log is, how long the client waits for indiserver, what token guards the write surface - are readable from the environment under one prefix. :class:Settings is that reader, and it is the only one: nothing else in the package calls :func:os.environ.get, and no flag carries a Typer envvar= any more. One variable, one reader, one place to look up what it means.

Nothing reads this implicitly. :class:~indikit.client.IndiClient, :class:~indikit.web.Bridge and :func:~indikit.web.create_app keep explicit parameters with their present defaults; the entrypoints - the CLI callback and :func:indikit.driver.run - read the settings and pass the values down. That is what keeps a library import free of ambient environment and keeps every object injectable, which is what the whole test suite and create_app(client=...) depend on.

This module imports nothing from driver/, web/ or client/. It is imported by both the CLI and the driver's run(), so any dependency it took would become an edge in the import graph that tests/test_layering.py holds flat.

Some of these settings also have a flag, and the rule between the two is one rule everywhere: an explicit flag beats the environment, and the flag's absence is what defers to it. :mod:indikit.cli spells that out by giving every such option a None default and resolving it in the command body, so the environment is read when the command runs rather than when the module is imported - a default evaluated at import time would freeze the first value the process ever saw and make the precedence a lie under any in-process test runner.

:attr:Settings.model_config sets extra="ignore", and that is load-bearing rather than tidy. The prefix is not reserved for this model: the test suite already ships INDIKIT_UPDATE_GOLDEN, and an operator's own tooling may set anything. Under pydantic's default forbid, one such name in the environment would raise out of :func:settings and take every entrypoint down - the CLI, the bridge and every driver at once.

LogLevel

Bases: StrEnum

The log levels INDIKIT_LOG_LEVEL and --log-level accept.

A closed set rather than a free string, because the level is passed on to uvicorn as well as to :mod:logging, and uvicorn knows only these five. A typo is then a parse error naming the variable instead of a stack trace out of uvicorn.Config.

Settings

Bases: BaseSettings

The INDIKIT_* environment, parsed and typed.

Every default is the value the code already used before this model existed, so reading the settings changes no behaviour on its own. They are the defaults for the flags too, since the flags have none of their own: an option that also names a variable defaults to None and falls through to the field below, which is what stops a flag's default and a variable's from drifting apart.

Attributes:

Name Type Description
log_level LogLevel

INDIKIT_LOG_LEVEL. The level :func:configure_logging <indikit.logging_config.configure_logging> sets on the root logger, and the level uvicorn is started at.

wire_log bool

INDIKIT_WIRE_LOG. Whether the indikit.wire logger is turned up to DEBUG, which puts one line on stderr per INDI message in each direction.

connect_timeout float

INDIKIT_CONNECT_TIMEOUT. Seconds :class:~indikit.client.IndiClient waits for each connection attempt.

reconnect_delay float

INDIKIT_RECONNECT_DELAY. Seconds between a lost connection and the next attempt.

message_history int

INDIKIT_MESSAGE_HISTORY. How many recent INDI message frames the bridge replays to a newly attached browser.

max_backlog int

INDIKIT_MAX_BACKLOG. How many live frames a browser may fall behind by before the bridge drops it.

token str

INDIKIT_TOKEN, or serve --token. The shared token /ws and /api require; "" leaves both open, which is what a loopback development server wants.

allowed_origins tuple of str

INDIKIT_ALLOWED_ORIGINS, or a repeated serve --allow-origin. Browser origins accepted on /ws besides the server's own, space separated in the environment; "*" accepts any.

allow_insecure_bind bool

INDIKIT_ALLOW_INSECURE_BIND, or serve --allow-insecure-bind. Whether serve may bind a non-loopback host with no token.

config_dir Path or None

INDIKIT_CONFIG_DIR. Where a driver's CONFIG_PROCESS saves and loads its properties, defaulting to ~/.indikit per :func:_default_config_dir - which does not consult XDG_CONFIG_HOME, so this variable is how the directory is moved. None means there is nowhere to save, and the persistence methods say so.

settings cached

settings() -> Settings

Return the process's settings, read from the environment once.

Cached because the environment does not change under a running process and every entrypoint would otherwise re-parse it. A test that manipulates the environment clears the cache with settings.cache_clear().

Returns:

Name Type Description
settings Settings

The parsed INDIKIT_* environment.

Raises:

Type Description
ValidationError

Raised if a variable is present but does not parse as its type; the error names the variable.

Source code in src/indikit/settings.py
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
@lru_cache(maxsize=1)
def settings() -> Settings:
    """Return the process's settings, read from the environment once.

    Cached because the environment does not change under a running process and
    every entrypoint would otherwise re-parse it. A test that manipulates the
    environment clears the cache with ``settings.cache_clear()``.

    Returns
    -------
    settings : Settings
        The parsed ``INDIKIT_*`` environment.

    Raises
    ------
    pydantic.ValidationError
        Raised if a variable is present but does not parse as its type; the
        error names the variable.
    """
    return Settings()

indikit.logging_config

The package's logging setup, and the shared indikit.wire logger.

:func:configure_logging is the only place INDIkit installs a log handler, and it is called from entrypoints alone - the CLI callback and :func:indikit.driver.run. A library import configures nothing, because a library that installs a root handler on import steals the application's logging.

Logs go to stderr, and that is a requirement rather than a default. A driver's stdout is the INDI wire: :func:indikit.driver.runtime._open_stdio writes serialised XML straight to sys.stdout.buffer, so a log line there corrupts the stream indiserver is parsing. indiserver relays a driver's stderr into its own log, so stderr is also where the operator will find it.

The wire logger. indikit.wire is one name for one question - "what is actually on the wire" - and four call sites answer it: the client's reader and writer, and the driver runtime's reader and writer. An operator should not have to learn that those are different modules, which is why they do not log this on their own module loggers. :func:log_wire guards on :meth:logging.Logger.isEnabledFor, so a run with wire logging off pays one flag check per message.

A BLOB's payload is never logged. One frame is megabytes, and rendering it would make the log the slowest thing in the process; the line reports the payload size read off the model instead, never off a copy made for the log.

configure_logging

configure_logging(level: str | int = 'INFO', *, wire: bool = False) -> None

Send INDIkit logging to stderr at level. Call from an entrypoint only.

Safe to call twice: the handler is installed once and kept, so a later call changes the levels without doubling every line.

The handler is installed outright rather than through :func:logging.basicConfig, which defers to a root that already has a handler. Deferring reads well and is wrong here: under a test runner, or in any process that touched :mod:logging first, it would quietly install nothing and the stderr guarantee above - the one that keeps a driver's stdout clean - would hold only when nothing else got there first.

Parameters:

Name Type Description Default
level str or int

A standard :mod:logging level, by name (case-insensitive) or number.

'INFO'
wire bool

Whether to turn the shared indikit.wire logger up to DEBUG. The root level is left alone, so --wire on its own gives wire traffic without the rest of the package's DEBUG output.

False

Raises:

Type Description
ValueError

Raised if level is a string naming no known level.

Source code in src/indikit/logging_config.py
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
def configure_logging(level: str | int = "INFO", *, wire: bool = False) -> None:
    """Send INDIkit logging to stderr at ``level``. Call from an entrypoint only.

    Safe to call twice: the handler is installed once and kept, so a later call
    changes the levels without doubling every line.

    The handler is installed outright rather than through
    :func:`logging.basicConfig`, which defers to a root that already has a
    handler. Deferring reads well and is wrong here: under a test runner, or in
    any process that touched :mod:`logging` first, it would quietly install
    nothing and the stderr guarantee above - the one that keeps a driver's
    stdout clean - would hold only when nothing else got there first.

    Parameters
    ----------
    level : str or int, optional
        A standard :mod:`logging` level, by name (case-insensitive) or number.
    wire : bool, optional
        Whether to turn the shared ``indikit.wire`` logger up to DEBUG. The
        root level is left alone, so ``--wire`` on its own gives wire traffic
        without the rest of the package's DEBUG output.

    Raises
    ------
    ValueError
        Raised if ``level`` is a string naming no known level.
    """
    global _handler
    if isinstance(level, str):
        level = level.upper()
    # Before installing anything: a bad level is a startup error, and it should
    # not leave a half-configured root behind on its way out.
    logging.getLogger().setLevel(level)
    if _handler is None:
        _handler = logging.StreamHandler(sys.stderr)
        _handler.setFormatter(logging.Formatter(_FORMAT))
        logging.getLogger().addHandler(_handler)
    # NOTSET rather than the root level: the wire logger then inherits, so
    # ``--log-level DEBUG`` without ``--wire`` still shows wire traffic, which is
    # what "show me everything" means.
    _wire.setLevel(logging.DEBUG if wire else logging.NOTSET)

log_wire

log_wire(direction: str, msg: IndiMessage, nbytes: int | None = None) -> None

Log one INDI message on the shared wire logger, if wire logging is on.

The message is named by its model tag (def, set, new, getProperties, ...) rather than by the XML element it would serialise to. The same message travels as XML upstream and as JSON to a browser, so the XML element name would be wrong for half of what this logger reports, and reproducing the codec's tag-stem rule here would put a fourth copy of it in the package.

Parameters:

Name Type Description Default
direction str

"<-" for a message that arrived, "->" for one being sent.

required
msg IndiMessage

The message to describe.

required
nbytes int

The serialised size, where the caller knows it - the writers do, the readers do not, because the parser frames a chunk into several messages.

None
Source code in src/indikit/logging_config.py
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
def log_wire(direction: str, msg: IndiMessage, nbytes: int | None = None) -> None:
    """Log one INDI message on the shared wire logger, if wire logging is on.

    The message is named by its **model tag** (``def``, ``set``, ``new``,
    ``getProperties``, ...) rather than by the XML element it would serialise to.
    The same message travels as XML upstream and as JSON to a browser, so the
    XML element name would be wrong for half of what this logger reports, and
    reproducing the codec's tag-stem rule here would put a fourth copy of it in
    the package.

    Parameters
    ----------
    direction : str
        ``"<-"`` for a message that arrived, ``"->"`` for one being sent.
    msg : IndiMessage
        The message to describe.
    nbytes : int, optional
        The serialised size, where the caller knows it - the writers do, the
        readers do not, because the parser frames a chunk into several messages.
    """
    if not _wire.isEnabledFor(logging.DEBUG):
        return
    size = "" if nbytes is None else f" ({nbytes} bytes)"
    _wire.debug("%s %s%s", direction, _describe(msg), size)