qnty 0.0.9__py3-none-any.whl → 0.1.1__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.
Files changed (92) hide show
  1. qnty/__init__.py +2 -3
  2. qnty/constants/__init__.py +10 -0
  3. qnty/constants/numerical.py +18 -0
  4. qnty/constants/solvers.py +6 -0
  5. qnty/constants/tests.py +6 -0
  6. qnty/dimensions/__init__.py +23 -0
  7. qnty/dimensions/base.py +97 -0
  8. qnty/dimensions/field_dims.py +126 -0
  9. qnty/dimensions/field_dims.pyi +128 -0
  10. qnty/dimensions/signature.py +111 -0
  11. qnty/equations/__init__.py +1 -1
  12. qnty/equations/equation.py +118 -155
  13. qnty/equations/system.py +68 -65
  14. qnty/expressions/__init__.py +25 -46
  15. qnty/expressions/formatter.py +188 -0
  16. qnty/expressions/functions.py +46 -68
  17. qnty/expressions/nodes.py +540 -384
  18. qnty/expressions/types.py +70 -0
  19. qnty/problems/__init__.py +145 -0
  20. qnty/problems/composition.py +1101 -0
  21. qnty/problems/problem.py +737 -0
  22. qnty/problems/rules.py +145 -0
  23. qnty/problems/solving.py +1216 -0
  24. qnty/problems/validation.py +127 -0
  25. qnty/quantities/__init__.py +28 -5
  26. qnty/quantities/base_qnty.py +677 -0
  27. qnty/quantities/field_converters.py +24004 -0
  28. qnty/quantities/field_qnty.py +1012 -0
  29. qnty/{generated/setters.py → quantities/field_setter.py} +3071 -2961
  30. qnty/{generated/quantities.py → quantities/field_vars.py} +829 -444
  31. qnty/{generated/quantities.pyi → quantities/field_vars.pyi} +1289 -1290
  32. qnty/solving/manager.py +50 -44
  33. qnty/solving/order.py +181 -133
  34. qnty/solving/solvers/__init__.py +2 -9
  35. qnty/solving/solvers/base.py +27 -37
  36. qnty/solving/solvers/iterative.py +115 -135
  37. qnty/solving/solvers/simultaneous.py +93 -165
  38. qnty/units/__init__.py +1 -0
  39. qnty/{generated/units.py → units/field_units.py} +1700 -991
  40. qnty/units/field_units.pyi +2461 -0
  41. qnty/units/prefixes.py +58 -105
  42. qnty/units/registry.py +76 -89
  43. qnty/utils/__init__.py +16 -0
  44. qnty/utils/caching/__init__.py +23 -0
  45. qnty/utils/caching/manager.py +401 -0
  46. qnty/utils/error_handling/__init__.py +66 -0
  47. qnty/utils/error_handling/context.py +39 -0
  48. qnty/utils/error_handling/exceptions.py +96 -0
  49. qnty/utils/error_handling/handlers.py +171 -0
  50. qnty/utils/logging.py +4 -4
  51. qnty/utils/protocols.py +164 -0
  52. qnty/utils/scope_discovery.py +420 -0
  53. {qnty-0.0.9.dist-info → qnty-0.1.1.dist-info}/METADATA +1 -1
  54. qnty-0.1.1.dist-info/RECORD +60 -0
  55. qnty/_backup/problem_original.py +0 -1251
  56. qnty/_backup/quantity.py +0 -63
  57. qnty/codegen/cli.py +0 -125
  58. qnty/codegen/generators/data/unit_data.json +0 -8807
  59. qnty/codegen/generators/data_processor.py +0 -345
  60. qnty/codegen/generators/dimensions_gen.py +0 -434
  61. qnty/codegen/generators/doc_generator.py +0 -141
  62. qnty/codegen/generators/out/dimension_mapping.json +0 -974
  63. qnty/codegen/generators/out/dimension_metadata.json +0 -123
  64. qnty/codegen/generators/out/units_metadata.json +0 -223
  65. qnty/codegen/generators/quantities_gen.py +0 -159
  66. qnty/codegen/generators/setters_gen.py +0 -178
  67. qnty/codegen/generators/stubs_gen.py +0 -167
  68. qnty/codegen/generators/units_gen.py +0 -295
  69. qnty/expressions/cache.py +0 -94
  70. qnty/generated/dimensions.py +0 -514
  71. qnty/problem/__init__.py +0 -91
  72. qnty/problem/base.py +0 -142
  73. qnty/problem/composition.py +0 -385
  74. qnty/problem/composition_mixin.py +0 -382
  75. qnty/problem/equations.py +0 -413
  76. qnty/problem/metaclass.py +0 -302
  77. qnty/problem/reconstruction.py +0 -1016
  78. qnty/problem/solving.py +0 -180
  79. qnty/problem/validation.py +0 -64
  80. qnty/problem/variables.py +0 -239
  81. qnty/quantities/expression_quantity.py +0 -314
  82. qnty/quantities/quantity.py +0 -428
  83. qnty/quantities/typed_quantity.py +0 -215
  84. qnty/validation/__init__.py +0 -0
  85. qnty/validation/registry.py +0 -0
  86. qnty/validation/rules.py +0 -167
  87. qnty-0.0.9.dist-info/RECORD +0 -63
  88. /qnty/{codegen → extensions}/__init__.py +0 -0
  89. /qnty/{codegen/generators → extensions/integration}/__init__.py +0 -0
  90. /qnty/{codegen/generators/utils → extensions/plotting}/__init__.py +0 -0
  91. /qnty/{generated → extensions/reporting}/__init__.py +0 -0
  92. {qnty-0.0.9.dist-info → qnty-0.1.1.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 CHANGED
@@ -4,11 +4,11 @@ import os
4
4
  _LOGGER: logging.Logger | None = None
5
5
 
6
6
 
7
- def get_logger(name: str = "optinova") -> logging.Logger:
7
+ def get_logger(name: str = "qnty") -> logging.Logger:
8
8
  """Return a module-level configured logger.
9
9
 
10
10
  Log level resolves in order:
11
- 1. Explicit environment variable OPTINOVA_LOG_LEVEL
11
+ 1. Explicit environment variable QNTY_LOG_LEVEL
12
12
  2. Existing logger level if already configured
13
13
  3. Defaults to INFO
14
14
  """
@@ -20,12 +20,12 @@ def get_logger(name: str = "optinova") -> logging.Logger:
20
20
  if not logger.handlers:
21
21
  # Basic handler (stdout)
22
22
  handler = logging.StreamHandler()
23
- fmt = os.getenv("OPTINOVA_LOG_FORMAT", "%(asctime)s | %(levelname)s | %(name)s | %(message)s")
23
+ fmt = os.getenv("QNTY_LOG_FORMAT", "%(asctime)s | %(levelname)s | %(name)s | %(message)s")
24
24
  handler.setFormatter(logging.Formatter(fmt))
25
25
  logger.addHandler(handler)
26
26
 
27
27
  # Resolve level
28
- level_str = os.getenv("OPTINOVA_LOG_LEVEL", "INFO").upper()
28
+ level_str = os.getenv("QNTY_LOG_LEVEL", "INFO").upper()
29
29
  level = getattr(logging, level_str, logging.INFO)
30
30
  logger.setLevel(level)
31
31
  logger.propagate = False
@@ -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)