karrio 2023.9.2__py3-none-any.whl → 2025.5rc1__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.5rc1.dist-info}/METADATA +9 -8
- karrio-2025.5rc1.dist-info/RECORD +57 -0
- {karrio-2023.9.2.dist-info → karrio-2025.5rc1.dist-info}/WHEEL +1 -1
- {karrio-2023.9.2.dist-info → karrio-2025.5rc1.dist-info}/top_level.txt +1 -0
- karrio-2023.9.2.dist-info/RECORD +0 -52
karrio/references.py
CHANGED
@@ -1,128 +1,532 @@
|
|
1
|
-
"""Karrio Interface references.
|
1
|
+
"""Karrio Interface references.
|
2
|
+
|
3
|
+
This module provides references to carrier integrations, address validators,
|
4
|
+
and other plugin-related functionality in the Karrio system.
|
5
|
+
"""
|
6
|
+
|
7
|
+
import os
|
2
8
|
import attr
|
3
9
|
import pydoc
|
4
10
|
import typing
|
11
|
+
import logging
|
5
12
|
import pkgutil
|
13
|
+
import functools
|
6
14
|
|
7
15
|
import karrio.lib as lib
|
8
|
-
import karrio.mappers as mappers
|
9
16
|
import karrio.core.units as units
|
17
|
+
import karrio.core.plugins as plugins
|
10
18
|
import karrio.core.metadata as metadata
|
11
19
|
|
20
|
+
# Configure logger with a higher default level to reduce noise
|
21
|
+
ENABLE_ALL_PLUGINS_BY_DEFAULT = bool(
|
22
|
+
os.environ.get("ENABLE_ALL_PLUGINS_BY_DEFAULT", True)
|
23
|
+
)
|
24
|
+
logger = logging.getLogger(__name__)
|
25
|
+
if not logger.level:
|
26
|
+
logger.setLevel(logging.INFO)
|
12
27
|
|
13
|
-
|
14
|
-
|
15
|
-
|
28
|
+
# Global references - DO NOT RENAME (used for backward compatibility)
|
29
|
+
PROVIDERS: typing.Dict[str, metadata.PluginMetadata] = {}
|
30
|
+
ADDRESS_VALIDATORS: typing.Dict[str, metadata.PluginMetadata] = {}
|
31
|
+
MAPPERS: typing.Dict[str, typing.Any] = {}
|
32
|
+
SCHEMAS: typing.Dict[str, typing.Any] = {}
|
33
|
+
FAILED_IMPORTS: typing.Dict[str, typing.Any] = {}
|
34
|
+
PLUGIN_METADATA: typing.Dict[str, metadata.PluginMetadata] = {}
|
35
|
+
REFERENCES: typing.Dict[str, typing.Any] = {}
|
16
36
|
|
17
37
|
|
18
|
-
def import_extensions() ->
|
19
|
-
|
20
|
-
modules
|
21
|
-
name: __import__(f"{mappers.__name__}.{name}", fromlist=[name])
|
22
|
-
for _, name, _ in pkgutil.iter_modules(mappers.__path__)
|
23
|
-
}
|
38
|
+
def import_extensions() -> None:
|
39
|
+
"""
|
40
|
+
Import extensions from main modules and plugins.
|
24
41
|
|
25
|
-
|
26
|
-
|
27
|
-
|
42
|
+
This method collects carriers, address validators and mappers from
|
43
|
+
built-in modules and plugins through multiple discovery methods:
|
44
|
+
|
45
|
+
1. Directory-based plugins
|
46
|
+
2. Entrypoint-based plugins (setuptools entry_points)
|
47
|
+
3. Built-in modules
|
48
|
+
"""
|
49
|
+
global PROVIDERS, ADDRESS_VALIDATORS, MAPPERS, SCHEMAS, FAILED_IMPORTS, PLUGIN_METADATA, REFERENCES
|
50
|
+
# Reset collections
|
51
|
+
PROVIDERS = {}
|
52
|
+
ADDRESS_VALIDATORS = {}
|
53
|
+
MAPPERS = {}
|
54
|
+
SCHEMAS = {}
|
55
|
+
FAILED_IMPORTS = {}
|
56
|
+
PLUGIN_METADATA = {}
|
57
|
+
REFERENCES = {}
|
58
|
+
|
59
|
+
# Load plugins (if not already loaded)
|
60
|
+
plugins.load_local_plugins()
|
61
|
+
|
62
|
+
# Discover and import modules from directory-based plugins
|
63
|
+
plugin_modules = plugins.discover_plugin_modules()
|
64
|
+
metadata_dict, failed_metadata = plugins.collect_plugin_metadata(plugin_modules)
|
65
|
+
PLUGIN_METADATA.update(metadata_dict)
|
66
|
+
|
67
|
+
# Discover and import modules from entrypoint-based plugins
|
68
|
+
entrypoint_plugins = plugins.discover_entrypoint_plugins()
|
69
|
+
entrypoint_metadata, entrypoint_failed = plugins.collect_plugin_metadata(entrypoint_plugins)
|
70
|
+
PLUGIN_METADATA.update(entrypoint_metadata)
|
71
|
+
|
72
|
+
# Update failed imports
|
73
|
+
FAILED_IMPORTS.update(plugins.get_failed_plugin_modules())
|
74
|
+
for key, value in failed_metadata.items():
|
75
|
+
FAILED_IMPORTS[f"metadata.{key}"] = value
|
76
|
+
for key, value in entrypoint_failed.items():
|
77
|
+
FAILED_IMPORTS[f"entrypoint.metadata.{key}"] = value
|
78
|
+
|
79
|
+
# Process collected metadata to find carriers, validators, and mappers
|
80
|
+
for plugin_name, metadata_obj in PLUGIN_METADATA.items():
|
81
|
+
if not isinstance(metadata_obj, metadata.PluginMetadata):
|
82
|
+
logger.error(f"Invalid metadata type in {plugin_name}, expected PluginMetadata")
|
83
|
+
continue
|
84
|
+
|
85
|
+
# Process the plugin based on its capabilities
|
86
|
+
# Capabilities are now automatically determined from registered components
|
87
|
+
if metadata_obj.is_carrier_integration():
|
88
|
+
_register_carrier(metadata_obj, plugin_name)
|
89
|
+
|
90
|
+
if metadata_obj.is_address_validator():
|
91
|
+
_register_validator(metadata_obj)
|
92
|
+
|
93
|
+
# Import packages from karrio.plugins (new plugin architecture)
|
94
|
+
try:
|
95
|
+
import karrio.plugins
|
96
|
+
# Use pkgutil to find all modules within karrio.plugins
|
97
|
+
for _, name, ispkg in pkgutil.iter_modules(karrio.plugins.__path__):
|
98
|
+
if name.startswith('_'):
|
99
|
+
continue
|
100
|
+
try:
|
101
|
+
module = __import__(f"karrio.plugins.{name}", fromlist=[name])
|
102
|
+
metadata_obj = getattr(module, 'METADATA', None)
|
103
|
+
if metadata_obj and isinstance(metadata_obj, metadata.PluginMetadata):
|
104
|
+
# Add to PLUGIN_METADATA so it's accessible via plugin management interfaces
|
105
|
+
PLUGIN_METADATA[name] = metadata_obj
|
106
|
+
if metadata_obj.is_carrier_integration():
|
107
|
+
_register_carrier(metadata_obj, name)
|
108
|
+
if metadata_obj.is_address_validator():
|
109
|
+
_register_validator(metadata_obj)
|
110
|
+
except (AttributeError, ImportError) as e:
|
111
|
+
logger.error(f"Failed to import plugin {name}: {str(e)}")
|
112
|
+
except ImportError:
|
113
|
+
logger.error("Could not import karrio.plugins")
|
114
|
+
|
115
|
+
# Import carriers from built-in karrio mappers (legacy approach for backward compatibility)
|
116
|
+
try:
|
117
|
+
import karrio.mappers
|
118
|
+
# Use pkgutil to find all modules within karrio.mappers
|
119
|
+
for _, name, ispkg in pkgutil.iter_modules(karrio.mappers.__path__):
|
120
|
+
if name.startswith('_'):
|
121
|
+
continue
|
122
|
+
try:
|
123
|
+
module = __import__(f"karrio.mappers.{name}", fromlist=[name])
|
124
|
+
metadata_obj = getattr(module, 'METADATA', None)
|
125
|
+
if metadata_obj and isinstance(metadata_obj, metadata.PluginMetadata):
|
126
|
+
# Add to PLUGIN_METADATA so it's accessible via plugin management interfaces
|
127
|
+
PLUGIN_METADATA[name] = metadata_obj
|
128
|
+
_register_carrier(metadata_obj, name)
|
129
|
+
except (AttributeError, ImportError) as e:
|
130
|
+
logger.error(f"Failed to import mapper {name}: {str(e)}")
|
131
|
+
except ImportError:
|
132
|
+
logger.error("Could not import karrio.mappers")
|
133
|
+
|
134
|
+
# Import address validators from built-in modules
|
135
|
+
try:
|
136
|
+
import karrio.validators
|
137
|
+
_import_validators_from_module(karrio.validators)
|
138
|
+
except ImportError:
|
139
|
+
logger.error("Could not import karrio.validators")
|
140
|
+
|
141
|
+
# Import carriers from built-in modules
|
142
|
+
try:
|
143
|
+
import karrio.providers
|
144
|
+
for provider_name in dir(karrio.providers):
|
145
|
+
if provider_name.startswith('_'):
|
146
|
+
continue
|
147
|
+
try:
|
148
|
+
provider = getattr(karrio.providers, provider_name)
|
149
|
+
metadata_obj = getattr(provider, 'METADATA', None)
|
150
|
+
if metadata_obj and isinstance(metadata_obj, metadata.PluginMetadata):
|
151
|
+
# Add to PLUGIN_METADATA so it's accessible via plugin management interfaces
|
152
|
+
PLUGIN_METADATA[provider_name] = metadata_obj
|
153
|
+
_register_carrier(metadata_obj, provider_name)
|
154
|
+
except (AttributeError, ImportError) as e:
|
155
|
+
logger.error(f"Failed to import provider {provider_name}: {str(e)}")
|
156
|
+
continue
|
157
|
+
except ImportError:
|
158
|
+
logger.error("Could not import karrio.providers")
|
159
|
+
|
160
|
+
# Sort PLUGIN_METADATA and PROVIDERS alphabetically by their keys
|
161
|
+
PLUGIN_METADATA = dict(sorted(PLUGIN_METADATA.items()))
|
162
|
+
PROVIDERS = dict(sorted(PROVIDERS.items()))
|
163
|
+
|
164
|
+
logger.info(f"> Loaded {len(PLUGIN_METADATA)} plugins")
|
165
|
+
|
166
|
+
|
167
|
+
def _import_validators_from_module(module):
|
168
|
+
"""
|
169
|
+
Import validators from a module by looking for METADATA in submodules.
|
170
|
+
"""
|
171
|
+
for validator_name in dir(module):
|
172
|
+
if validator_name.startswith('_'):
|
173
|
+
continue
|
174
|
+
try:
|
175
|
+
validator_module = getattr(module, validator_name)
|
176
|
+
metadata_obj = getattr(validator_module, 'METADATA', None)
|
177
|
+
if metadata_obj and isinstance(metadata_obj, metadata.PluginMetadata):
|
178
|
+
# Add to PLUGIN_METADATA so it's accessible via plugin management interfaces
|
179
|
+
PLUGIN_METADATA[validator_name] = metadata_obj
|
180
|
+
_register_validator(metadata_obj)
|
181
|
+
except (AttributeError, ImportError) as e:
|
182
|
+
logger.error(f"Failed to import validator {validator_name}: {str(e)}")
|
183
|
+
continue
|
184
|
+
|
185
|
+
|
186
|
+
def _register_carrier(metadata_obj: metadata.PluginMetadata, carrier_name: str) -> None:
|
187
|
+
"""
|
188
|
+
Register a carrier from its metadata.
|
189
|
+
|
190
|
+
This adds the carrier to providers and imports any mappers/schemas.
|
191
|
+
|
192
|
+
Args:
|
193
|
+
metadata_obj: The carrier plugin metadata
|
194
|
+
carrier_name: The name of the carrier
|
195
|
+
"""
|
196
|
+
carrier_id = metadata_obj.get("id")
|
197
|
+
|
198
|
+
if not carrier_id:
|
199
|
+
logger.error(f"Carrier metadata missing ID")
|
200
|
+
return
|
201
|
+
|
202
|
+
if not hasattr(metadata_obj, 'Mapper') or not metadata_obj.Mapper:
|
203
|
+
logger.error(f"Carrier {carrier_id} has no Mapper defined")
|
204
|
+
return
|
205
|
+
|
206
|
+
# Register carrier
|
207
|
+
PROVIDERS[carrier_id] = metadata_obj
|
208
|
+
|
209
|
+
# Register mapper
|
210
|
+
MAPPERS[carrier_id] = metadata_obj.Mapper
|
28
211
|
|
212
|
+
# Register schemas if available
|
213
|
+
if hasattr(metadata_obj, 'Settings'):
|
214
|
+
SCHEMAS[carrier_id] = metadata_obj.Settings
|
215
|
+
|
216
|
+
|
217
|
+
def _register_validator(metadata_obj: metadata.PluginMetadata) -> None:
|
218
|
+
"""
|
219
|
+
Register an address validator from its metadata.
|
220
|
+
|
221
|
+
Args:
|
222
|
+
metadata_obj: The validator plugin metadata
|
223
|
+
"""
|
224
|
+
validator_id = metadata_obj.get("id")
|
225
|
+
|
226
|
+
if not validator_id:
|
227
|
+
logger.error(f"Validator metadata missing ID")
|
228
|
+
return
|
229
|
+
|
230
|
+
if not hasattr(metadata_obj, 'Validator') or not metadata_obj.Validator:
|
231
|
+
logger.error(f"Address validator {validator_id} has no Validator defined")
|
232
|
+
return
|
233
|
+
|
234
|
+
# Register address validator
|
235
|
+
ADDRESS_VALIDATORS[validator_id] = metadata_obj
|
236
|
+
|
237
|
+
|
238
|
+
@functools.lru_cache(maxsize=1)
|
239
|
+
def get_providers() -> typing.Dict[str, metadata.PluginMetadata]:
|
240
|
+
"""
|
241
|
+
Get all available provider metadata.
|
242
|
+
|
243
|
+
Returns:
|
244
|
+
Dictionary of carrier ID to carrier metadata
|
245
|
+
"""
|
29
246
|
return PROVIDERS
|
30
247
|
|
31
248
|
|
32
|
-
|
33
|
-
|
34
|
-
|
249
|
+
@functools.lru_cache(maxsize=1)
|
250
|
+
def get_address_validators() -> typing.Dict[str, metadata.PluginMetadata]:
|
251
|
+
"""
|
252
|
+
Get all available address validator metadata.
|
253
|
+
|
254
|
+
Returns:
|
255
|
+
Dictionary of validator ID to validator metadata
|
256
|
+
"""
|
257
|
+
return ADDRESS_VALIDATORS
|
258
|
+
|
259
|
+
|
260
|
+
@functools.lru_cache(maxsize=1)
|
261
|
+
def get_mappers() -> typing.Dict[str, typing.Any]:
|
262
|
+
"""
|
263
|
+
Get all available carrier mappers.
|
264
|
+
|
265
|
+
Returns:
|
266
|
+
Dictionary of carrier ID to mapper class
|
267
|
+
"""
|
268
|
+
return MAPPERS
|
269
|
+
|
270
|
+
|
271
|
+
@functools.lru_cache(maxsize=1)
|
272
|
+
def get_schemas() -> typing.Dict[str, typing.Any]:
|
273
|
+
"""
|
274
|
+
Get all available carrier settings schemas.
|
275
|
+
|
276
|
+
Returns:
|
277
|
+
Dictionary of carrier ID to settings schema
|
278
|
+
"""
|
279
|
+
return SCHEMAS
|
280
|
+
|
281
|
+
|
282
|
+
@functools.lru_cache(maxsize=1)
|
283
|
+
def get_failed_imports() -> typing.Dict[str, typing.Any]:
|
284
|
+
"""
|
285
|
+
Get information about import failures.
|
286
|
+
|
287
|
+
Returns:
|
288
|
+
Dictionary containing error information for failed imports
|
289
|
+
"""
|
290
|
+
return FAILED_IMPORTS
|
291
|
+
|
292
|
+
|
293
|
+
def get_plugin_metadata() -> typing.Dict[str, metadata.PluginMetadata]:
|
294
|
+
"""
|
295
|
+
Get metadata for all discovered plugins.
|
296
|
+
|
297
|
+
Returns:
|
298
|
+
Dictionary of plugin name to plugin metadata
|
299
|
+
"""
|
300
|
+
return PLUGIN_METADATA
|
301
|
+
|
302
|
+
|
303
|
+
def collect_plugins_data() -> typing.Dict[str, dict]:
|
304
|
+
"""
|
305
|
+
Collect metadata for all plugins.
|
306
|
+
|
307
|
+
Returns:
|
308
|
+
Dict mapping plugin names to their metadata as dictionaries
|
309
|
+
"""
|
310
|
+
if not PLUGIN_METADATA:
|
35
311
|
import_extensions()
|
36
312
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
packaging_types=units.PackagingUnit,
|
41
|
-
options=units.ShippingOption,
|
42
|
-
),
|
43
|
-
**{
|
44
|
-
carrier_name: attr.asdict(metadata)
|
45
|
-
for carrier_name, metadata in PROVIDERS.items()
|
46
|
-
},
|
313
|
+
return {
|
314
|
+
plugin_name: attr.asdict(plugin_metadata)
|
315
|
+
for plugin_name, plugin_metadata in PLUGIN_METADATA.items()
|
47
316
|
}
|
48
317
|
|
49
|
-
|
318
|
+
|
319
|
+
def collect_failed_plugins_data() -> typing.Dict[str, dict]:
|
320
|
+
"""
|
321
|
+
Collect information about plugins that failed to load.
|
322
|
+
|
323
|
+
Returns:
|
324
|
+
Dict mapping plugin names to error information
|
325
|
+
"""
|
326
|
+
if not PLUGIN_METADATA:
|
327
|
+
import_extensions()
|
328
|
+
|
329
|
+
return FAILED_IMPORTS
|
330
|
+
|
331
|
+
|
332
|
+
def collect_providers_data() -> typing.Dict[str, metadata.PluginMetadata]:
|
333
|
+
"""
|
334
|
+
Collect metadata for carrier integration plugins.
|
335
|
+
|
336
|
+
Returns:
|
337
|
+
Dict mapping carrier names to their metadata as dictionaries
|
338
|
+
"""
|
339
|
+
if not PROVIDERS:
|
340
|
+
import_extensions()
|
341
|
+
|
342
|
+
return {
|
343
|
+
carrier_name: metadata_obj
|
344
|
+
for carrier_name, metadata_obj in PROVIDERS.items()
|
345
|
+
}
|
346
|
+
|
347
|
+
|
348
|
+
def collect_address_validators_data() -> typing.Dict[str, dict]:
|
349
|
+
"""
|
350
|
+
Collect address validator metadata from loaded validators.
|
351
|
+
|
352
|
+
Returns:
|
353
|
+
Dict mapping validator names to their metadata
|
354
|
+
"""
|
355
|
+
if not ADDRESS_VALIDATORS:
|
356
|
+
import_extensions()
|
357
|
+
|
358
|
+
return {
|
359
|
+
validator_name: attr.asdict(metadata_obj)
|
360
|
+
for validator_name, metadata_obj in ADDRESS_VALIDATORS.items()
|
361
|
+
}
|
50
362
|
|
51
363
|
|
52
364
|
def detect_capabilities(proxy_methods: typing.List[str]) -> typing.List[str]:
|
53
|
-
|
365
|
+
"""
|
366
|
+
Map proxy methods to carrier capabilities.
|
54
367
|
|
55
|
-
|
368
|
+
Args:
|
369
|
+
proxy_methods: List of method names from a Proxy class
|
370
|
+
|
371
|
+
Returns:
|
372
|
+
List of capability identifiers
|
373
|
+
"""
|
374
|
+
return list(set([units.CarrierCapabilities.map_capability(prop) for prop in proxy_methods]))
|
56
375
|
|
57
376
|
|
58
377
|
def detect_proxy_methods(proxy_type: object) -> typing.List[str]:
|
378
|
+
"""
|
379
|
+
Extract all public methods from a proxy type.
|
380
|
+
|
381
|
+
Args:
|
382
|
+
proxy_type: A Proxy class
|
383
|
+
|
384
|
+
Returns:
|
385
|
+
List of method names
|
386
|
+
"""
|
59
387
|
return [
|
60
388
|
prop
|
61
389
|
for prop in proxy_type.__dict__.keys()
|
62
390
|
if "_" not in prop[0] and prop != "settings"
|
63
391
|
]
|
64
392
|
|
393
|
+
# Fields to exclude when collecting connection fields
|
394
|
+
COMMON_FIELDS = ["id", "carrier_id", "test_mode", "carrier_name"]
|
395
|
+
|
396
|
+
|
397
|
+
def collect_references(
|
398
|
+
plugin_registry: dict = None,
|
399
|
+
) -> dict:
|
400
|
+
"""
|
401
|
+
Collect all references from carriers, validators, and plugins.
|
402
|
+
|
403
|
+
This function builds a comprehensive dictionary of all available
|
404
|
+
references in the system, including services, options, countries,
|
405
|
+
currencies, carriers, etc.
|
406
|
+
|
407
|
+
Returns:
|
408
|
+
Dictionary containing all reference data
|
409
|
+
"""
|
410
|
+
global REFERENCES, PROVIDERS
|
411
|
+
if not PROVIDERS:
|
412
|
+
import_extensions()
|
413
|
+
|
414
|
+
# If references have already been computed, return them
|
415
|
+
if REFERENCES and not plugin_registry:
|
416
|
+
return REFERENCES
|
65
417
|
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
418
|
+
registry = Registry(plugin_registry)
|
419
|
+
|
420
|
+
# Determine enabled carriers
|
421
|
+
enabled_carrier_ids = set(
|
422
|
+
carrier_id for carrier_id in PROVIDERS.keys()
|
423
|
+
if registry.get(f"{carrier_id.upper()}_ENABLED", registry.get("ENABLE_ALL_PLUGINS_BY_DEFAULT"))
|
424
|
+
)
|
70
425
|
|
71
426
|
services = {
|
72
|
-
key: {c.name: c.value for c in list(mapper
|
73
|
-
for key, mapper in
|
74
|
-
if mapper.get("services") is not None
|
427
|
+
key: {c.name: c.value for c in list(mapper.get("services", []))}
|
428
|
+
for key, mapper in PROVIDERS.items()
|
429
|
+
if key in enabled_carrier_ids and mapper.get("services") is not None
|
75
430
|
}
|
76
431
|
options = {
|
77
|
-
key: {c.name: dict(code=c.value.code) for c in list(mapper
|
78
|
-
for key, mapper in
|
79
|
-
if mapper.get("options") is not None
|
432
|
+
key: {c.name: dict(code=c.value.code, type=parse_type(c.value.type), default=c.value.default) for c in list(mapper.get("options", []))}
|
433
|
+
for key, mapper in PROVIDERS.items()
|
434
|
+
if key in enabled_carrier_ids and mapper.get("options") is not None
|
80
435
|
}
|
81
436
|
connection_configs = {
|
82
|
-
key: {
|
83
|
-
|
84
|
-
|
437
|
+
key: {
|
438
|
+
c.name: lib.to_dict(
|
439
|
+
dict(
|
440
|
+
name=c.name,
|
441
|
+
code=c.value.code,
|
442
|
+
required=False,
|
443
|
+
type=parse_type(c.value.type),
|
444
|
+
default=c.value.default,
|
445
|
+
enum=lib.identity(
|
446
|
+
None
|
447
|
+
if "enum" not in str(c.value.type).lower()
|
448
|
+
else [c.name for c in c.value.type]
|
449
|
+
),
|
450
|
+
)
|
451
|
+
)
|
452
|
+
for c in list(mapper.get("connection_configs", []))
|
453
|
+
}
|
454
|
+
for key, mapper in PROVIDERS.items()
|
455
|
+
if key in enabled_carrier_ids and mapper.get("connection_configs") is not None
|
456
|
+
}
|
457
|
+
|
458
|
+
# Build connection_fields with proper attrs class checking
|
459
|
+
connection_fields = {
|
460
|
+
key: {
|
461
|
+
_.name: lib.to_dict(
|
462
|
+
dict(
|
463
|
+
name=_.name,
|
464
|
+
type=parse_type(_.type),
|
465
|
+
required="NOTHING" in str(_.default),
|
466
|
+
default=lib.identity(
|
467
|
+
lib.to_dict(lib.to_json(_.default))
|
468
|
+
if ("NOTHING" not in str(_.default))
|
469
|
+
else None
|
470
|
+
),
|
471
|
+
enum=lib.identity(
|
472
|
+
None
|
473
|
+
if "enum" not in str(_.type).lower()
|
474
|
+
else [c.name for c in _.type]
|
475
|
+
),
|
476
|
+
)
|
477
|
+
)
|
478
|
+
for _ in getattr(mapper.get("Settings"), "__attrs_attrs__", [])
|
479
|
+
if (_.name not in COMMON_FIELDS)
|
480
|
+
or (mapper.get("has_intl_accounts") and _.name == "account_country_code")
|
481
|
+
} if mapper.get("Settings") is not None and hasattr(mapper.get("Settings"), "__attrs_attrs__") else {}
|
482
|
+
for key, mapper in PROVIDERS.items()
|
483
|
+
if key in enabled_carrier_ids
|
85
484
|
}
|
86
485
|
|
87
486
|
REFERENCES = {
|
88
|
-
"countries": {c.name: c.value for c in list(units.Country)},
|
89
|
-
"currencies": {c.name: c.value for c in list(units.Currency)},
|
90
|
-
"weight_units": {c.name: c.value for c in list(units.WeightUnit)},
|
91
|
-
"dimension_units": {c.name: c.value for c in list(units.DimensionUnit)},
|
487
|
+
"countries": {c.name: c.value for c in list(units.Country)}, # type: ignore
|
488
|
+
"currencies": {c.name: c.value for c in list(units.Currency)}, # type: ignore
|
489
|
+
"weight_units": {c.name: c.value for c in list(units.WeightUnit)}, # type: ignore
|
490
|
+
"dimension_units": {c.name: c.value for c in list(units.DimensionUnit)}, # type: ignore
|
92
491
|
"states": {
|
93
|
-
c.name: {s.name: s.value for s in list(c.value)}
|
94
|
-
for c in list(units.CountryState)
|
492
|
+
c.name: {s.name: s.value for s in list(c.value)} # type: ignore
|
493
|
+
for c in list(units.CountryState) # type: ignore
|
95
494
|
},
|
96
|
-
"payment_types": {c.name: c.value for c in list(units.PaymentType)},
|
495
|
+
"payment_types": {c.name: c.value for c in list(units.PaymentType)}, # type: ignore
|
97
496
|
"customs_content_type": {
|
98
|
-
c.name: c.value for c in list(units.CustomsContentType)
|
497
|
+
c.name: c.value for c in list(units.CustomsContentType) # type: ignore
|
99
498
|
},
|
100
|
-
"incoterms": {c.name: c.value for c in list(units.Incoterm)},
|
499
|
+
"incoterms": {c.name: c.value for c in list(units.Incoterm)}, # type: ignore
|
101
500
|
"carriers": {
|
102
|
-
|
501
|
+
carrier_id: metadata_obj.label for carrier_id, metadata_obj in PROVIDERS.items() if carrier_id in enabled_carrier_ids
|
103
502
|
},
|
104
503
|
"carrier_hubs": {
|
105
|
-
|
106
|
-
for
|
107
|
-
if
|
504
|
+
carrier_id: metadata_obj.label
|
505
|
+
for carrier_id, metadata_obj in PROVIDERS.items()
|
506
|
+
if carrier_id in enabled_carrier_ids and metadata_obj.is_hub
|
507
|
+
},
|
508
|
+
"address_validators": {
|
509
|
+
validator_id: metadata_obj.get("label", "")
|
510
|
+
for validator_id, metadata_obj in collect_address_validators_data().items()
|
108
511
|
},
|
109
512
|
"services": services,
|
110
513
|
"options": options,
|
514
|
+
"connection_fields": connection_fields,
|
111
515
|
"connection_configs": connection_configs,
|
112
516
|
"carrier_capabilities": {
|
113
|
-
key: detect_capabilities(detect_proxy_methods(mapper
|
114
|
-
for key, mapper in
|
115
|
-
if mapper.get("Proxy") is not None
|
517
|
+
key: detect_capabilities(detect_proxy_methods(mapper.get("Proxy")))
|
518
|
+
for key, mapper in PROVIDERS.items()
|
519
|
+
if key in enabled_carrier_ids and mapper.get("Proxy") is not None
|
116
520
|
},
|
117
521
|
"packaging_types": {
|
118
|
-
key: {c.name: c.value for c in list(mapper
|
119
|
-
for key, mapper in
|
120
|
-
if mapper.get("packaging_types") is not None
|
522
|
+
key: {c.name: c.value for c in list(mapper.get("packaging_types", []))}
|
523
|
+
for key, mapper in PROVIDERS.items()
|
524
|
+
if key in enabled_carrier_ids and mapper.get("packaging_types") is not None
|
121
525
|
},
|
122
526
|
"package_presets": {
|
123
|
-
key: {c.name: lib.to_dict(c.value) for c in list(mapper
|
124
|
-
for key, mapper in
|
125
|
-
if mapper.get("package_presets") is not None
|
527
|
+
key: {c.name: lib.to_dict(c.value) for c in list(mapper.get("package_presets", []))}
|
528
|
+
for key, mapper in PROVIDERS.items()
|
529
|
+
if key in enabled_carrier_ids and mapper.get("package_presets") is not None
|
126
530
|
},
|
127
531
|
"option_names": {
|
128
532
|
name: {key: key.upper().replace("_", " ") for key, _ in value.items()}
|
@@ -134,15 +538,196 @@ def collect_references() -> dict:
|
|
134
538
|
},
|
135
539
|
"service_levels": {
|
136
540
|
key: lib.to_dict(mapper.get("service_levels"))
|
137
|
-
for key, mapper in
|
138
|
-
if mapper.get("service_levels") is not None
|
541
|
+
for key, mapper in PROVIDERS.items()
|
542
|
+
if key in enabled_carrier_ids and mapper.get("service_levels") is not None
|
543
|
+
},
|
544
|
+
"integration_status": {
|
545
|
+
carrier_id: metadata_obj.status for carrier_id, metadata_obj in PROVIDERS.items() if carrier_id in enabled_carrier_ids
|
139
546
|
},
|
547
|
+
"address_validator_details": {
|
548
|
+
validator_id: {
|
549
|
+
"id": validator_id,
|
550
|
+
"provider": validator_id,
|
551
|
+
"display_name": metadata_obj.get("label", ""),
|
552
|
+
"integration_status": metadata_obj.get("status", ""),
|
553
|
+
"website": metadata_obj.get("website", ""),
|
554
|
+
"description": metadata_obj.get("description", ""),
|
555
|
+
"documentation": metadata_obj.get("documentation", ""),
|
556
|
+
"readme": metadata_obj.get("readme", ""),
|
557
|
+
}
|
558
|
+
for validator_id, metadata_obj in collect_address_validators_data().items()
|
559
|
+
},
|
560
|
+
"plugins": {
|
561
|
+
name: {
|
562
|
+
"id": metadata_obj.get("id", ""),
|
563
|
+
"name": name,
|
564
|
+
"display_name": metadata_obj.get("label", ""),
|
565
|
+
"integration_status": metadata_obj.get("status", ""),
|
566
|
+
"website": metadata_obj.get("website", ""),
|
567
|
+
"description": metadata_obj.get("description", ""),
|
568
|
+
"documentation": metadata_obj.get("documentation", ""),
|
569
|
+
"readme": metadata_obj.get("readme", ""),
|
570
|
+
"type": metadata_obj.plugin_type,
|
571
|
+
"types": metadata_obj.plugin_types,
|
572
|
+
"is_dual_purpose": metadata_obj.is_dual_purpose()
|
573
|
+
}
|
574
|
+
for name, metadata_obj in PLUGIN_METADATA.items()
|
575
|
+
},
|
576
|
+
"failed_plugins": collect_failed_plugins_data(),
|
140
577
|
}
|
141
578
|
|
579
|
+
logger.info(f"> Karrio references loaded. {len(PLUGIN_METADATA.keys())} plugins")
|
142
580
|
return REFERENCES
|
143
581
|
|
144
582
|
|
145
583
|
def get_carrier_capabilities(carrier_name) -> typing.List[str]:
|
584
|
+
"""
|
585
|
+
Get the capabilities of a specific carrier.
|
586
|
+
|
587
|
+
Args:
|
588
|
+
carrier_name: The name of the carrier
|
589
|
+
|
590
|
+
Returns:
|
591
|
+
List of capability identifiers
|
592
|
+
"""
|
146
593
|
proxy_class = pydoc.locate(f"karrio.mappers.{carrier_name}.Proxy")
|
147
594
|
proxy_methods = detect_proxy_methods(proxy_class)
|
148
595
|
return detect_capabilities(proxy_methods)
|
596
|
+
|
597
|
+
|
598
|
+
def parse_type(_type: type) -> str:
|
599
|
+
"""
|
600
|
+
Parse a Python type into a string representation.
|
601
|
+
|
602
|
+
Args:
|
603
|
+
_type: Python type object
|
604
|
+
|
605
|
+
Returns:
|
606
|
+
String representation of the type
|
607
|
+
"""
|
608
|
+
_name = getattr(_type, "__name__", None)
|
609
|
+
|
610
|
+
if _name is not None and _name == "bool":
|
611
|
+
return "boolean"
|
612
|
+
if _name is not None and _name == "str":
|
613
|
+
return "string"
|
614
|
+
if _name is not None and (_name == "int" or "to_int" in _name):
|
615
|
+
return "integer"
|
616
|
+
if _name is not None and _name == "float":
|
617
|
+
return "float"
|
618
|
+
if _name is not None and "money" in _name:
|
619
|
+
return "float"
|
620
|
+
if "Address" in str(_type):
|
621
|
+
return "Address"
|
622
|
+
if "enum" in str(_type):
|
623
|
+
return "string"
|
624
|
+
if _name is not None and ("list" in _name or "List" in _name):
|
625
|
+
return "list"
|
626
|
+
if _name is not None and ("dict" in _name or "Dict" in _name):
|
627
|
+
return "object"
|
628
|
+
|
629
|
+
return str(_type)
|
630
|
+
|
631
|
+
|
632
|
+
def get_carrier_details(
|
633
|
+
plugin_code: str,
|
634
|
+
contextual_reference: dict = None,
|
635
|
+
plugin_registry: dict = None,
|
636
|
+
) -> dict:
|
637
|
+
"""
|
638
|
+
Get detailed information about a carrier.
|
639
|
+
|
640
|
+
Args:
|
641
|
+
carrier_id: The ID of the carrier
|
642
|
+
contextual_reference: Optional pre-computed references dictionary
|
643
|
+
plugin_registry: Optional plugin registry dictionary
|
644
|
+
Returns:
|
645
|
+
Dictionary with detailed carrier information
|
646
|
+
"""
|
647
|
+
metadata_obj: metadata.PluginMetadata = collect_providers_data().get(plugin_code)
|
648
|
+
references = contextual_reference or collect_references()
|
649
|
+
registry = Registry(plugin_registry)
|
650
|
+
|
651
|
+
return dict(
|
652
|
+
id=plugin_code,
|
653
|
+
carrier_name=plugin_code,
|
654
|
+
display_name=getattr(metadata_obj, "label", ""),
|
655
|
+
integration_status=getattr(metadata_obj, "status", ""),
|
656
|
+
website=getattr(metadata_obj, "website", ""),
|
657
|
+
description=getattr(metadata_obj, "description", ""),
|
658
|
+
documentation=getattr(metadata_obj, "documentation", ""),
|
659
|
+
is_enabled=registry.get(f"{plugin_code.upper()}_ENABLED", registry.get("ENABLE_ALL_PLUGINS_BY_DEFAULT")),
|
660
|
+
capabilities=references["carrier_capabilities"].get(plugin_code, {}),
|
661
|
+
connection_fields=references["connection_fields"].get(plugin_code, {}),
|
662
|
+
config_fields=references["connection_configs"].get(plugin_code, {}),
|
663
|
+
shipping_services=references["services"].get(plugin_code, {}),
|
664
|
+
shipping_options=references["options"].get(plugin_code, {}),
|
665
|
+
readme=metadata_obj.readme,
|
666
|
+
)
|
667
|
+
|
668
|
+
|
669
|
+
def get_validator_details(validator_id: str, contextual_reference: dict = None) -> dict:
|
670
|
+
"""
|
671
|
+
Get detailed information about an address validator plugin.
|
672
|
+
|
673
|
+
Args:
|
674
|
+
validator_id: The ID of the validator plugin
|
675
|
+
contextual_reference: Optional pre-computed references dictionary
|
676
|
+
|
677
|
+
Returns:
|
678
|
+
Dictionary with detailed validator information
|
679
|
+
"""
|
680
|
+
references = contextual_reference or collect_references()
|
681
|
+
return references["address_validator_details"].get(validator_id, {})
|
682
|
+
|
683
|
+
|
684
|
+
def get_plugin_details(plugin_name: str, contextual_reference: dict = None) -> dict:
|
685
|
+
"""
|
686
|
+
Get detailed information about any plugin by name.
|
687
|
+
|
688
|
+
Args:
|
689
|
+
plugin_name: The name of the plugin
|
690
|
+
contextual_reference: Optional pre-computed references dictionary
|
691
|
+
|
692
|
+
Returns:
|
693
|
+
Dictionary with detailed plugin information
|
694
|
+
"""
|
695
|
+
references = contextual_reference or collect_references()
|
696
|
+
return references["plugins"].get(plugin_name, {})
|
697
|
+
|
698
|
+
|
699
|
+
class Registry(dict):
|
700
|
+
def __init__(self, registry: typing.Any = None):
|
701
|
+
self.registry = registry
|
702
|
+
self._config_loaded = False
|
703
|
+
|
704
|
+
def _ensure_config_loaded(self):
|
705
|
+
if not self._config_loaded and self.registry is None:
|
706
|
+
try:
|
707
|
+
from constance import config
|
708
|
+
# Don't access config.ENABLE_ALL_PLUGINS_BY_DEFAULT here
|
709
|
+
# Just store the config object
|
710
|
+
self.registry = config
|
711
|
+
self._config_loaded = True
|
712
|
+
except Exception:
|
713
|
+
self.registry = {}
|
714
|
+
self._config_loaded = True
|
715
|
+
|
716
|
+
def get(self, key, default=None):
|
717
|
+
self._ensure_config_loaded()
|
718
|
+
try:
|
719
|
+
if isinstance(self.registry, dict):
|
720
|
+
return self.registry.get(key, os.environ.get(key, default))
|
721
|
+
else:
|
722
|
+
return getattr(self.registry, key, os.environ.get(key, default))
|
723
|
+
except Exception:
|
724
|
+
return os.environ.get(key, default)
|
725
|
+
|
726
|
+
def __setitem__(self, key: str, value: typing.Any):
|
727
|
+
try:
|
728
|
+
if isinstance(self.registry, dict):
|
729
|
+
self.registry[key] = value
|
730
|
+
else:
|
731
|
+
setattr(self.registry, key, value)
|
732
|
+
except Exception as e:
|
733
|
+
logger.error(f"Failed to set item {key} in registry: {e}")
|