gl-plugin 0.0.1b2__tar.gz

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.
@@ -0,0 +1,16 @@
1
+ Metadata-Version: 2.4
2
+ Name: gl-plugin
3
+ Version: 0.0.1b2
4
+ Summary: GL SDK's Plugin Architecture Implementation
5
+ Requires-Python: <3.14,>=3.11
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: python-dotenv>=1.2.1
8
+ Provides-Extra: dev
9
+ Requires-Dist: pre-commit<4.0.0,>=3.6.2; extra == "dev"
10
+ Requires-Dist: pytest<9.0.0,>=8.3.4; extra == "dev"
11
+ Requires-Dist: pytest-mock<4.0.0,>=3.14.0; extra == "dev"
12
+ Requires-Dist: pytest-asyncio<1.0.0,>=0.25.3; extra == "dev"
13
+ Requires-Dist: coverage<8.0.0,>=7.6.10; extra == "dev"
14
+ Requires-Dist: mypy<2.0.0,>=1.11.2; extra == "dev"
15
+ Requires-Dist: ruff<1.0.0,>=0.9.9; extra == "dev"
16
+ Requires-Dist: pytest-cov<6.0.0,>=5.0.0; extra == "dev"
File without changes
@@ -0,0 +1 @@
1
+ """GL SDK's Plugin Architecture Implementation."""
@@ -0,0 +1 @@
1
+ """Plugin Modules."""
@@ -0,0 +1,55 @@
1
+ """PluginHandler interface for plugin system.
2
+
3
+ Authors:
4
+ Samuel Lusandi (samuel.lusandi@gdplabs.id)
5
+
6
+ References:
7
+ None
8
+ """
9
+
10
+ from abc import ABC, abstractmethod
11
+ from typing import Any, Dict, Type
12
+
13
+
14
+ class PluginHandler(ABC):
15
+ """Base interface for classes that can provide injections to the plugin system."""
16
+
17
+ @classmethod
18
+ @abstractmethod
19
+ def create_injections(cls, instance: Any) -> Dict[Type, Any]:
20
+ """Create injection mappings for this interface.
21
+
22
+ Args:
23
+ instance: The instance that will provide the injections
24
+
25
+ Returns:
26
+ Dictionary mapping service types to their instances
27
+ """
28
+
29
+ @classmethod
30
+ @abstractmethod
31
+ def initialize_plugin(cls, instance: Any, plugin: Any) -> None:
32
+ """Initialize plugin-specific resources for this interface.
33
+
34
+ This method is called after the plugin is created and services are injected.
35
+ Override this method to perform any plugin-specific initialization.
36
+
37
+ Args:
38
+ instance: The instance that provides the initialization
39
+ plugin: The plugin instance to initialize
40
+ """
41
+
42
+ @classmethod
43
+ async def ainitialize_plugin(cls, instance: Any, plugin: Any) -> None: # pragma: no cover
44
+ """Initialize plugin-specific resources for this interface.
45
+
46
+ This method is called after the plugin is created and services are injected.
47
+ Override this method to perform any plugin-specific initialization. This method is
48
+ provided for compatibility with async plugins. By default it just calls the sync
49
+ version of the method.
50
+
51
+ Args:
52
+ instance: The instance that provides the initialization
53
+ plugin: The plugin instance to initialize
54
+ """
55
+ cls.initialize_plugin(instance, plugin)
@@ -0,0 +1,395 @@
1
+ """Plugin manager.
2
+
3
+ Authors:
4
+ Samuel Lusandi (samuel.lusandi@gdplabs.id)
5
+
6
+ References:
7
+ None
8
+ """
9
+
10
+ import asyncio
11
+ import logging
12
+ from pathlib import Path
13
+ from threading import Lock
14
+ from typing import Any, Callable, Dict, List, Optional, Tuple, Type, TypeVar, Union, get_origin
15
+
16
+ from dotenv import load_dotenv
17
+
18
+ from gl_plugin.plugin.handler import PluginHandler
19
+ from gl_plugin.plugin.plugin import Plugin
20
+ from gl_plugin.plugin.registry import ServiceRegistry
21
+ from gl_plugin.services.config import ConfigService
22
+
23
+ T = TypeVar("T", bound=PluginHandler)
24
+
25
+
26
+ class HandlerNotFoundError(RuntimeError):
27
+ """Raised when a requested plugin handler is not found."""
28
+
29
+
30
+ class PluginManager:
31
+ """Manages plugin lifecycle and service injection.
32
+
33
+ This manager can handle different types of plugins based on the services provided.
34
+ Services are automatically registered based on the plugin handler interfaces provided.
35
+
36
+ This class follows the singleton pattern - only one instance will ever exist.
37
+ Thread-safe implementation using double-checked locking pattern.
38
+ """
39
+
40
+ _instance = None
41
+ _multi_instance = {}
42
+ _lock = Lock()
43
+ _initialized = False
44
+ _multi_initialized = {} # Track initialization per key
45
+
46
+ def __new__(
47
+ cls,
48
+ *,
49
+ handlers: Optional[List[PluginHandler]] = None,
50
+ env_file: Optional[str] = None,
51
+ global_services: List[Any] = (),
52
+ key_instance: Optional[str] = None,
53
+ ) -> "PluginManager":
54
+ """Create or return the singleton instance or multi-instance.
55
+
56
+ Args:
57
+ handlers: List of plugin handlers that provide injections
58
+ env_file: Optional environment file for loading environment variables
59
+ global_services: List of services to register globally
60
+ key_instance: Optional key for multi-instance pattern. If None, uses singleton.
61
+
62
+ Returns:
63
+ The singleton PluginManager instance or specified multi-instance
64
+
65
+ Raises:
66
+ ValueError: If handlers is not provided during first initialization
67
+ """
68
+ with cls._lock:
69
+ if key_instance is not None:
70
+ # Multi-instance pattern
71
+ if key_instance not in cls._multi_instance:
72
+ if handlers is None:
73
+ raise ValueError(
74
+ f"handlers must be provided when creating instance '{key_instance}' for the first time"
75
+ )
76
+ instance = super().__new__(cls)
77
+ instance._key_instance = key_instance # Store the key for this instance
78
+ instance._initialized = False
79
+ cls._multi_instance[key_instance] = instance
80
+ cls._multi_initialized[key_instance] = False
81
+ return cls._multi_instance[key_instance]
82
+ else:
83
+ # Singleton pattern
84
+ if cls._instance is None:
85
+ if handlers is None:
86
+ raise ValueError("handlers must be provided when creating the first instance")
87
+ instance = super().__new__(cls)
88
+ instance._key_instance = None # Mark as singleton
89
+ instance._initialized = False
90
+ cls._instance = instance
91
+ return cls._instance
92
+
93
+ def __init__(
94
+ self,
95
+ *,
96
+ handlers: Optional[List[PluginHandler]] = None,
97
+ env_file: Optional[str] = None,
98
+ global_services: List[Any] = (),
99
+ key_instance: Optional[str] = None,
100
+ ):
101
+ """Initialize plugin manager.
102
+
103
+ This will only run once per instance (singleton or multi-instance).
104
+
105
+ Args:
106
+ handlers: List of plugin handlers that provide injections
107
+ env_file: Optional environment file for loading environment variables
108
+ global_services: List of custom services to be injected into the global registry
109
+ key_instance: Optional key for multi-instance pattern
110
+ """
111
+ with self._lock:
112
+ # Check if this specific instance is already initialized
113
+ if key_instance is not None:
114
+ if self._multi_initialized.get(key_instance, False):
115
+ return
116
+ self._multi_initialized[key_instance] = True
117
+ else:
118
+ if self._initialized:
119
+ return
120
+ self._initialized = True
121
+
122
+ if not handlers:
123
+ raise ValueError("At least one plugin handler must be provided")
124
+
125
+ self._load_environment(env_file)
126
+
127
+ self._handler_registries: Dict[Type[PluginHandler], ServiceRegistry] = {}
128
+ self._plugins: Dict[str, Plugin] = {}
129
+ self._handlers: Dict[Type[PluginHandler], PluginHandler] = {}
130
+
131
+ for handler in handlers:
132
+ handler_type = type(handler)
133
+ registry = ServiceRegistry()
134
+ registry.register(ConfigService, ConfigService())
135
+
136
+ for service in global_services:
137
+ registry.register(type(service), service)
138
+
139
+ for service_type, service_instance in handler_type.create_injections(handler).items():
140
+ registry.register(service_type, service_instance)
141
+
142
+ self._handler_registries[handler_type] = registry
143
+ self._handlers[handler_type] = handler
144
+
145
+ def _load_environment(self, env_file: Optional[str] = None) -> None:
146
+ """Load environment variables from .env file.
147
+
148
+ If env_file is provided, loads from that file.
149
+ Otherwise, searches for .env in current directory and up to 3 levels up.
150
+
151
+ Args:
152
+ env_file: Optional path to environment file
153
+ """
154
+ if env_file:
155
+ load_dotenv(env_file)
156
+ return
157
+
158
+ current_dir = Path.cwd()
159
+ env_file_path = None
160
+
161
+ for _ in range(4):
162
+ if (current_dir / ".env").exists():
163
+ env_file_path = current_dir / ".env"
164
+ break
165
+ current_dir = current_dir.parent
166
+
167
+ if env_file_path:
168
+ load_dotenv(env_file_path)
169
+
170
+ def _prepare_plugin_registration( # noqa: PLR0912
171
+ self, plugin_class: Type[Plugin], additional_params: Optional[Dict[str, Any]] = None
172
+ ) -> Tuple[Any, ServiceRegistry, Type, Plugin]:
173
+ """Prepare plugin registration by validating and setting up plugin instance.
174
+
175
+ Args:
176
+ plugin_class: Plugin class to register
177
+ additional_params: Optional dictionary of keyword arguments to pass to the plugin constructor
178
+
179
+ Returns:
180
+ Tuple containing:
181
+ - compatible_handler: The handler instance compatible with this plugin
182
+ - compatible_registry: The service registry to use for this plugin
183
+ - handler_type: The type of handler required by the plugin
184
+ - plugin: The instantiated plugin instance
185
+
186
+ Raises:
187
+ ValueError: If plugin doesn't specify a handler type
188
+ RuntimeError: If no compatible handler is found
189
+ """
190
+ if not plugin_class.get_handler_type():
191
+ raise ValueError(
192
+ f"Plugin {plugin_class.__name__} must specify a handler type using @Plugin.for_handler decorator"
193
+ )
194
+
195
+ # Find compatible handler (exact match or subclass)
196
+ handler_type = plugin_class.get_handler_type()
197
+ compatible_handler = None
198
+ compatible_registry = None
199
+
200
+ for registered_type, handler in self._handlers.items():
201
+ if issubclass(type(handler), handler_type):
202
+ compatible_handler = handler
203
+ compatible_registry = self._handler_registries[registered_type]
204
+ break
205
+
206
+ if not compatible_handler:
207
+ raise RuntimeError(
208
+ f"Plugin {plugin_class.__name__} requires handler {handler_type.__name__} or a subclass, "
209
+ "but none was provided"
210
+ )
211
+
212
+ logging.info(f"Initializing plugin {plugin_class.__name__}")
213
+
214
+ # Validate plugin requirements before instantiation
215
+ hints = plugin_class.__annotations__ if hasattr(plugin_class, "__annotations__") else {}
216
+
217
+ # Check for Union types and required services against handler-specific registry
218
+ unsupported_unions = []
219
+ missing_services = []
220
+
221
+ for field_name, service in hints.items():
222
+ if isinstance(service, type) and service.__module__ == "builtins":
223
+ continue
224
+
225
+ # Check for both Union[] syntax and | syntax (PEP 604)
226
+ is_union = (hasattr(service, "__origin__") and service.__origin__ is Union) or (
227
+ hasattr(service, "__or__")
228
+ and not isinstance(service, type)
229
+ and any(arg is type(None) for arg in service.__args__)
230
+ )
231
+
232
+ if is_union:
233
+ unsupported_unions.append(field_name)
234
+ else:
235
+ target_type = get_origin(service) or service
236
+
237
+ service_found = False
238
+ for registered_type in compatible_registry._services:
239
+ registered_base = get_origin(registered_type) or registered_type
240
+ try:
241
+ if isinstance(registered_base, type) and issubclass(registered_base, target_type):
242
+ service_found = True
243
+ break
244
+ except TypeError:
245
+ continue
246
+
247
+ if not service_found:
248
+ missing_services.append(getattr(service, "__name__", str(service)))
249
+
250
+ if unsupported_unions:
251
+ logging.warning(
252
+ f"Plugin {plugin_class.__name__} using handler {handler_type.__name__} has "
253
+ f"Union types which are not supported for injection: {', '.join(unsupported_unions)}",
254
+ )
255
+
256
+ if missing_services:
257
+ logging.warning(
258
+ f"Plugin {plugin_class.__name__} using handler {handler_type.__name__} "
259
+ f"could not inject the following services: {', '.join(missing_services)}",
260
+ )
261
+
262
+ # Set handler-specific registry and initialize plugin
263
+ plugin_class.set_registry(compatible_registry)
264
+ plugin = plugin_class(**additional_params) if additional_params else plugin_class()
265
+ self._plugins[plugin.name] = plugin
266
+
267
+ return compatible_handler, compatible_registry, handler_type, plugin
268
+
269
+ def register_plugin( # noqa: PLR0912
270
+ self,
271
+ plugin_class: Type[Plugin],
272
+ custom_initializer: Optional[Callable[[Plugin], None]] = None,
273
+ additional_params: Optional[Dict[str, Any]] = None,
274
+ ) -> None:
275
+ """Register and initialize a plugin.
276
+
277
+ Args:
278
+ plugin_class: Plugin class to register
279
+ custom_initializer: Optional callable that will be called with the plugin instance after initialization
280
+ additional_params: Optional dictionary of keyword arguments to pass to the plugin constructor
281
+
282
+ Raises:
283
+ ValueError: If plugin doesn't specify a handler type
284
+ """
285
+ compatible_handler, _, _, plugin = self._prepare_plugin_registration(plugin_class, additional_params)
286
+
287
+ # Initialize plugin with its specific handler
288
+ type(compatible_handler).initialize_plugin(compatible_handler, plugin)
289
+
290
+ # Call custom initializer if provided
291
+ if custom_initializer is not None:
292
+ custom_initializer(plugin)
293
+
294
+ async def aregister_plugin(
295
+ self,
296
+ plugin_class: Type[Plugin],
297
+ custom_initializer: Optional[Callable[[Plugin], Any]] = None,
298
+ additional_params: Optional[Dict[str, Any]] = None,
299
+ ) -> None:
300
+ """Register and initialize a plugin asynchronously.
301
+
302
+ This is backwards compatible with the synchronous version;
303
+ passing a synchronous plugin will be handled appropriately.
304
+
305
+ Args:
306
+ plugin_class: Plugin class to register
307
+ custom_initializer: Optional callable that will be called with the plugin instance after initialization.
308
+ Can be either a synchronous or asynchronous function.
309
+ additional_params: Optional dictionary of keyword arguments to pass to the plugin constructor
310
+
311
+ Raises:
312
+ ValueError: If plugin doesn't specify a handler type
313
+ """
314
+ compatible_handler, _, _, plugin = self._prepare_plugin_registration(plugin_class, additional_params)
315
+
316
+ if asyncio.iscoroutinefunction(type(compatible_handler).ainitialize_plugin):
317
+ await type(compatible_handler).ainitialize_plugin(compatible_handler, plugin)
318
+ else:
319
+ type(compatible_handler).initialize_plugin(compatible_handler, plugin)
320
+
321
+ # Call custom initializer if provided
322
+ if custom_initializer is not None:
323
+ if asyncio.iscoroutinefunction(custom_initializer):
324
+ await custom_initializer(plugin)
325
+ else:
326
+ custom_initializer(plugin)
327
+
328
+ def get_plugin(self, name: str) -> Optional[Plugin]:
329
+ """Get a plugin by name.
330
+
331
+ Args:
332
+ name: Name of plugin to get
333
+
334
+ Returns:
335
+ Plugin instance if found, None otherwise
336
+ """
337
+ return self._plugins.get(name)
338
+
339
+ def get_plugins(
340
+ self, handler_type: Optional[Type[PluginHandler]] = None, plugin_names: Optional[List[str]] = None
341
+ ) -> List[Plugin]:
342
+ """Get all registered plugins, optionally filtered by handler type and names.
343
+
344
+ Args:
345
+ handler_type: Optional handler type to filter plugins by. If provided,
346
+ only returns plugins that have this handler type registered.
347
+ plugin_names: Optional list of plugin names to filter by. If provided,
348
+ only returns plugins whose names are in this list.
349
+
350
+ Returns:
351
+ List of plugin instances
352
+ """
353
+ plugins = list(self._plugins.values())
354
+
355
+ if handler_type is not None:
356
+ plugins = [
357
+ plugin for plugin in plugins if plugin.handler_type and issubclass(plugin.handler_type, handler_type)
358
+ ]
359
+
360
+ if plugin_names is not None:
361
+ plugins = [plugin for plugin in plugins if plugin.name in plugin_names]
362
+
363
+ return plugins
364
+
365
+ def get_handlers(self, handler_type: Optional[Type[PluginHandler]] = None) -> List[PluginHandler]:
366
+ """Get all registered handlers, optionally filtered by type.
367
+
368
+ Args:
369
+ handler_type: Optional handler type to filter by. If provided,
370
+ only returns handlers that are instances of this type.
371
+
372
+ Returns:
373
+ List of handler instances
374
+ """
375
+ if handler_type is None:
376
+ return list(self._handlers.values())
377
+
378
+ return [handler for handler in self._handlers.values() if isinstance(handler, handler_type)]
379
+
380
+ def get_handler(self, handler_type: Type[T]) -> T:
381
+ """Get a handler of the specified type.
382
+
383
+ Args:
384
+ handler_type: The type of handler to get
385
+
386
+ Returns:
387
+ The handler instance of the specified type
388
+
389
+ Raises:
390
+ HandlerNotFoundError: If no handler of the specified type is found
391
+ """
392
+ for handler in self._handlers.values():
393
+ if isinstance(handler, handler_type):
394
+ return handler # type: ignore
395
+ raise HandlerNotFoundError(f"No handler of type {handler_type.__name__} found")
@@ -0,0 +1,99 @@
1
+ """Plugin base class for the Plugin Architecture.
2
+
3
+ Authors:
4
+ Samuel Lusandi (samuel.lusandi@gdplabs.id)
5
+
6
+ References:
7
+ None
8
+ """
9
+
10
+ from typing import Callable, Optional, Type
11
+
12
+ from gl_plugin.plugin.handler import PluginHandler
13
+ from gl_plugin.plugin.registry import ServiceRegistry
14
+
15
+
16
+ class Plugin:
17
+ """Base class for BOSA Plugins."""
18
+
19
+ name: str = None
20
+ description: str = None
21
+ version: str = None
22
+ _handler_type: Optional[Type["PluginHandler"]] = None
23
+
24
+ _registry: Optional[ServiceRegistry] = None
25
+
26
+ def __init__(self, *args, **kwargs):
27
+ """Initialize plugin instance.
28
+
29
+ Raises:
30
+ ValueError: If required class attributes are not set
31
+ """
32
+ super().__init__(*args, **kwargs)
33
+
34
+ if not all([self.name, self.description, self.version]):
35
+ raise ValueError(
36
+ f"Plugin class {self.__class__.__name__} must set name, description, and version as class attributes"
37
+ )
38
+
39
+ @classmethod
40
+ def get_handler_type(cls) -> Optional[Type["PluginHandler"]]:
41
+ """Get the handler type for this plugin class.
42
+
43
+ This method walks up the inheritance chain to find the handler_type.
44
+
45
+ Returns:
46
+ The handler type for this plugin, or None if not set
47
+ """
48
+ for base in cls.__mro__:
49
+ if "_handler_type" in base.__dict__:
50
+ return base._handler_type
51
+ return None
52
+
53
+ @classmethod
54
+ def for_handler(cls, handler_type: Type["PluginHandler"]) -> Callable[[Type["Plugin"]], Type["Plugin"]]:
55
+ """Decorator to specify which handler this plugin is designed for.
56
+
57
+ Args:
58
+ handler_type: The type of handler this plugin works with
59
+
60
+ Returns:
61
+ A decorator function that sets the handler type on the plugin class
62
+ """
63
+
64
+ def decorator(plugin_cls: Type["Plugin"]) -> Type["Plugin"]:
65
+ plugin_cls._handler_type = handler_type
66
+ return plugin_cls
67
+
68
+ return decorator
69
+
70
+ @classmethod
71
+ def set_registry(cls, registry: ServiceRegistry) -> None:
72
+ """Set the service registry for this plugin class.
73
+
74
+ Args:
75
+ registry: Service registry to use for dependency injection
76
+ """
77
+ cls._registry = registry
78
+
79
+ @property
80
+ def handler_type(self) -> Optional[Type["PluginHandler"]]:
81
+ """Get the handler type for this plugin instance.
82
+
83
+ Returns:
84
+ The handler type for this plugin, or None if not set
85
+ """
86
+ return self.__class__.get_handler_type()
87
+
88
+ def __new__(cls, *args, **kwargs):
89
+ """Create a new plugin instance with injected services.
90
+
91
+ This is called before __init__ and allows us to inject services
92
+ before the instance is initialized.
93
+ """
94
+ instance = super().__new__(cls)
95
+
96
+ if cls._registry is not None:
97
+ cls._registry.inject_services(instance)
98
+
99
+ return instance
@@ -0,0 +1,92 @@
1
+ """Service registry for core services that can be injected into plugins.
2
+
3
+ Authors:
4
+ Samuel Lusandi (samuel.lusandi@gdplabs.id)
5
+
6
+ References:
7
+ None
8
+ """
9
+
10
+ from typing import Any, Dict, Type, TypeVar, get_origin, get_type_hints
11
+
12
+ T = TypeVar("T")
13
+
14
+
15
+ class ServiceRegistry:
16
+ """Registry for core services that can be injected into plugins."""
17
+
18
+ def __init__(self):
19
+ """Initialize an empty service registry."""
20
+ self._services: Dict[Type, Any] = {}
21
+
22
+ def register(self, service_type: Type[T], instance: T) -> None:
23
+ """Register a service instance for a given type.
24
+
25
+ Args:
26
+ service_type: The type of the service (usually its class)
27
+ instance: The service instance
28
+ """
29
+ self._services[service_type] = instance
30
+
31
+ def get(self, service_type: Type[T]) -> T:
32
+ """Get a service instance by its type.
33
+
34
+ Args:
35
+ service_type: The type of service to retrieve
36
+
37
+ Returns:
38
+ The service instance
39
+
40
+ Raises:
41
+ TypeError: If the service type is a built-in type
42
+ KeyError: If the service type is not registered
43
+ """
44
+ if isinstance(service_type, type) and service_type.__module__ == "builtins":
45
+ raise TypeError(
46
+ f"Built-in type {service_type.__name__} cannot be injected. Pass this value directly instead."
47
+ )
48
+
49
+ if service_type not in self._services:
50
+ raise KeyError(f"Service {getattr(service_type, '__name__', str(service_type))} not registered")
51
+ return self._services[service_type]
52
+
53
+ def inject_services(self, target: Any) -> None:
54
+ """Inject registered services into an object based on its type hints.
55
+
56
+ This will look for class-level type hints and inject matching services.
57
+ If a service is registered that is a subclass of the requested type,
58
+ it will be injected. Handles both regular and generic types.
59
+
60
+ Args:
61
+ target: The object to inject services into
62
+ """
63
+ hints = get_type_hints(target.__class__)
64
+
65
+ for attr_name, attr_type in hints.items():
66
+ # First try exact type match
67
+ service = self._services.get(attr_type)
68
+
69
+ # If no exact match, look for a subclass that implements the interface
70
+ if service is None:
71
+ target_type = get_origin(attr_type) or attr_type
72
+
73
+ for registered_type, registered_service in self._services.items():
74
+ registered_base = get_origin(registered_type) or registered_type
75
+ try:
76
+ if isinstance(registered_base, type) and issubclass(registered_base, target_type):
77
+ service = registered_service
78
+ break
79
+ except TypeError:
80
+ # Skip if type checking fails (e.g. with complex generic types)
81
+ continue
82
+
83
+ if service is not None:
84
+ # This is created in order to accommodate services that are to be injected
85
+ # as a new object. Usually, services are to be injected as a singleton or
86
+ # as a class method. However, some services are to be injected as a new
87
+ # object so their behavior is not shared. An example is the Router;
88
+ # each Plugin *must* have its own Router otherwise, a shared route can
89
+ # cause a conflict with another plugin.
90
+ if callable(service) and not isinstance(service, type):
91
+ service = service()
92
+ setattr(target, attr_name, service)
@@ -0,0 +1,5 @@
1
+ """Services module for BOSA Core."""
2
+
3
+ from gl_plugin.services.config import ConfigService
4
+
5
+ __all__ = ["ConfigService"]
@@ -0,0 +1,97 @@
1
+ """Configuration service for accessing environment variables."""
2
+
3
+ import os
4
+ from typing import Optional, Type, TypeVar, cast
5
+
6
+ U = TypeVar("U")
7
+
8
+
9
+ class ConfigService:
10
+ """Service for accessing environment variables with type conversion."""
11
+
12
+ @staticmethod
13
+ def get_string(key: str, default: Optional[str] = None) -> Optional[str]:
14
+ """Get environment variable as string."""
15
+ return os.environ.get(key, default)
16
+
17
+ @staticmethod
18
+ def get_int(key: str, default: Optional[int] = None) -> Optional[int]:
19
+ """Get environment variable as integer."""
20
+ value = os.environ.get(key)
21
+ if value is None:
22
+ return default
23
+ try:
24
+ return int(value)
25
+ except ValueError:
26
+ return default
27
+
28
+ @staticmethod
29
+ def get_float(key: str, default: Optional[float] = None) -> Optional[float]:
30
+ """Get environment variable as float."""
31
+ value = os.environ.get(key)
32
+ if value is None:
33
+ return default
34
+ try:
35
+ return float(value)
36
+ except ValueError:
37
+ return default
38
+
39
+ @staticmethod
40
+ def get_bool(key: str, default: Optional[bool] = None) -> Optional[bool]:
41
+ """Get environment variable as boolean.
42
+
43
+ 'true', 'yes', '1', 'on' are considered True
44
+ 'false', 'no', '0', 'off' are considered False
45
+ """
46
+ value = os.environ.get(key)
47
+ if value is None:
48
+ return default
49
+
50
+ value = value.lower()
51
+ if value in ("true", "yes", "1", "on"):
52
+ return True
53
+ if value in ("false", "no", "0", "off"):
54
+ return False
55
+ return default
56
+
57
+ @staticmethod
58
+ def get_list(key: str, separator: str = ",", default: Optional[list[str]] = None) -> Optional[list[str]]:
59
+ """Get environment variable as list of strings."""
60
+ value = os.environ.get(key)
61
+ if value is None:
62
+ return default
63
+ return [item.strip() for item in value.split(separator)]
64
+
65
+ @staticmethod
66
+ def require(key: str) -> str:
67
+ """Get required environment variable.
68
+
69
+ Raises:
70
+ ValueError: If environment variable is not set
71
+ """
72
+ value = os.environ.get(key)
73
+ if value is None:
74
+ raise ValueError(f"Required environment variable {key} is not set")
75
+ return value
76
+
77
+ @staticmethod
78
+ def require_as(key: str, type_: Type[U]) -> U:
79
+ """Get required environment variable with type conversion.
80
+
81
+ Args:
82
+ key: Environment variable key
83
+ type_: Type to convert to (int, float, bool)
84
+
85
+ Raises:
86
+ ValueError: If environment variable is not set or cannot be converted
87
+ """
88
+ value = ConfigService.require(key)
89
+ try:
90
+ if type_ == bool: # noqa: E721 # pylint: disable=unidiomatic-typecheck
91
+ result = ConfigService.get_bool(key)
92
+ if result is None:
93
+ raise ValueError
94
+ return cast(U, result)
95
+ return type_(value) # type: ignore
96
+ except ValueError as exc:
97
+ raise ValueError(f"Environment variable {key} cannot be converted to {type_.__name__}") from exc
@@ -0,0 +1,16 @@
1
+ Metadata-Version: 2.4
2
+ Name: gl-plugin
3
+ Version: 0.0.1b2
4
+ Summary: GL SDK's Plugin Architecture Implementation
5
+ Requires-Python: <3.14,>=3.11
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: python-dotenv>=1.2.1
8
+ Provides-Extra: dev
9
+ Requires-Dist: pre-commit<4.0.0,>=3.6.2; extra == "dev"
10
+ Requires-Dist: pytest<9.0.0,>=8.3.4; extra == "dev"
11
+ Requires-Dist: pytest-mock<4.0.0,>=3.14.0; extra == "dev"
12
+ Requires-Dist: pytest-asyncio<1.0.0,>=0.25.3; extra == "dev"
13
+ Requires-Dist: coverage<8.0.0,>=7.6.10; extra == "dev"
14
+ Requires-Dist: mypy<2.0.0,>=1.11.2; extra == "dev"
15
+ Requires-Dist: ruff<1.0.0,>=0.9.9; extra == "dev"
16
+ Requires-Dist: pytest-cov<6.0.0,>=5.0.0; extra == "dev"
@@ -0,0 +1,16 @@
1
+ README.md
2
+ pyproject.toml
3
+ setup.py
4
+ gl_plugin/__init__.py
5
+ gl_plugin.egg-info/PKG-INFO
6
+ gl_plugin.egg-info/SOURCES.txt
7
+ gl_plugin.egg-info/dependency_links.txt
8
+ gl_plugin.egg-info/requires.txt
9
+ gl_plugin.egg-info/top_level.txt
10
+ gl_plugin/plugin/__init__.py
11
+ gl_plugin/plugin/handler.py
12
+ gl_plugin/plugin/manager.py
13
+ gl_plugin/plugin/plugin.py
14
+ gl_plugin/plugin/registry.py
15
+ gl_plugin/services/__init__.py
16
+ gl_plugin/services/config.py
@@ -0,0 +1,11 @@
1
+ python-dotenv>=1.2.1
2
+
3
+ [dev]
4
+ pre-commit<4.0.0,>=3.6.2
5
+ pytest<9.0.0,>=8.3.4
6
+ pytest-mock<4.0.0,>=3.14.0
7
+ pytest-asyncio<1.0.0,>=0.25.3
8
+ coverage<8.0.0,>=7.6.10
9
+ mypy<2.0.0,>=1.11.2
10
+ ruff<1.0.0,>=0.9.9
11
+ pytest-cov<6.0.0,>=5.0.0
@@ -0,0 +1 @@
1
+ gl_plugin
@@ -0,0 +1,29 @@
1
+ [project]
2
+ name = "gl-plugin"
3
+ version = "0.0.1b2"
4
+ description = "GL SDK's Plugin Architecture Implementation"
5
+ readme = "README.md"
6
+ requires-python = ">=3.11,<3.14"
7
+
8
+ dependencies = [
9
+ "python-dotenv>=1.2.1",
10
+ ]
11
+
12
+ [project.optional-dependencies]
13
+ dev = [
14
+ "pre-commit>=3.6.2,<4.0.0",
15
+ "pytest>=8.3.4,<9.0.0",
16
+ "pytest-mock>=3.14.0,<4.0.0",
17
+ "pytest-asyncio>=0.25.3,<1.0.0",
18
+ "coverage>=7.6.10,<8.0.0",
19
+ "mypy>=1.11.2,<2.0.0",
20
+ "ruff>=0.9.9,<1.0.0",
21
+ "pytest-cov>=5.0.0,<6.0.0",
22
+ ]
23
+
24
+ [build-system]
25
+ requires = ["setuptools>=61.0", "wheel"]
26
+ build-backend = "setuptools.build_meta"
27
+
28
+ [tool.setuptools]
29
+ packages = { find = { include = ["gl_plugin*"] } }
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,10 @@
1
+ """Setup script for GLLM Modules."""
2
+
3
+ from setuptools import setup
4
+
5
+ if __name__ == "__main__":
6
+ setup(
7
+ build_with_nuitka=True,
8
+ name="-binary",
9
+ version="0.0.1b2",
10
+ )