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
@@ -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.
@@ -198,7 +224,11 @@ class Shipments:
198
224
  "carrier": rate_provider,
199
225
  "service_name": service_name,
200
226
  "rate_provider": rate_provider, # TODO: deprecate 'rate_provider' in favor of 'carrier'
201
- **({"custom_carrier_name": custom_carrier_name} if custom_carrier_name else {}),
227
+ **(
228
+ {"custom_carrier_name": custom_carrier_name}
229
+ if custom_carrier_name
230
+ else {}
231
+ ),
202
232
  }
203
233
 
204
234
  def process_selected_rate() -> dict:
@@ -288,6 +318,7 @@ class Shipments:
288
318
  )
289
319
 
290
320
  @staticmethod
321
+ @utils.with_telemetry("shipment_cancel")
291
322
  def cancel(
292
323
  payload: dict, carrier: providers.Carrier = None, **carrier_filters
293
324
  ) -> datatypes.ConfirmationResponse:
@@ -338,6 +369,7 @@ class Shipments:
338
369
  )
339
370
 
340
371
  @staticmethod
372
+ @utils.with_telemetry("tracking_fetch")
341
373
  def track(
342
374
  payload: dict,
343
375
  carrier: providers.Carrier = None,
@@ -427,6 +459,7 @@ class Shipments:
427
459
 
428
460
  class Pickups:
429
461
  @staticmethod
462
+ @utils.with_telemetry("pickup_schedule")
430
463
  def schedule(
431
464
  payload: dict, carrier: providers.Carrier = None, **carrier_filters
432
465
  ) -> datatypes.PickupResponse:
@@ -476,6 +509,7 @@ class Pickups:
476
509
  )
477
510
 
478
511
  @staticmethod
512
+ @utils.with_telemetry("pickup_update")
479
513
  def update(
480
514
  payload: dict, carrier: providers.Carrier = None, **carrier_filters
481
515
  ) -> datatypes.PickupResponse:
@@ -516,6 +550,7 @@ class Pickups:
516
550
  )
517
551
 
518
552
  @staticmethod
553
+ @utils.with_telemetry("pickup_cancel")
519
554
  def cancel(
520
555
  payload: dict, carrier: providers.Carrier = None, **carrier_filters
521
556
  ) -> datatypes.ConfirmationResponse:
@@ -564,6 +599,7 @@ class Rates:
564
599
  post_process_functions: typing.List[typing.Callable] = []
565
600
 
566
601
  @staticmethod
602
+ @utils.with_telemetry("rates_fetch")
567
603
  def fetch(
568
604
  payload: dict,
569
605
  carriers: typing.List[providers.Carrier] = None,
@@ -642,6 +678,7 @@ class Rates:
642
678
 
643
679
  class Documents:
644
680
  @staticmethod
681
+ @utils.with_telemetry("document_upload")
645
682
  def upload(
646
683
  payload: dict,
647
684
  carrier: providers.Carrier = None,
@@ -689,6 +726,7 @@ class Documents:
689
726
 
690
727
  class Manifests:
691
728
  @staticmethod
729
+ @utils.with_telemetry("manifest_create")
692
730
  def create(
693
731
  payload: dict, carrier: providers.Carrier = None, **carrier_filters
694
732
  ) -> datatypes.ManifestResponse:
@@ -736,3 +774,238 @@ class Manifests:
736
774
  ),
737
775
  messages=messages,
738
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
+ )