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.
Files changed (55) hide show
  1. mindtrace/apps/mindtrace/apps/__init__.py +0 -0
  2. mindtrace/automation/mindtrace/automation/__init__.py +0 -0
  3. mindtrace/cluster/mindtrace/cluster/__init__.py +0 -0
  4. mindtrace/core/mindtrace/core/__init__.py +34 -0
  5. mindtrace/core/mindtrace/core/base/__init__.py +7 -0
  6. mindtrace/core/mindtrace/core/base/mindtrace_base.py +374 -0
  7. mindtrace/core/mindtrace/core/config/__init__.py +3 -0
  8. mindtrace/core/mindtrace/core/config/config.py +27 -0
  9. mindtrace/core/mindtrace/core/logging/__init__.py +3 -0
  10. mindtrace/core/mindtrace/core/logging/logger.py +112 -0
  11. mindtrace/core/mindtrace/core/observables/context_listener.py +73 -0
  12. mindtrace/core/mindtrace/core/observables/event_bus.py +72 -0
  13. mindtrace/core/mindtrace/core/observables/observable_context.py +140 -0
  14. mindtrace/core/mindtrace/core/types/task_schema.py +11 -0
  15. mindtrace/core/mindtrace/core/utils/__init__.py +12 -0
  16. mindtrace/core/mindtrace/core/utils/checks.py +39 -0
  17. mindtrace/core/mindtrace/core/utils/dynamic.py +57 -0
  18. mindtrace/core/mindtrace/core/utils/lambdas.py +35 -0
  19. mindtrace/core/mindtrace/core/utils/timers.py +291 -0
  20. mindtrace/database/mindtrace/database/__init__.py +13 -0
  21. mindtrace/database/mindtrace/database/backends/local_odm_backend.py +23 -0
  22. mindtrace/database/mindtrace/database/backends/mindtrace_odm_backend.py +27 -0
  23. mindtrace/database/mindtrace/database/backends/mongo_odm_backend.py +20 -0
  24. mindtrace/database/mindtrace/database/backends/redis_odm_backend.py +20 -0
  25. mindtrace/database/mindtrace/database/core/mindtrace_odm.py +24 -0
  26. mindtrace/datalake/mindtrace/datalake/__init__.py +0 -0
  27. mindtrace/hardware/mindtrace/hardware/__init__.py +0 -0
  28. mindtrace/jobs/mindtrace/jobs/__init__.py +0 -0
  29. mindtrace/models/mindtrace/models/__init__.py +0 -0
  30. mindtrace/registry/mindtrace/registry/__init__.py +18 -0
  31. mindtrace/registry/mindtrace/registry/archivers/config_archiver.py +26 -0
  32. mindtrace/registry/mindtrace/registry/backends/__init__.py +0 -0
  33. mindtrace/registry/mindtrace/registry/backends/gcp_registry_backend.py +24 -0
  34. mindtrace/registry/mindtrace/registry/backends/local_registry_backend.py +548 -0
  35. mindtrace/registry/mindtrace/registry/backends/minio_registry_backend.py +647 -0
  36. mindtrace/registry/mindtrace/registry/backends/registry_backend.py +233 -0
  37. mindtrace/registry/mindtrace/registry/core/__init__.py +0 -0
  38. mindtrace/registry/mindtrace/registry/core/archiver.py +33 -0
  39. mindtrace/registry/mindtrace/registry/core/registry.py +1032 -0
  40. mindtrace/services/mindtrace/services/__init__.py +27 -0
  41. mindtrace/services/mindtrace/services/core/__init__.py +0 -0
  42. mindtrace/services/mindtrace/services/core/connection_manager.py +140 -0
  43. mindtrace/services/mindtrace/services/core/launcher.py +72 -0
  44. mindtrace/services/mindtrace/services/core/service.py +456 -0
  45. mindtrace/services/mindtrace/services/core/types.py +104 -0
  46. mindtrace/services/mindtrace/services/core/utils.py +187 -0
  47. mindtrace/services/mindtrace/services/sample/echo_service.py +35 -0
  48. mindtrace/storage/mindtrace/storage/__init__.py +3 -0
  49. mindtrace/storage/mindtrace/storage/base.py +286 -0
  50. mindtrace/storage/mindtrace/storage/gcs.py +196 -0
  51. mindtrace/ui/mindtrace/ui/__init__.py +0 -0
  52. mindtrace-0.1.0.dist-info/METADATA +121 -0
  53. mindtrace-0.1.0.dist-info/RECORD +55 -0
  54. mindtrace-0.1.0.dist-info/WHEEL +5 -0
  55. 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,7 @@
1
+ from mindtrace.core.base.mindtrace_base import Mindtrace, MindtraceABC, MindtraceMeta
2
+
3
+ __all__ = [
4
+ "Mindtrace",
5
+ "MindtraceABC",
6
+ "MindtraceMeta",
7
+ ]
@@ -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,3 @@
1
+ from mindtrace.core.config.config import Config
2
+
3
+ __all__ = ["Config"]
@@ -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,3 @@
1
+ from mindtrace.core.logging.logger import Logger
2
+
3
+ __all__ = ["Logger"]
@@ -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