karrio-server-core 2025.5rc31__py3-none-any.whl → 2026.1.1__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.
Files changed (56) hide show
  1. karrio/server/core/authentication.py +38 -20
  2. karrio/server/core/config.py +31 -0
  3. karrio/server/core/datatypes.py +30 -4
  4. karrio/server/core/dataunits.py +26 -7
  5. karrio/server/core/exceptions.py +287 -17
  6. karrio/server/core/filters.py +14 -0
  7. karrio/server/core/gateway.py +284 -11
  8. karrio/server/core/logging.py +403 -0
  9. karrio/server/core/middleware.py +104 -2
  10. karrio/server/core/models/base.py +34 -1
  11. karrio/server/core/oauth_validators.py +2 -3
  12. karrio/server/core/permissions.py +1 -2
  13. karrio/server/core/serializers.py +154 -7
  14. karrio/server/core/signals.py +22 -28
  15. karrio/server/core/telemetry.py +573 -0
  16. karrio/server/core/tests/__init__.py +27 -0
  17. karrio/server/core/{tests.py → tests/base.py} +6 -7
  18. karrio/server/core/tests/test_exception_level.py +159 -0
  19. karrio/server/core/tests/test_resource_token.py +593 -0
  20. karrio/server/core/utils.py +688 -38
  21. karrio/server/core/validators.py +144 -222
  22. karrio/server/core/views/oauth.py +13 -12
  23. karrio/server/core/views/references.py +2 -2
  24. karrio/server/iam/apps.py +1 -4
  25. karrio/server/iam/migrations/0002_setup_carrier_permission_groups.py +103 -0
  26. karrio/server/iam/migrations/0003_remove_permission_groups.py +91 -0
  27. karrio/server/iam/permissions.py +7 -134
  28. karrio/server/iam/serializers.py +9 -3
  29. karrio/server/iam/signals.py +2 -4
  30. karrio/server/providers/admin.py +1 -1
  31. karrio/server/providers/migrations/0085_populate_dhl_parcel_de_oauth_credentials.py +82 -0
  32. karrio/server/providers/migrations/0086_rename_dhl_parcel_de_customer_number_to_billing_number.py +71 -0
  33. karrio/server/providers/migrations/0087_alter_carrier_capabilities.py +38 -0
  34. karrio/server/providers/migrations/0088_servicelevel_surcharges.py +24 -0
  35. karrio/server/providers/migrations/0089_servicelevel_cost_max_volume.py +31 -0
  36. karrio/server/providers/migrations/0090_ratesheet_surcharges_servicelevel_zone_surcharge_ids.py +47 -0
  37. karrio/server/providers/migrations/0091_migrate_legacy_zones_surcharges.py +154 -0
  38. karrio/server/providers/models/carrier.py +101 -29
  39. karrio/server/providers/models/service.py +182 -125
  40. karrio/server/providers/models/sheet.py +342 -198
  41. karrio/server/providers/serializers/base.py +263 -2
  42. karrio/server/providers/signals.py +2 -4
  43. karrio/server/providers/templates/providers/oauth_callback.html +105 -0
  44. karrio/server/providers/tests/__init__.py +5 -0
  45. karrio/server/providers/tests/test_connections.py +895 -0
  46. karrio/server/providers/views/carriers.py +1 -3
  47. karrio/server/providers/views/connections.py +322 -2
  48. karrio/server/serializers/abstract.py +112 -21
  49. karrio/server/tracing/utils.py +5 -8
  50. karrio/server/user/models.py +36 -34
  51. karrio/server/user/serializers.py +1 -0
  52. {karrio_server_core-2025.5rc31.dist-info → karrio_server_core-2026.1.1.dist-info}/METADATA +2 -2
  53. {karrio_server_core-2025.5rc31.dist-info → karrio_server_core-2026.1.1.dist-info}/RECORD +55 -38
  54. karrio/server/providers/tests.py +0 -3
  55. {karrio_server_core-2025.5rc31.dist-info → karrio_server_core-2026.1.1.dist-info}/WHEEL +0 -0
  56. {karrio_server_core-2025.5rc31.dist-info → karrio_server_core-2026.1.1.dist-info}/top_level.txt +0 -0
@@ -21,6 +21,7 @@ class ShipmentStatus(utils.Enum):
21
21
 
22
22
  class TrackerStatus(utils.Enum):
23
23
  pending = "pending"
24
+ picked_up = "picked_up"
24
25
  unknown = "unknown"
25
26
  on_hold = "on_hold"
26
27
  cancelled = "cancelled"
@@ -49,6 +50,9 @@ CUSTOMS_CONTENT_TYPE = [(c.name, c.name) for c in list(units.CustomsContentType)
49
50
  UPLOAD_DOCUMENT_TYPE = [(c.name, c.name) for c in list(units.UploadDocumentType)]
50
51
  LABEL_TYPES = [(c.name, c.name) for c in list(units.LabelType)]
51
52
  LABEL_TEMPLATE_TYPES = [("SVG", "SVG"), ("ZPL", "ZPL")]
53
+ TRACKING_INCIDENT_REASONS = [
54
+ (c.name, c.name) for c in list(units.TrackingIncidentReason)
55
+ ]
52
56
 
53
57
 
54
58
  class CarrierDetails(serializers.Serializer):
@@ -129,6 +133,7 @@ class APIError(serializers.Serializer):
129
133
  required=False, help_text="The error or warning message"
130
134
  )
131
135
  code = serializers.CharField(required=False, help_text="The message code")
136
+ level = serializers.CharField(required=False, help_text="The message level")
132
137
  details = serializers.DictField(required=False, help_text="any additional details")
133
138
 
134
139
 
@@ -686,6 +691,7 @@ class RateRequest(validators.OptionDefaultSerializer):
686
691
  {
687
692
  "currency": "USD",
688
693
  "insurance": 100.00,
694
+ "insured_by": "carrier",
689
695
  "cash_on_delivery": 30.00,
690
696
  "dangerous_good": true,
691
697
  "declared_value": 150.00,
@@ -727,6 +733,18 @@ class RateRequest(validators.OptionDefaultSerializer):
727
733
  allow_null=True,
728
734
  help_text="The shipment reference",
729
735
  )
736
+ payment = Payment(
737
+ required=False,
738
+ allow_null=True,
739
+ help_text="The payment details",
740
+ )
741
+ customs = CustomsData(
742
+ required=False,
743
+ allow_null=True,
744
+ help_text="""The customs details.<br/>
745
+ **Note that this is required for international shipments.**
746
+ """,
747
+ )
730
748
  carrier_ids = serializers.StringListField(
731
749
  required=False,
732
750
  allow_null=True,
@@ -1068,11 +1086,24 @@ class TrackingEvent(serializers.Serializer):
1068
1086
  date = serializers.CharField(
1069
1087
  required=False, help_text="The tracking event's date. Format: `YYYY-MM-DD`"
1070
1088
  )
1071
- description = serializers.CharField(
1072
- required=False, help_text="The tracking event's description"
1089
+ time = serializers.CharField(
1090
+ required=False,
1091
+ allow_blank=True,
1092
+ allow_null=True,
1093
+ help_text="The tracking event's time. Format: `HH:MM AM/PM`",
1073
1094
  )
1074
- location = serializers.CharField(
1075
- required=False, help_text="The tracking event's location"
1095
+ timestamp = serializers.CharField(
1096
+ required=False,
1097
+ allow_blank=True,
1098
+ allow_null=True,
1099
+ help_text="The tracking event's timestamp. Format: `YYYY-MM-DDTHH:MM:SS.sssZ` (ISO 8601)",
1100
+ )
1101
+ status = serializers.ChoiceField(
1102
+ required=False,
1103
+ allow_blank=True,
1104
+ allow_null=True,
1105
+ choices=TRACKER_STATUS,
1106
+ help_text="The normalized status of this specific event",
1076
1107
  )
1077
1108
  code = serializers.CharField(
1078
1109
  required=False,
@@ -1080,11 +1111,18 @@ class TrackingEvent(serializers.Serializer):
1080
1111
  allow_null=True,
1081
1112
  help_text="The tracking event's code",
1082
1113
  )
1083
- time = serializers.CharField(
1114
+ reason = serializers.ChoiceField(
1084
1115
  required=False,
1085
1116
  allow_blank=True,
1086
1117
  allow_null=True,
1087
- help_text="The tracking event's time. Format: `HH:MM AM/PM`",
1118
+ choices=TRACKING_INCIDENT_REASONS,
1119
+ help_text="The normalized incident reason (for exception events only)",
1120
+ )
1121
+ description = serializers.CharField(
1122
+ required=False, help_text="The tracking event's description"
1123
+ )
1124
+ location = serializers.CharField(
1125
+ required=False, help_text="The tracking event's location"
1088
1126
  )
1089
1127
  latitude = serializers.FloatField(
1090
1128
  required=False,
@@ -1179,6 +1217,62 @@ class TrackingStatus(TrackerDetails):
1179
1217
  )
1180
1218
 
1181
1219
 
1220
+ class ShippingDocument(serializers.Serializer):
1221
+ """Serializer for shipping document download response."""
1222
+
1223
+ category = serializers.CharField(
1224
+ required=True,
1225
+ help_text="The document category (e.g., 'label', 'invoice', 'manifest')",
1226
+ )
1227
+ format = serializers.CharField(
1228
+ required=True,
1229
+ help_text="The document format (e.g., 'PDF', 'ZPL')",
1230
+ )
1231
+ url = serializers.CharField(
1232
+ required=False,
1233
+ allow_blank=True,
1234
+ allow_null=True,
1235
+ help_text="The document URL",
1236
+ )
1237
+ base64 = serializers.CharField(
1238
+ required=False,
1239
+ allow_blank=True,
1240
+ allow_null=True,
1241
+ help_text="The document content encoded in base64",
1242
+ )
1243
+
1244
+
1245
+ class ShippingDocument(serializers.Serializer):
1246
+ """Serializer for shipping document download response."""
1247
+
1248
+ category = serializers.CharField(
1249
+ required=True,
1250
+ help_text="The document category (e.g., 'label', 'invoice', 'manifest')",
1251
+ )
1252
+ format = serializers.CharField(
1253
+ required=True,
1254
+ help_text="The document format (e.g., 'PDF', 'ZPL')",
1255
+ )
1256
+ print_format = serializers.CharField(
1257
+ required=False,
1258
+ allow_blank=True,
1259
+ allow_null=True,
1260
+ help_text="The document print format (e.g., 'A4', '6x4', '8.5x11')",
1261
+ )
1262
+ url = serializers.CharField(
1263
+ required=False,
1264
+ allow_blank=True,
1265
+ allow_null=True,
1266
+ help_text="The document URL",
1267
+ )
1268
+ base64 = serializers.CharField(
1269
+ required=False,
1270
+ allow_blank=True,
1271
+ allow_null=True,
1272
+ help_text="The document content encoded in base64",
1273
+ )
1274
+
1275
+
1182
1276
  class Documents(serializers.Serializer):
1183
1277
  label = serializers.CharField(
1184
1278
  required=False,
@@ -1611,6 +1705,7 @@ class ShipmentContent(serializers.Serializer):
1611
1705
  )
1612
1706
 
1613
1707
 
1708
+ @validators.shipment_documents_accessor
1614
1709
  class Shipment(serializers.EntitySerializer, ShipmentContent, ShipmentDetails):
1615
1710
  docs = None
1616
1711
  label_url = serializers.URLField(
@@ -1625,6 +1720,12 @@ class Shipment(serializers.EntitySerializer, ShipmentContent, ShipmentDetails):
1625
1720
  allow_null=True,
1626
1721
  help_text="The shipment invoice URL",
1627
1722
  )
1723
+ shipping_documents = ShippingDocument(
1724
+ required=False,
1725
+ many=True,
1726
+ default=[],
1727
+ help_text="The list of shipping documents",
1728
+ )
1628
1729
 
1629
1730
 
1630
1731
  class ShipmentCancelRequest(serializers.Serializer):
@@ -1776,7 +1877,10 @@ class OperationConfirmation(Operation):
1776
1877
  required=True, help_text="The operation carrier"
1777
1878
  )
1778
1879
  carrier_id = serializers.CharField(
1779
- required=True, help_text="The targeted carrier's name (unique identifier)"
1880
+ required=False,
1881
+ allow_blank=True,
1882
+ allow_null=True,
1883
+ help_text="The targeted carrier's name (unique identifier)",
1780
1884
  )
1781
1885
 
1782
1886
 
@@ -1913,6 +2017,49 @@ class DocumentUploadRecord(serializers.EntitySerializer):
1913
2017
  )
1914
2018
 
1915
2019
 
2020
+ class OAuthAuthorizeData(serializers.Serializer):
2021
+ """OAuth authorization request data serializer."""
2022
+
2023
+ state = serializers.CharField(
2024
+ required=True,
2025
+ help_text="A unique state value for CSRF protection.",
2026
+ )
2027
+ redirect_uri = serializers.CharField(
2028
+ required=False,
2029
+ allow_null=True,
2030
+ help_text="The URI to redirect to after authorization.",
2031
+ )
2032
+ options = serializers.PlainDictField(
2033
+ required=False,
2034
+ default={},
2035
+ help_text="Additional carrier-specific options.",
2036
+ )
2037
+
2038
+
2039
+ class OAuthCallbackData(serializers.Serializer):
2040
+ """OAuth callback request data serializer."""
2041
+
2042
+ url = serializers.CharField(
2043
+ required=True,
2044
+ help_text="The full callback URL.",
2045
+ )
2046
+ query = serializers.PlainDictField(
2047
+ required=False,
2048
+ default={},
2049
+ help_text="The query parameters from the callback URL.",
2050
+ )
2051
+ body = serializers.PlainDictField(
2052
+ required=False,
2053
+ default={},
2054
+ help_text="The request body if any.",
2055
+ )
2056
+ headers = serializers.PlainDictField(
2057
+ required=False,
2058
+ default={},
2059
+ help_text="The request headers.",
2060
+ )
2061
+
2062
+
1916
2063
  class ErrorMessages(serializers.Serializer):
1917
2064
  messages = Message(
1918
2065
  many=True,
@@ -1,57 +1,51 @@
1
- import logging
2
1
  from django.conf import settings
3
2
  from django.dispatch import receiver
4
3
  from constance import config
5
4
  from constance.signals import config_updated
6
5
  from django.core.signals import request_started
7
6
 
8
- logger = logging.getLogger(__name__)
7
+ from karrio.server.core.logging import logger
9
8
 
10
9
 
11
10
  def register_signals():
12
11
  config_updated.connect(constance_updated)
13
- # Defer config initialization until after Django is fully loaded
14
12
  request_started.connect(initialize_settings)
15
13
 
16
- logger.info("karrio.core signals registered...")
14
+ logger.info("Signal registration complete", module="karrio.core")
17
15
 
18
16
 
19
- def initialize_settings(sender=None, **kwargs):
20
- # Only run once
21
- if not getattr(initialize_settings, 'has_run', False):
22
- try:
23
- update_settings(config)
24
- initialize_settings.has_run = True
25
- except Exception as e:
26
- logger.error(f"Failed to initialize settings: {e}")
17
+ def initialize_settings(_sender=None, **_kwargs):
18
+ """Initialize Django settings from constance on first request."""
19
+ if getattr(initialize_settings, "has_run", False):
20
+ return
21
+
22
+ try:
23
+ update_settings(config)
24
+ initialize_settings.has_run = True
25
+ except Exception as e:
26
+ logger.error("Failed to initialize settings", error=str(e))
27
27
 
28
28
 
29
29
  @receiver(config_updated)
30
- def constance_updated(sender, key, old_value, new_value, **kwargs):
31
- logger.info(f"Updated config {key} to {new_value}")
30
+ def constance_updated(sender, **_kwargs):
32
31
  update_settings(sender)
33
32
 
34
33
 
35
34
  def update_settings(current):
36
- CONSTANCE_CONFIG_KEYS = [
37
- key for key in settings.CONSTANCE_CONFIG.keys() if hasattr(settings, key)
38
- ]
35
+ """Sync constance values to Django settings."""
36
+ keys = [k for k in settings.CONSTANCE_CONFIG.keys() if hasattr(settings, k)]
39
37
 
40
- for key in CONSTANCE_CONFIG_KEYS:
38
+ for key in keys:
41
39
  try:
42
40
  setattr(settings, key, getattr(current, key))
43
- except Exception as e:
44
- logger.error(f"Failed to update setting {key}: {e}")
41
+ except Exception:
42
+ pass # Ignore errors during test/initialization when constance table may not exist
45
43
 
46
- # Check EMAIL_ENABLED after all settings are updated
44
+ # Update EMAIL_ENABLED based on email config
47
45
  try:
48
46
  settings.EMAIL_ENABLED = all(
49
- cfg is not None and cfg != ""
50
- for cfg in [
51
- current.EMAIL_HOST,
52
- current.EMAIL_HOST_USER,
53
- ]
47
+ cfg not in (None, "")
48
+ for cfg in [current.EMAIL_HOST, current.EMAIL_HOST_USER]
54
49
  )
55
- except Exception as e:
56
- logger.error(f"Failed to set EMAIL_ENABLED: {e}")
50
+ except Exception:
57
51
  settings.EMAIL_ENABLED = False