levelapp 0.1.15__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 (70) hide show
  1. levelapp/__init__.py +0 -0
  2. levelapp/aspects/__init__.py +8 -0
  3. levelapp/aspects/loader.py +253 -0
  4. levelapp/aspects/logger.py +59 -0
  5. levelapp/aspects/monitor.py +617 -0
  6. levelapp/aspects/sanitizer.py +168 -0
  7. levelapp/clients/__init__.py +122 -0
  8. levelapp/clients/anthropic.py +112 -0
  9. levelapp/clients/gemini.py +130 -0
  10. levelapp/clients/groq.py +101 -0
  11. levelapp/clients/huggingface.py +162 -0
  12. levelapp/clients/ionos.py +126 -0
  13. levelapp/clients/mistral.py +106 -0
  14. levelapp/clients/openai.py +116 -0
  15. levelapp/comparator/__init__.py +5 -0
  16. levelapp/comparator/comparator.py +232 -0
  17. levelapp/comparator/extractor.py +108 -0
  18. levelapp/comparator/schemas.py +61 -0
  19. levelapp/comparator/scorer.py +269 -0
  20. levelapp/comparator/utils.py +136 -0
  21. levelapp/config/__init__.py +5 -0
  22. levelapp/config/endpoint.py +199 -0
  23. levelapp/config/prompts.py +57 -0
  24. levelapp/core/__init__.py +0 -0
  25. levelapp/core/base.py +386 -0
  26. levelapp/core/schemas.py +24 -0
  27. levelapp/core/session.py +336 -0
  28. levelapp/endpoint/__init__.py +0 -0
  29. levelapp/endpoint/client.py +188 -0
  30. levelapp/endpoint/client_test.py +41 -0
  31. levelapp/endpoint/manager.py +114 -0
  32. levelapp/endpoint/parsers.py +119 -0
  33. levelapp/endpoint/schemas.py +38 -0
  34. levelapp/endpoint/tester.py +52 -0
  35. levelapp/evaluator/__init__.py +3 -0
  36. levelapp/evaluator/evaluator.py +307 -0
  37. levelapp/metrics/__init__.py +63 -0
  38. levelapp/metrics/embedding.py +56 -0
  39. levelapp/metrics/embeddings/__init__.py +0 -0
  40. levelapp/metrics/embeddings/sentence_transformer.py +30 -0
  41. levelapp/metrics/embeddings/torch_based.py +56 -0
  42. levelapp/metrics/exact.py +182 -0
  43. levelapp/metrics/fuzzy.py +80 -0
  44. levelapp/metrics/token.py +103 -0
  45. levelapp/plugins/__init__.py +0 -0
  46. levelapp/repository/__init__.py +3 -0
  47. levelapp/repository/filesystem.py +203 -0
  48. levelapp/repository/firestore.py +291 -0
  49. levelapp/simulator/__init__.py +3 -0
  50. levelapp/simulator/schemas.py +116 -0
  51. levelapp/simulator/simulator.py +531 -0
  52. levelapp/simulator/utils.py +134 -0
  53. levelapp/visualization/__init__.py +7 -0
  54. levelapp/visualization/charts.py +358 -0
  55. levelapp/visualization/dashboard.py +240 -0
  56. levelapp/visualization/exporter.py +167 -0
  57. levelapp/visualization/templates/base.html +158 -0
  58. levelapp/visualization/templates/comparator_dashboard.html +57 -0
  59. levelapp/visualization/templates/simulator_dashboard.html +111 -0
  60. levelapp/workflow/__init__.py +6 -0
  61. levelapp/workflow/base.py +192 -0
  62. levelapp/workflow/config.py +96 -0
  63. levelapp/workflow/context.py +64 -0
  64. levelapp/workflow/factory.py +42 -0
  65. levelapp/workflow/registration.py +6 -0
  66. levelapp/workflow/runtime.py +19 -0
  67. levelapp-0.1.15.dist-info/METADATA +571 -0
  68. levelapp-0.1.15.dist-info/RECORD +70 -0
  69. levelapp-0.1.15.dist-info/WHEEL +4 -0
  70. levelapp-0.1.15.dist-info/licenses/LICENSE +0 -0
levelapp/__init__.py ADDED
File without changes
@@ -0,0 +1,8 @@
1
+ from .logger import logger
2
+ from .loader import DataLoader
3
+ from .sanitizer import JSONSanitizer
4
+ from .monitor import MonitoringAspect, FunctionMonitor, MetricType, ExecutionMetrics
5
+
6
+
7
+ __all__ = ['logger', 'DataLoader', 'JSONSanitizer', 'MonitoringAspect',
8
+ 'FunctionMonitor', 'MetricType', 'ExecutionMetrics']
@@ -0,0 +1,253 @@
1
+ """levelapp/aspects/loader.py"""
2
+ import os
3
+ import yaml
4
+ import json
5
+ import logging
6
+
7
+ from collections.abc import Mapping, Sequence
8
+ from typing import Any, Type, TypeVar, List, Optional, Dict, Tuple
9
+
10
+ from dotenv import load_dotenv
11
+ from pydantic import BaseModel, create_model, ValidationError
12
+
13
+ from rapidfuzz import utils
14
+
15
+
16
+ logger = logging.getLogger(__name__)
17
+ Model = TypeVar("Model", bound=BaseModel)
18
+
19
+
20
+ class DynamicModelBuilder:
21
+ """
22
+ A utility for creating dynamic Pydantic models at runtime from arbitrary Python
23
+ data structures (dicts, lists, primitives).
24
+
25
+ Features:
26
+ ---------
27
+ - **Dynamic Model Generation**: Builds `pydantic.BaseModel` subclasses on the fly
28
+ based on the structure of the input data.
29
+ - **Recursive Nesting**: Handles arbitrarily nested dictionaries and lists by
30
+ generating nested models automatically. (Don't freak out, it's for shallow traversal).
31
+ - **Field Name Sanitization**: Ensures generated field names are valid Python identifiers.
32
+ - **Caching**: Maintains a cache of previously generated models to avoid redundant
33
+ re-construction and improve performance.
34
+
35
+ Use Cases:
36
+ ----------
37
+ - Converting arbitrary JSON/dict data into structured, type-safe Pydantic models.
38
+ - Prototyping with dynamic/unknown payloads where upfront schema definition is not feasible.
39
+
40
+ Notes:
41
+ ------
42
+ - Field names are sanitized using `utils.default_process` (from rapidfuzz),
43
+ replacing spaces with underscores and handling invalid identifiers.
44
+ - Lists are typed based on their first element only; heterogeneous lists
45
+ may not be fully captured (Sorry?).
46
+ - Model caching is based on `(model_name, str(data) or str(sorted(keys)))`.
47
+ This improves performance but may cause collisions if `str(data)` is ambiguous.
48
+ """
49
+ def __init__(self):
50
+ """
51
+ Initialize a DynamicModelBuilder instance.
52
+
53
+ Attributes:
54
+ -----------
55
+ model_cache : Dict[Tuple[str, str], Type[BaseModel]]
56
+ Cache of generated models keyed by (model_name, data_signature).
57
+ Ensures models are reused instead of rebuilt.
58
+ """
59
+ self.model_cache: Dict[Tuple[str, str], Type[BaseModel]] = {}
60
+
61
+ def clear_cache(self):
62
+ """
63
+ Clear the internal model cache.
64
+
65
+ Use when schema changes are expected or to free memory in long-running processes.
66
+ """
67
+ self.model_cache.clear()
68
+
69
+ @staticmethod
70
+ def _sanitize_field_name(name: str) -> str:
71
+ """
72
+ Normalize and sanitize field names into valid Python identifiers.
73
+
74
+ - Converts to lowercase and strips unwanted characters using `utils.default_process`.
75
+ - Replaces spaces with underscores.
76
+ - Ensures field names are not empty; substitutes `"field_default"` if so.
77
+ - Prepends `"field_"` if the name starts with a digit.
78
+
79
+ Args:
80
+ name (str): The original field name from input data.
81
+
82
+ Returns:
83
+ str: A valid Python identifier for use in a Pydantic model.
84
+ """
85
+ name = utils.default_process(name).replace(' ', '_')
86
+ if not name:
87
+ return "field_default"
88
+ if name[0].isdigit():
89
+ return f"field_{name}"
90
+ return name
91
+
92
+ def _get_field_type(self, value: Any, model_name: str, key: str) -> Tuple[Any, Any]:
93
+ """
94
+ Infer the field type and default value for a given data value.
95
+
96
+ Supported cases:
97
+ - **Mapping (dict-like)**: Creates a nested dynamic model recursively.
98
+ - **Sequence (list/tuple, non-string)**:
99
+ - Empty list → `List[BaseModel]`
100
+ - List of dicts → `List[<nested model>]`
101
+ - List of primitives → `List[primitive type]`
102
+ - **Primitive**: Wraps in `Optional` to allow nulls.
103
+
104
+ Args:
105
+ value (Any): The raw field value to inspect.
106
+ model_name (str): Name of the parent model.
107
+ key (str): Field name (before sanitization).
108
+
109
+ Returns:
110
+ Tuple[Any, Any]: A `(type, default_value)` tuple suitable for `create_model`.
111
+ """
112
+ if isinstance(value, Mapping):
113
+ nested_model = self.create_dynamic_model(model_name=f"{model_name}_{key}", data=value)
114
+ return Optional[nested_model], None
115
+
116
+ elif isinstance(value, Sequence) and not isinstance(value, (str, bytes)):
117
+ if not value:
118
+ return List[BaseModel], ...
119
+
120
+ elif isinstance(value[0], Mapping):
121
+ nested_model = self.create_dynamic_model(model_name=f"{model_name}_{key}", data=value[0])
122
+ return Optional[List[nested_model]], None
123
+
124
+ else:
125
+ field_type = type(value[0]) if value[0] is not None else Any
126
+ return Optional[List[field_type]], None
127
+
128
+ else:
129
+ field_type = Optional[type(value)] if value is not None else Optional[Any]
130
+ return field_type, None
131
+
132
+ def create_dynamic_model(self, model_name: str, data: Any) -> Type[BaseModel]:
133
+ """
134
+ Create a dynamic Pydantic model from arbitrary input data.
135
+
136
+ - Handles nested dictionaries and lists by recursively generating
137
+ sub-models.
138
+ - Uses caching to avoid rebuilding models for the same schema.
139
+
140
+ Args:
141
+ model_name (str): Suggested name of the generated model.
142
+ data (Any): Input data (dict, list, or primitive).
143
+
144
+ Returns:
145
+ Type[BaseModel]: A dynamically created Pydantic model class.
146
+ """
147
+ model_name = self._sanitize_field_name(name=model_name)
148
+ cache_key = (model_name, str(data) if not isinstance(data, dict) else str(sorted(data.keys())))
149
+
150
+ if cache_key in self.model_cache:
151
+ return self.model_cache[cache_key]
152
+
153
+ if isinstance(data, Mapping):
154
+ fields = {
155
+ self._sanitize_field_name(name=key): self._get_field_type(value=value, model_name=model_name, key=key)
156
+ for key, value in data.items()
157
+ }
158
+ model = create_model(model_name, **fields)
159
+
160
+ else:
161
+ field_type = Optional[type(data)] if data else Optional[Any]
162
+ model = create_model(model_name, value=(field_type, None))
163
+
164
+ self.model_cache[cache_key] = model
165
+
166
+ return model
167
+
168
+
169
+ class DataLoader:
170
+ """Main utility tool for loading configuration and reference data"""
171
+ def __init__(self):
172
+ self.builder = DynamicModelBuilder()
173
+ self._name = self.__class__.__name__
174
+ load_dotenv()
175
+
176
+ @staticmethod
177
+ def load_raw_data(path: str | None = None):
178
+ """
179
+ Load raw data from JSON or YAML files.
180
+
181
+ Args:
182
+ path (str): path to the JSON or YAML file.
183
+
184
+ Returns:
185
+ A dictionary containing the raw data.
186
+
187
+ Raises:
188
+ FileNotFoundError: if the file was not found in the path.
189
+ YAMLError: if there was an error parsing the YAML file.
190
+ JSONDecoderError: if there was an error parsing the JSON file.
191
+ IOError: if the file is corrupt and cannot be read.
192
+ """
193
+ try:
194
+ if not path:
195
+ path = os.getenv('WORKFLOW_CONFIG_PATH', 'no-file')
196
+
197
+ if not os.path.exists(path):
198
+ raise FileNotFoundError(f"The provided configuration file path '{path}' does not exist.")
199
+
200
+ with open(path, 'r', encoding='utf-8') as f:
201
+ if path.endswith((".yaml", ".yml")):
202
+ content = yaml.safe_load(f)
203
+
204
+ elif path.endswith(".json"):
205
+ content = json.load(f)
206
+
207
+ else:
208
+ raise ValueError("[WorkflowConfiguration] Unsupported file format.")
209
+
210
+ return content
211
+
212
+ except FileNotFoundError as e:
213
+ raise FileNotFoundError(f"[EndpointConfig] Payload template file '{e.filename}' not found in path.")
214
+
215
+ except yaml.YAMLError as e:
216
+ raise ValueError(f"[EndpointConfig] Error parsing YAML file:\n{e}")
217
+
218
+ except json.JSONDecodeError as e:
219
+ raise ValueError(f"[EndpointConfig] Error parsing JSON file:\n{e}")
220
+
221
+ except IOError as e:
222
+ raise IOError(f"[EndpointConfig] Error reading file:\n{e}")
223
+
224
+ def create_dynamic_model(
225
+ self,
226
+ data: Dict[str, Any],
227
+ model_name: str = "ExtractedData"
228
+ ) -> BaseModel | None:
229
+ """
230
+ Load data into a dynamically created Pydantic model instance.
231
+
232
+ Args:
233
+ data (Dict[str, Any]): The data to load.
234
+ model_name (str, optional): The name of the model. Defaults to "ExtractedData".
235
+
236
+ Returns:
237
+ An Pydantic model instance.
238
+
239
+ Raises:
240
+ ValidationError: If a validation error occurs.
241
+ Exception: If an unexpected error occurs.
242
+ """
243
+ try:
244
+ self.builder.clear_cache()
245
+ dynamic_model = self.builder.create_dynamic_model(model_name=model_name, data=data)
246
+ model_instance = dynamic_model.model_validate(data)
247
+ return model_instance
248
+
249
+ except ValidationError as e:
250
+ logger.exception(f"[{self._name}] Validation Error: {e.errors()}")
251
+
252
+ except Exception as e:
253
+ logger.error(f"[{self._name}] An error occurred: {e}")
@@ -0,0 +1,59 @@
1
+ """levelapp/aspects/logger.py"""
2
+ import logging
3
+ import sys
4
+ from typing import Optional
5
+
6
+
7
+ class Logger:
8
+ """Centralized logger for Levelapp (AOP-style)."""
9
+
10
+ _instance: Optional[logging.Logger] = None
11
+
12
+ @classmethod
13
+ def get_logger(
14
+ cls,
15
+ name: str = "levelapp",
16
+ level: int = logging.INFO,
17
+ log_to_file: Optional[str] = None,
18
+ ) -> logging.Logger:
19
+ """
20
+ Get a configured logger instance (singleton).
21
+
22
+ Args:
23
+ name (str): Logger name.
24
+ level (int): Logging level (default=INFO).
25
+ log_to_file (Optional[str]): File path for logging (default=None).
26
+
27
+ Returns:
28
+ logging.Logger: Configured logger instance.
29
+ """
30
+ if cls._instance:
31
+ return cls._instance
32
+
33
+ logger_ = logging.getLogger(name)
34
+ logger_.setLevel(level)
35
+ logger_.propagate = False # prevent double logging
36
+
37
+ # Formatter with contextual info
38
+ formatter = logging.Formatter(
39
+ fmt="%(asctime)s | %(levelname)-8s | %(name)s | %(message)s",
40
+ datefmt="%Y-%m-%d %H:%M:%S",
41
+ )
42
+
43
+ # Console handler
44
+ console_handler = logging.StreamHandler(sys.stdout)
45
+ console_handler.setFormatter(formatter)
46
+ logger_.addHandler(console_handler)
47
+
48
+ # Optional file handler
49
+ if log_to_file:
50
+ file_handler = logging.FileHandler(log_to_file)
51
+ file_handler.setFormatter(formatter)
52
+ logger_.addHandler(file_handler)
53
+
54
+ cls._instance = logger_
55
+ return logger_
56
+
57
+
58
+ # Global access
59
+ logger = Logger.get_logger()