🏠 Home | ⚙️ API Reference | 🛠️ Common API
🛠️ Common API (Utilities)
This section describes the shared utility functions of the codex-core library.
Phone Normalization
Tools for correctly formatting phone numbers (supports +, 00, and local 0).
phone
Utilities for normalizing phone numbers to a canonical digit-only format.
Framework-agnostic (zero Django / framework dependencies). Suitable for use in any Python 3.10+ environment including async workers.
The module intentionally does not validate that the resulting number is reachable (no libphonenumber dependency); it only ensures a consistent digit-only representation that can be safely stored, compared, and passed to SMS / telephony APIs.
Functions
normalize_phone(phone, default_country='49')
Normalize a phone number to a digit-only international string.
Handles the three most common input variants encountered in European / German-locale data:
- International
+prefix —+49 151 1234567 - International
00prefix —0049 151 1234567 - Local
0prefix —0151 1234567(expanded using default_country) - Already normalized —
491511234567(returned as-is)
Non-digit, non-plus characters (spaces, hyphens, parentheses) are stripped before prefix detection.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
phone
|
str
|
Raw phone string in any common format. Empty string or
strings containing no digits/plus return |
required |
default_country
|
str
|
ITU-T country code (digits only, no |
'49'
|
Returns:
| Type | Description |
|---|---|
str
|
Digit-only string representing the international phone number, |
str
|
or an empty string if the input is blank or contains no |
str
|
recognizable digits. |
Note
No length or reachability validation is performed. The caller is responsible for validating that the result conforms to the expected E.164 length for the target country.
Example
normalize_phone("0151 1234567") # → "491511234567"
normalize_phone("+49 151 1234567") # → "491511234567"
normalize_phone("0049 151 1234567") # → "491511234567"
normalize_phone("+1-800-555-0100", "1") # → "18005550100"
normalize_phone("") # → ""
Source code in src/codex_core/common/phone.py
13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 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 | |
Text Processing
Tools for name normalization, transliteration, and string cleaning.
text
Text processing utilities for name normalization, transliteration, and SMS safety.
Framework-agnostic (zero Django / framework dependencies). All functions are pure and stateless: they accept a string, return a string, and produce no side effects.
Functions are organized around three distinct concerns:
- Normalization — :func:
normalize_name, :func:clean_stringproduce canonical, whitespace-clean representations. - Transliteration — :func:
transliterateconverts Cyrillic to Latin for systems that do not support Unicode input. - Sanitization — :func:
sanitize_for_smsstrips characters illegal or problematic in SMS payloads.
Functions
clean_string(text)
Collapse multiple whitespace characters into a single space.
Strips leading / trailing whitespace and replaces any internal
sequence of whitespace (spaces, tabs, newlines) with a single
ASCII space. Invisible Unicode whitespace is also collapsed
because str.split() is Unicode-aware.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
text
|
str
|
Raw string that may contain irregular whitespace. |
required |
Returns:
| Type | Description |
|---|---|
str
|
Cleaned string with normalized whitespace, or an empty string |
str
|
if text is falsy. |
Example
clean_string(" hello\t world\n") # → "hello world"
clean_string("") # → ""
Source code in src/codex_core/common/text.py
58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 | |
normalize_name(name)
Capitalize each word in a personal name, preserving hyphens.
Collapses repeated whitespace, then title-cases every name segment separated by a space or hyphen while leaving the delimiters unchanged.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
Raw name string in any case with arbitrary spacing
(e.g. |
required |
Returns:
| Type | Description |
|---|---|
str
|
Normalized name with each segment capitalized |
str
|
(e.g. |
str
|
name is falsy. |
Example
normalize_name("ivan ivanov-petrov") # → "Ivan Ivanov-Petrov"
normalize_name("ANNA MÜLLER") # → "Anna Müller"
normalize_name("") # → ""
Source code in src/codex_core/common/text.py
20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 | |
sanitize_for_sms(text, max_length=50)
Produce a safe, length-bounded string suitable for SMS payloads.
Applies two successive regex passes:
- Replaces
\r,\n,\tsequences with a single space to eliminate line breaks that most SMS gateways convert to encoding errors. - Strips all characters outside the set
[\w\s.\-](word characters, spaces, dots, hyphens).
The result is then truncated to max_length characters and stripped of trailing whitespace.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
text
|
str
|
Arbitrary user-supplied or templated string. |
required |
max_length
|
int
|
Maximum number of characters in the output.
Defaults to |
50
|
Returns:
| Type | Description |
|---|---|
str
|
Sanitized and truncated string, or an empty string if text |
str
|
is falsy. |
Note
Truncation happens before the final strip(), so the
effective output length may be slightly less than max_length
when the truncation boundary falls on whitespace.
Example
sanitize_for_sms("Hello\nWorld!!! <script>", 20)
# → "Hello World script"
sanitize_for_sms("Appointment at 10:00", 15)
# → "Appointment at"
Source code in src/codex_core/common/text.py
189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 | |
transliterate(text)
Transliterate Cyrillic characters to their Latin equivalents.
Uses a hard-coded character-level mapping (str.maketrans)
covering all 33 letters of the Russian alphabet in both cases.
Non-Cyrillic characters pass through unchanged.
The mapping follows a simplified scientific transliteration standard (not GOST 7.79-2000) optimised for readability in Latin-script contexts such as SMS gateways and URL slugs.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
text
|
str
|
Input string containing Cyrillic characters. |
required |
Returns:
| Type | Description |
|---|---|
str
|
String with each Cyrillic character replaced by its Latin |
str
|
approximation, or an empty string if text is falsy. |
Note
ъ (hard sign) and ь (soft sign) map to an empty
string, reducing the output length relative to the input.
Example
transliterate("Привет") # → "Privet"
transliterate("Щукин") # → "Shchukin"
transliterate("Hello World") # → "Hello World" (passthrough)
Source code in src/codex_core/common/text.py
84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 | |
Logging
Helpers for structured logging and Loguru configuration.
log_context
Structured logging context bound to a named task or worker.
Provides :class:TaskLogContext — a thin, stateful wrapper around
logging.Logger that automatically enriches every log record with
a fixed set of structured fields (task name, worker name, arbitrary
extras).
This module intentionally avoids any dependency on Loguru so that it
works with any log sink: standard-library logging, Loguru (via
:class:~codex_core.common.loguru_setup.InterceptHandler), JSON
formatters, ELK, or Loki.
Example
from codex_core.common.log_context import TaskLogContext
log = TaskLogContext("send_booking_notification", worker="notification_worker")
log.info("Processing appointment", extra={"appointment_id": 123})
# LogRecord contains: task="send_booking_notification",
# worker="notification_worker", appointment_id=123
Classes
TaskLogContext
Structured logging adapter that binds context fields to every record.
Wraps a standard logging.Logger and automatically merges a
fixed _base_extra dict (built at construction time) with any
per-call extra dict before forwarding the record. This
eliminates repetitive extra={"task": ...} boilerplate in
worker loops.
The class is stateful in the sense that _base_extra is
set once at construction and shared across all log calls. It is
not thread-safe to mutate _base_extra after construction;
create a new instance per task if context must change.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
task_name
|
str
|
Logical name of the current operation
(e.g. |
required |
logger_name
|
str | None
|
Name passed to |
None
|
**extra
|
Any
|
Arbitrary keyword arguments added to |
{}
|
Example
log = TaskLogContext(
"slot_calculation",
logger_name="codex.booking",
worker="slot_worker",
tenant_id=42,
)
log.debug("Starting slot search")
log.error("Slot not found", extra={"slot_id": 7})
Source code in src/codex_core/common/log_context.py
28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 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 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 | |
Functions
debug(msg, *args, extra=None, **kwargs)
Emit a DEBUG record enriched with the bound context fields.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
msg
|
str
|
Log message format string. |
required |
*args
|
Any
|
Positional arguments passed to |
()
|
extra
|
dict[str, Any] | None
|
Per-call structured fields merged with |
None
|
**kwargs
|
Any
|
Additional keyword arguments forwarded to |
{}
|
Source code in src/codex_core/common/log_context.py
83 84 85 86 87 88 89 90 91 92 | |
error(msg, *args, extra=None, **kwargs)
Emit an ERROR record enriched with the bound context fields.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
msg
|
str
|
Log message format string. |
required |
*args
|
Any
|
Positional arguments passed to |
()
|
extra
|
dict[str, Any] | None
|
Per-call structured fields merged with |
None
|
**kwargs
|
Any
|
Additional keyword arguments forwarded to |
{}
|
Source code in src/codex_core/common/log_context.py
116 117 118 119 120 121 122 123 124 125 | |
exception(msg, *args, extra=None, **kwargs)
Emit an ERROR record with exception traceback and bound context.
Equivalent to :meth:error but always captures the current
exception info (exc_info=True is implicit). Call inside an
except block.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
msg
|
str
|
Log message format string. |
required |
*args
|
Any
|
Positional arguments passed to |
()
|
extra
|
dict[str, Any] | None
|
Per-call structured fields merged with |
None
|
**kwargs
|
Any
|
Additional keyword arguments forwarded to |
{}
|
Source code in src/codex_core/common/log_context.py
127 128 129 130 131 132 133 134 135 136 137 138 139 140 | |
info(msg, *args, extra=None, **kwargs)
Emit an INFO record enriched with the bound context fields.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
msg
|
str
|
Log message format string. |
required |
*args
|
Any
|
Positional arguments passed to |
()
|
extra
|
dict[str, Any] | None
|
Per-call structured fields merged with |
None
|
**kwargs
|
Any
|
Additional keyword arguments forwarded to |
{}
|
Source code in src/codex_core/common/log_context.py
94 95 96 97 98 99 100 101 102 103 | |
warning(msg, *args, extra=None, **kwargs)
Emit a WARNING record enriched with the bound context fields.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
msg
|
str
|
Log message format string. |
required |
*args
|
Any
|
Positional arguments passed to |
()
|
extra
|
dict[str, Any] | None
|
Per-call structured fields merged with |
None
|
**kwargs
|
Any
|
Additional keyword arguments forwarded to |
{}
|
Source code in src/codex_core/common/log_context.py
105 106 107 108 109 110 111 112 113 114 | |
loguru_setup
Application-level Loguru configuration helpers (optional dependency).
Provides opinionated, zero-boilerplate Loguru setup for codex_tools
applications. The codex_core library itself never calls these
helpers; it uses the standard logging module exclusively so that
consumers retain full control over their log infrastructure.
Three configurable sinks are created by both setup functions:
- stdout — colourised, human-readable format for local development.
- debug.log — rotating plain-text file,
enqueue=Truefor async-safe writing. - errors.json — rotating JSON-serialised file capturing
ERRORand above; suitable for ingestion by ELK / Loki pipelines.
Standard-library logging records are bridged via
:class:InterceptHandler so that third-party libraries (SQLAlchemy,
httpx, aiogram, etc.) are automatically captured by Loguru.
Availability
loguru is an optional dependency. Both :func:setup_logging
and :func:setup_universal_logging raise :exc:ImportError with an
actionable message when loguru is not installed.
Classes
InterceptHandler
Bases: Handler
Bridge standard-library logging records to the Loguru sink.
Install this handler on the root logger (or any named logger) to
forward all logging-based records into Loguru transparently.
The handler resolves the correct call-stack depth so that Loguru
reports the original call site rather than the handler frame.
This class is stateless and thread-safe; a single instance may be shared across all intercepted loggers.
Example
import logging
from codex_core.common.loguru_setup import InterceptHandler
logging.basicConfig(handlers=[InterceptHandler()], level=0, force=True)
Source code in src/codex_core/common/loguru_setup.py
41 42 43 44 45 46 47 48 49 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 91 92 93 94 | |
Functions
emit(record)
Forward a single logging.LogRecord to the active Loguru logger.
Resolves the Loguru level name from the record's level name, falls back to the integer level when the name is unknown, then walks up the call stack to find the frame that originally issued the log call — ensuring Loguru displays the correct source location instead of the handler internals.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
record
|
LogRecord
|
The log record produced by the standard-library logging infrastructure. |
required |
Note
If loguru is not installed (logger is None), this
method returns silently to avoid masking the original
ImportError.
Source code in src/codex_core/common/loguru_setup.py
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 91 92 93 94 | |
LoggingSettingsProtocol
Bases: Protocol
Structural protocol describing the logging-related subset of settings.
Any settings object that exposes these five attributes satisfies this
protocol and can be passed to :func:setup_logging without explicit
inheritance. :class:~codex_core.settings.BaseCommonSettings
satisfies this protocol out of the box when its subclass adds the
required logging fields.
Attributes:
| Name | Type | Description |
|---|---|---|
log_level_console |
str
|
Minimum Loguru level for stdout
(e.g. |
log_level_file |
str
|
Minimum Loguru level for the rotating debug
file (e.g. |
log_rotation |
str
|
Loguru rotation threshold
(e.g. |
log_dir |
str
|
Base directory for log files. A service-name
subdirectory is appended automatically by
:func: |
debug |
bool
|
When |
Source code in src/codex_core/common/loguru_setup.py
201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 | |
Functions
setup_logging(settings, service_name, intercept_loggers=None, log_levels=None)
Configure Loguru from a settings object conforming to :class:LoggingSettingsProtocol.
Preferred entry-point for applications that use
:class:~codex_core.settings.BaseCommonSettings. Configuration
is read from settings rather than raw arguments, enabling
environment-driven log levels without code changes.
PII masking is not handled here; it is the responsibility of
:class:~codex_core.core.base_dto.BaseDTO.__repr__ at the DTO
level. Do not log raw user input through this logger.
Side effects
- Removes all existing Loguru handlers (
logger.remove()). - Creates
<settings.log_dir>/<service_name>/directory tree. - Replaces the root
logginghandler with :class:InterceptHandler. - Optionally replaces handlers on loggers listed in intercept_loggers.
- Optionally sets log levels on loggers listed in log_levels.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
settings
|
LoggingSettingsProtocol
|
Any object satisfying :class: |
required |
service_name
|
str
|
Identifies the service in log output and is used
as the leaf directory name under |
required |
intercept_loggers
|
list[str] | None
|
Optional list of logger names whose handlers
should be replaced with :class: |
None
|
log_levels
|
dict[str, int] | None
|
Optional mapping of |
None
|
Raises:
| Type | Description |
|---|---|
ImportError
|
If |
Example
import logging
from codex_core.common.loguru_setup import setup_logging
setup_logging(
settings=app_settings,
service_name="api",
intercept_loggers=["uvicorn", "sqlalchemy.engine"],
log_levels={"httpx": logging.WARNING},
)
Source code in src/codex_core/common/loguru_setup.py
232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 | |
setup_universal_logging(log_dir, service_name='App', console_level='INFO', file_level='DEBUG', rotation='10 MB', is_debug=False)
Configure Loguru with three sinks and standard-library interception.
Intended for applications that do not use
:class:~codex_core.settings.BaseCommonSettings but still want the
full codex_core logging stack. Callers supply raw configuration
values directly rather than a settings object.
Sink layout after the call:
- stdout — colourised,
console_levelthreshold. /debug.log — rotating plain text,file_levelthreshold,backtrace/diagnoseenabled whenis_debug=True./errors.json — rotating JSON,ERRORthreshold, async-enqueued.- Root logger — intercepted via :class:
InterceptHandler.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
log_dir
|
Path
|
Directory where log files are created. Created recursively if it does not exist. |
required |
service_name
|
str
|
Label embedded in the console format string to distinguish output from multiple co-running services. |
'App'
|
console_level
|
str
|
Minimum level for stdout output (e.g. |
'INFO'
|
file_level
|
str
|
Minimum level for the debug log file
(e.g. |
'DEBUG'
|
rotation
|
str
|
Loguru rotation threshold string (e.g. |
'10 MB'
|
is_debug
|
bool
|
When |
False
|
Raises:
| Type | Description |
|---|---|
ImportError
|
If |
Example
from pathlib import Path
from codex_core.common.loguru_setup import setup_universal_logging
setup_universal_logging(
log_dir=Path("/var/log/myapp"),
service_name="booking-worker",
is_debug=True,
)
Source code in src/codex_core/common/loguru_setup.py
97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 | |