qnty 0.0.8__py3-none-any.whl → 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.
- qnty/__init__.py +140 -59
- qnty/constants/__init__.py +10 -0
- qnty/constants/numerical.py +18 -0
- qnty/constants/solvers.py +6 -0
- qnty/constants/tests.py +6 -0
- qnty/dimensions/__init__.py +23 -0
- qnty/dimensions/base.py +97 -0
- qnty/dimensions/field_dims.py +126 -0
- qnty/dimensions/field_dims.pyi +128 -0
- qnty/dimensions/signature.py +111 -0
- qnty/equations/__init__.py +4 -0
- qnty/equations/equation.py +220 -0
- qnty/equations/system.py +130 -0
- qnty/expressions/__init__.py +40 -0
- qnty/expressions/formatter.py +188 -0
- qnty/expressions/functions.py +74 -0
- qnty/expressions/nodes.py +701 -0
- qnty/expressions/types.py +70 -0
- qnty/extensions/plotting/__init__.py +0 -0
- qnty/extensions/reporting/__init__.py +0 -0
- qnty/problems/__init__.py +145 -0
- qnty/problems/composition.py +1031 -0
- qnty/problems/problem.py +695 -0
- qnty/problems/rules.py +145 -0
- qnty/problems/solving.py +1216 -0
- qnty/problems/validation.py +127 -0
- qnty/quantities/__init__.py +29 -0
- qnty/quantities/base_qnty.py +677 -0
- qnty/quantities/field_converters.py +24004 -0
- qnty/quantities/field_qnty.py +1012 -0
- qnty/quantities/field_setter.py +12320 -0
- qnty/quantities/field_vars.py +6325 -0
- qnty/quantities/field_vars.pyi +4191 -0
- qnty/solving/__init__.py +0 -0
- qnty/solving/manager.py +96 -0
- qnty/solving/order.py +403 -0
- qnty/solving/solvers/__init__.py +13 -0
- qnty/solving/solvers/base.py +82 -0
- qnty/solving/solvers/iterative.py +165 -0
- qnty/solving/solvers/simultaneous.py +475 -0
- qnty/units/__init__.py +1 -0
- qnty/units/field_units.py +10507 -0
- qnty/units/field_units.pyi +2461 -0
- qnty/units/prefixes.py +203 -0
- qnty/{unit.py → units/registry.py} +89 -61
- qnty/utils/__init__.py +16 -0
- qnty/utils/caching/__init__.py +23 -0
- qnty/utils/caching/manager.py +401 -0
- qnty/utils/error_handling/__init__.py +66 -0
- qnty/utils/error_handling/context.py +39 -0
- qnty/utils/error_handling/exceptions.py +96 -0
- qnty/utils/error_handling/handlers.py +171 -0
- qnty/utils/logging.py +40 -0
- qnty/utils/protocols.py +164 -0
- qnty/utils/scope_discovery.py +420 -0
- qnty-0.1.0.dist-info/METADATA +199 -0
- qnty-0.1.0.dist-info/RECORD +60 -0
- qnty/dimension.py +0 -186
- qnty/equation.py +0 -297
- qnty/expression.py +0 -553
- qnty/prefixes.py +0 -229
- qnty/unit_types/base.py +0 -47
- qnty/units.py +0 -8113
- qnty/variable.py +0 -300
- qnty/variable_types/base.py +0 -58
- qnty/variable_types/expression_variable.py +0 -106
- qnty/variable_types/typed_variable.py +0 -87
- qnty/variables.py +0 -2298
- qnty/variables.pyi +0 -6148
- qnty-0.0.8.dist-info/METADATA +0 -355
- qnty-0.0.8.dist-info/RECORD +0 -19
- /qnty/{unit_types → extensions}/__init__.py +0 -0
- /qnty/{variable_types → extensions/integration}/__init__.py +0 -0
- {qnty-0.0.8.dist-info → qnty-0.1.0.dist-info}/WHEEL +0 -0
@@ -0,0 +1,171 @@
|
|
1
|
+
"""
|
2
|
+
Error handlers and logging functionality for the Qnty library.
|
3
|
+
|
4
|
+
This module provides centralized error handling with consistent logging
|
5
|
+
and context management.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import logging
|
9
|
+
from typing import Any
|
10
|
+
|
11
|
+
from .context import ErrorContext, create_context, get_dimension_string
|
12
|
+
from .exceptions import (
|
13
|
+
DimensionalError,
|
14
|
+
DivisionByZeroError,
|
15
|
+
EquationSolvingError,
|
16
|
+
ExpressionEvaluationError,
|
17
|
+
QntyError,
|
18
|
+
UnitConversionError,
|
19
|
+
VariableNotFoundError,
|
20
|
+
)
|
21
|
+
|
22
|
+
|
23
|
+
class ErrorHandler:
|
24
|
+
"""
|
25
|
+
Centralized error handling with consistent logging and context management.
|
26
|
+
|
27
|
+
Provides methods for handling common error patterns across the library.
|
28
|
+
"""
|
29
|
+
|
30
|
+
def __init__(self, logger: logging.Logger | None = None):
|
31
|
+
"""Initialize error handler with optional custom logger."""
|
32
|
+
self.logger = logger or logging.getLogger(__name__)
|
33
|
+
|
34
|
+
def handle_dimensional_error(self, operation: str, left: Any, right: Any, context: ErrorContext | None = None) -> None:
|
35
|
+
"""Handle dimensional compatibility errors."""
|
36
|
+
left_dim = get_dimension_string(left)
|
37
|
+
right_dim = get_dimension_string(right)
|
38
|
+
|
39
|
+
error_context = context.to_dict() if context else {}
|
40
|
+
|
41
|
+
self.logger.error(f"Dimensional error in {operation}: {left_dim} vs {right_dim}", extra=error_context)
|
42
|
+
raise DimensionalError(operation, left_dim, right_dim, error_context)
|
43
|
+
|
44
|
+
def handle_unit_conversion_error(self, from_unit: str, to_unit: str, reason: str = "", context: ErrorContext | None = None) -> None:
|
45
|
+
"""Handle unit conversion errors."""
|
46
|
+
error_context = context.to_dict() if context else {}
|
47
|
+
|
48
|
+
self.logger.error(f"Unit conversion failed: {from_unit} -> {to_unit} ({reason})", extra=error_context)
|
49
|
+
raise UnitConversionError(from_unit, to_unit, reason, error_context)
|
50
|
+
|
51
|
+
def handle_variable_not_found(self, variable_name: str, available_vars: list[str] | None = None, context: ErrorContext | None = None) -> None:
|
52
|
+
"""Handle variable not found errors."""
|
53
|
+
error_context = context.to_dict() if context else {}
|
54
|
+
available_list = available_vars or []
|
55
|
+
|
56
|
+
self.logger.error(f"Variable not found: {variable_name} (available: {available_list})", extra=error_context)
|
57
|
+
raise VariableNotFoundError(variable_name, available_list, error_context)
|
58
|
+
|
59
|
+
def handle_equation_solving_error(self, equation_name: str, target_var: str, reason: str = "", context: ErrorContext | None = None) -> None:
|
60
|
+
"""Handle equation solving errors."""
|
61
|
+
error_context = context.to_dict() if context else {}
|
62
|
+
|
63
|
+
self.logger.error(f"Equation solving failed: {equation_name} for {target_var} ({reason})", extra=error_context)
|
64
|
+
raise EquationSolvingError(equation_name, target_var, reason, error_context)
|
65
|
+
|
66
|
+
def handle_expression_evaluation_error(self, expression: str, reason: str = "", context: ErrorContext | None = None) -> None:
|
67
|
+
"""Handle expression evaluation errors."""
|
68
|
+
error_context = context.to_dict() if context else {}
|
69
|
+
|
70
|
+
self.logger.error(f"Expression evaluation failed: {expression} ({reason})", extra=error_context)
|
71
|
+
raise ExpressionEvaluationError(expression, reason, error_context)
|
72
|
+
|
73
|
+
def handle_division_by_zero(self, dividend: Any, context: ErrorContext | None = None) -> None:
|
74
|
+
"""Handle division by zero errors."""
|
75
|
+
error_context = context.to_dict() if context else {}
|
76
|
+
dividend_str = str(dividend)
|
77
|
+
|
78
|
+
self.logger.error(f"Division by zero: {dividend_str}", extra=error_context)
|
79
|
+
raise DivisionByZeroError(dividend_str, error_context)
|
80
|
+
|
81
|
+
def handle_unexpected_error(self, original_error: Exception, operation: str, context: ErrorContext | None = None) -> None:
|
82
|
+
"""Handle unexpected errors with proper context and chaining."""
|
83
|
+
error_context = context.to_dict() if context else {}
|
84
|
+
|
85
|
+
self.logger.error(f"Unexpected error in {operation}: {original_error}", extra=error_context, exc_info=True)
|
86
|
+
|
87
|
+
# Re-raise as QntyError with context
|
88
|
+
raise QntyError(f"Unexpected error in {operation}: {original_error}", error_context) from original_error
|
89
|
+
|
90
|
+
@staticmethod
|
91
|
+
def create_context(module: str, function: str, operation: str, **kwargs) -> ErrorContext:
|
92
|
+
"""Create error context for consistent error reporting."""
|
93
|
+
return create_context(module, function, operation, **kwargs)
|
94
|
+
|
95
|
+
|
96
|
+
class ErrorHandlerMixin:
|
97
|
+
"""
|
98
|
+
Mixin class that provides error handling methods to any class.
|
99
|
+
|
100
|
+
Usage:
|
101
|
+
class MyClass(ErrorHandlerMixin):
|
102
|
+
def some_method(self):
|
103
|
+
try:
|
104
|
+
# risky operation
|
105
|
+
pass
|
106
|
+
except Exception as e:
|
107
|
+
self.handle_error(e, "some_operation")
|
108
|
+
"""
|
109
|
+
|
110
|
+
def __init__(self, *args, **kwargs):
|
111
|
+
super().__init__(*args, **kwargs)
|
112
|
+
self._error_handler = ErrorHandler(logging.getLogger(self.__class__.__module__))
|
113
|
+
|
114
|
+
def handle_error(self, error: Exception, operation: str, **context_kwargs) -> None:
|
115
|
+
"""Handle errors with automatic context creation."""
|
116
|
+
context = ErrorContext(module=self.__class__.__module__, function=operation, operation=operation, additional_info=context_kwargs)
|
117
|
+
self._error_handler.handle_unexpected_error(error, operation, context)
|
118
|
+
|
119
|
+
def require_variable(self, var_name: str, variables: dict[str, Any]) -> Any:
|
120
|
+
"""Require a variable to exist, raising consistent error if not found."""
|
121
|
+
if var_name not in variables:
|
122
|
+
context = self._error_handler.create_context(self.__class__.__module__, "require_variable", "variable_lookup", requested_variable=var_name)
|
123
|
+
self._error_handler.handle_variable_not_found(var_name, list(variables.keys()), context)
|
124
|
+
return variables[var_name]
|
125
|
+
|
126
|
+
def ensure_dimensional_compatibility(self, left: Any, right: Any, operation: str) -> None:
|
127
|
+
"""Ensure two quantities have compatible dimensions for the given operation."""
|
128
|
+
if hasattr(left, "_dimension_sig") and hasattr(right, "_dimension_sig"):
|
129
|
+
if left._dimension_sig != right._dimension_sig:
|
130
|
+
context = self._error_handler.create_context(self.__class__.__module__, "ensure_dimensional_compatibility", operation)
|
131
|
+
self._error_handler.handle_dimensional_error(operation, left, right, context)
|
132
|
+
|
133
|
+
|
134
|
+
# Global error handler instance
|
135
|
+
_default_error_handler = ErrorHandler()
|
136
|
+
|
137
|
+
|
138
|
+
def get_error_handler() -> ErrorHandler:
|
139
|
+
"""Get the global error handler instance."""
|
140
|
+
return _default_error_handler
|
141
|
+
|
142
|
+
|
143
|
+
def set_error_handler(handler: ErrorHandler) -> None:
|
144
|
+
"""Set a custom global error handler."""
|
145
|
+
global _default_error_handler
|
146
|
+
_default_error_handler = handler
|
147
|
+
|
148
|
+
|
149
|
+
# Convenience functions for common error patterns
|
150
|
+
def require_variable(var_name: str, variables: dict[str, Any], context: ErrorContext | None = None) -> Any:
|
151
|
+
"""Require a variable to exist, raising consistent error if not found."""
|
152
|
+
if var_name not in variables:
|
153
|
+
_default_error_handler.handle_variable_not_found(var_name, list(variables.keys()), context)
|
154
|
+
return variables[var_name]
|
155
|
+
|
156
|
+
|
157
|
+
def ensure_not_zero(value: Any, context: ErrorContext | None = None) -> None:
|
158
|
+
"""Ensure a value is not zero for division operations."""
|
159
|
+
if hasattr(value, "value") and abs(value.value) < 1e-15:
|
160
|
+
_default_error_handler.handle_division_by_zero(value, context)
|
161
|
+
elif isinstance(value, int | float) and abs(value) < 1e-15:
|
162
|
+
_default_error_handler.handle_division_by_zero(value, context)
|
163
|
+
|
164
|
+
|
165
|
+
def safe_evaluate(expression: Any, variables: dict[str, Any], context: ErrorContext | None = None) -> Any:
|
166
|
+
"""Safely evaluate an expression with consistent error handling."""
|
167
|
+
try:
|
168
|
+
return expression.evaluate(variables)
|
169
|
+
except Exception as e:
|
170
|
+
error_context = context or ErrorContext("unknown", "safe_evaluate", "expression_evaluation")
|
171
|
+
_default_error_handler.handle_expression_evaluation_error(str(expression), str(e), error_context)
|
qnty/utils/logging.py
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
import logging
|
2
|
+
import os
|
3
|
+
|
4
|
+
_LOGGER: logging.Logger | None = None
|
5
|
+
|
6
|
+
|
7
|
+
def get_logger(name: str = "qnty") -> logging.Logger:
|
8
|
+
"""Return a module-level configured logger.
|
9
|
+
|
10
|
+
Log level resolves in order:
|
11
|
+
1. Explicit environment variable QNTY_LOG_LEVEL
|
12
|
+
2. Existing logger level if already configured
|
13
|
+
3. Defaults to INFO
|
14
|
+
"""
|
15
|
+
global _LOGGER
|
16
|
+
if _LOGGER is not None:
|
17
|
+
return _LOGGER
|
18
|
+
|
19
|
+
logger = logging.getLogger(name)
|
20
|
+
if not logger.handlers:
|
21
|
+
# Basic handler (stdout)
|
22
|
+
handler = logging.StreamHandler()
|
23
|
+
fmt = os.getenv("QNTY_LOG_FORMAT", "%(asctime)s | %(levelname)s | %(name)s | %(message)s")
|
24
|
+
handler.setFormatter(logging.Formatter(fmt))
|
25
|
+
logger.addHandler(handler)
|
26
|
+
|
27
|
+
# Resolve level
|
28
|
+
level_str = os.getenv("QNTY_LOG_LEVEL", "INFO").upper()
|
29
|
+
level = getattr(logging, level_str, logging.INFO)
|
30
|
+
logger.setLevel(level)
|
31
|
+
logger.propagate = False
|
32
|
+
|
33
|
+
_LOGGER = logger
|
34
|
+
return logger
|
35
|
+
|
36
|
+
|
37
|
+
def set_log_level(level: str):
|
38
|
+
"""Dynamically adjust log level at runtime."""
|
39
|
+
logger = get_logger()
|
40
|
+
logger.setLevel(getattr(logging, level.upper(), logging.INFO))
|
qnty/utils/protocols.py
ADDED
@@ -0,0 +1,164 @@
|
|
1
|
+
"""
|
2
|
+
Performance-Optimized Protocols for Qnty
|
3
|
+
=========================================
|
4
|
+
|
5
|
+
Type protocols and registration system to avoid duck typing and circular imports
|
6
|
+
while maintaining maximum performance.
|
7
|
+
"""
|
8
|
+
|
9
|
+
from abc import abstractmethod
|
10
|
+
from typing import Protocol, runtime_checkable
|
11
|
+
|
12
|
+
|
13
|
+
@runtime_checkable
|
14
|
+
class ExpressionProtocol(Protocol):
|
15
|
+
"""
|
16
|
+
Protocol for objects that can be evaluated as expressions.
|
17
|
+
|
18
|
+
This avoids the need to import the actual Expression class,
|
19
|
+
preventing circular imports while maintaining type safety.
|
20
|
+
"""
|
21
|
+
|
22
|
+
@abstractmethod
|
23
|
+
def get_variables(self) -> set[str]:
|
24
|
+
"""Get all variable symbols used in this expression."""
|
25
|
+
...
|
26
|
+
|
27
|
+
@abstractmethod
|
28
|
+
def evaluate(self, variable_values: dict) -> object:
|
29
|
+
"""Evaluate the expression given variable values."""
|
30
|
+
...
|
31
|
+
|
32
|
+
|
33
|
+
@runtime_checkable
|
34
|
+
class VariableProtocol(Protocol):
|
35
|
+
"""
|
36
|
+
Protocol for variable objects that can be discovered in scope.
|
37
|
+
|
38
|
+
This defines the interface that the scope discovery service expects
|
39
|
+
without importing the actual variable classes.
|
40
|
+
"""
|
41
|
+
|
42
|
+
@property
|
43
|
+
@abstractmethod
|
44
|
+
def name(self) -> str:
|
45
|
+
"""Variable name."""
|
46
|
+
...
|
47
|
+
|
48
|
+
@property
|
49
|
+
@abstractmethod
|
50
|
+
def symbol(self) -> str | None:
|
51
|
+
"""Variable symbol (preferred over name for equations)."""
|
52
|
+
...
|
53
|
+
|
54
|
+
@property
|
55
|
+
@abstractmethod
|
56
|
+
def quantity(self) -> object | None:
|
57
|
+
"""The underlying quantity object."""
|
58
|
+
...
|
59
|
+
|
60
|
+
|
61
|
+
class TypeRegistry:
|
62
|
+
"""
|
63
|
+
High-performance type registry using class caching.
|
64
|
+
|
65
|
+
This eliminates the need for duck typing by maintaining a cache
|
66
|
+
of known types and their capabilities.
|
67
|
+
"""
|
68
|
+
|
69
|
+
# Class-level caches for maximum performance
|
70
|
+
_expression_types: set[type] = set()
|
71
|
+
_variable_types: set[type] = set()
|
72
|
+
_type_cache: dict[type, tuple[bool, bool]] = {} # (is_expression, is_variable)
|
73
|
+
|
74
|
+
@classmethod
|
75
|
+
def register_expression_type(cls, expression_type: type) -> None:
|
76
|
+
"""Register a type as an expression type."""
|
77
|
+
cls._expression_types.add(expression_type)
|
78
|
+
cls._invalidate_cache_for_type(expression_type)
|
79
|
+
|
80
|
+
@classmethod
|
81
|
+
def register_variable_type(cls, variable_type: type) -> None:
|
82
|
+
"""Register a type as a variable type."""
|
83
|
+
cls._variable_types.add(variable_type)
|
84
|
+
cls._invalidate_cache_for_type(variable_type)
|
85
|
+
|
86
|
+
@classmethod
|
87
|
+
def is_expression(cls, obj: object) -> bool:
|
88
|
+
"""
|
89
|
+
Check if object is an expression with maximum performance.
|
90
|
+
|
91
|
+
Uses cached type checking to avoid repeated isinstance calls.
|
92
|
+
"""
|
93
|
+
obj_type = type(obj)
|
94
|
+
|
95
|
+
if obj_type not in cls._type_cache:
|
96
|
+
# Check if this type is registered as an expression type
|
97
|
+
is_expr = any(isinstance(obj, expr_type) for expr_type in cls._expression_types)
|
98
|
+
|
99
|
+
# Also check protocol compliance as fallback
|
100
|
+
if not is_expr:
|
101
|
+
is_expr = isinstance(obj, ExpressionProtocol)
|
102
|
+
|
103
|
+
# Check if it's a variable too (for dual-purpose cache entry)
|
104
|
+
is_var = any(isinstance(obj, var_type) for var_type in cls._variable_types)
|
105
|
+
if not is_var:
|
106
|
+
is_var = isinstance(obj, VariableProtocol)
|
107
|
+
|
108
|
+
cls._type_cache[obj_type] = (is_expr, is_var)
|
109
|
+
|
110
|
+
return cls._type_cache[obj_type][0]
|
111
|
+
|
112
|
+
@classmethod
|
113
|
+
def is_variable(cls, obj: object) -> bool:
|
114
|
+
"""
|
115
|
+
Check if object is a variable with maximum performance.
|
116
|
+
|
117
|
+
Uses cached type checking to avoid repeated isinstance calls.
|
118
|
+
"""
|
119
|
+
obj_type = type(obj)
|
120
|
+
|
121
|
+
if obj_type not in cls._type_cache:
|
122
|
+
# This will populate the cache for both expression and variable
|
123
|
+
cls.is_expression(obj)
|
124
|
+
|
125
|
+
return cls._type_cache[obj_type][1]
|
126
|
+
|
127
|
+
@classmethod
|
128
|
+
def _invalidate_cache_for_type(cls, type_to_invalidate: type) -> None:
|
129
|
+
"""Invalidate cache entries for a specific type."""
|
130
|
+
# Remove any cached entries that might be affected
|
131
|
+
keys_to_remove = [k for k in cls._type_cache.keys() if issubclass(k, type_to_invalidate)]
|
132
|
+
for key in keys_to_remove:
|
133
|
+
del cls._type_cache[key]
|
134
|
+
|
135
|
+
@classmethod
|
136
|
+
def clear_cache(cls) -> None:
|
137
|
+
"""Clear all caches (for testing)."""
|
138
|
+
cls._type_cache.clear()
|
139
|
+
|
140
|
+
@classmethod
|
141
|
+
def get_cache_stats(cls) -> dict[str, int]:
|
142
|
+
"""Get cache statistics for monitoring."""
|
143
|
+
return {"expression_types_registered": len(cls._expression_types), "variable_types_registered": len(cls._variable_types), "cached_types": len(cls._type_cache)}
|
144
|
+
|
145
|
+
|
146
|
+
# Convenience functions for backwards compatibility
|
147
|
+
def register_expression_type(expression_type: type) -> None:
|
148
|
+
"""Register a type as an expression type."""
|
149
|
+
TypeRegistry.register_expression_type(expression_type)
|
150
|
+
|
151
|
+
|
152
|
+
def register_variable_type(variable_type: type) -> None:
|
153
|
+
"""Register a type as a variable type."""
|
154
|
+
TypeRegistry.register_variable_type(variable_type)
|
155
|
+
|
156
|
+
|
157
|
+
def is_expression(obj: object) -> bool:
|
158
|
+
"""Check if object is an expression."""
|
159
|
+
return TypeRegistry.is_expression(obj)
|
160
|
+
|
161
|
+
|
162
|
+
def is_variable(obj: object) -> bool:
|
163
|
+
"""Check if object is a variable."""
|
164
|
+
return TypeRegistry.is_variable(obj)
|