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.
- karrio/server/core/authentication.py +38 -20
- karrio/server/core/config.py +31 -0
- karrio/server/core/datatypes.py +30 -4
- karrio/server/core/dataunits.py +26 -7
- karrio/server/core/exceptions.py +287 -17
- karrio/server/core/filters.py +14 -0
- karrio/server/core/gateway.py +284 -11
- karrio/server/core/logging.py +403 -0
- karrio/server/core/middleware.py +104 -2
- karrio/server/core/models/base.py +34 -1
- karrio/server/core/oauth_validators.py +2 -3
- karrio/server/core/permissions.py +1 -2
- karrio/server/core/serializers.py +154 -7
- karrio/server/core/signals.py +22 -28
- karrio/server/core/telemetry.py +573 -0
- karrio/server/core/tests/__init__.py +27 -0
- karrio/server/core/{tests.py → tests/base.py} +6 -7
- karrio/server/core/tests/test_exception_level.py +159 -0
- karrio/server/core/tests/test_resource_token.py +593 -0
- karrio/server/core/utils.py +688 -38
- karrio/server/core/validators.py +144 -222
- karrio/server/core/views/oauth.py +13 -12
- karrio/server/core/views/references.py +2 -2
- karrio/server/iam/apps.py +1 -4
- karrio/server/iam/migrations/0002_setup_carrier_permission_groups.py +103 -0
- karrio/server/iam/migrations/0003_remove_permission_groups.py +91 -0
- karrio/server/iam/permissions.py +7 -134
- karrio/server/iam/serializers.py +9 -3
- karrio/server/iam/signals.py +2 -4
- karrio/server/providers/admin.py +1 -1
- karrio/server/providers/migrations/0085_populate_dhl_parcel_de_oauth_credentials.py +82 -0
- karrio/server/providers/migrations/0086_rename_dhl_parcel_de_customer_number_to_billing_number.py +71 -0
- karrio/server/providers/migrations/0087_alter_carrier_capabilities.py +38 -0
- karrio/server/providers/migrations/0088_servicelevel_surcharges.py +24 -0
- karrio/server/providers/migrations/0089_servicelevel_cost_max_volume.py +31 -0
- karrio/server/providers/migrations/0090_ratesheet_surcharges_servicelevel_zone_surcharge_ids.py +47 -0
- karrio/server/providers/migrations/0091_migrate_legacy_zones_surcharges.py +154 -0
- karrio/server/providers/models/carrier.py +101 -29
- karrio/server/providers/models/service.py +182 -125
- karrio/server/providers/models/sheet.py +342 -198
- karrio/server/providers/serializers/base.py +263 -2
- karrio/server/providers/signals.py +2 -4
- karrio/server/providers/templates/providers/oauth_callback.html +105 -0
- karrio/server/providers/tests/__init__.py +5 -0
- karrio/server/providers/tests/test_connections.py +895 -0
- karrio/server/providers/views/carriers.py +1 -3
- karrio/server/providers/views/connections.py +322 -2
- karrio/server/serializers/abstract.py +112 -21
- karrio/server/tracing/utils.py +5 -8
- karrio/server/user/models.py +36 -34
- karrio/server/user/serializers.py +1 -0
- {karrio_server_core-2025.5rc31.dist-info → karrio_server_core-2026.1.1.dist-info}/METADATA +2 -2
- {karrio_server_core-2025.5rc31.dist-info → karrio_server_core-2026.1.1.dist-info}/RECORD +55 -38
- karrio/server/providers/tests.py +0 -3
- {karrio_server_core-2025.5rc31.dist-info → karrio_server_core-2026.1.1.dist-info}/WHEEL +0 -0
- {karrio_server_core-2025.5rc31.dist-info → karrio_server_core-2026.1.1.dist-info}/top_level.txt +0 -0
karrio/server/core/gateway.py
CHANGED
|
@@ -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
|
-
|
|
137
|
-
|
|
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
|
-
|
|
140
|
-
|
|
152
|
+
request = karrio.Address.validate(
|
|
153
|
+
lib.to_object(datatypes.AddressValidationRequest, payload)
|
|
154
|
+
)
|
|
141
155
|
|
|
142
|
-
|
|
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
|
-
{
|
|
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
|
-
**(
|
|
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
|
+
)
|