mindtrace 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.
- mindtrace/apps/mindtrace/apps/__init__.py +0 -0
- mindtrace/automation/mindtrace/automation/__init__.py +0 -0
- mindtrace/cluster/mindtrace/cluster/__init__.py +0 -0
- mindtrace/core/mindtrace/core/__init__.py +34 -0
- mindtrace/core/mindtrace/core/base/__init__.py +7 -0
- mindtrace/core/mindtrace/core/base/mindtrace_base.py +374 -0
- mindtrace/core/mindtrace/core/config/__init__.py +3 -0
- mindtrace/core/mindtrace/core/config/config.py +27 -0
- mindtrace/core/mindtrace/core/logging/__init__.py +3 -0
- mindtrace/core/mindtrace/core/logging/logger.py +112 -0
- mindtrace/core/mindtrace/core/observables/context_listener.py +73 -0
- mindtrace/core/mindtrace/core/observables/event_bus.py +72 -0
- mindtrace/core/mindtrace/core/observables/observable_context.py +140 -0
- mindtrace/core/mindtrace/core/types/task_schema.py +11 -0
- mindtrace/core/mindtrace/core/utils/__init__.py +12 -0
- mindtrace/core/mindtrace/core/utils/checks.py +39 -0
- mindtrace/core/mindtrace/core/utils/dynamic.py +57 -0
- mindtrace/core/mindtrace/core/utils/lambdas.py +35 -0
- mindtrace/core/mindtrace/core/utils/timers.py +291 -0
- mindtrace/database/mindtrace/database/__init__.py +13 -0
- mindtrace/database/mindtrace/database/backends/local_odm_backend.py +23 -0
- mindtrace/database/mindtrace/database/backends/mindtrace_odm_backend.py +27 -0
- mindtrace/database/mindtrace/database/backends/mongo_odm_backend.py +20 -0
- mindtrace/database/mindtrace/database/backends/redis_odm_backend.py +20 -0
- mindtrace/database/mindtrace/database/core/mindtrace_odm.py +24 -0
- mindtrace/datalake/mindtrace/datalake/__init__.py +0 -0
- mindtrace/hardware/mindtrace/hardware/__init__.py +0 -0
- mindtrace/jobs/mindtrace/jobs/__init__.py +0 -0
- mindtrace/models/mindtrace/models/__init__.py +0 -0
- mindtrace/registry/mindtrace/registry/__init__.py +18 -0
- mindtrace/registry/mindtrace/registry/archivers/config_archiver.py +26 -0
- mindtrace/registry/mindtrace/registry/backends/__init__.py +0 -0
- mindtrace/registry/mindtrace/registry/backends/gcp_registry_backend.py +24 -0
- mindtrace/registry/mindtrace/registry/backends/local_registry_backend.py +548 -0
- mindtrace/registry/mindtrace/registry/backends/minio_registry_backend.py +647 -0
- mindtrace/registry/mindtrace/registry/backends/registry_backend.py +233 -0
- mindtrace/registry/mindtrace/registry/core/__init__.py +0 -0
- mindtrace/registry/mindtrace/registry/core/archiver.py +33 -0
- mindtrace/registry/mindtrace/registry/core/registry.py +1032 -0
- mindtrace/services/mindtrace/services/__init__.py +27 -0
- mindtrace/services/mindtrace/services/core/__init__.py +0 -0
- mindtrace/services/mindtrace/services/core/connection_manager.py +140 -0
- mindtrace/services/mindtrace/services/core/launcher.py +72 -0
- mindtrace/services/mindtrace/services/core/service.py +456 -0
- mindtrace/services/mindtrace/services/core/types.py +104 -0
- mindtrace/services/mindtrace/services/core/utils.py +187 -0
- mindtrace/services/mindtrace/services/sample/echo_service.py +35 -0
- mindtrace/storage/mindtrace/storage/__init__.py +3 -0
- mindtrace/storage/mindtrace/storage/base.py +286 -0
- mindtrace/storage/mindtrace/storage/gcs.py +196 -0
- mindtrace/ui/mindtrace/ui/__init__.py +0 -0
- mindtrace-0.1.0.dist-info/METADATA +121 -0
- mindtrace-0.1.0.dist-info/RECORD +55 -0
- mindtrace-0.1.0.dist-info/WHEEL +5 -0
- mindtrace-0.1.0.dist-info/top_level.txt +1 -0
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from mindtrace.core.base import Mindtrace, MindtraceABC, MindtraceMeta
|
|
2
|
+
from mindtrace.core.config import Config
|
|
3
|
+
from mindtrace.core.logging.logger import setup_logger
|
|
4
|
+
from mindtrace.core.observables.context_listener import ContextListener
|
|
5
|
+
from mindtrace.core.observables.event_bus import EventBus
|
|
6
|
+
from mindtrace.core.observables.observable_context import ObservableContext
|
|
7
|
+
from mindtrace.core.types.task_schema import TaskSchema
|
|
8
|
+
from mindtrace.core.utils.checks import check_libs, first_not_none, ifnone, ifnone_url
|
|
9
|
+
from mindtrace.core.utils.dynamic import get_class, instantiate_target
|
|
10
|
+
from mindtrace.core.utils.lambdas import named_lambda
|
|
11
|
+
from mindtrace.core.utils.timers import Timeout, Timer, TimerCollection
|
|
12
|
+
|
|
13
|
+
setup_logger() # Initialize the default logger
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"check_libs",
|
|
17
|
+
"ContextListener",
|
|
18
|
+
"Config",
|
|
19
|
+
"EventBus",
|
|
20
|
+
"first_not_none",
|
|
21
|
+
"get_class",
|
|
22
|
+
"ifnone",
|
|
23
|
+
"ifnone_url",
|
|
24
|
+
"instantiate_target",
|
|
25
|
+
"Mindtrace",
|
|
26
|
+
"MindtraceABC",
|
|
27
|
+
"MindtraceMeta",
|
|
28
|
+
"named_lambda",
|
|
29
|
+
"ObservableContext",
|
|
30
|
+
"TaskSchema",
|
|
31
|
+
"Timer",
|
|
32
|
+
"TimerCollection",
|
|
33
|
+
"Timeout",
|
|
34
|
+
]
|
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
"""Mindtrace class. Provides unified configuration, logging and context management."""
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
import logging
|
|
5
|
+
import traceback
|
|
6
|
+
from abc import ABC, ABCMeta
|
|
7
|
+
from functools import wraps
|
|
8
|
+
from typing import Callable, Optional
|
|
9
|
+
|
|
10
|
+
from mindtrace.core.config import Config
|
|
11
|
+
from mindtrace.core.logging.logger import get_logger
|
|
12
|
+
from mindtrace.core.utils import ifnone
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class MindtraceMeta(type):
|
|
16
|
+
"""Metaclass for Mindtrace class.
|
|
17
|
+
|
|
18
|
+
The MindtraceMeta metaclass enables classes deriving from Mindtrace to automatically use the same default logger within
|
|
19
|
+
class methods as it does within instance methods. I.e. consider the following class:
|
|
20
|
+
|
|
21
|
+
Example, logging in both class methods and instance methods::
|
|
22
|
+
|
|
23
|
+
from mindtrace.core import Mindtrace
|
|
24
|
+
|
|
25
|
+
class MyClass(Mindtrace):
|
|
26
|
+
def __init__(self):
|
|
27
|
+
super().__init__()
|
|
28
|
+
|
|
29
|
+
def instance_method(self):
|
|
30
|
+
self.logger.info(f"Using logger: {self.logger.name}") # Using logger: mindtrace.my_module.MyClass
|
|
31
|
+
|
|
32
|
+
@classmethod
|
|
33
|
+
def class_method(cls):
|
|
34
|
+
cls.logger.info(f"Using logger: {cls.logger.name}") # Using logger: mindtrace.my_module.MyClass
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(cls, name, bases, attr_dict):
|
|
38
|
+
super().__init__(name, bases, attr_dict)
|
|
39
|
+
cls._logger = None
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def logger(cls):
|
|
43
|
+
if cls._logger is None:
|
|
44
|
+
cls._logger = get_logger(cls.unique_name)
|
|
45
|
+
return cls._logger
|
|
46
|
+
|
|
47
|
+
@logger.setter
|
|
48
|
+
def logger(cls, new_logger):
|
|
49
|
+
cls._logger = new_logger
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def unique_name(self) -> str:
|
|
53
|
+
return self.__module__ + "." + self.__name__
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class Mindtrace(metaclass=MindtraceMeta):
|
|
57
|
+
"""Base class for all Mindtrace package core classes.
|
|
58
|
+
|
|
59
|
+
The Mindtrace class adds default context manager and logging methods. All classes that derive from Mindtrace can be
|
|
60
|
+
used as context managers and will use a unified logging format.
|
|
61
|
+
|
|
62
|
+
The class automatically provides logging capabilities for both class methods and instance methods.
|
|
63
|
+
For example:
|
|
64
|
+
|
|
65
|
+
.. code-block:: python
|
|
66
|
+
|
|
67
|
+
from mindtrace.core import Mindtrace
|
|
68
|
+
|
|
69
|
+
class MyClass(Mindtrace):
|
|
70
|
+
def __init__(self):
|
|
71
|
+
super().__init__()
|
|
72
|
+
|
|
73
|
+
def instance_method(self):
|
|
74
|
+
self.logger.info(f"Using logger: {self.logger.name}") # Using logger: mindtrace.my_module.MyClass
|
|
75
|
+
|
|
76
|
+
@classmethod
|
|
77
|
+
def class_method(cls):
|
|
78
|
+
cls.logger.info(f"Using logger: {cls.logger.name}") # Using logger: mindtrace.my_module.MyClass
|
|
79
|
+
|
|
80
|
+
The logging functionality is automatically provided through the MindtraceMeta metaclass,
|
|
81
|
+
which ensures consistent logging behavior across all method types.
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
config = Config()
|
|
85
|
+
|
|
86
|
+
def __init__(self, suppress: bool = False, **kwargs):
|
|
87
|
+
"""
|
|
88
|
+
Initialize the Mindtrace object.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
suppress (bool): Whether to suppress exceptions in context manager use.
|
|
92
|
+
**kwargs: Additional keyword arguments. Logger-related kwargs are passed to `get_logger`.
|
|
93
|
+
Valid logger kwargs: log_dir, logger_level, stream_level, file_level,
|
|
94
|
+
file_mode, propagate, max_bytes, backup_count
|
|
95
|
+
"""
|
|
96
|
+
# Initialize parent classes first (cooperative inheritance)
|
|
97
|
+
try:
|
|
98
|
+
super().__init__(**kwargs)
|
|
99
|
+
except TypeError:
|
|
100
|
+
# If parent classes don't accept some kwargs, try without logger-specific ones
|
|
101
|
+
logger_param_names = {
|
|
102
|
+
"log_dir",
|
|
103
|
+
"logger_level",
|
|
104
|
+
"stream_level",
|
|
105
|
+
"file_level",
|
|
106
|
+
"file_mode",
|
|
107
|
+
"propagate",
|
|
108
|
+
"max_bytes",
|
|
109
|
+
"backup_count",
|
|
110
|
+
}
|
|
111
|
+
remaining_kwargs = {k: v for k, v in kwargs.items() if k not in logger_param_names}
|
|
112
|
+
try:
|
|
113
|
+
super().__init__(**remaining_kwargs)
|
|
114
|
+
except TypeError:
|
|
115
|
+
# If that still fails, try with no kwargs
|
|
116
|
+
super().__init__()
|
|
117
|
+
|
|
118
|
+
# Set Mindtrace-specific attributes
|
|
119
|
+
self.suppress = suppress
|
|
120
|
+
|
|
121
|
+
# Filter logger-specific kwargs for logger setup
|
|
122
|
+
logger_param_names = {
|
|
123
|
+
"log_dir",
|
|
124
|
+
"logger_level",
|
|
125
|
+
"stream_level",
|
|
126
|
+
"file_level",
|
|
127
|
+
"file_mode",
|
|
128
|
+
"propagate",
|
|
129
|
+
"max_bytes",
|
|
130
|
+
"backup_count",
|
|
131
|
+
}
|
|
132
|
+
logger_kwargs = {k: v for k, v in kwargs.items() if k in logger_param_names}
|
|
133
|
+
|
|
134
|
+
# Set up the logger
|
|
135
|
+
self.logger = get_logger(self.unique_name, **logger_kwargs)
|
|
136
|
+
|
|
137
|
+
@property
|
|
138
|
+
def unique_name(self) -> str:
|
|
139
|
+
return self.__module__ + "." + type(self).__name__
|
|
140
|
+
|
|
141
|
+
@property
|
|
142
|
+
def name(self) -> str:
|
|
143
|
+
return type(self).__name__
|
|
144
|
+
|
|
145
|
+
def __enter__(self):
|
|
146
|
+
self.logger.debug(f"Initializing {self.name} as a context manager.")
|
|
147
|
+
return self
|
|
148
|
+
|
|
149
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
150
|
+
self.logger.debug(f"Exiting context manager for {self.name}.")
|
|
151
|
+
if exc_type is not None:
|
|
152
|
+
info = (exc_type, exc_val, exc_tb)
|
|
153
|
+
self.logger.exception("Exception occurred", exc_info=info)
|
|
154
|
+
return self.suppress
|
|
155
|
+
return False
|
|
156
|
+
|
|
157
|
+
@classmethod
|
|
158
|
+
def autolog(
|
|
159
|
+
cls,
|
|
160
|
+
log_level=logging.DEBUG,
|
|
161
|
+
prefix_formatter: Optional[Callable] = None,
|
|
162
|
+
suffix_formatter: Optional[Callable] = None,
|
|
163
|
+
exception_formatter: Optional[Callable] = None,
|
|
164
|
+
self: Optional["Mindtrace"] = None,
|
|
165
|
+
):
|
|
166
|
+
"""Decorator that adds logger.log calls to the decorated method before and after the method is called.
|
|
167
|
+
|
|
168
|
+
By default, the autolog decorator will log the method name, arguments and keyword arguments before the method
|
|
169
|
+
is called, and the method name and result after the method completes. This behavior can be modified by passing
|
|
170
|
+
in prefix and suffix formatters.
|
|
171
|
+
|
|
172
|
+
The autolog decorator will also catch and log all Exceptions, re-raising any exception after logging it. The
|
|
173
|
+
behavior for autologging exceptions can be modified by passing in an exception_formatter.
|
|
174
|
+
|
|
175
|
+
The autolog decorator expects a logger to exist at self.logger, and hence can only be used by Mindtrace
|
|
176
|
+
subclasses or classes that have a logger attribute.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
log_level: The log_level passed to logger.log().
|
|
180
|
+
prefix_formatter: The formatter used to log the command before the wrapped method runs. The prefix_formatter
|
|
181
|
+
will be given (and must accept) three arguments, in the following order:
|
|
182
|
+
- function: The function being wrapped.
|
|
183
|
+
- args: The args passed into the function.
|
|
184
|
+
- kwargs: The kwargs passed into the function.
|
|
185
|
+
suffix_formatter: The formatter used to log the command after the wrapped method runs. The suffix_formatter
|
|
186
|
+
will be given (and must accept) two arguments, in the following order:
|
|
187
|
+
- function: The function being wrapped.
|
|
188
|
+
- result: The result returned from the wrapped method.
|
|
189
|
+
exception_formatter: The formatter used to log any errors. The exception_formatter will be given (and must
|
|
190
|
+
accept) three arguments, in the following order:
|
|
191
|
+
- function: The function being wrapped.
|
|
192
|
+
- error: The caught Exception.
|
|
193
|
+
- stack trace: The stack trace, as provided by traceback.format_exc().
|
|
194
|
+
self: The instance of the class that the method is being called on. Self only needs to be passed in if the
|
|
195
|
+
wrapped method does not have self as the first argument. Refer to the example below for more details.
|
|
196
|
+
|
|
197
|
+
Example::
|
|
198
|
+
|
|
199
|
+
from mindtrace.core import Mindtrace
|
|
200
|
+
|
|
201
|
+
class MyClass(Mindtrace):
|
|
202
|
+
def __init__(self):
|
|
203
|
+
super().__init__()
|
|
204
|
+
|
|
205
|
+
@Mindtrace.autolog()
|
|
206
|
+
def divide(self, arg1, arg2):
|
|
207
|
+
self.logger.info("We are about to divide")
|
|
208
|
+
result = arg1 / arg2
|
|
209
|
+
self.logger.info("We have divided")
|
|
210
|
+
return result
|
|
211
|
+
|
|
212
|
+
my_instance = MyClass()
|
|
213
|
+
my_instance.divide(1, 2)
|
|
214
|
+
my_instance.divide(1, 0)
|
|
215
|
+
|
|
216
|
+
The resulting log file should contain something similar to the following:
|
|
217
|
+
|
|
218
|
+
.. code-block:: text
|
|
219
|
+
|
|
220
|
+
MyClass - DEBUG - Calling divide with args: (1, 2) and kwargs: {}
|
|
221
|
+
MyClass - INFO - We are about to divide
|
|
222
|
+
MyClass - INFO - We have divided
|
|
223
|
+
MyClass - DEBUG - Finished divide with result: 0.5
|
|
224
|
+
MyClass - DEBUG - Calling divide with args: (1, 0) and kwargs: {}
|
|
225
|
+
MyClass - INFO - We are about to divide
|
|
226
|
+
MyClass - ERROR - division by zero
|
|
227
|
+
Traceback (most recent call last):
|
|
228
|
+
...
|
|
229
|
+
|
|
230
|
+
If the wrapped method does not have self as the first argument, self must be passed in as an argument to the
|
|
231
|
+
autolog decorator.
|
|
232
|
+
|
|
233
|
+
.. code-block:: python
|
|
234
|
+
|
|
235
|
+
from fastapi import FastAPI
|
|
236
|
+
from mindtrace.core import Mindtrace
|
|
237
|
+
|
|
238
|
+
class MyClass(Mindtrace):
|
|
239
|
+
def __init__():
|
|
240
|
+
super().__init__()
|
|
241
|
+
|
|
242
|
+
def create_app(self):
|
|
243
|
+
app_ = FastAPI()
|
|
244
|
+
|
|
245
|
+
@Mindtrace.autolog(self=self) # self must be passed in as an argument as it is not captured in status()
|
|
246
|
+
@app_.post("/status")
|
|
247
|
+
def status():
|
|
248
|
+
return {"status": "Available"}
|
|
249
|
+
|
|
250
|
+
return app_
|
|
251
|
+
|
|
252
|
+
"""
|
|
253
|
+
prefix_formatter = ifnone(
|
|
254
|
+
prefix_formatter,
|
|
255
|
+
default=lambda function,
|
|
256
|
+
args,
|
|
257
|
+
kwargs: f"Calling {function.__name__} with args: {args} and kwargs: {kwargs}",
|
|
258
|
+
)
|
|
259
|
+
suffix_formatter = ifnone(
|
|
260
|
+
suffix_formatter, default=lambda function, result: f"Finished {function.__name__} with result: {result}"
|
|
261
|
+
)
|
|
262
|
+
exception_formatter = ifnone(
|
|
263
|
+
exception_formatter,
|
|
264
|
+
default=lambda function,
|
|
265
|
+
e,
|
|
266
|
+
stack_trace: f"{function.__name__} failed to complete with the following error: {e}\n{stack_trace}",
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
def decorator(function):
|
|
270
|
+
is_async = inspect.iscoroutinefunction(function)
|
|
271
|
+
|
|
272
|
+
if self is None:
|
|
273
|
+
if is_async:
|
|
274
|
+
|
|
275
|
+
@wraps(function)
|
|
276
|
+
async def wrapper(self, *args, **kwargs):
|
|
277
|
+
self.logger.log(log_level, prefix_formatter(function, args, kwargs))
|
|
278
|
+
try:
|
|
279
|
+
result = await function(self, *args, **kwargs)
|
|
280
|
+
except Exception as e:
|
|
281
|
+
self.logger.error(exception_formatter(function, e, traceback.format_exc()))
|
|
282
|
+
raise
|
|
283
|
+
else:
|
|
284
|
+
self.logger.log(log_level, suffix_formatter(function, result))
|
|
285
|
+
return result
|
|
286
|
+
else:
|
|
287
|
+
|
|
288
|
+
@wraps(function)
|
|
289
|
+
def wrapper(self, *args, **kwargs):
|
|
290
|
+
self.logger.log(log_level, prefix_formatter(function, args, kwargs))
|
|
291
|
+
try:
|
|
292
|
+
result = function(self, *args, **kwargs)
|
|
293
|
+
except Exception as e:
|
|
294
|
+
self.logger.error(exception_formatter(function, e, traceback.format_exc()))
|
|
295
|
+
raise
|
|
296
|
+
else:
|
|
297
|
+
self.logger.log(log_level, suffix_formatter(function, result))
|
|
298
|
+
return result
|
|
299
|
+
|
|
300
|
+
else:
|
|
301
|
+
if is_async:
|
|
302
|
+
|
|
303
|
+
@wraps(function)
|
|
304
|
+
async def wrapper(*args, **kwargs):
|
|
305
|
+
self.logger.log(log_level, prefix_formatter(function, args, kwargs))
|
|
306
|
+
try:
|
|
307
|
+
result = await function(*args, **kwargs)
|
|
308
|
+
except Exception as e:
|
|
309
|
+
self.logger.error(exception_formatter(function, e, traceback.format_exc()))
|
|
310
|
+
raise
|
|
311
|
+
else:
|
|
312
|
+
self.logger.log(log_level, suffix_formatter(function, result))
|
|
313
|
+
return result
|
|
314
|
+
else:
|
|
315
|
+
|
|
316
|
+
@wraps(function)
|
|
317
|
+
def wrapper(*args, **kwargs):
|
|
318
|
+
self.logger.log(log_level, prefix_formatter(function, args, kwargs))
|
|
319
|
+
try:
|
|
320
|
+
result = function(*args, **kwargs)
|
|
321
|
+
except Exception as e:
|
|
322
|
+
self.logger.error(exception_formatter(function, e, traceback.format_exc()))
|
|
323
|
+
raise
|
|
324
|
+
else:
|
|
325
|
+
self.logger.log(log_level, suffix_formatter(function, result))
|
|
326
|
+
return result
|
|
327
|
+
|
|
328
|
+
return wrapper
|
|
329
|
+
|
|
330
|
+
return decorator
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
class MindtraceABCMeta(MindtraceMeta, ABCMeta):
|
|
334
|
+
"""Metaclass that combines MindtraceMeta and ABC metaclasses.
|
|
335
|
+
|
|
336
|
+
This metaclass resolves metaclass conflicts when creating classes that need to be both
|
|
337
|
+
abstract (using ABC) and have MindtraceMeta functionality. Python only allows a class to
|
|
338
|
+
have one metaclass, so this combined metaclass allows classes to inherit from both
|
|
339
|
+
Mindtrace class and ABC simultaneously.
|
|
340
|
+
|
|
341
|
+
Without this combined metaclass, trying to create a class that inherits from both Mindtrace class
|
|
342
|
+
and ABC would raise a metaclass conflict error since they each have different metaclasses.
|
|
343
|
+
"""
|
|
344
|
+
|
|
345
|
+
pass
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
class MindtraceABC(Mindtrace, ABC, metaclass=MindtraceABCMeta):
|
|
349
|
+
"""Abstract base class combining Mindtrace class functionality with ABC support.
|
|
350
|
+
|
|
351
|
+
This class enables creating abstract classes that also have access to all Mindtrace features
|
|
352
|
+
such as logging, configuration, and context management. Use this class instead of
|
|
353
|
+
Mindtrace when you need to define abstract methods or properties in your class.
|
|
354
|
+
|
|
355
|
+
Example:
|
|
356
|
+
from mindtrace.core import MindtraceABC
|
|
357
|
+
from abc import abstractmethod
|
|
358
|
+
|
|
359
|
+
class MyAbstractService(MindtraceABC):
|
|
360
|
+
def __init__(self):
|
|
361
|
+
super().__init__()
|
|
362
|
+
|
|
363
|
+
@abstractmethod
|
|
364
|
+
def process_data(self, data):
|
|
365
|
+
'''Must be implemented by concrete subclasses.'''
|
|
366
|
+
pass
|
|
367
|
+
|
|
368
|
+
Note:
|
|
369
|
+
Without this class, attempting to create a class that inherits from both Mindtrace class and ABC
|
|
370
|
+
would fail due to metaclass conflicts. MindtraceABC resolves this by using the CombinedABCMeta.
|
|
371
|
+
"""
|
|
372
|
+
|
|
373
|
+
def __init__(self, *args, **kwargs):
|
|
374
|
+
super().__init__(*args, **kwargs)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Config(dict):
|
|
5
|
+
"""Template Config class."""
|
|
6
|
+
|
|
7
|
+
def __init__(self, **kwargs):
|
|
8
|
+
default_config = {
|
|
9
|
+
"MINDTRACE_TEMP_DIR": "~/.cache/mindtrace/temp",
|
|
10
|
+
"MINDTRACE_DEFAULT_REGISTRY_DIR": "~/.cache/mindtrace/registry",
|
|
11
|
+
"MINDTRACE_DEFAULT_HOST_URLS": {
|
|
12
|
+
"Service": "http://localhost:8000",
|
|
13
|
+
},
|
|
14
|
+
"MINDTRACE_MINIO_REGISTRY_URI": "~/.cache/mindtrace/minio-registry",
|
|
15
|
+
"MINDTRACE_MINIO_ENDPOINT": "localhost:9000",
|
|
16
|
+
"MINDTRACE_MINIO_ACCESS_KEY": "minioadmin",
|
|
17
|
+
"MINDTRACE_MINIO_SECRET_KEY": "minioadmin",
|
|
18
|
+
"MINDTRACE_SERVER_PIDS_DIR_PATH": "~/.cache/mindtrace/pids",
|
|
19
|
+
"MINDTRACE_LOGGER_DIR": "~/.cache/mindtrace/logs",
|
|
20
|
+
}
|
|
21
|
+
# Update defaults with any provided kwargs
|
|
22
|
+
default_config.update(kwargs)
|
|
23
|
+
# Expand ~ only if it is at the start of the string
|
|
24
|
+
for k, v in default_config.items():
|
|
25
|
+
if isinstance(v, str) and v.startswith("~/"):
|
|
26
|
+
default_config[k] = os.path.expanduser(v)
|
|
27
|
+
super().__init__(default_config)
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
from logging import Logger
|
|
4
|
+
from logging.handlers import RotatingFileHandler
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
from mindtrace.core.config import Config
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def default_formatter(fmt: Optional[str] = None) -> logging.Formatter:
|
|
12
|
+
"""
|
|
13
|
+
Returns a logging formatter with a default format if none is specified.
|
|
14
|
+
"""
|
|
15
|
+
default_fmt = "[%(asctime)s] %(levelname)s: %(name)s: %(message)s"
|
|
16
|
+
return logging.Formatter(fmt or default_fmt)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def setup_logger(
|
|
20
|
+
name: str = "mindtrace",
|
|
21
|
+
log_dir: Optional[Path] = None,
|
|
22
|
+
logger_level: int = logging.DEBUG,
|
|
23
|
+
stream_level: int = logging.ERROR,
|
|
24
|
+
file_level: int = logging.DEBUG,
|
|
25
|
+
file_mode: str = "a",
|
|
26
|
+
propagate: bool = False,
|
|
27
|
+
max_bytes: int = 10 * 1024 * 1024, # 10 MB
|
|
28
|
+
backup_count: int = 5,
|
|
29
|
+
) -> Logger:
|
|
30
|
+
"""Configure and initialize logging for Mindtrace components programmatically.
|
|
31
|
+
|
|
32
|
+
Sets up a rotating file handler and a console handler on the given logger.
|
|
33
|
+
Log file defaults to ~/.cache/mindtrace/{name}.log.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
name (str): Logger name, defaults to "mindtrace".
|
|
37
|
+
log_dir (Optional[Path]): Custom directory for log file.
|
|
38
|
+
logger_level (int): Overall logger level.
|
|
39
|
+
stream_level (int): StreamHandler level (e.g., ERROR).
|
|
40
|
+
file_level (int): FileHandler level (e.g., DEBUG).
|
|
41
|
+
file_mode (str): Mode for file handler, default is 'a' (append).
|
|
42
|
+
propagate (bool): Whether the logger should propagate messages to ancestor loggers.
|
|
43
|
+
max_bytes (int): Maximum size in bytes before rotating log file.
|
|
44
|
+
backup_count (int): Number of backup files to retain.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
Logger: Configured logger instance.
|
|
48
|
+
"""
|
|
49
|
+
logger = logging.getLogger(name)
|
|
50
|
+
logger.handlers.clear()
|
|
51
|
+
logger.setLevel(logger_level)
|
|
52
|
+
logger.propagate = propagate
|
|
53
|
+
|
|
54
|
+
# Set up stream handler
|
|
55
|
+
stream_handler = logging.StreamHandler()
|
|
56
|
+
stream_handler.setLevel(stream_level)
|
|
57
|
+
stream_handler.setFormatter(default_formatter())
|
|
58
|
+
logger.addHandler(stream_handler)
|
|
59
|
+
|
|
60
|
+
# Set up file handler
|
|
61
|
+
default_config = Config()
|
|
62
|
+
if name == "mindtrace":
|
|
63
|
+
child_log_path = f"{name}.log"
|
|
64
|
+
else:
|
|
65
|
+
child_log_path = os.path.join("modules", f"{name}.log")
|
|
66
|
+
|
|
67
|
+
if log_dir:
|
|
68
|
+
log_file_path = os.path.join(log_dir, child_log_path)
|
|
69
|
+
else:
|
|
70
|
+
log_file_path = os.path.join(default_config["MINDTRACE_LOGGER_DIR"], child_log_path)
|
|
71
|
+
|
|
72
|
+
os.makedirs(Path(log_file_path).parent, exist_ok=True)
|
|
73
|
+
file_handler = RotatingFileHandler(
|
|
74
|
+
filename=str(log_file_path), maxBytes=max_bytes, backupCount=backup_count, mode=file_mode
|
|
75
|
+
)
|
|
76
|
+
file_handler.setLevel(file_level)
|
|
77
|
+
file_handler.setFormatter(default_formatter())
|
|
78
|
+
logger.addHandler(file_handler)
|
|
79
|
+
|
|
80
|
+
return logger
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def get_logger(name: str = "mindtrace", **kwargs) -> logging.Logger:
|
|
84
|
+
"""
|
|
85
|
+
Create or retrieve a named logger instance.
|
|
86
|
+
|
|
87
|
+
This function wraps Python's built-in ``logging.getLogger()`` to provide a
|
|
88
|
+
standardized logger for Mindtrace components. If the logger with the given
|
|
89
|
+
name already exists, it returns the existing instance; otherwise, it creates
|
|
90
|
+
a new one with optional configuration overrides.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
name (str): The name of the logger. Defaults to "mindtrace".
|
|
94
|
+
**kwargs: Additional keyword arguments to be passed to `setup_logger`.
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
logging.Logger: A configured logger instance.
|
|
98
|
+
|
|
99
|
+
Example:
|
|
100
|
+
.. code-block:: python
|
|
101
|
+
|
|
102
|
+
from mindtrace.core.logging.logger import get_logger
|
|
103
|
+
|
|
104
|
+
logger = get_logger("core.module", stream_level=logging.INFO, propagate=True)
|
|
105
|
+
logger.info("Logger configured with custom settings.")
|
|
106
|
+
"""
|
|
107
|
+
if not name:
|
|
108
|
+
name = "mindtrace"
|
|
109
|
+
|
|
110
|
+
full_name = name if name.startswith("mindtrace") else f"mindtrace.{name}"
|
|
111
|
+
kwargs.setdefault("propagate", True)
|
|
112
|
+
return setup_logger(full_name, **kwargs)
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from mindtrace.core import Mindtrace
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ContextListener(Mindtrace):
|
|
8
|
+
"""A context listener that can subscribe to a ObservableContext.
|
|
9
|
+
|
|
10
|
+
The ContextListener class provides two main benefits:
|
|
11
|
+
|
|
12
|
+
1. Deriving from Mindtrace, it provides for uniform logging of events.
|
|
13
|
+
|
|
14
|
+
Example::
|
|
15
|
+
|
|
16
|
+
from mindtrace.core import ContextListener, Logger
|
|
17
|
+
|
|
18
|
+
class MyListener(ContextListener):
|
|
19
|
+
def x_changed(self, source, old, new):
|
|
20
|
+
self.logger.info(f"x changed: {old} → {new}") # Uses Mindtrace logging by default
|
|
21
|
+
|
|
22
|
+
my_listener = MyListener(logger=Logger("MyListener")) # May pass in a custom logger
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
2. The default ContextListener can be used to automatically log changes to variables.
|
|
26
|
+
|
|
27
|
+
Example::
|
|
28
|
+
|
|
29
|
+
from mindtrace.core import ContextListener, ObservableContext
|
|
30
|
+
|
|
31
|
+
@ObservableContext(vars={"x": int, "y": int})
|
|
32
|
+
class MyContext:
|
|
33
|
+
def __init__(self):
|
|
34
|
+
self.x = 0
|
|
35
|
+
self.y = 0
|
|
36
|
+
|
|
37
|
+
my_context = MyContext()
|
|
38
|
+
my_context.subscribe(ContextListener(autolog=["x", "y"]))
|
|
39
|
+
|
|
40
|
+
my_context.x = 1
|
|
41
|
+
my_context.y = 2
|
|
42
|
+
|
|
43
|
+
# Logs:
|
|
44
|
+
# [MyContext] x changed: 0 → 1
|
|
45
|
+
# [MyContext] y changed: 0 → 2
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def __init__(self, autolog: list[str] = None, log_level: int = logging.ERROR, logger: Any = None, **kwargs):
|
|
49
|
+
"""Initialize the context listener.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
autolog: A list of variables to log automatically.
|
|
53
|
+
log_level: The log level to use for the logger.
|
|
54
|
+
"""
|
|
55
|
+
super().__init__(**kwargs)
|
|
56
|
+
|
|
57
|
+
if logger is not None:
|
|
58
|
+
self.logger = logger
|
|
59
|
+
|
|
60
|
+
if autolog is not None:
|
|
61
|
+
for var in autolog:
|
|
62
|
+
method_name = f"{var}_changed"
|
|
63
|
+
|
|
64
|
+
# Only attach if the child class hasn't already defined it
|
|
65
|
+
if not hasattr(self, method_name):
|
|
66
|
+
# Use a factory to capture var correctly in loop
|
|
67
|
+
setattr(self, method_name, self._make_auto_logger(var, log_level))
|
|
68
|
+
|
|
69
|
+
def _make_auto_logger(self, varname: str, log_level: int):
|
|
70
|
+
def _logger(source: str, old: Any, new: Any):
|
|
71
|
+
self.logger.log(log_level, f"[{source}] {varname} changed: {old} → {new}")
|
|
72
|
+
|
|
73
|
+
return _logger
|