devscontext 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,148 @@
1
+ """Custom exceptions for DevsContext.
2
+
3
+ This module defines a hierarchy of exceptions used throughout DevsContext.
4
+ All exceptions inherit from DevsContextError, making it easy to catch
5
+ all DevsContext-related errors in one place.
6
+
7
+ Exception Hierarchy:
8
+ DevsContextError (base)
9
+ ├── ConfigError - Configuration loading/validation failures
10
+ ├── AdapterError (base for adapter failures)
11
+ │ ├── JiraAdapterError
12
+ │ ├── FirefliesAdapterError
13
+ │ └── LocalDocsAdapterError
14
+ ├── SynthesisError - LLM synthesis failures
15
+ └── CacheError - Cache operation failures
16
+ """
17
+
18
+ from typing import Any
19
+
20
+
21
+ class DevsContextError(Exception):
22
+ """Base exception for all DevsContext errors.
23
+
24
+ Args:
25
+ message: Human-readable error message.
26
+ details: Optional dictionary with additional error context.
27
+ """
28
+
29
+ def __init__(self, message: str, details: dict[str, Any] | None = None) -> None:
30
+ super().__init__(message)
31
+ self.message = message
32
+ self.details = details or {}
33
+
34
+ def __str__(self) -> str:
35
+ if self.details:
36
+ return f"{self.message} | Details: {self.details}"
37
+ return self.message
38
+
39
+
40
+ class ConfigError(DevsContextError):
41
+ """Raised when configuration loading or validation fails.
42
+
43
+ Examples:
44
+ - Invalid YAML syntax in .devscontext.yaml
45
+ - Missing required configuration values
46
+ - Environment variable not found
47
+ """
48
+
49
+
50
+ class AdapterError(DevsContextError):
51
+ """Base exception for adapter-related errors.
52
+
53
+ All adapter-specific exceptions should inherit from this class.
54
+ This allows catching all adapter errors with a single except clause.
55
+
56
+ Args:
57
+ message: Human-readable error message.
58
+ adapter_name: Name of the adapter that raised the error.
59
+ details: Optional dictionary with additional error context.
60
+ """
61
+
62
+ def __init__(
63
+ self,
64
+ message: str,
65
+ adapter_name: str,
66
+ details: dict[str, Any] | None = None,
67
+ ) -> None:
68
+ super().__init__(message, details)
69
+ self.adapter_name = adapter_name
70
+
71
+ def __str__(self) -> str:
72
+ base = f"[{self.adapter_name}] {self.message}"
73
+ if self.details:
74
+ return f"{base} | Details: {self.details}"
75
+ return base
76
+
77
+
78
+ class JiraAdapterError(AdapterError):
79
+ """Raised when Jira API operations fail.
80
+
81
+ Examples:
82
+ - Authentication failure (401)
83
+ - Ticket not found (404)
84
+ - Rate limiting (429)
85
+ - Network errors
86
+ """
87
+
88
+ def __init__(
89
+ self,
90
+ message: str,
91
+ details: dict[str, Any] | None = None,
92
+ ) -> None:
93
+ super().__init__(message, adapter_name="jira", details=details)
94
+
95
+
96
+ class FirefliesAdapterError(AdapterError):
97
+ """Raised when Fireflies API operations fail.
98
+
99
+ Examples:
100
+ - Authentication failure
101
+ - GraphQL query errors
102
+ - Rate limiting
103
+ - Network errors
104
+ """
105
+
106
+ def __init__(
107
+ self,
108
+ message: str,
109
+ details: dict[str, Any] | None = None,
110
+ ) -> None:
111
+ super().__init__(message, adapter_name="fireflies", details=details)
112
+
113
+
114
+ class LocalDocsAdapterError(AdapterError):
115
+ """Raised when local documentation operations fail.
116
+
117
+ Examples:
118
+ - Directory not found
119
+ - Permission denied
120
+ - Invalid file encoding
121
+ """
122
+
123
+ def __init__(
124
+ self,
125
+ message: str,
126
+ details: dict[str, Any] | None = None,
127
+ ) -> None:
128
+ super().__init__(message, adapter_name="local_docs", details=details)
129
+
130
+
131
+ class SynthesisError(DevsContextError):
132
+ """Raised when LLM synthesis operations fail.
133
+
134
+ Examples:
135
+ - LLM API rate limiting
136
+ - Invalid response format
137
+ - Context too long
138
+ - LLM not configured
139
+ """
140
+
141
+
142
+ class CacheError(DevsContextError):
143
+ """Raised when cache operations fail.
144
+
145
+ Examples:
146
+ - Serialization errors
147
+ - Memory allocation failures
148
+ """
devscontext/logging.py ADDED
@@ -0,0 +1,181 @@
1
+ """Logging configuration for DevsContext.
2
+
3
+ This module provides structured logging setup for the entire application.
4
+ All modules should use `logging.getLogger(__name__)` to get their logger.
5
+
6
+ Usage:
7
+ from devscontext.logging import setup_logging, get_logger
8
+
9
+ # At application startup
10
+ setup_logging()
11
+
12
+ # In each module
13
+ logger = get_logger(__name__)
14
+ logger.info("Operation completed", extra={"duration_ms": 150, "source": "jira"})
15
+ """
16
+
17
+ import logging
18
+ import sys
19
+ from typing import TYPE_CHECKING, Any
20
+
21
+ if TYPE_CHECKING:
22
+ from collections.abc import Callable
23
+
24
+ from devscontext.constants import LOG_DATE_FORMAT, LOG_FORMAT
25
+
26
+
27
+ class StructuredFormatter(logging.Formatter):
28
+ """A formatter that outputs structured log messages.
29
+
30
+ Includes extra fields in the log output for better observability.
31
+ """
32
+
33
+ def format(self, record: logging.LogRecord) -> str:
34
+ """Format a log record with structured fields.
35
+
36
+ Args:
37
+ record: The log record to format.
38
+
39
+ Returns:
40
+ Formatted log string.
41
+ """
42
+ # Get the base formatted message
43
+ base_message = super().format(record)
44
+
45
+ # Extract extra fields (exclude standard LogRecord attributes)
46
+ standard_attrs = {
47
+ "name",
48
+ "msg",
49
+ "args",
50
+ "created",
51
+ "filename",
52
+ "funcName",
53
+ "levelname",
54
+ "levelno",
55
+ "lineno",
56
+ "module",
57
+ "msecs",
58
+ "pathname",
59
+ "process",
60
+ "processName",
61
+ "relativeCreated",
62
+ "stack_info",
63
+ "exc_info",
64
+ "exc_text",
65
+ "thread",
66
+ "threadName",
67
+ "taskName",
68
+ "message",
69
+ }
70
+
71
+ extra_fields: dict[str, Any] = {}
72
+ for key, value in record.__dict__.items():
73
+ if key not in standard_attrs and not key.startswith("_"):
74
+ extra_fields[key] = value
75
+
76
+ # Append extra fields if present
77
+ if extra_fields:
78
+ fields_str = " | ".join(f"{k}={v}" for k, v in extra_fields.items())
79
+ return f"{base_message} | {fields_str}"
80
+
81
+ return base_message
82
+
83
+
84
+ def setup_logging(
85
+ level: int = logging.INFO,
86
+ *,
87
+ include_timestamp: bool = True,
88
+ ) -> None:
89
+ """Configure logging for the application.
90
+
91
+ Sets up a structured formatter with consistent output format.
92
+ Should be called once at application startup.
93
+
94
+ Args:
95
+ level: The logging level (default: INFO).
96
+ include_timestamp: Whether to include timestamps in output.
97
+ """
98
+ # Create formatter
99
+ if include_timestamp:
100
+ formatter = StructuredFormatter(LOG_FORMAT, datefmt=LOG_DATE_FORMAT)
101
+ else:
102
+ formatter = StructuredFormatter("%(levelname)-8s | %(name)s | %(message)s")
103
+
104
+ # Configure root logger
105
+ root_logger = logging.getLogger()
106
+ root_logger.setLevel(level)
107
+
108
+ # Remove existing handlers
109
+ for handler in root_logger.handlers[:]:
110
+ root_logger.removeHandler(handler)
111
+
112
+ # Add stderr handler
113
+ handler = logging.StreamHandler(sys.stderr)
114
+ handler.setFormatter(formatter)
115
+ root_logger.addHandler(handler)
116
+
117
+ # Set devscontext loggers to the specified level
118
+ logging.getLogger("devscontext").setLevel(level)
119
+
120
+ # Reduce noise from third-party libraries
121
+ logging.getLogger("httpx").setLevel(logging.WARNING)
122
+ logging.getLogger("httpcore").setLevel(logging.WARNING)
123
+
124
+
125
+ def get_logger(name: str) -> logging.Logger:
126
+ """Get a logger for the given module.
127
+
128
+ This is a convenience wrapper around logging.getLogger that
129
+ ensures consistent naming.
130
+
131
+ Args:
132
+ name: The module name (typically __name__).
133
+
134
+ Returns:
135
+ A configured Logger instance.
136
+ """
137
+ return logging.getLogger(name)
138
+
139
+
140
+ class LogContext:
141
+ """Context manager for adding structured fields to log messages.
142
+
143
+ Usage:
144
+ with LogContext(logger, adapter="jira", ticket_id="PROJ-123"):
145
+ logger.info("Fetching ticket")
146
+ # The log will include: adapter=jira | ticket_id=PROJ-123
147
+ """
148
+
149
+ def __init__(self, logger: logging.Logger, **fields: Any) -> None:
150
+ """Initialize the log context.
151
+
152
+ Args:
153
+ logger: The logger to use.
154
+ **fields: Extra fields to include in all log messages.
155
+ """
156
+ self.logger = logger
157
+ self.fields = fields
158
+ self._old_factory: Callable[..., logging.LogRecord] | None = None
159
+
160
+ def __enter__(self) -> "LogContext":
161
+ """Enter the context and set up the log record factory."""
162
+ old_factory = logging.getLogRecordFactory()
163
+ self._old_factory = old_factory
164
+ fields = self.fields
165
+
166
+ def record_factory(
167
+ *args: Any,
168
+ **kwargs: Any,
169
+ ) -> logging.LogRecord:
170
+ record = old_factory(*args, **kwargs)
171
+ for key, value in fields.items():
172
+ setattr(record, key, value)
173
+ return record
174
+
175
+ logging.setLogRecordFactory(record_factory)
176
+ return self
177
+
178
+ def __exit__(self, *args: Any) -> None:
179
+ """Exit the context and restore the original log record factory."""
180
+ if self._old_factory is not None:
181
+ logging.setLogRecordFactory(self._old_factory)