Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 127 additions & 0 deletions docs/LOGGING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
# Logging in `ruf_common`

`ruf_common` uses the Python standard-library `logging` module and follows the
standard "library" contract:

- Every module logs to a named logger obtained with `logging.getLogger(__name__)`
(e.g. `ruf_common.network`, `ruf_common.data`, `ruf_common.lfs`, ...). These are
all descendants of the top-level `ruf_common` logger.
- The package installs a single `logging.NullHandler` on the `ruf_common` logger.
The library **never** adds a real handler, never writes to a file, never calls
`logging.basicConfig()`, and never touches the root logger.

The practical consequences:

- The library is **silent by default**. `INFO` and `DEBUG` messages are suppressed
unless you opt in.
- `WARNING`/`ERROR`/`CRITICAL` will surface if — and only where — *your*
application has configured logging. If your app has configured nothing at all,
Python's built-in "last resort" prints `WARNING` and above to `stderr`. (This is
standard-library behaviour, not something `ruf_common` does.)
- **You** decide entirely where logs go and how log files are managed. The library
contributes log *records*; your handlers decide the destination, formatting,
rotation, retention, etc.

## Turning logging on

Enabling is a one-liner: point a handler at the `ruf_common` logger and set the
level you want.

```python
import logging
import ruf_common

# Send ruf_common's logs to the console at DEBUG.
logging.basicConfig(level=logging.WARNING) # your app's root config
logging.getLogger("ruf_common").setLevel(logging.DEBUG) # this library, verbose
```

`basicConfig` installs a handler on the **root** logger; because `ruf_common`
loggers propagate, their records reach it. The per-library `setLevel` call is what
turns `INFO`/`DEBUG` on for this library specifically without making the rest of
your application verbose.

### Per-library and finer-grained control

The logger name gives you as much granularity as you want:

```python
import logging

logging.getLogger("ruf_common").setLevel(logging.INFO) # whole library
logging.getLogger("ruf_common.network").setLevel(logging.DEBUG) # just one module
```

### Sending logs somewhere other than the console

Attach whatever handler you like directly to the library logger — a file, a
rotating file, syslog, etc. The library never does this for you:

```python
import logging
from logging.handlers import RotatingFileHandler

handler = RotatingFileHandler("myapp.log", maxBytes=10_000_000, backupCount=3)
handler.setFormatter(logging.Formatter(
"%(asctime)s | %(levelname)-8s | %(name)s:%(funcName)s - %(message)s"
))

log = logging.getLogger("ruf_common")
log.setLevel(logging.DEBUG)
log.addHandler(handler)
```

### Turning it back off

```python
import logging
logging.getLogger("ruf_common").setLevel(logging.WARNING) # silence INFO/DEBUG again
```

## Advanced: incorporating `ruf_common` logs into loguru

If your application uses [loguru](https://loguru.readthedocs.io/) instead of the
standard library, `ruf_common`'s records won't reach loguru automatically — the two
systems are separate. `ruf_common` emits standard-library records; loguru manages
its own sinks. You bridge them **once, in your application**, by adding a small
stdlib handler that forwards records into loguru (loguru's documented
`InterceptHandler` pattern). `ruf_common` needs no changes — it just keeps emitting
records.

```python
import logging
import sys
from loguru import logger

# --- your application's own loguru configuration (sinks, files, formats) ---
logger.remove() # drop loguru's default handler
logger.add(sys.stderr, level="INFO") # console
logger.add("app.log", level="DEBUG", # rotating file, managed by YOU
rotation="10 MB", retention="1 week", compression="zip")

# --- bridge: forward stdlib records (from ruf_common, etc.) into loguru ---
class InterceptHandler(logging.Handler):
def emit(self, record):
try:
level = logger.level(record.levelname).name
except ValueError:
level = record.levelno
logger.opt(depth=6, exception=record.exc_info).log(level, record.getMessage())

# Route just this library into loguru, and choose how verbose it should be:
lib = logging.getLogger("ruf_common")
lib.setLevel(logging.DEBUG) # let DEBUG/INFO through for ruf_common
lib.addHandler(InterceptHandler()) # send its records to your loguru sinks
lib.propagate = False # optional: don't also hit the stdlib root logger
```

From here on, everything `ruf_common` logs flows into whatever loguru sinks your
application configured (console, `app.log`, etc.), formatted and rotated entirely
by your application's rules.

> Incorporating `ruf_common` into **other** logging frameworks is typically the
> same shape: because `ruf_common` emits ordinary standard-library log records, any
> framework that can consume them works. For example, `structlog` reads stdlib
> records via its `ProcessorFormatter`; you attach that formatter's handler to the
> `ruf_common` logger exactly as shown above. Only the "install a handler that
> adapts stdlib records into framework X" step differs.
7 changes: 4 additions & 3 deletions docs/PUBLISHING.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,16 @@ git commit -m "Bump version to vX.Y.Z"
git push
```

### 3. Tag the commit
### 3. Merge with Main
https://github.com/brian-ruf/ruf-common-python/pulls

### 4. Tag the commit

```bash
git tag vX.Y.Z
git push origin vX.Y.Z
```

### 4. Merge with Main

### 5. Create the GitHub Release

1. Go to [Releases → New release](https://github.com/brian-ruf/ruf-common-python/releases/new)
Expand Down
3 changes: 1 addition & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "ruf-common"
version = "2.1.0"
version = "2.2.0"
description = "Functions common to several of Brian's Python projects."
requires-python = ">=3.9"
license = "MIT"
Expand All @@ -18,7 +18,6 @@ classifiers = [
]
keywords = ["utilities", "common", "helpers"]
dependencies = [
"loguru>=0.7.3",
"elementpath>=4.7.0",
"pytz>=2025.2",
"tzlocal>=5.3.1",
Expand Down
62 changes: 31 additions & 31 deletions ruf_common/__init__.py
Original file line number Diff line number Diff line change
@@ -1,31 +1,31 @@
from loguru import logger
from . import country_code_converter
from . import data
from . import database
from . import helper
from . import html_to_markdown
from . import lfs
from . import network
from . import logging
from . import stats
from . import timezone_lookup
from . import xml_formatter
# Disable logging by default; consumers can enable with:
# logger.enable("ruf_common")
logger.disable("ruf_common")
__all__ = [
"data",
"database",
"lfs",
"helper",
"network",
"logging",
"stats",
"country_code_converter",
"html_to_markdown",
"timezone_lookup",
"xml_formatter"
]
import logging

# The library emits log records but installs no handler of its own, so it stays
# silent unless the calling application configures logging. Consumers enable it
# with e.g. ``logging.getLogger("ruf_common").setLevel(logging.INFO)`` plus a
# handler of their choosing. See docs/LOGGING.md.
logging.getLogger("ruf_common").addHandler(logging.NullHandler())

from . import country_code_converter
from . import data
from . import database
from . import helper
from . import html_to_markdown
from . import lfs
from . import network
from . import stats
from . import timezone_lookup
from . import xml_formatter

__all__ = [
"data",
"database",
"lfs",
"helper",
"network",
"stats",
"country_code_converter",
"html_to_markdown",
"timezone_lookup",
"xml_formatter"
]
4 changes: 3 additions & 1 deletion ruf_common/country_code_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
Python functions to convert country names to ISO 3166-1 alpha-2 country codes.
Provides multiple approaches from simple dictionary lookup to library-based solutions.
"""
from loguru import logger
import logging

logger = logging.getLogger(__name__)

# Method 1: Simple dictionary approach (most common countries)
COUNTRY_TO_CODE = {
Expand Down
4 changes: 3 additions & 1 deletion ruf_common/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@
import elementpath
import xml.etree.ElementTree as ET
from xml.etree.ElementTree import tostring
from loguru import logger
import logging

from typing import Any, cast

logger = logging.getLogger(__name__)

# -------------------------------------------------------------------------
def detect_data_format(content: str) -> str:
"""Detect whether the content is XML, JSON, or YAML based on its starting characters."""
Expand Down
6 changes: 4 additions & 2 deletions ruf_common/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@
# =============================================================================
import sqlite3
import uuid as uuid_module
import logging
from typing import Any, Optional, Union
from loguru import logger
from . import helper
from . import helper
from . import database_sqlite3

logger = logging.getLogger(__name__)
# import asyncio

# List of supported databses:
Expand Down
4 changes: 3 additions & 1 deletion ruf_common/database_sqlite3.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
These functions assume that the SQLite3 database is already created and
"""
import os
from loguru import logger
import logging
import pickle
from typing import Any, Optional, Dict

logger = logging.getLogger(__name__)
import zlib
import sqlite3
from .helper import convert_datetime_format
Expand Down
4 changes: 3 additions & 1 deletion ruf_common/helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@
from tzlocal import get_localzone
import json
import getpass as gt
from loguru import logger
import logging
from typing import Dict, Any, Union

logger = logging.getLogger(__name__)

# -----------------------------------------------------------------------------
# =============================================================================
# DATE/TIME FUNCTIONS
Expand Down
4 changes: 3 additions & 1 deletion ruf_common/lfs.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@
import errno
import sys
import json
import logging
from ruf_common.helper import normalize_content, datetime_string
from loguru import logger
from pathlib import Path

logger = logging.getLogger(__name__)
# from datetime import datetime

# =============================================================================
Expand Down
Loading
Loading