karrio-server-core 2025.5rc12__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 (112) hide show
  1. karrio/server/core/authentication.py +59 -25
  2. karrio/server/core/config.py +31 -0
  3. karrio/server/core/datatypes.py +30 -4
  4. karrio/server/core/dataunits.py +53 -22
  5. karrio/server/core/exceptions.py +287 -17
  6. karrio/server/core/filters.py +14 -0
  7. karrio/server/core/gateway.py +285 -10
  8. karrio/server/core/logging.py +403 -0
  9. karrio/server/core/management/commands/runserver.py +5 -0
  10. karrio/server/core/middleware.py +104 -2
  11. karrio/server/core/migrations/0006_add_api_log_requested_at_index.py +22 -0
  12. karrio/server/core/models/base.py +34 -1
  13. karrio/server/core/oauth_validators.py +2 -3
  14. karrio/server/core/permissions.py +1 -2
  15. karrio/server/core/serializers.py +183 -10
  16. karrio/server/core/signals.py +22 -28
  17. karrio/server/core/telemetry.py +573 -0
  18. karrio/server/core/tests/__init__.py +27 -0
  19. karrio/server/core/{tests.py → tests/base.py} +6 -7
  20. karrio/server/core/tests/test_exception_level.py +159 -0
  21. karrio/server/core/tests/test_resource_token.py +593 -0
  22. karrio/server/core/utils.py +688 -38
  23. karrio/server/core/validators.py +144 -222
  24. karrio/server/core/views/oauth.py +13 -12
  25. karrio/server/core/views/references.py +2 -2
  26. karrio/server/iam/apps.py +1 -4
  27. karrio/server/iam/migrations/0002_setup_carrier_permission_groups.py +103 -0
  28. karrio/server/iam/migrations/0003_remove_permission_groups.py +91 -0
  29. karrio/server/iam/permissions.py +7 -134
  30. karrio/server/iam/serializers.py +17 -2
  31. karrio/server/iam/signals.py +2 -4
  32. karrio/server/providers/admin.py +1 -1
  33. karrio/server/providers/management/commands/migrate_rate_sheets.py +101 -0
  34. karrio/server/providers/migrations/0082_add_zone_identifiers.py +50 -0
  35. karrio/server/providers/migrations/0083_add_optimized_rate_sheet_structure.py +33 -0
  36. karrio/server/providers/migrations/0084_alter_servicelevel_currency.py +168 -0
  37. karrio/server/providers/migrations/0085_populate_dhl_parcel_de_oauth_credentials.py +82 -0
  38. karrio/server/providers/migrations/0086_rename_dhl_parcel_de_customer_number_to_billing_number.py +71 -0
  39. karrio/server/providers/migrations/0087_alter_carrier_capabilities.py +38 -0
  40. karrio/server/providers/migrations/0088_servicelevel_surcharges.py +24 -0
  41. karrio/server/providers/migrations/0089_servicelevel_cost_max_volume.py +31 -0
  42. karrio/server/providers/migrations/0090_ratesheet_surcharges_servicelevel_zone_surcharge_ids.py +47 -0
  43. karrio/server/providers/migrations/0091_migrate_legacy_zones_surcharges.py +154 -0
  44. karrio/server/providers/models/__init__.py +1 -2
  45. karrio/server/providers/models/carrier.py +103 -18
  46. karrio/server/providers/models/service.py +188 -1
  47. karrio/server/providers/models/sheet.py +371 -0
  48. karrio/server/providers/serializers/base.py +263 -2
  49. karrio/server/providers/signals.py +2 -4
  50. karrio/server/providers/templates/providers/oauth_callback.html +105 -0
  51. karrio/server/providers/tests/__init__.py +5 -0
  52. karrio/server/providers/tests/test_connections.py +895 -0
  53. karrio/server/providers/views/carriers.py +1 -3
  54. karrio/server/providers/views/connections.py +322 -2
  55. karrio/server/samples.py +1 -1
  56. karrio/server/serializers/abstract.py +116 -21
  57. karrio/server/tracing/migrations/0007_tracingrecord_tracing_created_at_idx.py +19 -0
  58. karrio/server/tracing/models.py +2 -0
  59. karrio/server/tracing/utils.py +5 -8
  60. karrio/server/user/migrations/0007_user_metadata.py +25 -0
  61. karrio/server/user/models.py +38 -23
  62. karrio/server/user/serializers.py +1 -0
  63. karrio/server/user/templates/registration/registration_confirm_email.html +1 -1
  64. {karrio_server_core-2025.5rc12.dist-info → karrio_server_core-2026.1.1.dist-info}/METADATA +2 -2
  65. {karrio_server_core-2025.5rc12.dist-info → karrio_server_core-2026.1.1.dist-info}/RECORD +67 -86
  66. karrio/server/providers/extension/__init__.py +0 -1
  67. karrio/server/providers/extension/models/__init__.py +0 -1
  68. karrio/server/providers/extension/models/allied_express.py +0 -22
  69. karrio/server/providers/extension/models/allied_express_local.py +0 -22
  70. karrio/server/providers/extension/models/amazon_shipping.py +0 -27
  71. karrio/server/providers/extension/models/aramex.py +0 -25
  72. karrio/server/providers/extension/models/asendia_us.py +0 -21
  73. karrio/server/providers/extension/models/australiapost.py +0 -20
  74. karrio/server/providers/extension/models/boxknight.py +0 -19
  75. karrio/server/providers/extension/models/bpost.py +0 -21
  76. karrio/server/providers/extension/models/canadapost.py +0 -21
  77. karrio/server/providers/extension/models/canpar.py +0 -19
  78. karrio/server/providers/extension/models/chronopost.py +0 -22
  79. karrio/server/providers/extension/models/colissimo.py +0 -22
  80. karrio/server/providers/extension/models/dhl_express.py +0 -23
  81. karrio/server/providers/extension/models/dhl_parcel_de.py +0 -25
  82. karrio/server/providers/extension/models/dhl_poland.py +0 -22
  83. karrio/server/providers/extension/models/dhl_universal.py +0 -19
  84. karrio/server/providers/extension/models/dicom.py +0 -20
  85. karrio/server/providers/extension/models/dpd.py +0 -37
  86. karrio/server/providers/extension/models/dpdhl.py +0 -26
  87. karrio/server/providers/extension/models/easypost.py +0 -20
  88. karrio/server/providers/extension/models/eshipper.py +0 -21
  89. karrio/server/providers/extension/models/fedex.py +0 -25
  90. karrio/server/providers/extension/models/fedex_ws.py +0 -24
  91. karrio/server/providers/extension/models/freightcom.py +0 -21
  92. karrio/server/providers/extension/models/generic.py +0 -35
  93. karrio/server/providers/extension/models/geodis.py +0 -22
  94. karrio/server/providers/extension/models/hay_post.py +0 -22
  95. karrio/server/providers/extension/models/laposte.py +0 -19
  96. karrio/server/providers/extension/models/locate2u.py +0 -22
  97. karrio/server/providers/extension/models/nationex.py +0 -22
  98. karrio/server/providers/extension/models/purolator.py +0 -21
  99. karrio/server/providers/extension/models/roadie.py +0 -18
  100. karrio/server/providers/extension/models/royalmail.py +0 -19
  101. karrio/server/providers/extension/models/sendle.py +0 -22
  102. karrio/server/providers/extension/models/tge.py +0 -63
  103. karrio/server/providers/extension/models/tnt.py +0 -23
  104. karrio/server/providers/extension/models/ups.py +0 -23
  105. karrio/server/providers/extension/models/usps.py +0 -23
  106. karrio/server/providers/extension/models/usps_international.py +0 -23
  107. karrio/server/providers/extension/models/usps_wt.py +0 -24
  108. karrio/server/providers/extension/models/usps_wt_international.py +0 -24
  109. karrio/server/providers/extension/models/zoom2u.py +0 -23
  110. karrio/server/providers/tests.py +0 -3
  111. {karrio_server_core-2025.5rc12.dist-info → karrio_server_core-2026.1.1.dist-info}/WHEEL +0 -0
  112. {karrio_server_core-2025.5rc12.dist-info → karrio_server_core-2026.1.1.dist-info}/top_level.txt +0 -0
@@ -1,12 +1,12 @@
1
1
  import uuid
2
2
  import typing
3
- import logging
4
3
  import datetime
5
4
 
6
5
  from django.db.models import Q
7
6
  from django.conf import settings
8
7
  from rest_framework import status
9
8
  from rest_framework.exceptions import NotFound
9
+ from karrio.server.core.logging import logger
10
10
 
11
11
  import karrio.lib as lib
12
12
  import karrio.sdk as karrio
@@ -14,14 +14,11 @@ import karrio.server.core.utils as utils
14
14
  import karrio.server.core.models as core
15
15
  import karrio.server.core.datatypes as datatypes
16
16
  import karrio.server.core.dataunits as dataunits
17
- import karrio.server.core.validators as validators
18
17
  import karrio.server.core.exceptions as exceptions
19
18
  import karrio.server.providers.models as providers
20
19
  import karrio.server.serializers as base_serializers
21
20
  import karrio.server.core.serializers as serializers
22
21
 
23
- logger = logging.getLogger(__name__)
24
-
25
22
 
26
23
  class Carriers:
27
24
  @staticmethod
@@ -133,17 +130,36 @@ class Carriers:
133
130
 
134
131
  class Address:
135
132
  @staticmethod
136
- def validate(payload: dict) -> datatypes.AddressValidation:
137
- validation = validators.Address.validate(datatypes.Address(**payload))
133
+ @utils.with_telemetry("address_validate")
134
+ def validate(
135
+ payload: dict,
136
+ provider: providers.Carrier = None,
137
+ **carrier_filters,
138
+ ) -> datatypes.AddressValidation:
139
+ provider = provider or Carriers.first(
140
+ **{
141
+ **dict(active=True, raise_not_found=True),
142
+ **carrier_filters,
143
+ }
144
+ )
145
+
146
+ if "validate_address" not in provider.gateway.proxy_methods:
147
+ raise exceptions.APIException(
148
+ detail=f"address validation is not supported by carrier: '{provider.carrier_id}'",
149
+ status_code=status.HTTP_406_NOT_ACCEPTABLE,
150
+ )
138
151
 
139
- if validation.success is False:
140
- raise exceptions.APIException(detail=validation, code="invalid_address")
152
+ request = karrio.Address.validate(
153
+ lib.to_object(datatypes.AddressValidationRequest, payload)
154
+ )
141
155
 
142
- return validation
156
+ # The request is wrapped in utils.identity to simplify mocking in tests.
157
+ return utils.identity(lambda: request.from_(provider.gateway).parse())
143
158
 
144
159
 
145
160
  class Shipments:
146
161
  @staticmethod
162
+ @utils.with_telemetry("shipment_create")
147
163
  @utils.require_selected_rate
148
164
  def create(
149
165
  payload: dict,
@@ -167,9 +183,19 @@ class Shipments:
167
183
  if carrier is None:
168
184
  raise NotFound("No active carrier connection found to process the request")
169
185
 
186
+ payload = {
187
+ **lib.to_dict(payload),
188
+ "options": {
189
+ **(selected_rate.meta or {}),
190
+ **(payload.get("options") or {}),
191
+ },
192
+ }
170
193
  request = lib.to_object(
171
194
  datatypes.ShipmentRequest,
172
- {**lib.to_dict(payload), "service": selected_rate.service},
195
+ {
196
+ **lib.to_dict(payload),
197
+ "service": selected_rate.service,
198
+ },
173
199
  )
174
200
 
175
201
  # The request is wrapped in utils.identity to simplify mocking in tests.
@@ -190,6 +216,7 @@ class Shipments:
190
216
  rate_provider = (
191
217
  (parent.meta or {}).get("rate_provider") or carrier.carrier_name
192
218
  ).lower()
219
+ custom_carrier_name = carrier.credentials.get("custom_carrier_name")
193
220
 
194
221
  return {
195
222
  **(parent.meta or {}),
@@ -197,6 +224,11 @@ class Shipments:
197
224
  "carrier": rate_provider,
198
225
  "service_name": service_name,
199
226
  "rate_provider": rate_provider, # TODO: deprecate 'rate_provider' in favor of 'carrier'
227
+ **(
228
+ {"custom_carrier_name": custom_carrier_name}
229
+ if custom_carrier_name
230
+ else {}
231
+ ),
200
232
  }
201
233
 
202
234
  def process_selected_rate() -> dict:
@@ -286,6 +318,7 @@ class Shipments:
286
318
  )
287
319
 
288
320
  @staticmethod
321
+ @utils.with_telemetry("shipment_cancel")
289
322
  def cancel(
290
323
  payload: dict, carrier: providers.Carrier = None, **carrier_filters
291
324
  ) -> datatypes.ConfirmationResponse:
@@ -336,6 +369,7 @@ class Shipments:
336
369
  )
337
370
 
338
371
  @staticmethod
372
+ @utils.with_telemetry("tracking_fetch")
339
373
  def track(
340
374
  payload: dict,
341
375
  carrier: providers.Carrier = None,
@@ -425,6 +459,7 @@ class Shipments:
425
459
 
426
460
  class Pickups:
427
461
  @staticmethod
462
+ @utils.with_telemetry("pickup_schedule")
428
463
  def schedule(
429
464
  payload: dict, carrier: providers.Carrier = None, **carrier_filters
430
465
  ) -> datatypes.PickupResponse:
@@ -474,6 +509,7 @@ class Pickups:
474
509
  )
475
510
 
476
511
  @staticmethod
512
+ @utils.with_telemetry("pickup_update")
477
513
  def update(
478
514
  payload: dict, carrier: providers.Carrier = None, **carrier_filters
479
515
  ) -> datatypes.PickupResponse:
@@ -514,6 +550,7 @@ class Pickups:
514
550
  )
515
551
 
516
552
  @staticmethod
553
+ @utils.with_telemetry("pickup_cancel")
517
554
  def cancel(
518
555
  payload: dict, carrier: providers.Carrier = None, **carrier_filters
519
556
  ) -> datatypes.ConfirmationResponse:
@@ -562,6 +599,7 @@ class Rates:
562
599
  post_process_functions: typing.List[typing.Callable] = []
563
600
 
564
601
  @staticmethod
602
+ @utils.with_telemetry("rates_fetch")
565
603
  def fetch(
566
604
  payload: dict,
567
605
  carriers: typing.List[providers.Carrier] = None,
@@ -640,6 +678,7 @@ class Rates:
640
678
 
641
679
  class Documents:
642
680
  @staticmethod
681
+ @utils.with_telemetry("document_upload")
643
682
  def upload(
644
683
  payload: dict,
645
684
  carrier: providers.Carrier = None,
@@ -687,6 +726,7 @@ class Documents:
687
726
 
688
727
  class Manifests:
689
728
  @staticmethod
729
+ @utils.with_telemetry("manifest_create")
690
730
  def create(
691
731
  payload: dict, carrier: providers.Carrier = None, **carrier_filters
692
732
  ) -> datatypes.ManifestResponse:
@@ -734,3 +774,238 @@ class Manifests:
734
774
  ),
735
775
  messages=messages,
736
776
  )
777
+
778
+
779
+ class Insurance:
780
+ @staticmethod
781
+ @utils.with_telemetry("insurance_apply")
782
+ def apply(
783
+ payload: dict, provider: providers.Carrier = None, **provider_filters
784
+ ) -> datatypes.InsuranceDetails:
785
+ provider = provider or Carriers.first(
786
+ **{
787
+ **dict(active=True, raise_not_found=True),
788
+ **provider_filters,
789
+ }
790
+ )
791
+
792
+ if "apply_insurance" not in provider.gateway.proxy_methods:
793
+ raise exceptions.APIException(
794
+ detail=f"insurance application is not supported by carrier: '{provider.carrier_id}'",
795
+ status_code=status.HTTP_406_NOT_ACCEPTABLE,
796
+ )
797
+
798
+ request = karrio.Insurance.apply(
799
+ lib.to_object(datatypes.InsuranceRequest, payload)
800
+ )
801
+
802
+ # The request call is wrapped in utils.identity to simplify mocking in tests
803
+ insurance, messages = utils.identity(
804
+ lambda: request.from_(provider.gateway).parse()
805
+ )
806
+
807
+ if insurance is None:
808
+ raise exceptions.APIException(
809
+ detail=messages,
810
+ status_code=status.HTTP_424_FAILED_DEPENDENCY,
811
+ )
812
+
813
+ def process_meta(parent) -> dict:
814
+ return {
815
+ **(parent.meta or {}),
816
+ "ext": provider.ext,
817
+ }
818
+
819
+ return datatypes.InsuranceResponse(
820
+ insurance=datatypes.Insurance(
821
+ **{
822
+ **payload,
823
+ **lib.to_dict(insurance),
824
+ "id": f"ins_{uuid.uuid4().hex}",
825
+ "test_mode": provider.test_mode,
826
+ "meta": process_meta(insurance),
827
+ "messages": messages,
828
+ }
829
+ ),
830
+ messages=messages,
831
+ )
832
+
833
+
834
+ class Duties:
835
+ @staticmethod
836
+ @utils.with_telemetry("duties_calculate")
837
+ def calculate(
838
+ payload: dict,
839
+ provider: providers.Carrier = None,
840
+ **provider_filters,
841
+ ) -> datatypes.DutiesResponse:
842
+ provider = provider or Carriers.first(
843
+ **{
844
+ **dict(active=True, raise_not_found=True),
845
+ **provider_filters,
846
+ }
847
+ )
848
+
849
+ if "calculate_duties" not in provider.gateway.proxy_methods:
850
+ raise exceptions.APIException(
851
+ detail=f"duties calculation is not supported by carrier: '{provider.carrier_id}'",
852
+ status_code=status.HTTP_406_NOT_ACCEPTABLE,
853
+ )
854
+
855
+ request = karrio.Duty.calculate(
856
+ lib.to_object(datatypes.DutiesCalculationRequest, payload)
857
+ )
858
+
859
+ # The request is wrapped in utils.identity to simplify mocking in tests.
860
+ duties, messages = utils.identity(
861
+ lambda: request.from_(provider.gateway).parse()
862
+ )
863
+
864
+ if duties is None:
865
+ raise exceptions.APIException(
866
+ detail=messages,
867
+ status_code=status.HTTP_424_FAILED_DEPENDENCY,
868
+ )
869
+
870
+ return datatypes.DutiesResponse(duties=duties, messages=messages)
871
+
872
+
873
+ class Webhooks:
874
+ @staticmethod
875
+ @utils.with_telemetry("webhook_register")
876
+ def register(
877
+ payload: dict,
878
+ carrier: providers.Carrier = None,
879
+ **carrier_filters,
880
+ ) -> datatypes.DocumentUploadResponse:
881
+ carrier = carrier or Carriers.first(
882
+ **{
883
+ **dict(active=True, raise_not_found=True),
884
+ **carrier_filters,
885
+ }
886
+ )
887
+
888
+ if "register_webhook" not in carrier.gateway.proxy_methods:
889
+ raise exceptions.APIException(
890
+ detail=f"webhook registration is not supported by carrier: '{carrier.carrier_id}'",
891
+ status_code=status.HTTP_406_NOT_ACCEPTABLE,
892
+ )
893
+
894
+ request = karrio.Webhook.register(
895
+ lib.to_object(datatypes.WebhookRegistrationRequest, payload)
896
+ )
897
+
898
+ # The request is wrapped in utils.identity to simplify mocking in tests.
899
+ return utils.identity(lambda: request.from_(carrier.gateway).parse())
900
+
901
+ @staticmethod
902
+ @utils.with_telemetry("webhook_unregister")
903
+ def unregister(
904
+ payload: dict,
905
+ carrier: providers.Carrier = None,
906
+ **carrier_filters,
907
+ ) -> datatypes.ConfirmationResponse:
908
+ carrier = carrier or Carriers.first(
909
+ **{
910
+ **dict(active=True, raise_not_found=True),
911
+ **carrier_filters,
912
+ }
913
+ )
914
+
915
+ if "deregister_webhook" not in carrier.gateway.proxy_methods:
916
+ raise exceptions.APIException(
917
+ detail=f"webhook deregistration is not supported by carrier: '{carrier.carrier_id}'",
918
+ status_code=status.HTTP_406_NOT_ACCEPTABLE,
919
+ )
920
+
921
+ request = karrio.Webhook.deregister(
922
+ lib.to_object(datatypes.WebhookDeregistrationRequest, payload)
923
+ )
924
+
925
+ # The request is wrapped in utils.identity to simplify mocking in tests.
926
+ return utils.identity(lambda: request.from_(carrier.gateway).parse())
927
+
928
+
929
+ class Hooks:
930
+
931
+ @staticmethod
932
+ def create_stub_gateway(
933
+ carrier_name: str, test_mode: bool = False
934
+ ) -> karrio.Gateway:
935
+ import karrio.server.core.middleware as middleware
936
+ import karrio.server.core.config as system_config
937
+ import django.core.cache as caching
938
+
939
+ _context = middleware.SessionContext.get_current_request()
940
+ _tracer = getattr(_context, "tracer", lib.Tracer())
941
+ _cache = lib.Cache(caching.cache)
942
+ _config = lib.SystemConfig(system_config.config)
943
+
944
+ return karrio.gateway[carrier_name].create(
945
+ dict(
946
+ carrier_id=carrier_name,
947
+ test_mode=test_mode,
948
+ ),
949
+ _tracer,
950
+ _cache,
951
+ _config,
952
+ is_stub=True,
953
+ )
954
+
955
+ @staticmethod
956
+ @utils.with_telemetry("hook_webhook_event")
957
+ def on_webhook_event(
958
+ payload: dict, carrier: providers.Carrier = None, **carrier_filters
959
+ ) -> typing.Tuple[datatypes.WebhookEventDetails, typing.List[datatypes.Message]]:
960
+ carrier = carrier or Carriers.first(
961
+ **{
962
+ **dict(active=True, raise_not_found=True),
963
+ **carrier_filters,
964
+ }
965
+ )
966
+
967
+ if carrier is None:
968
+ raise NotFound("No active carrier connection found to process the request")
969
+
970
+ request = karrio.Hooks.on_webhook_event(
971
+ lib.to_object(datatypes.RequestPayload, lib.to_dict(payload))
972
+ )
973
+
974
+ # The request call is wrapped in utils.identity to simplify mocking in tests
975
+ return utils.identity(lambda: request.from_(carrier.gateway).parse())
976
+
977
+ @staticmethod
978
+ @utils.with_telemetry("hook_oauth_authorize")
979
+ def on_oauth_authorize(
980
+ payload: dict,
981
+ carrier: providers.Carrier = None,
982
+ carrier_name: str = None,
983
+ test_mode: bool = False,
984
+ **kwargs,
985
+ ) -> typing.Tuple[datatypes.OAuthAuthorizeRequest, typing.List[datatypes.Message]]:
986
+ gateway = lib.identity(
987
+ getattr(carrier, "gateway", None)
988
+ or Hooks.create_stub_gateway(carrier_name, test_mode)
989
+ )
990
+
991
+ return utils.identity(
992
+ lambda: karrio.Hooks.on_oauth_authorize(payload).from_(gateway).parse()
993
+ )
994
+
995
+ @staticmethod
996
+ @utils.with_telemetry("hook_oauth_callback")
997
+ def on_oauth_callback(
998
+ payload: dict,
999
+ carrier_name: str = None,
1000
+ test_mode: bool = False,
1001
+ carrier: providers.Carrier = None,
1002
+ **kwargs,
1003
+ ) -> typing.Tuple[typing.List[typing.Dict], typing.List[datatypes.Message]]:
1004
+ gateway = lib.identity(
1005
+ getattr(carrier, "gateway", None)
1006
+ or Hooks.create_stub_gateway(carrier_name, test_mode)
1007
+ )
1008
+
1009
+ return utils.identity(
1010
+ lambda: karrio.Hooks.on_oauth_callback(payload).from_(gateway).parse()
1011
+ )