Skip to content

Logging and Secret Redaction

stonepy is deliberately quiet. It does not log your HTTP requests, responses, headers, or payloads. It writes warnings for failed proactive session refresh. Its own object representations mask values keyed as app key, password, session, authorization, or proxy. Usernames are not masked, and logging from the external HTTP stack is not sanitized by stonepy.

This guide covers two distinct mechanisms:

  1. The stdlib logging usage (warnings only).
  2. Secret redaction at the repr() level, which protects ClientConfig and the internal Request object regardless of how you log them.

What stonepy logs

stonepy uses Python's standard logging module.

stonepy.pipeline emits one warning when a proactive session refresh raises a stonepy error:

proactive session refresh failed; continuing with existing token

This refresh behavior is fail-soft. The warning contains no credentials or token, and the request proceeds with the existing session so reactive 401 recovery remains available.

Note

There is no request/response logging built into stonepy. The client does not log URLs, auth headers, order payloads, or rate-limit activity. If you need wire-level tracing you must enable it on the underlying HTTP stack (see the caution at the end of this guide).

Enabling logging

Because stonepy logs through the stdlib, you control it with the normal logging configuration. The pipeline logger lives under the stonepy hierarchy, so configuring stonepy captures its warnings:

import logging

# Simplest: configure the root logger and let propagation carry stonepy records.
logging.basicConfig(level=logging.WARNING)

# Or target the stonepy hierarchy specifically.
stonepy_logger = logging.getLogger("stonepy")
stonepy_logger.setLevel(logging.WARNING)

A failed proactive refresh then produces output similar to:

WARNING:stonepy.pipeline:proactive session refresh failed; continuing with existing token

Tip

Set the level on "stonepy" (the parent) rather than "stonepy.pipeline" if you want a single switch that will also pick up any future stonepy loggers.

How secrets are redacted

Redaction in stonepy happens when an object is converted to its string representation, not inside a logging handler. Values under the recognized keys are therefore hidden in a deliberate log.info(config), an f-string, a print(), or an exception that captures the object. Other values, including usernames, are not masked. The helpers live in stonepy._core.logging.

The core primitive replaces any non-empty value with a fixed mask (it does not truncate or partially reveal the value):

def redact(value: str) -> str:
    if not value:
        return value
    return "***"

The default secret key set

Mapping, header, URL-query, and generated-model redaction share SECRET_KEYS, defined in stonepy/_core/logging.py:

SECRET_KEYS = frozenset(
    {
        "api-key",
        "app-key",
        "app_key",
        "appkey",
        "authorization",
        "cookie",
        "newpassword",
        "password",
        "proxy",
        "proxy-authorization",
        "session",
        "set-cookie",
        "token",
        "twofatoken",
        "x-api-key",
    }
)

Matching is case-insensitive: every candidate key is lowercased before it is compared against this set, so Password, AppKey, Session, and Authorization all match. Any matching value is rendered as '***'.

The secret query-string key set

URL query parameters use the same set in stonepy/_core/transport.py:

_SECRET_QUERY_KEYS = SECRET_KEYS

When the internal Request object is repr'd, _redact_url_query() parses the URL's query string and masks any parameter whose (lowercased) name is in that set, leaving the rest intact.

ClientConfig repr redaction

ClientConfig uses the standard dataclass repr. Its app_key, password, and proxy fields have repr=False, so their names and values are omitted. safe_repr redacts mapping keys and falls back to ordinary repr for other objects, including dataclasses. Given:

from stonepy import ClientConfig

config = ClientConfig(
    base_url="https://ciapi.cityindex.com/TradingAPI",
    app_key="SECRET-KEY",
    username="alice",
    password="hunter2",
    proxy="http://corp-proxy:9999",
)
print(repr(config))

you get (truncated for readability) - note app_key, password, and proxy are omitted while non-secret fields such as username and base_url are shown verbatim:

ClientConfig(base_url='https://ciapi.cityindex.com/TradingAPI', username='alice', ..., user_agent='stonepy/<version>', ...)

Note

username is not in the secret set, so it is printed in full. Only app_key, password, and proxy among the credential-bearing ClientConfig fields are omitted.

Request repr redaction

The internal Request dataclass (used to build every HTTP call) defines a custom __repr__ that redacts on three fronts: the URL query string, the headers mapping, and the params mapping. The body content is never shown - only its length:

def __repr__(self) -> str:
    content = "None" if self.content is None else f"<redacted {len(self.content)} bytes>"
    return (
        f"{type(self).__qualname__}(method={self.method!r}, "
        f"url={_redact_url_query(self.url)!r}, "
        f"headers={safe_repr(self.headers)}, params={safe_repr(self.params)}, "
        f"content={content})"
    )

For mappings, safe_repr returns the repr of a dict whose secret-keyed values are replaced with '***'. A representative repr looks like:

Request(method='GET', url='https://ciapi.cityindex.com/TradingAPI/order/123?Session=***&UserName=alice&Password=***', headers={'Authorization': '***', 'Session': '***', 'UserName': 'alice'}, params={'Session': '***', 'MarketId': '99'}, content=<redacted 7 bytes>)

Here Session, Password, and Authorization are masked in both the URL and the headers/params, while UserName and MarketId pass through, and the request body is reduced to a byte count.

Custom secret keys

safe_repr accepts an optional secret_keys argument that is merged (after lowercasing) into the defaults:

def safe_repr(obj: object, secret_keys: set[str] | None = None) -> str: ...

So calling safe_repr(some_mapping, secret_keys={"custom_secret"}) masks custom_secret in addition to the 15 default keys.

Warning

This extra-keys capability is an internal helper in stonepy._core.logging; it is not exported from the public stonepy API. Request.__repr__ calls safe_repr(...) on headers and params without custom keys. ClientConfig relies on dataclass field omission. There is no ClientConfig setting to register additional secret key names.

Caution: the HTTP layer is not redacted

stonepy's redaction only applies to its own ClientConfig and Request reprs. The underlying transport is httpx, which has its own loggers (for example httpx and httpcore). httpx logs request lines, including URLs, at INFO; DEBUG logging also exposes connection details. stonepy does not redact these logs, and those URLs can contain Session, AppKey, and similar query parameters in clear text.

Warning

Keep the httpx/httpcore loggers at WARNING in production, including when the root logger uses INFO or DEBUG, to keep credentials out of your logs:

import logging

logging.getLogger("httpx").setLevel(logging.WARNING)
logging.getLogger("httpcore").setLevel(logging.WARNING)