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/__init__.py CHANGED
@@ -1,101 +1 @@
1
- """Karrio package root module
2
-
3
- Karrio makes shipping carrier webservices integration easy by providing
4
- a modern and dev friendly library and by providing a unified extensible API
5
- to communicate with all supported carriers.
6
-
7
- -- Speak Karrio, Speak carriers...
8
-
9
- Example:
10
- Examples can be given using either the ``Example`` or ``Examples``
11
- sections. Sections support any reStructuredText formatting, including
12
- literal blocks::
13
-
14
- >>> import karrio
15
- >>> from karrio.core.utils import DP
16
- >>> from karrio.core.models import Address, Parcel, RateRequest
17
- >>> canadapost = karrio.gateway["canadapost"].create(
18
- ... {
19
- ... "username": "username",
20
- ... "password": "password",
21
- ... "customer_number": "123456789",
22
- ... "test": True
23
- ... }
24
- ... )
25
- >>> shipper = Address(
26
- ... postal_code= "V6M2V9",
27
- ... city= "Vancouver",
28
- ... country_code= "CA",
29
- ... state_code= "BC",
30
- ... address_line1= "5840 Oak St"
31
- ... )
32
- >>> recipient = Address(
33
- ... postal_code= "E1C4Z8",
34
- ... city= "Moncton",
35
- ... country_code= "CA",
36
- ... state_code= "NB",
37
- ... residential= False,
38
- ... address_line1= "125 Church St"
39
- ... )
40
- >>> parcel = Parcel(
41
- ... height= 3.0,
42
- ... length= 6.0,
43
- ... width= 3.0,
44
- ... weight= 0.5
45
- ... )
46
- >>> request = karrio.Rating.fetch(
47
- ... RateRequest(
48
- ... shipper=shipper,
49
- ... recipient=recipient,
50
- ... parcels=[parcel],
51
- ... services=["canadapost_priority"]
52
- ... )
53
- ... )
54
- >>> rates = request.from_(canadapost).parse()
55
- >>> print(DP.to_dict(rates))
56
- [
57
- [],
58
- [
59
- {
60
- "base_charge": 12.26,
61
- "carrier_name": "canadapost",
62
- "carrier_id": "canadapost",
63
- "currency": "CAD",
64
- "transit_days": 2,
65
- "extra_charges": [
66
- {"amount": -0.37, "currency": "CAD", "name": "Automation discount"},
67
- {"amount": 1.75, "currency": "CAD", "name": "Fuel surcharge"}
68
- ],
69
- "service": "canadapost_xpresspost",
70
- "total_charge": 13.64
71
- }
72
- ]
73
- ]
74
-
75
- Attributes:
76
- gateway (GatewayInitializer): Gateway initializer singleton instance
77
-
78
- """
79
1
  __path__ = __import__("pkgutil").extend_path(__path__, __name__) # type: ignore
80
-
81
- from karrio.api.gateway import GatewayInitializer
82
- import karrio.api.interface as interface
83
-
84
- gateway = GatewayInitializer.get_instance()
85
- Pickup = interface.Pickup
86
- Rating = interface.Rating
87
- Shipment = interface.Shipment
88
- Tracking = interface.Tracking
89
- Address = interface.Address
90
- Document = interface.Document
91
-
92
-
93
- __all__ = [
94
- "gateway",
95
- "Pickup",
96
- "Rating",
97
- "Shipment",
98
- "Tracking",
99
- "Address",
100
- "Document",
101
- ]
karrio/addons/renderer.py CHANGED
@@ -12,7 +12,7 @@ LINE_SEPARATOR = """
12
12
  """
13
13
 
14
14
 
15
- def generate_pdf_from_svg_label(content: str, **kwargs) -> Image:
15
+ def generate_pdf_from_svg_label(content: str, **kwargs):
16
16
  template = fromstring(content)
17
17
  label = Image.new("L", (1200, 1800), "white")
18
18
  draw = ImageDraw.Draw(label)
karrio/api/gateway.py CHANGED
@@ -1,19 +1,16 @@
1
1
  """Karrio API Gateway definition modules."""
2
+
2
3
  import attr
4
+ import typing
3
5
  import logging
4
- from typing import Callable, Optional, Union, List
5
-
6
- from karrio.api.proxy import Proxy
7
- from karrio.api.mapper import Mapper
8
- from karrio.core import Settings
9
- from karrio.core.utils import DP, Tracer
10
- from karrio.core.models import Message
11
- from karrio.core.errors import ShippingSDKError
12
- from karrio.references import (
13
- import_extensions,
14
- detect_capabilities,
15
- detect_proxy_methods,
16
- )
6
+
7
+ import karrio.core as core
8
+ import karrio.api.proxy as proxy
9
+ import karrio.core.utils as utils
10
+ import karrio.api.mapper as mapper
11
+ import karrio.core.models as models
12
+ import karrio.core.errors as errors
13
+ import karrio.references as references
17
14
 
18
15
  logger = logging.getLogger(__name__)
19
16
 
@@ -23,25 +20,25 @@ class Gateway:
23
20
  """The carrier connection instance"""
24
21
 
25
22
  is_hub: bool
26
- proxy: Proxy
27
- mapper: Mapper
28
- tracer: Tracer
29
- settings: Settings
23
+ proxy: proxy.Proxy
24
+ mapper: mapper.Mapper
25
+ tracer: utils.Tracer
26
+ settings: core.Settings
30
27
 
31
28
  @property
32
- def capabilities(self) -> List[str]:
33
- return detect_capabilities(self.proxy_methods)
29
+ def capabilities(self) -> typing.List[str]:
30
+ return references.detect_capabilities(self.proxy_methods)
34
31
 
35
32
  @property
36
- def proxy_methods(self) -> List[str]:
37
- return detect_proxy_methods(self.proxy.__class__)
33
+ def proxy_methods(self) -> typing.List[str]:
34
+ return references.detect_proxy_methods(self.proxy.__class__)
38
35
 
39
36
  def check(self, request: str, origin_country_code: str = None):
40
37
  messages = []
41
38
 
42
39
  if request not in self.proxy_methods:
43
40
  messages.append(
44
- Message(
41
+ models.Message(
45
42
  carrier_id=self.settings.carrier_id,
46
43
  carrier_name=self.settings.carrier_name,
47
44
  code="SHIPPING_SDK_NON_SUPPORTED_ERROR",
@@ -55,7 +52,7 @@ class Gateway:
55
52
  and (origin_country_code != self.settings.account_country_code)
56
53
  ):
57
54
  messages.append(
58
- Message(
55
+ models.Message(
59
56
  carrier_id=self.settings.carrier_id,
60
57
  carrier_name=self.settings.carrier_name,
61
58
  code="SHIPPING_SDK_ORIGIN_NOT_SERVICED_ERROR",
@@ -72,9 +69,21 @@ class Gateway:
72
69
  class ICreate:
73
70
  """A gateway initializer type class"""
74
71
 
75
- initializer: Callable[[Union[Settings, dict], Optional[Tracer]], Gateway]
76
-
77
- def create(self, settings: Union[Settings, dict], tracer: Tracer = None) -> Gateway:
72
+ initializer: typing.Callable[
73
+ [
74
+ typing.Union[core.Settings, dict],
75
+ typing.Optional[utils.Tracer],
76
+ typing.Optional[utils.Cache],
77
+ ],
78
+ Gateway,
79
+ ]
80
+
81
+ def create(
82
+ self,
83
+ settings: typing.Union[core.Settings, dict],
84
+ tracer: utils.Tracer = None,
85
+ cache: utils.Cache = None,
86
+ ) -> Gateway:
78
87
  """A gateway factory with a fluent API interface.
79
88
 
80
89
  Args:
@@ -83,7 +92,7 @@ class ICreate:
83
92
  Returns:
84
93
  Gateway: The carrier connection instance
85
94
  """
86
- return self.initializer(settings, tracer)
95
+ return self.initializer(settings, tracer, cache)
87
96
 
88
97
 
89
98
  class GatewayInitializer:
@@ -111,7 +120,9 @@ class GatewayInitializer:
111
120
  provider = self.providers[key]
112
121
 
113
122
  def initializer(
114
- settings: Union[Settings, dict], tracer: Tracer = None
123
+ settings: typing.Union[core.Settings, dict],
124
+ tracer: utils.Tracer = None,
125
+ cache: utils.Cache = None,
115
126
  ) -> Gateway:
116
127
  """Initialize a provider gateway with the required settings
117
128
 
@@ -121,31 +132,43 @@ class GatewayInitializer:
121
132
  Returns:
122
133
  Gateway: a gateway instance
123
134
  """
135
+
124
136
  try:
125
- _tracer = tracer or Tracer()
126
- settings_value: Settings = (
127
- DP.to_object(provider.Settings, settings)
137
+ _tracer = tracer or utils.Tracer()
138
+ _cache = cache or utils.Cache()
139
+ settings_value: core.Settings = (
140
+ utils.DP.to_object(provider.Settings, settings)
128
141
  if isinstance(settings, dict)
129
142
  else settings
130
143
  )
144
+
145
+ # set cache handle to all carrier settings
146
+ setattr(settings_value, "cache", _cache)
147
+
148
+ # set tracer handle to all carrier settings
149
+ setattr(settings_value, "tracer", _tracer)
150
+
131
151
  return Gateway(
132
152
  tracer=_tracer,
133
153
  is_hub=provider.is_hub,
134
154
  settings=settings_value,
135
155
  mapper=provider.Mapper(settings_value),
136
- proxy=provider.Proxy(settings_value, tracer=_tracer),
156
+ proxy=provider.Proxy(settings_value),
137
157
  )
158
+
138
159
  except Exception as er:
139
- raise ShippingSDKError(f"Failed to setup provider '{key}'") from er
160
+ raise errors.ShippingSDKError(
161
+ f"Failed to setup provider '{key}'"
162
+ ) from er
140
163
 
141
164
  return ICreate(initializer)
142
165
  except KeyError as e:
143
166
  logger.error(e)
144
- raise ShippingSDKError(f"Unknown provider '{key}'")
167
+ raise errors.ShippingSDKError(f"Unknown provider '{key}'")
145
168
 
146
169
  @property
147
170
  def providers(self):
148
- return import_extensions()
171
+ return references.collect_providers_data()
149
172
 
150
173
  @staticmethod
151
174
  def get_instance() -> "GatewayInitializer":
karrio/api/interface.py CHANGED
@@ -139,7 +139,7 @@ class IRequestFromMany:
139
139
 
140
140
  def from_(self, *gateways: gateway.Gateway) -> IDeserialize:
141
141
  """Execute the request action(s) from the provided gateway(s)"""
142
- return self.action(list(gateways))
142
+ return self.action(list({_.settings.carrier_id: _ for _ in gateways}.values()))
143
143
 
144
144
 
145
145
  class Address:
@@ -309,9 +309,9 @@ class Rating:
309
309
 
310
310
  return IDeserialize(deserialize)
311
311
 
312
- deserializable_collection: typing.List[
313
- IDeserialize
314
- ] = lib.run_asynchronously(lambda g: fail_safe(g)(process)(g), gateways)
312
+ deserializable_collection: typing.List[IDeserialize] = (
313
+ lib.run_asynchronously(lambda g: fail_safe(g)(process)(g), gateways)
314
+ )
315
315
 
316
316
  def flatten(*args):
317
317
  responses = [p.parse() for p in deserializable_collection]
@@ -485,3 +485,40 @@ class Document:
485
485
  return IDeserialize(deserialize)
486
486
 
487
487
  return IRequestFrom(action)
488
+
489
+
490
+ class Manifest:
491
+ """The unified Manifest API fluent interface"""
492
+
493
+ @staticmethod
494
+ def create(args: typing.Union[models.ManifestRequest, dict]) -> IRequestFrom:
495
+ """Submit a manifest creation to a carrier.
496
+ This operation is often referred to as manifesting a batch of shipments
497
+
498
+ Args:
499
+ args (Union[ManifestRequest, dict]): the manifest creation request payload
500
+
501
+ Returns:
502
+ IRequestWith: a lazy request dataclass instance
503
+ """
504
+ logger.debug(f"create a manifest. payload: {lib.to_json(args)}")
505
+ payload = lib.to_object(models.ManifestRequest, lib.to_dict(args))
506
+
507
+ def action(gateway: gateway.Gateway):
508
+ is_valid, abortion = check_operation(
509
+ gateway,
510
+ "create_manifest",
511
+ )
512
+ if not is_valid:
513
+ return abortion
514
+
515
+ request: lib.Serializable = gateway.mapper.create_manifest_request(payload)
516
+ response: lib.Deserializable = gateway.proxy.create_manifest(request)
517
+
518
+ @fail_safe(gateway)
519
+ def deserialize():
520
+ return gateway.mapper.parse_manifest_response(response)
521
+
522
+ return IDeserialize(deserialize)
523
+
524
+ return IRequestFrom(action)
karrio/api/mapper.py CHANGED
@@ -178,6 +178,25 @@ class Mapper(abc.ABC):
178
178
  self.settings.carrier_name,
179
179
  )
180
180
 
181
+ def create_manifest_request(
182
+ self, payload: models.ManifestRequest
183
+ ) -> lib.Serializable:
184
+ """Create a carrier specific manifest request data from payload
185
+
186
+ Args:
187
+ payload (ManifestRequest): the manifest request payload
188
+
189
+ Returns:
190
+ Serializable: a carrier specific serializable request data type
191
+
192
+ Raises:
193
+ MethodNotSupportedError: Is raised when the carrier integration does not implement this method
194
+ """
195
+ raise errors.MethodNotSupportedError(
196
+ self.__class__.create_manifest_request.__name__,
197
+ self.settings.carrier_name,
198
+ )
199
+
181
200
  """Response Parsers"""
182
201
 
183
202
  def parse_address_validation_response(
@@ -354,3 +373,23 @@ class Mapper(abc.ABC):
354
373
  self.__class__.parse_document_upload_response.__name__,
355
374
  self.settings.carrier_name,
356
375
  )
376
+
377
+ def parse_manifest_response(
378
+ self, response: lib.Deserializable
379
+ ) -> typing.Tuple[models.ManifestDetails, typing.List[models.Message]]:
380
+ """Create a unified API manifest result from carrier response
381
+
382
+ Args:
383
+ response (Deserializable): a deserializable manifest response (xml, json, text...)
384
+
385
+ Returns:
386
+ Tuple[ManifestDetails, List[Message]]: the manifest details
387
+ as well as errors and messages returned
388
+
389
+ Raises:
390
+ MethodNotSupportedError: Is raised when the carrier integration does not implement this method
391
+ """
392
+ raise errors.MethodNotSupportedError(
393
+ self.__class__.parse_manifest_response.__name__,
394
+ self.settings.carrier_name,
395
+ )
karrio/api/proxy.py CHANGED
@@ -13,15 +13,12 @@ class Proxy(abc.ABC):
13
13
  """Unified Shipping API Proxy (Interface)"""
14
14
 
15
15
  settings: settings.Settings
16
- tracer: lib.Tracer = attr.field(factory=lib.Tracer)
17
16
 
18
17
  def trace(self, *args, **kwargs):
19
- return self.tracer.with_metadata(dict(connection=self.settings))(
20
- *args, **kwargs
21
- )
18
+ return self.settings.trace(*args, **kwargs)
22
19
 
23
20
  def trace_as(self, format: str):
24
- return functools.partial(self.trace, format=format)
21
+ return self.settings.trace_as(format)
25
22
 
26
23
  def get_rates(self, request: lib.Serializable) -> lib.Deserializable:
27
24
  """Send one or many request(s) to get shipment rates from a carrier webservice
@@ -166,3 +163,19 @@ class Proxy(abc.ABC):
166
163
  raise errors.MethodNotSupportedError(
167
164
  self.__class__.upload_document.__name__, self.settings.carrier_name
168
165
  )
166
+
167
+ def create_manifest(self, request: lib.Serializable) -> lib.Deserializable:
168
+ """Send one or many request(s) to create a manifest from a carrier webservice
169
+
170
+ Args:
171
+ request (Serializable): a carrier specific serializable request data type
172
+
173
+ Returns:
174
+ Deserializable: a Deserializable rate response (xml, json, text...)
175
+
176
+ Raises:
177
+ MethodNotSupportedError: Is raised when the carrier integration does not implement this method
178
+ """
179
+ raise errors.MethodNotSupportedError(
180
+ self.__class__.create_manifest.__name__, self.settings.carrier_name
181
+ )
karrio/core/__init__.py CHANGED
@@ -1,2 +1,6 @@
1
- """The Karrio core module"""
1
+ """
2
+ Karrio Core Module.
3
+
4
+ This module provides core functionality for the Karrio shipping system.
5
+ """
2
6
  from karrio.core.settings import Settings
karrio/core/errors.py CHANGED
@@ -1,9 +1,10 @@
1
1
  """Karrio Custom Errors(Exception) definition modules"""
2
- from enum import Enum
3
- from typing import Dict
4
2
 
3
+ import enum
4
+ import typing
5
5
 
6
- class FieldErrorCode(Enum):
6
+
7
+ class FieldErrorCode(enum.Enum):
7
8
  required = dict(code="required", message="This field is required")
8
9
  invalid = dict(code="invalid", message="This field is invalid")
9
10
  exceeds = dict(code="exceeds", message="This field exceeds the max value")
@@ -28,7 +29,7 @@ class FieldError(ShippingSDKDetailedError):
28
29
 
29
30
  code = "SHIPPING_SDK_FIELD_ERROR"
30
31
 
31
- def __init__(self, fields: Dict[str, FieldErrorCode]):
32
+ def __init__(self, fields: typing.Dict[str, FieldErrorCode]):
32
33
  super().__init__("Invalid request payload")
33
34
  self.details = {name: code.value for name, code in fields.items()}
34
35
 
@@ -38,7 +39,7 @@ class ParsedMessagesError(ShippingSDKDetailedError):
38
39
 
39
40
  code = "SHIPPING_SDK_FIELD_ERROR"
40
41
 
41
- def __init__(self, messages = []):
42
+ def __init__(self, messages=[]):
42
43
  super().__init__("Invalid request payload")
43
44
  self.messages = messages
44
45
 
karrio/core/metadata.py CHANGED
@@ -1,7 +1,13 @@
1
- """Karrio Extension Metadata definition module."""
1
+ """
2
+ Karrio Plugin Metadata Module.
3
+
4
+ This module provides the PluginMetadata class and related functionality
5
+ for defining and working with Karrio plugin metadata.
6
+ """
7
+
2
8
  import attr
3
9
  from enum import Enum
4
- from typing import Optional, Type, List, Dict
10
+ from typing import Optional, Type, List, Dict, Any, Literal
5
11
 
6
12
  from karrio.api.proxy import Proxy
7
13
  from karrio.api.mapper import Mapper
@@ -10,29 +16,116 @@ from karrio.core.models import ServiceLevel
10
16
 
11
17
 
12
18
  @attr.s(auto_attribs=True)
13
- class Metadata:
14
- """Karrio extension metadata.
15
- Used to describe and define a karrio compatible extension for a carrier webservice integration.
19
+ class PluginMetadata:
16
20
  """
21
+ Metadata for a Karrio plugin.
17
22
 
23
+ This class defines the metadata for a Karrio plugin, including its ID,
24
+ label, description, capabilities, and other attributes.
25
+ """
26
+ id: str
18
27
  label: str
28
+ description: Optional[str] = ""
29
+ status: Optional[str] = "beta"
30
+
31
+ # Components that will be registered if present
32
+ Proxy: Any = None
33
+ Mapper: Any = None
34
+ Validator: Any = None
35
+ Settings: Any = None
36
+
37
+ # Optional metadata
38
+ options: Any = None
39
+ services: Any = None
40
+ countries: Any = None
41
+ packaging_types: Any = None
42
+ package_presets: Any = None
43
+ service_levels: Any = None
44
+ connection_configs: Any = None
19
45
 
20
- # Integrations
21
- Mapper: Type[Mapper]
22
- Proxy: Type[Proxy]
23
- Settings: Type[Settings]
24
-
25
- # Data Units
26
- options: Optional[Type[Enum]] = None
27
- services: Optional[Type[Enum]] = None
28
- package_presets: Optional[Type[Enum]] = None
29
- packaging_types: Optional[Type[Enum]] = None
30
- connection_configs: Optional[Type[Enum]] = None
31
- service_levels: Optional[List[ServiceLevel]] = None
32
-
33
- id: Optional[str] = None
34
- is_hub: Optional[bool] = False
46
+ # Extra metadata
47
+ website: Optional[str] = None
48
+ documentation: Optional[str] = None
49
+ readme: Optional[str] = None
50
+ is_hub: bool = False
35
51
  hub_carriers: Optional[Dict[str, str]] = None
52
+ has_intl_accounts: Optional[bool] = False
53
+
54
+ def is_carrier_integration(self) -> bool:
55
+ """Check if this plugin is a carrier integration based on registered components."""
56
+ return bool(self.Mapper) and bool(self.Proxy) and bool(self.Settings)
57
+
58
+ def is_address_validator(self) -> bool:
59
+ """Check if this plugin is an address validator based on registered components."""
60
+ return bool(self.Validator)
61
+
62
+ def is_dual_purpose(self) -> bool:
63
+ """Check if this plugin serves both as a carrier integration and address validator."""
64
+ return self.is_carrier_integration() and self.is_address_validator()
65
+
66
+ @property
67
+ def plugin_types(self) -> List[str]:
68
+ """
69
+ Get a list of all functionality types provided by this plugin.
70
+
71
+ Returns:
72
+ List[str]: List of plugin types, e.g. ["carrier_integration", "address_validator"]
73
+ """
74
+ types = []
75
+ if self.is_carrier_integration():
76
+ types.append("carrier_integration")
77
+ if self.is_address_validator():
78
+ types.append("address_validator")
79
+ if not types:
80
+ types.append("unknown")
81
+ return types
36
82
 
83
+ @property
84
+ def plugin_type(self) -> str:
85
+ """
86
+ Determine the primary type of plugin based on registered components.
87
+ For dual-purpose plugins, returns "dual_purpose".
88
+
89
+ Returns:
90
+ str: "carrier_integration", "address_validator", "dual_purpose", or "unknown"
91
+ """
92
+ if self.is_carrier_integration() and self.is_address_validator():
93
+ return "dual_purpose"
94
+ elif self.is_carrier_integration():
95
+ return "carrier_integration"
96
+ elif self.is_address_validator():
97
+ return "address_validator"
98
+ else:
99
+ return "unknown"
100
+
101
+ @property
102
+ def supports_carrier_integration(self) -> bool:
103
+ """Check if this plugin provides carrier integration functionality."""
104
+ return self.is_carrier_integration()
105
+
106
+ @property
107
+ def supports_address_validation(self) -> bool:
108
+ """Check if this plugin provides address validation functionality."""
109
+ return self.is_address_validator()
110
+
111
+ # Dictionary-like access methods for backward compatibility
37
112
  def __getitem__(self, item):
38
113
  return getattr(self, item)
114
+
115
+ def get(self, key, default=None):
116
+ """
117
+ Dictionary-like get method for backward compatibility.
118
+
119
+ Args:
120
+ key: Attribute name to retrieve
121
+ default: Default value if attribute doesn't exist
122
+
123
+ Returns:
124
+ The attribute value or default if not found
125
+ """
126
+ return getattr(self, key, default)
127
+
128
+
129
+ # Legacy compatibility aliases
130
+ Metadata = PluginMetadata
131
+ AddressValidatorMetadata = PluginMetadata