Skip to content

Commit caa36c1

Browse files
refactor: distribute utility packages across repositories
Co-authored-by: Jurie <smit.jurie@gmail.com>
1 parent 9df4a7c commit caa36c1

6 files changed

Lines changed: 621 additions & 2 deletions

File tree

codeflow_engine/utils/__init__.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,23 @@
1-
"""Utility modules for CodeFlow Engine."""
1+
"""
2+
Utility modules for CodeFlow Engine.
3+
4+
Note: Core portable utilities (logging, resilience) are also available
5+
in the standalone codeflow-utils package at tools/packages/codeflow-utils-python.
6+
This module re-exports engine-specific versions that integrate with CodeFlowSettings.
7+
"""
28

39
from codeflow_engine.utils.logging import get_logger, log_with_context, setup_logging
10+
from codeflow_engine.utils.resilience import (
11+
CircuitBreaker,
12+
CircuitBreakerOpenError,
13+
CircuitBreakerState,
14+
)
415

5-
__all__ = ["get_logger", "log_with_context", "setup_logging"]
16+
__all__ = [
17+
"get_logger",
18+
"log_with_context",
19+
"setup_logging",
20+
"CircuitBreaker",
21+
"CircuitBreakerOpenError",
22+
"CircuitBreakerState",
23+
]
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
[build-system]
2+
requires = ["hatchling"]
3+
build-backend = "hatchling.build"
4+
5+
[project]
6+
name = "codeflow-utils"
7+
version = "0.1.0"
8+
description = "Shared utility functions for CodeFlow Python applications"
9+
readme = "README.md"
10+
license = "MIT"
11+
requires-python = ">=3.12"
12+
keywords = ["codeflow", "utils", "utilities", "resilience", "logging"]
13+
classifiers = [
14+
"Development Status :: 4 - Beta",
15+
"Intended Audience :: Developers",
16+
"License :: OSI Approved :: MIT License",
17+
"Operating System :: OS Independent",
18+
"Programming Language :: Python :: 3",
19+
"Programming Language :: Python :: 3.12",
20+
"Programming Language :: Python :: 3.13",
21+
"Typing :: Typed",
22+
]
23+
24+
dependencies = []
25+
26+
[project.optional-dependencies]
27+
logging = ["structlog>=24.0.0"]
28+
resilience = []
29+
dev = [
30+
"pytest>=8.0.0",
31+
"pytest-asyncio>=0.24.0",
32+
"mypy>=1.11.0",
33+
"ruff>=0.6.0",
34+
]
35+
36+
[project.urls]
37+
Documentation = "https://github.com/JustAGhosT/codeflow-engine/tree/main/tools/packages/codeflow-utils-python"
38+
Source = "https://github.com/JustAGhosT/codeflow-engine"
39+
40+
[tool.hatch.build.targets.sdist]
41+
include = [
42+
"/src",
43+
]
44+
45+
[tool.hatch.build.targets.wheel]
46+
packages = ["src/codeflow_utils"]
47+
48+
[tool.mypy]
49+
python_version = "3.12"
50+
strict = true
51+
52+
[tool.ruff]
53+
line-length = 100
54+
target-version = "py312"
55+
56+
[tool.ruff.lint]
57+
select = ["E", "F", "I", "N", "W", "B", "UP"]
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
"""
2+
codeflow-utils - Shared utility functions for CodeFlow Python applications.
3+
4+
This package provides portable utilities that can be used across CodeFlow projects
5+
without tight coupling to any specific application.
6+
7+
Modules:
8+
logging: Structured logging utilities
9+
resilience: Circuit breaker and retry patterns
10+
"""
11+
12+
from codeflow_utils.logging import (
13+
JsonFormatter,
14+
TextFormatter,
15+
get_logger,
16+
log_with_context,
17+
)
18+
from codeflow_utils.resilience import (
19+
CircuitBreaker,
20+
CircuitBreakerConfig,
21+
CircuitBreakerOpenError,
22+
CircuitBreakerState,
23+
CircuitBreakerStats,
24+
)
25+
26+
__version__ = "0.1.0"
27+
28+
__all__ = [
29+
# Logging
30+
"JsonFormatter",
31+
"TextFormatter",
32+
"get_logger",
33+
"log_with_context",
34+
# Resilience
35+
"CircuitBreaker",
36+
"CircuitBreakerConfig",
37+
"CircuitBreakerOpenError",
38+
"CircuitBreakerState",
39+
"CircuitBreakerStats",
40+
]
Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
"""
2+
Structured logging utilities for CodeFlow applications.
3+
4+
Provides JSON-formatted structured logging with correlation IDs and
5+
contextual information. This is a portable version that doesn't depend
6+
on codeflow_engine internals.
7+
"""
8+
9+
import json
10+
import logging
11+
import sys
12+
from datetime import datetime, timezone
13+
from pathlib import Path
14+
from typing import Any
15+
16+
17+
class JsonFormatter(logging.Formatter):
18+
"""JSON formatter for structured logging."""
19+
20+
def __init__(self, service_name: str = "codeflow"):
21+
"""
22+
Initialize the JSON formatter.
23+
24+
Args:
25+
service_name: Name of the service for log identification
26+
"""
27+
super().__init__()
28+
self.service_name = service_name
29+
30+
def format(self, record: logging.LogRecord) -> str:
31+
"""
32+
Format log record as JSON.
33+
34+
Args:
35+
record: Log record to format
36+
37+
Returns:
38+
JSON-formatted log string
39+
"""
40+
log_data: dict[str, Any] = {
41+
"timestamp": datetime.now(timezone.utc).isoformat(),
42+
"level": record.levelname,
43+
"service": self.service_name,
44+
"component": record.name,
45+
"message": record.getMessage(),
46+
"module": record.module,
47+
"function": record.funcName,
48+
"line": record.lineno,
49+
}
50+
51+
# Add extra fields from record
52+
if hasattr(record, "request_id"):
53+
log_data["request_id"] = record.request_id
54+
if hasattr(record, "user_id"):
55+
log_data["user_id"] = record.user_id
56+
if hasattr(record, "duration_ms"):
57+
log_data["duration_ms"] = record.duration_ms
58+
if hasattr(record, "correlation_id"):
59+
log_data["correlation_id"] = record.correlation_id
60+
61+
# Add exception info if present
62+
if record.exc_info:
63+
log_data["exception"] = self.formatException(record.exc_info)
64+
log_data["exception_type"] = (
65+
record.exc_info[0].__name__ if record.exc_info[0] else None
66+
)
67+
68+
# Add any additional extra fields
69+
excluded_keys = {
70+
"name",
71+
"msg",
72+
"args",
73+
"created",
74+
"filename",
75+
"funcName",
76+
"levelname",
77+
"levelno",
78+
"lineno",
79+
"module",
80+
"msecs",
81+
"message",
82+
"pathname",
83+
"process",
84+
"processName",
85+
"relativeCreated",
86+
"thread",
87+
"threadName",
88+
"exc_info",
89+
"exc_text",
90+
"stack_info",
91+
}
92+
for key, value in record.__dict__.items():
93+
if key not in excluded_keys and not key.startswith("_"):
94+
log_data[key] = value
95+
96+
return json.dumps(log_data, default=str)
97+
98+
99+
class TextFormatter(logging.Formatter):
100+
"""Human-readable text formatter for development."""
101+
102+
def format(self, record: logging.LogRecord) -> str:
103+
"""
104+
Format log record as human-readable text.
105+
106+
Args:
107+
record: Log record to format
108+
109+
Returns:
110+
Formatted log string
111+
"""
112+
timestamp = datetime.fromtimestamp(record.created, tz=timezone.utc).strftime(
113+
"%Y-%m-%d %H:%M:%S UTC"
114+
)
115+
level = record.levelname.ljust(8)
116+
component = record.name
117+
message = record.getMessage()
118+
119+
# Build context string
120+
context_parts = []
121+
if hasattr(record, "request_id"):
122+
context_parts.append(f"request_id={record.request_id}")
123+
if hasattr(record, "user_id"):
124+
context_parts.append(f"user_id={record.user_id}")
125+
if hasattr(record, "duration_ms"):
126+
context_parts.append(f"duration={record.duration_ms}ms")
127+
128+
context = f" [{', '.join(context_parts)}]" if context_parts else ""
129+
130+
log_line = f"{timestamp} | {level} | {component} | {message}{context}"
131+
132+
# Add exception if present
133+
if record.exc_info:
134+
log_line += f"\n{self.formatException(record.exc_info)}"
135+
136+
return log_line
137+
138+
139+
def setup_logging(
140+
level: str = "INFO",
141+
format_type: str = "json",
142+
output: str = "stdout",
143+
log_file: str | Path | None = None,
144+
service_name: str = "codeflow",
145+
) -> None:
146+
"""
147+
Configure structured logging.
148+
149+
Args:
150+
level: Log level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
151+
format_type: Format type ('json' or 'text')
152+
output: Output destination ('stdout', 'file', or 'both')
153+
log_file: Path to log file (required if output is 'file' or 'both')
154+
service_name: Name of the service for log identification
155+
"""
156+
root_logger = logging.getLogger()
157+
root_logger.setLevel(getattr(logging, level.upper(), logging.INFO))
158+
root_logger.handlers.clear()
159+
160+
if format_type == "json":
161+
formatter = JsonFormatter(service_name=service_name)
162+
else:
163+
formatter = TextFormatter()
164+
165+
if output in ("stdout", "both"):
166+
stdout_handler = logging.StreamHandler(sys.stdout)
167+
stdout_handler.setFormatter(formatter)
168+
stdout_handler.setLevel(getattr(logging, level.upper(), logging.INFO))
169+
root_logger.addHandler(stdout_handler)
170+
171+
if output in ("file", "both"):
172+
if log_file is None:
173+
raise ValueError("log_file is required when output is 'file' or 'both'")
174+
log_path = Path(log_file)
175+
log_path.parent.mkdir(parents=True, exist_ok=True)
176+
177+
file_handler = logging.FileHandler(log_file)
178+
file_handler.setFormatter(formatter)
179+
file_handler.setLevel(getattr(logging, level.upper(), logging.INFO))
180+
root_logger.addHandler(file_handler)
181+
182+
# Reduce noise from third-party libraries
183+
logging.getLogger("urllib3").setLevel(logging.WARNING)
184+
logging.getLogger("httpx").setLevel(logging.WARNING)
185+
logging.getLogger("asyncio").setLevel(logging.WARNING)
186+
187+
188+
def get_logger(name: str) -> logging.Logger:
189+
"""
190+
Get a logger instance for a module.
191+
192+
Args:
193+
name: Logger name (typically __name__)
194+
195+
Returns:
196+
Logger instance
197+
"""
198+
return logging.getLogger(name)
199+
200+
201+
def log_with_context(
202+
logger: logging.Logger,
203+
level: int,
204+
message: str,
205+
**context: Any,
206+
) -> None:
207+
"""
208+
Log a message with additional context.
209+
210+
Args:
211+
logger: Logger instance
212+
level: Log level (logging.INFO, etc.)
213+
message: Log message
214+
**context: Additional context fields
215+
"""
216+
logger.log(level, message, extra=context)

tools/packages/codeflow-utils-python/src/codeflow_utils/py.typed

Whitespace-only changes.

0 commit comments

Comments
 (0)