karrio 2023.9.2__py3-none-any.whl → 2025.5rc3__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.
- karrio/__init__.py +0 -100
- karrio/addons/renderer.py +1 -1
- karrio/api/gateway.py +58 -35
- karrio/api/interface.py +41 -4
- karrio/api/mapper.py +39 -0
- karrio/api/proxy.py +18 -5
- karrio/core/__init__.py +5 -1
- karrio/core/metadata.py +113 -20
- karrio/core/models.py +64 -5
- karrio/core/plugins.py +606 -0
- karrio/core/settings.py +39 -2
- karrio/core/units.py +574 -29
- karrio/core/utils/datetime.py +62 -2
- karrio/core/utils/dict.py +5 -0
- karrio/core/utils/enum.py +98 -13
- karrio/core/utils/helpers.py +83 -32
- karrio/core/utils/number.py +52 -8
- karrio/core/utils/string.py +52 -1
- karrio/core/utils/transformer.py +9 -4
- karrio/core/validators.py +88 -0
- karrio/lib.py +147 -2
- karrio/plugins/__init__.py +6 -0
- karrio/references.py +652 -67
- karrio/sdk.py +102 -0
- karrio/universal/mappers/rating_proxy.py +35 -9
- karrio/validators/__init__.py +6 -0
- {karrio-2023.9.2.dist-info → karrio-2025.5rc3.dist-info}/METADATA +9 -8
- karrio-2025.5rc3.dist-info/RECORD +57 -0
- {karrio-2023.9.2.dist-info → karrio-2025.5rc3.dist-info}/WHEEL +1 -1
- {karrio-2023.9.2.dist-info → karrio-2025.5rc3.dist-info}/top_level.txt +1 -0
- karrio-2023.9.2.dist-info/RECORD +0 -52
karrio/core/plugins.py
ADDED
@@ -0,0 +1,606 @@
|
|
1
|
+
"""
|
2
|
+
Karrio Plugins Module.
|
3
|
+
|
4
|
+
This module provides functionality for loading and managing Karrio plugins.
|
5
|
+
Plugins allow extending Karrio's functionality with custom carriers, mappers,
|
6
|
+
and address validators.
|
7
|
+
|
8
|
+
Usage:
|
9
|
+
There are several ways to use plugins with Karrio:
|
10
|
+
|
11
|
+
1. Default plugins directory:
|
12
|
+
By default, Karrio looks for plugins in a 'plugins' directory in the current working directory.
|
13
|
+
|
14
|
+
2. Custom plugin directory via environment variable:
|
15
|
+
Set the KARRIO_PLUGINS environment variable to the path of your plugins directory:
|
16
|
+
|
17
|
+
export KARRIO_PLUGINS=/path/to/your/plugins
|
18
|
+
|
19
|
+
3. Programmatically add plugin directories:
|
20
|
+
|
21
|
+
import karrio.plugins as plugins
|
22
|
+
plugins.add_plugin_directory('/path/to/your/plugins')
|
23
|
+
|
24
|
+
Plugin Directory Structure:
|
25
|
+
A valid plugin directory can have either of the following structures:
|
26
|
+
|
27
|
+
1. New Structure (Recommended):
|
28
|
+
/your_plugin_directory/
|
29
|
+
├── plugin_name/
|
30
|
+
│ └── karrio/
|
31
|
+
│ ├── plugins/ # For plugins (carrier integrations, address validators, etc...)
|
32
|
+
│ │ └── plugin_name/
|
33
|
+
│ │ └── __init__.py # Contains METADATA
|
34
|
+
│ ├── mappers/ # For carrier integrations
|
35
|
+
│ │ └── plugin_name/
|
36
|
+
│ │ ├── __init__.py # Contains METADATA
|
37
|
+
│ │ ├── mapper.py # Implementation of the mapper
|
38
|
+
│ │ ├── proxy.py # Implementation of the proxy
|
39
|
+
│ │ └── settings.py # Settings schema
|
40
|
+
│ ├── validators/ # For address validators
|
41
|
+
│ │ └── plugin_name/
|
42
|
+
│ │ ├── __init__.py # Contains METADATA
|
43
|
+
│ │ └── validator.py # Implementation of the validator
|
44
|
+
│ ├── providers/ # Provider-specific implementations
|
45
|
+
│ │ └── plugin_name/
|
46
|
+
│ │ ├── __init__.py
|
47
|
+
│ │ ├── error.py # Error handling
|
48
|
+
│ │ ├── units.py # Units and enums
|
49
|
+
│ │ ├── utils.py # Utility functions
|
50
|
+
│ │ ├── rate.py # Rating functionality
|
51
|
+
│ │ ├── tracking.py # Tracking functionality
|
52
|
+
│ │ ├── manifest.py # Manifest functionality
|
53
|
+
│ │ ├── shipment/ # Shipment operations
|
54
|
+
│ │ │ ├── __init__.py
|
55
|
+
│ │ │ ├── create.py
|
56
|
+
│ │ │ └── cancel.py
|
57
|
+
│ │ └── pickup/ # Pickup operations
|
58
|
+
│ │ ├── __init__.py
|
59
|
+
│ │ ├── create.py
|
60
|
+
│ │ ├── update.py
|
61
|
+
│ │ └── cancel.py
|
62
|
+
│ └── schemas/ # API schema definitions
|
63
|
+
│ └── plugin_name/
|
64
|
+
│ ├── __init__.py
|
65
|
+
│ └── various schema files...
|
66
|
+
|
67
|
+
2. Legacy Structure (Supported for backward compatibility):
|
68
|
+
/your_plugin_directory/
|
69
|
+
├── plugin_name/
|
70
|
+
│ └── karrio/
|
71
|
+
│ ├── mappers/ # For carrier integrations
|
72
|
+
│ │ └── plugin_name/
|
73
|
+
│ │ ├── __init__.py # Contains METADATA
|
74
|
+
│ │ ├── mapper.py # Implementation of the mapper
|
75
|
+
│ │ ├── proxy.py # Implementation of the proxy
|
76
|
+
│ │ └── settings.py # Settings schema
|
77
|
+
│ ├── validators/ # For address validators
|
78
|
+
│ │ └── plugin_name/
|
79
|
+
│ │ ├── __init__.py # Contains METADATA
|
80
|
+
│ │ └── validator.py # Implementation of the validator
|
81
|
+
│ ├── providers/ # Provider-specific implementations
|
82
|
+
│ │ └── plugin_name/
|
83
|
+
│ │ ├── __init__.py
|
84
|
+
│ │ ├── error.py # Error handling
|
85
|
+
│ │ ├── units.py # Units and enums
|
86
|
+
│ │ ├── utils.py # Utility functions
|
87
|
+
│ │ ├── rate.py # Rating functionality
|
88
|
+
│ │ ├── tracking.py # Tracking functionality
|
89
|
+
│ │ ├── manifest.py # Manifest functionality
|
90
|
+
│ │ ├── shipment/ # Shipment operations
|
91
|
+
│ │ │ ├── __init__.py
|
92
|
+
│ │ │ ├── create.py
|
93
|
+
│ │ │ └── cancel.py
|
94
|
+
│ │ └── pickup/ # Pickup operations
|
95
|
+
│ │ ├── __init__.py
|
96
|
+
│ │ ├── create.py
|
97
|
+
│ │ ├── update.py
|
98
|
+
│ │ └── cancel.py
|
99
|
+
│ └── schemas/ # API schema definitions
|
100
|
+
│ └── plugin_name/
|
101
|
+
│ ├── __init__.py
|
102
|
+
│ └── various schema files...
|
103
|
+
|
104
|
+
Plugin Metadata:
|
105
|
+
Each plugin must define a METADATA object of type PluginMetadata in its __init__.py file.
|
106
|
+
The metadata should specify the plugin's capabilities, features, and other relevant information.
|
107
|
+
|
108
|
+
The plugin's capabilities (carrier integration, address validation, etc.) are automatically
|
109
|
+
determined based on the components defined in the metadata and registered modules.
|
110
|
+
"""
|
111
|
+
|
112
|
+
import os
|
113
|
+
import sys
|
114
|
+
import inspect
|
115
|
+
import logging
|
116
|
+
import importlib
|
117
|
+
import traceback
|
118
|
+
import pkgutil
|
119
|
+
from typing import List, Optional, Dict, Any, Tuple
|
120
|
+
import importlib.metadata as importlib_metadata
|
121
|
+
|
122
|
+
# Configure logger with a higher default level to reduce noise
|
123
|
+
logger = logging.getLogger(__name__)
|
124
|
+
if not logger.level:
|
125
|
+
logger.setLevel(logging.INFO)
|
126
|
+
|
127
|
+
# Default plugin directories to scan
|
128
|
+
DEFAULT_PLUGINS = [
|
129
|
+
os.path.join(os.getcwd(), "plugins"), # Local plugins directory
|
130
|
+
os.path.join(os.getcwd(), "community/plugins"), # Community plugins directory
|
131
|
+
]
|
132
|
+
|
133
|
+
# Track failed plugin loads
|
134
|
+
FAILED_PLUGIN_MODULES: Dict[str, Any] = {}
|
135
|
+
|
136
|
+
# Entrypoint group for Karrio plugins
|
137
|
+
ENTRYPOINT_GROUP = "karrio.plugins"
|
138
|
+
|
139
|
+
# Ensure the karrio.plugins module exists
|
140
|
+
try:
|
141
|
+
import karrio.plugins
|
142
|
+
except ImportError:
|
143
|
+
# Create an empty module for plugins if it doesn't exist
|
144
|
+
try:
|
145
|
+
import types
|
146
|
+
import karrio
|
147
|
+
karrio.plugins = types.ModuleType('karrio.plugins')
|
148
|
+
karrio.plugins.__path__ = []
|
149
|
+
sys.modules['karrio.plugins'] = karrio.plugins
|
150
|
+
logger.debug("Created karrio.plugins module")
|
151
|
+
except (ImportError, AttributeError) as e:
|
152
|
+
logger.error(f"Failed to create karrio.plugins module: {e}")
|
153
|
+
|
154
|
+
def get_custom_plugin_dirs() -> List[str]:
|
155
|
+
"""
|
156
|
+
Get custom plugin directory from environment variable.
|
157
|
+
|
158
|
+
Checks if the KARRIO_PLUGINS environment variable is defined
|
159
|
+
and if the directory exists. Returns a list containing the
|
160
|
+
custom plugin directory if it's valid.
|
161
|
+
|
162
|
+
Returns:
|
163
|
+
List[str]: List of valid custom plugin directories from environment variables
|
164
|
+
"""
|
165
|
+
custom_dirs = []
|
166
|
+
env_plugins = os.environ.get("KARRIO_PLUGINS", "")
|
167
|
+
|
168
|
+
if env_plugins and os.path.exists(env_plugins):
|
169
|
+
custom_dirs.append(env_plugins)
|
170
|
+
|
171
|
+
return custom_dirs
|
172
|
+
|
173
|
+
# Initialize DEFAULT_PLUGINS with environment variable directories
|
174
|
+
custom_dirs = get_custom_plugin_dirs()
|
175
|
+
for directory in custom_dirs:
|
176
|
+
if directory not in DEFAULT_PLUGINS:
|
177
|
+
DEFAULT_PLUGINS.append(directory)
|
178
|
+
|
179
|
+
def add_plugin_directory(directory: str) -> None:
|
180
|
+
"""
|
181
|
+
Add a custom plugin directory programmatically.
|
182
|
+
|
183
|
+
This function checks if the directory exists and if it's not already
|
184
|
+
in the DEFAULT_PLUGINS list. If it passes these checks, the directory
|
185
|
+
is added to the list and the extensions are reloaded.
|
186
|
+
|
187
|
+
Args:
|
188
|
+
directory (str): Path to the plugin directory to add
|
189
|
+
|
190
|
+
Example:
|
191
|
+
>>> import karrio.plugins as plugins
|
192
|
+
>>> plugins.add_plugin_directory('/path/to/your/plugins')
|
193
|
+
"""
|
194
|
+
global DEFAULT_PLUGINS
|
195
|
+
if os.path.exists(directory) and directory not in DEFAULT_PLUGINS:
|
196
|
+
DEFAULT_PLUGINS.append(directory)
|
197
|
+
# Trigger a reload of plugins when a new directory is added
|
198
|
+
try:
|
199
|
+
import karrio.references
|
200
|
+
if hasattr(karrio.references, "import_extensions"):
|
201
|
+
karrio.references.import_extensions()
|
202
|
+
except (ImportError, AttributeError):
|
203
|
+
pass # Silently ignore if references module can't be imported
|
204
|
+
|
205
|
+
def discover_plugins(plugin_dirs: Optional[List[str]] = None) -> List[str]:
|
206
|
+
"""
|
207
|
+
Discover available plugins in the specified directories.
|
208
|
+
|
209
|
+
Scans the given directories (or DEFAULT_PLUGINS if none specified)
|
210
|
+
for valid Karrio plugin structures. A valid plugin structure must
|
211
|
+
have a 'karrio' subdirectory with plugins, mappers and/or validators.
|
212
|
+
|
213
|
+
Args:
|
214
|
+
plugin_dirs: List of directories to scan for plugins.
|
215
|
+
If None, uses DEFAULT_PLUGINS.
|
216
|
+
|
217
|
+
Returns:
|
218
|
+
List of paths to valid plugin directories
|
219
|
+
"""
|
220
|
+
if plugin_dirs is None:
|
221
|
+
plugin_dirs = DEFAULT_PLUGINS
|
222
|
+
else:
|
223
|
+
# Ensure plugin_dirs has unique entries
|
224
|
+
plugin_dirs = list(dict.fromkeys(plugin_dirs))
|
225
|
+
|
226
|
+
plugins = []
|
227
|
+
for plugin_dir in plugin_dirs:
|
228
|
+
if not os.path.exists(plugin_dir):
|
229
|
+
continue
|
230
|
+
|
231
|
+
# Look for directories that might be plugins
|
232
|
+
for item in os.listdir(plugin_dir):
|
233
|
+
item_path = os.path.join(plugin_dir, item)
|
234
|
+
if os.path.isdir(item_path):
|
235
|
+
# Check if this directory has a karrio subdirectory
|
236
|
+
karrio_dir = os.path.join(item_path, "karrio")
|
237
|
+
|
238
|
+
# Skip if karrio directory doesn't exist
|
239
|
+
if not os.path.isdir(karrio_dir):
|
240
|
+
continue
|
241
|
+
|
242
|
+
# Check for plugins, mappers or validators subdirectories
|
243
|
+
plugins_dir = os.path.join(karrio_dir, "plugins")
|
244
|
+
mappers_dir = os.path.join(karrio_dir, "mappers")
|
245
|
+
validators_dir = os.path.join(karrio_dir, "validators")
|
246
|
+
|
247
|
+
if (os.path.isdir(plugins_dir) or
|
248
|
+
os.path.isdir(mappers_dir) or
|
249
|
+
os.path.isdir(validators_dir)):
|
250
|
+
plugins.append(item_path)
|
251
|
+
|
252
|
+
# Ensure returned plugin paths are unique
|
253
|
+
return list(dict.fromkeys(plugins))
|
254
|
+
|
255
|
+
def discover_plugin_modules(plugin_dirs: Optional[List[str]] = None,
|
256
|
+
module_types: List[str] = None) -> Dict[str, Dict[str, Any]]:
|
257
|
+
"""
|
258
|
+
Discover and collect modules from plugins by type.
|
259
|
+
|
260
|
+
Scans plugin directories for modules of specified types (plugins, mappers, validators, etc.)
|
261
|
+
and returns them organized by plugin name and module type.
|
262
|
+
|
263
|
+
Args:
|
264
|
+
plugin_dirs: List of plugin directories to scan (uses DEFAULT_PLUGINS if None)
|
265
|
+
module_types: List of module types to discover (defaults to ["plugins", "mappers", "validators"])
|
266
|
+
|
267
|
+
Returns:
|
268
|
+
Dict mapping plugin names to a dict of module types and their module objects
|
269
|
+
"""
|
270
|
+
global FAILED_PLUGIN_MODULES
|
271
|
+
FAILED_PLUGIN_MODULES = {}
|
272
|
+
|
273
|
+
if module_types is None:
|
274
|
+
module_types = ["plugins", "mappers", "validators"]
|
275
|
+
|
276
|
+
plugin_paths = discover_plugins(plugin_dirs)
|
277
|
+
plugin_modules: Dict[str, Dict[str, Any]] = {}
|
278
|
+
|
279
|
+
for plugin_path in plugin_paths:
|
280
|
+
plugin_name = os.path.basename(plugin_path)
|
281
|
+
karrio_dir = os.path.join(plugin_path, "karrio")
|
282
|
+
|
283
|
+
# Skip if karrio directory doesn't exist
|
284
|
+
if not os.path.isdir(karrio_dir):
|
285
|
+
continue
|
286
|
+
|
287
|
+
plugin_modules[plugin_name] = {}
|
288
|
+
|
289
|
+
# Check for each module type
|
290
|
+
for module_type in module_types:
|
291
|
+
module_dir = os.path.join(karrio_dir, module_type)
|
292
|
+
|
293
|
+
# Skip if this module type doesn't exist in the plugin
|
294
|
+
if not os.path.isdir(module_dir):
|
295
|
+
continue
|
296
|
+
|
297
|
+
# Look for plugin-specific submodules (e.g., plugins/plugin_name or mappers/plugin_name)
|
298
|
+
for subitem in os.listdir(module_dir):
|
299
|
+
subitem_path = os.path.join(module_dir, subitem)
|
300
|
+
submodule_init = os.path.join(subitem_path, "__init__.py")
|
301
|
+
|
302
|
+
if os.path.isdir(subitem_path) and os.path.exists(submodule_init):
|
303
|
+
submodule_name = subitem
|
304
|
+
|
305
|
+
# Try to import the module
|
306
|
+
try:
|
307
|
+
# Add the plugin's parent dir to sys.path if not already there
|
308
|
+
plugin_parent = os.path.dirname(plugin_path)
|
309
|
+
if plugin_parent not in sys.path:
|
310
|
+
sys.path.insert(0, plugin_parent)
|
311
|
+
|
312
|
+
# Import the module
|
313
|
+
module_path = f"karrio.{module_type}.{submodule_name}"
|
314
|
+
module = importlib.import_module(module_path)
|
315
|
+
|
316
|
+
# Store successful imports
|
317
|
+
if module_type not in plugin_modules[plugin_name]:
|
318
|
+
plugin_modules[plugin_name][module_type] = {}
|
319
|
+
|
320
|
+
plugin_modules[plugin_name][module_type][submodule_name] = module
|
321
|
+
|
322
|
+
except Exception as e:
|
323
|
+
# Track failed module imports
|
324
|
+
logger.error(f"Error importing {module_type}.{submodule_name} from {plugin_name}: {str(e)}")
|
325
|
+
key = f"{plugin_name}.{module_type}.{submodule_name}"
|
326
|
+
FAILED_PLUGIN_MODULES[key] = {
|
327
|
+
"plugin": plugin_name,
|
328
|
+
"module_type": module_type,
|
329
|
+
"submodule": submodule_name,
|
330
|
+
"error": str(e),
|
331
|
+
"traceback": traceback.format_exc()
|
332
|
+
}
|
333
|
+
|
334
|
+
return plugin_modules
|
335
|
+
|
336
|
+
def collect_plugin_metadata(plugin_modules: Dict[str, Dict[str, Any]]) -> Tuple[Dict[str, Any], Dict[str, Any]]:
|
337
|
+
"""
|
338
|
+
Collect metadata from discovered plugin modules.
|
339
|
+
|
340
|
+
Args:
|
341
|
+
plugin_modules: Dictionary of plugin modules organized by plugin name and module type
|
342
|
+
|
343
|
+
Returns:
|
344
|
+
Tuple containing:
|
345
|
+
- Dictionary of successful plugin metadata
|
346
|
+
- Dictionary of failed plugin metadata attempts
|
347
|
+
"""
|
348
|
+
plugin_metadata = {}
|
349
|
+
failed_metadata = {}
|
350
|
+
|
351
|
+
for plugin_name, modules_by_type in plugin_modules.items():
|
352
|
+
metadata_found = False
|
353
|
+
|
354
|
+
# First try to find metadata in plugins (new structure)
|
355
|
+
if "plugins" in modules_by_type:
|
356
|
+
for submodule_name, module in modules_by_type["plugins"].items():
|
357
|
+
try:
|
358
|
+
if hasattr(module, "METADATA"):
|
359
|
+
plugin_metadata[plugin_name] = module.METADATA
|
360
|
+
metadata_found = True
|
361
|
+
break
|
362
|
+
except Exception as e:
|
363
|
+
key = f"{plugin_name}.plugins.{submodule_name}"
|
364
|
+
failed_metadata[key] = {
|
365
|
+
"plugin": plugin_name,
|
366
|
+
"module_type": "plugins",
|
367
|
+
"submodule": submodule_name,
|
368
|
+
"error": str(e)
|
369
|
+
}
|
370
|
+
|
371
|
+
# If not found in plugins, try mappers (legacy structure)
|
372
|
+
if not metadata_found and "mappers" in modules_by_type:
|
373
|
+
for submodule_name, module in modules_by_type["mappers"].items():
|
374
|
+
try:
|
375
|
+
if hasattr(module, "METADATA"):
|
376
|
+
plugin_metadata[plugin_name] = module.METADATA
|
377
|
+
metadata_found = True
|
378
|
+
break
|
379
|
+
except Exception as e:
|
380
|
+
key = f"{plugin_name}.mappers.{submodule_name}"
|
381
|
+
failed_metadata[key] = {
|
382
|
+
"plugin": plugin_name,
|
383
|
+
"module_type": "mappers",
|
384
|
+
"submodule": submodule_name,
|
385
|
+
"error": str(e)
|
386
|
+
}
|
387
|
+
|
388
|
+
# If not found in mappers, try validators (legacy structure)
|
389
|
+
if not metadata_found and "validators" in modules_by_type:
|
390
|
+
for submodule_name, module in modules_by_type["validators"].items():
|
391
|
+
try:
|
392
|
+
if hasattr(module, "METADATA"):
|
393
|
+
plugin_metadata[plugin_name] = module.METADATA
|
394
|
+
metadata_found = True
|
395
|
+
break
|
396
|
+
except Exception as e:
|
397
|
+
key = f"{plugin_name}.validators.{submodule_name}"
|
398
|
+
failed_metadata[key] = {
|
399
|
+
"plugin": plugin_name,
|
400
|
+
"module_type": "validators",
|
401
|
+
"submodule": submodule_name,
|
402
|
+
"error": str(e)
|
403
|
+
}
|
404
|
+
|
405
|
+
if not metadata_found and modules_by_type:
|
406
|
+
# Record error only if we have some modules but no metadata
|
407
|
+
failed_metadata[plugin_name] = {
|
408
|
+
"plugin": plugin_name,
|
409
|
+
"error": "No METADATA found in any module"
|
410
|
+
}
|
411
|
+
|
412
|
+
# NEW: Try direct import of karrio.plugins.[plugin_name] and karrio.mappers.[plugin_name] if not found
|
413
|
+
if not metadata_found:
|
414
|
+
for modtype in ["plugins", "mappers"]:
|
415
|
+
try:
|
416
|
+
module_path = f"karrio.{modtype}.{plugin_name}"
|
417
|
+
module = importlib.import_module(module_path)
|
418
|
+
if hasattr(module, "METADATA"):
|
419
|
+
plugin_metadata[plugin_name] = module.METADATA
|
420
|
+
metadata_found = True
|
421
|
+
break
|
422
|
+
except Exception as e:
|
423
|
+
key = f"{plugin_name}.{modtype}.__init__"
|
424
|
+
failed_metadata[key] = {
|
425
|
+
"plugin": plugin_name,
|
426
|
+
"module_type": modtype,
|
427
|
+
"submodule": "__init__",
|
428
|
+
"error": str(e)
|
429
|
+
}
|
430
|
+
|
431
|
+
return plugin_metadata, failed_metadata
|
432
|
+
|
433
|
+
def load_local_plugins(plugin_dirs: Optional[List[str]] = None) -> List[str]:
|
434
|
+
"""
|
435
|
+
Load plugins from local directories into the karrio namespace.
|
436
|
+
|
437
|
+
This function:
|
438
|
+
1. Discovers plugins in the specified directories
|
439
|
+
2. Adds the plugin parent directory to sys.path
|
440
|
+
3. Extends the karrio namespace to include the plugin modules
|
441
|
+
4. Refreshes karrio.references (if not called from references)
|
442
|
+
|
443
|
+
Args:
|
444
|
+
plugin_dirs: List of directories to scan for plugins.
|
445
|
+
If None, uses DEFAULT_PLUGINS.
|
446
|
+
|
447
|
+
Returns:
|
448
|
+
List of plugin names that were successfully loaded
|
449
|
+
"""
|
450
|
+
|
451
|
+
plugins = discover_plugins(plugin_dirs)
|
452
|
+
loaded_plugins = []
|
453
|
+
already_processed = set() # Track which plugins have been processed to avoid duplicates
|
454
|
+
|
455
|
+
# Ensure all required namespaces exist
|
456
|
+
required_namespaces = ["plugins", "mappers", "providers", "schemas", "validators"]
|
457
|
+
for namespace in required_namespaces:
|
458
|
+
module_name = f"karrio.{namespace}"
|
459
|
+
try:
|
460
|
+
# Try to import the module
|
461
|
+
importlib.import_module(module_name)
|
462
|
+
except ImportError:
|
463
|
+
# Create the module if it doesn't exist
|
464
|
+
try:
|
465
|
+
import types
|
466
|
+
import karrio
|
467
|
+
module = types.ModuleType(module_name)
|
468
|
+
module.__path__ = []
|
469
|
+
setattr(karrio, namespace, module)
|
470
|
+
sys.modules[module_name] = module
|
471
|
+
logger.debug(f"Created {module_name} module")
|
472
|
+
except (ImportError, AttributeError) as e:
|
473
|
+
logger.error(f"Failed to create {module_name} module: {e}")
|
474
|
+
|
475
|
+
for plugin_path in plugins:
|
476
|
+
# Skip if we've already processed this plugin path
|
477
|
+
if plugin_path in already_processed:
|
478
|
+
continue
|
479
|
+
|
480
|
+
already_processed.add(plugin_path)
|
481
|
+
plugin_name = os.path.basename(plugin_path)
|
482
|
+
|
483
|
+
# Add the plugin's parent directory to sys.path
|
484
|
+
plugin_parent = os.path.dirname(plugin_path)
|
485
|
+
if plugin_parent not in sys.path:
|
486
|
+
sys.path.insert(0, plugin_parent)
|
487
|
+
|
488
|
+
# Check if the plugin has the necessary structure
|
489
|
+
karrio_dir = os.path.join(plugin_path, "karrio")
|
490
|
+
|
491
|
+
# Skip if karrio directory doesn't exist
|
492
|
+
if not os.path.isdir(karrio_dir):
|
493
|
+
logger.error(f"Invalid plugin structure: missing karrio directory in {plugin_path}")
|
494
|
+
continue
|
495
|
+
|
496
|
+
# Look for plugins, mappers, providers, schemas, and validators directories
|
497
|
+
for module_name in required_namespaces:
|
498
|
+
module_dir = os.path.join(karrio_dir, module_name)
|
499
|
+
|
500
|
+
# Skip if directory doesn't exist
|
501
|
+
if not os.path.isdir(module_dir):
|
502
|
+
continue
|
503
|
+
|
504
|
+
# Try to extend the corresponding karrio namespace
|
505
|
+
try:
|
506
|
+
target_module_name = f"karrio.{module_name}"
|
507
|
+
target_module = importlib.import_module(target_module_name)
|
508
|
+
|
509
|
+
# Extend the module's __path__ to include our plugin directory
|
510
|
+
if hasattr(target_module, "__path__"):
|
511
|
+
extended_path = pkgutil.extend_path(target_module.__path__, target_module.__name__)
|
512
|
+
if module_dir not in extended_path:
|
513
|
+
extended_path.append(module_dir)
|
514
|
+
target_module.__path__ = extended_path
|
515
|
+
except ImportError as e:
|
516
|
+
logger.error(f"Could not import {target_module_name}: {e}")
|
517
|
+
continue
|
518
|
+
|
519
|
+
# Mark plugin as loaded
|
520
|
+
loaded_plugins.append(plugin_name)
|
521
|
+
|
522
|
+
# To prevent recursion, only refresh references if we're not being called from references
|
523
|
+
# This check uses the stack frame inspection to see if we're being called from import_extensions
|
524
|
+
calling_module = ''
|
525
|
+
frame = inspect.currentframe()
|
526
|
+
if frame and frame.f_back:
|
527
|
+
calling_module = frame.f_back.f_globals.get('__name__', '')
|
528
|
+
if 'karrio.references' not in calling_module:
|
529
|
+
try:
|
530
|
+
import karrio.references
|
531
|
+
if hasattr(karrio.references, "import_extensions"):
|
532
|
+
karrio.references.import_extensions()
|
533
|
+
logger.info("Refreshed karrio.references providers")
|
534
|
+
except (ImportError, AttributeError) as e:
|
535
|
+
logger.error(f"Could not refresh karrio.references: {e}")
|
536
|
+
|
537
|
+
return loaded_plugins
|
538
|
+
|
539
|
+
def get_failed_plugin_modules() -> Dict[str, Any]:
|
540
|
+
"""
|
541
|
+
Get information about plugin modules that failed to load.
|
542
|
+
|
543
|
+
Returns:
|
544
|
+
Dict containing information about failed plugin module loads
|
545
|
+
"""
|
546
|
+
return FAILED_PLUGIN_MODULES
|
547
|
+
|
548
|
+
def discover_entrypoint_plugins() -> Dict[str, Dict[str, Any]]:
|
549
|
+
"""
|
550
|
+
Discover plugins registered via setuptools entrypoints.
|
551
|
+
|
552
|
+
This function looks for plugins registered under the 'karrio.plugins'
|
553
|
+
entrypoint group. Each entrypoint should point to a module with a METADATA
|
554
|
+
object that defines the plugin's capabilities.
|
555
|
+
|
556
|
+
Returns:
|
557
|
+
Dict mapping plugin names to a dict containing the plugin module
|
558
|
+
"""
|
559
|
+
global FAILED_PLUGIN_MODULES
|
560
|
+
entrypoint_plugins = {}
|
561
|
+
|
562
|
+
try:
|
563
|
+
# Find all entry points in the karrio.plugins group
|
564
|
+
entry_points = importlib_metadata.entry_points()
|
565
|
+
|
566
|
+
# Handle different entry_points behavior in different versions of importlib_metadata
|
567
|
+
if hasattr(entry_points, 'select'): # Python 3.10+
|
568
|
+
plugin_entry_points = entry_points.select(group=ENTRYPOINT_GROUP)
|
569
|
+
elif hasattr(entry_points, 'get'): # Python 3.8, 3.9
|
570
|
+
plugin_entry_points = entry_points.get(ENTRYPOINT_GROUP, [])
|
571
|
+
else: # Older versions or different implementation
|
572
|
+
plugin_entry_points = [
|
573
|
+
ep for ep in entry_points
|
574
|
+
if getattr(ep, 'group', None) == ENTRYPOINT_GROUP
|
575
|
+
]
|
576
|
+
|
577
|
+
for entry_point in plugin_entry_points:
|
578
|
+
plugin_name = entry_point.name
|
579
|
+
|
580
|
+
try:
|
581
|
+
# Load the plugin module
|
582
|
+
plugin_module = entry_point.load()
|
583
|
+
|
584
|
+
# Create a structured dict similar to discover_plugin_modules output
|
585
|
+
if plugin_name not in entrypoint_plugins:
|
586
|
+
entrypoint_plugins[plugin_name] = {
|
587
|
+
"entrypoint": {
|
588
|
+
plugin_name: plugin_module
|
589
|
+
}
|
590
|
+
}
|
591
|
+
|
592
|
+
except Exception as e:
|
593
|
+
# Track failed entrypoint loads
|
594
|
+
logger.error(f"Error loading entrypoint plugin {plugin_name}: {str(e)}")
|
595
|
+
key = f"entrypoint.{plugin_name}"
|
596
|
+
FAILED_PLUGIN_MODULES[key] = {
|
597
|
+
"plugin": plugin_name,
|
598
|
+
"module_type": "entrypoint",
|
599
|
+
"error": str(e),
|
600
|
+
"traceback": traceback.format_exc()
|
601
|
+
}
|
602
|
+
|
603
|
+
except Exception as e:
|
604
|
+
logger.error(f"Error discovering entrypoint plugins: {str(e)}")
|
605
|
+
|
606
|
+
return entrypoint_plugins
|
karrio/core/settings.py
CHANGED
@@ -3,11 +3,12 @@
|
|
3
3
|
import abc
|
4
4
|
import attr
|
5
5
|
import typing
|
6
|
+
import functools
|
6
7
|
|
7
8
|
|
8
9
|
@attr.s(auto_attribs=True)
|
9
10
|
class Settings(abc.ABC):
|
10
|
-
"""Unified API carrier
|
11
|
+
"""Unified API carrier connection settings (Interface)"""
|
11
12
|
|
12
13
|
carrier_id: str
|
13
14
|
account_country_code: str = None
|
@@ -15,6 +16,7 @@ class Settings(abc.ABC):
|
|
15
16
|
metadata: dict = {}
|
16
17
|
config: dict = {}
|
17
18
|
id: str = None
|
19
|
+
tracer = None # Will be set during Gateway initialization
|
18
20
|
|
19
21
|
@property
|
20
22
|
def carrier_name(self) -> typing.Optional[str]:
|
@@ -32,4 +34,39 @@ class Settings(abc.ABC):
|
|
32
34
|
def connection_config(self):
|
33
35
|
import karrio.lib as lib
|
34
36
|
|
35
|
-
return lib.to_connection_config(
|
37
|
+
return lib.to_connection_config(
|
38
|
+
self.config or {},
|
39
|
+
option_type=lib.units.create_enum(
|
40
|
+
"ConnectionConfig",
|
41
|
+
dict(
|
42
|
+
label_type=lib.units.create_enum(
|
43
|
+
"LabelType", ["PDF", "ZPL"]
|
44
|
+
),
|
45
|
+
)
|
46
|
+
),
|
47
|
+
)
|
48
|
+
|
49
|
+
@property
|
50
|
+
def connection_cache(self):
|
51
|
+
import karrio.lib as lib
|
52
|
+
|
53
|
+
return getattr(self, "cache", None) or lib.Cache()
|
54
|
+
|
55
|
+
def trace(self, *args, **kwargs):
|
56
|
+
if self.tracer is None:
|
57
|
+
import karrio.lib as lib
|
58
|
+
self.tracer = lib.Tracer()
|
59
|
+
|
60
|
+
return self.tracer.with_metadata(
|
61
|
+
dict(
|
62
|
+
connection=dict(
|
63
|
+
id=self.id,
|
64
|
+
test_mode=self.test_mode,
|
65
|
+
carrier_id=self.carrier_id,
|
66
|
+
carrier_name=self.carrier_name,
|
67
|
+
)
|
68
|
+
)
|
69
|
+
)(*args, **kwargs)
|
70
|
+
|
71
|
+
def trace_as(self, format: str):
|
72
|
+
return functools.partial(self.trace, format=format)
|