karrio 2023.5.1__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/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
- PROVIDERS = None
14
- PROVIDERS_DATA = None
15
- REFERENCES = None
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() -> typing.Dict[str, metadata.Metadata]:
19
- global PROVIDERS
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
- PROVIDERS = {
26
- carrier_name: module.METADATA for carrier_name, module in modules.items()
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
- def collect_providers_data() -> typing.Dict[str, dict]:
33
- global PROVIDERS_DATA
34
- if PROVIDERS is None:
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
- PROVIDERS_DATA = {
38
- "universal": dict(
39
- label="Multi-carrier (karrio)",
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
- return PROVIDERS_DATA
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
- r = set([units.CarrierCapabilities.map_capability(prop) for prop in proxy_methods])
365
+ """
366
+ Map proxy methods to carrier capabilities.
54
367
 
55
- return list(r)
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
- def collect_references() -> dict:
67
- global REFERENCES
68
- if PROVIDERS_DATA is None:
69
- collect_providers_data()
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["services"])} # type: ignore
73
- for key, mapper in PROVIDERS_DATA.items()
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["options"])} # type: ignore
78
- for key, mapper in PROVIDERS_DATA.items()
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: {c.name: dict(code=c.value.code) for c in list(mapper["connection_configs"])} # type: ignore
83
- for key, mapper in PROVIDERS_DATA.items()
84
- if mapper.get("connection_configs") is not None
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
- carrier_name: metadata.label for carrier_name, metadata in PROVIDERS.items()
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
- carrier_name: metadata.label
106
- for carrier_name, metadata in PROVIDERS.items()
107
- if metadata.is_hub
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["Proxy"]))
114
- for key, mapper in PROVIDERS_DATA.items()
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["packaging_types"])} # type: ignore
119
- for key, mapper in PROVIDERS_DATA.items()
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["package_presets"])} # type: ignore
124
- for key, mapper in PROVIDERS_DATA.items()
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 PROVIDERS_DATA.items()
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}")