karrio-server-core 2025.5__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/conf.py +54 -0
- karrio/server/core/__init__.py +3 -0
- karrio/server/core/admin.py +1 -0
- karrio/server/core/apps.py +10 -0
- karrio/server/core/authentication.py +347 -0
- karrio/server/core/config.py +31 -0
- karrio/server/core/context_processors.py +12 -0
- karrio/server/core/datatypes.py +394 -0
- karrio/server/core/dataunits.py +187 -0
- karrio/server/core/exceptions.py +404 -0
- karrio/server/core/fields.py +12 -0
- karrio/server/core/filters.py +837 -0
- karrio/server/core/gateway.py +1011 -0
- karrio/server/core/logging.py +403 -0
- karrio/server/core/management/commands/cli.py +19 -0
- karrio/server/core/management/commands/create_oauth_client.py +41 -0
- karrio/server/core/management/commands/runserver.py +5 -0
- karrio/server/core/middleware.py +197 -0
- karrio/server/core/migrations/0001_initial.py +28 -0
- karrio/server/core/migrations/0002_apilogindex.py +69 -0
- karrio/server/core/migrations/0003_apilogindex_test_mode.py +62 -0
- karrio/server/core/migrations/0004_metafield.py +74 -0
- karrio/server/core/migrations/0005_alter_metafield_type_alter_metafield_value.py +23 -0
- karrio/server/core/migrations/0006_add_api_log_requested_at_index.py +22 -0
- karrio/server/core/migrations/__init__.py +0 -0
- karrio/server/core/models/__init__.py +48 -0
- karrio/server/core/models/base.py +103 -0
- karrio/server/core/models/entity.py +24 -0
- karrio/server/core/models/metafield.py +144 -0
- karrio/server/core/models/third_party.py +21 -0
- karrio/server/core/oauth_validators.py +170 -0
- karrio/server/core/permissions.py +36 -0
- karrio/server/core/renderers.py +11 -0
- karrio/server/core/router.py +3 -0
- karrio/server/core/serializers.py +1971 -0
- karrio/server/core/signals.py +55 -0
- karrio/server/core/telemetry.py +573 -0
- karrio/server/core/tests.py +99 -0
- karrio/server/core/tests_resource_token.py +411 -0
- karrio/server/core/urls.py +12 -0
- karrio/server/core/utils.py +1025 -0
- karrio/server/core/validators.py +264 -0
- karrio/server/core/views/__init__.py +2 -0
- karrio/server/core/views/api.py +133 -0
- karrio/server/core/views/metadata.py +44 -0
- karrio/server/core/views/oauth.py +75 -0
- karrio/server/core/views/references.py +82 -0
- karrio/server/core/views/schema.py +310 -0
- karrio/server/filters/__init__.py +2 -0
- karrio/server/filters/abstract.py +26 -0
- karrio/server/iam/__init__.py +0 -0
- karrio/server/iam/admin.py +3 -0
- karrio/server/iam/apps.py +21 -0
- karrio/server/iam/migrations/0001_initial.py +33 -0
- karrio/server/iam/migrations/__init__.py +0 -0
- karrio/server/iam/models.py +48 -0
- karrio/server/iam/permissions.py +155 -0
- karrio/server/iam/serializers.py +54 -0
- karrio/server/iam/signals.py +18 -0
- karrio/server/iam/tests.py +3 -0
- karrio/server/iam/views.py +3 -0
- karrio/server/openapi.py +75 -0
- karrio/server/providers/__init__.py +1 -0
- karrio/server/providers/admin.py +364 -0
- karrio/server/providers/apps.py +10 -0
- karrio/server/providers/management/commands/migrate_rate_sheets.py +101 -0
- karrio/server/providers/migrations/0001_initial.py +140 -0
- karrio/server/providers/migrations/0002_carrier_active.py +18 -0
- karrio/server/providers/migrations/0003_auto_20201230_0820.py +24 -0
- karrio/server/providers/migrations/0004_auto_20210212_0554.py +178 -0
- karrio/server/providers/migrations/0005_auto_20210212_0555.py +18 -0
- karrio/server/providers/migrations/0006_australiapostsettings.py +29 -0
- karrio/server/providers/migrations/0007_auto_20210213_0206.py +21 -0
- karrio/server/providers/migrations/0008_auto_20210214_0409.py +30 -0
- karrio/server/providers/migrations/0009_auto_20210308_0302.py +18 -0
- karrio/server/providers/migrations/0010_auto_20210409_0852.py +32 -0
- karrio/server/providers/migrations/0011_auto_20210409_0853.py +21 -0
- karrio/server/providers/migrations/0012_alter_carrier_options.py +17 -0
- karrio/server/providers/migrations/0013_tntsettings.py +30 -0
- karrio/server/providers/migrations/0014_auto_20210612_1608.py +46 -0
- karrio/server/providers/migrations/0015_auto_20210615_1601.py +28 -0
- karrio/server/providers/migrations/0016_alter_purolatorsettings_user_token.py +18 -0
- karrio/server/providers/migrations/0017_auto_20210805_0359.py +1293 -0
- karrio/server/providers/migrations/0018_alter_fedexsettings_user_key.py +18 -0
- karrio/server/providers/migrations/0019_dhlpolandsettings_servicelevel.py +65 -0
- karrio/server/providers/migrations/0020_genericsettings_labeltemplate.py +52 -0
- karrio/server/providers/migrations/0021_auto_20211231_2353.py +40 -0
- karrio/server/providers/migrations/0022_carrier_metadata.py +18 -0
- karrio/server/providers/migrations/0023_auto_20220124_1916.py +27 -0
- karrio/server/providers/migrations/0024_alter_genericsettings_custom_carrier_name.py +19 -0
- karrio/server/providers/migrations/0025_alter_servicelevel_service_code.py +19 -0
- karrio/server/providers/migrations/0026_auto_20220208_0132.py +59 -0
- karrio/server/providers/migrations/0027_auto_20220304_1340.py +29 -0
- karrio/server/providers/migrations/0028_auto_20220323_1500.py +33 -0
- karrio/server/providers/migrations/0029_easypostsettings.py +27 -0
- karrio/server/providers/migrations/0030_amazonmwssettings.py +29 -0
- karrio/server/providers/migrations/0031_delete_amazonmwssettings.py +18 -0
- karrio/server/providers/migrations/0032_alter_carrier_test.py +18 -0
- karrio/server/providers/migrations/0033_auto_20220708_1350.py +22 -0
- karrio/server/providers/migrations/0034_amazonmwssettings_dpdhlsettings.py +47 -0
- karrio/server/providers/migrations/0035_alter_carrier_capabilities.py +43 -0
- karrio/server/providers/migrations/0036_upsfreightsettings.py +31 -0
- karrio/server/providers/migrations/0037_chronopostsettings.py +29 -0
- karrio/server/providers/migrations/0038_alter_genericsettings_label_template.py +19 -0
- karrio/server/providers/migrations/0039_auto_20220906_0612.py +23 -0
- karrio/server/providers/migrations/0040_dpdhlsettings_services.py +18 -0
- karrio/server/providers/migrations/0041_auto_20221105_0705.py +38 -0
- karrio/server/providers/migrations/0042_auto_20221215_1642.py +23 -0
- karrio/server/providers/migrations/0043_alter_genericsettings_account_number_and_more.py +39 -0
- karrio/server/providers/migrations/0044_carrier_carrier_capabilities.py +64 -0
- karrio/server/providers/migrations/0045_alter_carrier_active_alter_carrier_carrier_id.py +31 -0
- karrio/server/providers/migrations/0046_remove_dpdhlsettings_signature_and_more.py +41 -0
- karrio/server/providers/migrations/0047_dpdsettings.py +286 -0
- karrio/server/providers/migrations/0048_servicelevel_min_weight_servicelevel_transit_days_and_more.py +64 -0
- karrio/server/providers/migrations/0049_boxknightsettings_geodissettings_lapostesettings_and_more.py +156 -0
- karrio/server/providers/migrations/0050_carrier_is_system_alter_carrier_metadata_and_more.py +106 -0
- karrio/server/providers/migrations/0051_rename_username_upssettings_client_id_and_more.py +31 -0
- karrio/server/providers/migrations/0052_alter_upssettings_account_number_and_more.py +20 -0
- karrio/server/providers/migrations/0053_locate2usettings.py +281 -0
- karrio/server/providers/migrations/0054_zoom2usettings.py +280 -0
- karrio/server/providers/migrations/0055_rename_amazonmwssettings_amazonshippingsettings_and_more.py +44 -0
- karrio/server/providers/migrations/0056_asendiaussettings_geodissettings_code_client_and_more.py +75 -0
- karrio/server/providers/migrations/0057_alter_servicelevel_weight_unit_belgianpostsettings.py +51 -0
- karrio/server/providers/migrations/0058_alliedexpresssettings.py +38 -0
- karrio/server/providers/migrations/0059_ratesheet.py +81 -0
- karrio/server/providers/migrations/0060_belgianpostsettings_rate_sheet_and_more.py +73 -0
- karrio/server/providers/migrations/0061_alliedexpresssettings_service_type.py +17 -0
- karrio/server/providers/migrations/0062_sendlesettings_account_country_code.py +257 -0
- karrio/server/providers/migrations/0063_servicelevel_metadata.py +25 -0
- karrio/server/providers/migrations/0064_alliedexpresslocalsettings.py +43 -0
- karrio/server/providers/migrations/0065_servicelevel_carrier_service_code_and_more.py +66 -0
- karrio/server/providers/migrations/0066_rename_fedexsettings_fedexwssettings_and_more.py +28 -0
- karrio/server/providers/migrations/0067_fedexsettings.py +283 -0
- karrio/server/providers/migrations/0068_fedexsettings_track_api_key_and_more.py +38 -0
- karrio/server/providers/migrations/0069_alter_canadapostsettings_contract_id_and_more.py +23 -0
- karrio/server/providers/migrations/0070_tgesettings_alter_carrier_capabilities.py +65 -0
- karrio/server/providers/migrations/0071_alter_tgesettings_my_toll_token.py +18 -0
- karrio/server/providers/migrations/0072_rename_eshippersettings_eshipperxmlsettings_and_more.py +28 -0
- karrio/server/providers/migrations/0073_delete_eshipperxmlsettings.py +41 -0
- karrio/server/providers/migrations/0074_eshippersettings.py +38 -0
- karrio/server/providers/migrations/0075_haypostsettings.py +40 -0
- karrio/server/providers/migrations/0076_rename_customer_registration_id_uspsinternationalsettings_account_number_and_more.py +125 -0
- karrio/server/providers/migrations/0077_uspswtinternationalsettings_uspswtsettings_and_more.py +165 -0
- karrio/server/providers/migrations/0078_auto_20240813_1552.py +120 -0
- karrio/server/providers/migrations/0079_alter_carrier_options_alter_ratesheet_created_by.py +31 -0
- karrio/server/providers/migrations/0080_alter_aramexsettings_account_country_code_and_more.py +3025 -0
- karrio/server/providers/migrations/0081_remove_alliedexpresssettings_carrier_ptr_and_more.py +338 -0
- karrio/server/providers/migrations/0082_add_zone_identifiers.py +50 -0
- karrio/server/providers/migrations/0083_add_optimized_rate_sheet_structure.py +33 -0
- karrio/server/providers/migrations/0084_alter_servicelevel_currency.py +168 -0
- 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/__init__.py +0 -0
- karrio/server/providers/models/__init__.py +16 -0
- karrio/server/providers/models/carrier.py +387 -0
- karrio/server/providers/models/config.py +30 -0
- karrio/server/providers/models/service.py +192 -0
- karrio/server/providers/models/sheet.py +287 -0
- karrio/server/providers/models/template.py +39 -0
- karrio/server/providers/models/utils.py +58 -0
- karrio/server/providers/router.py +3 -0
- karrio/server/providers/serializers/__init__.py +3 -0
- karrio/server/providers/serializers/base.py +538 -0
- karrio/server/providers/signals.py +25 -0
- 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/urls.py +11 -0
- karrio/server/providers/views/__init__.py +0 -0
- karrio/server/providers/views/carriers.py +267 -0
- karrio/server/providers/views/connections.py +496 -0
- karrio/server/samples.py +352 -0
- karrio/server/serializers/__init__.py +2 -0
- karrio/server/serializers/abstract.py +602 -0
- karrio/server/tracing/__init__.py +0 -0
- karrio/server/tracing/admin.py +63 -0
- karrio/server/tracing/apps.py +8 -0
- karrio/server/tracing/migrations/0001_initial.py +41 -0
- karrio/server/tracing/migrations/0002_auto_20220710_1307.py +22 -0
- karrio/server/tracing/migrations/0003_auto_20221105_0317.py +43 -0
- karrio/server/tracing/migrations/0004_tracingrecord_carrier_account_idx.py +24 -0
- karrio/server/tracing/migrations/0005_optimise_tracingrecord_request_log_idx.py +25 -0
- karrio/server/tracing/migrations/0006_alter_tracingrecord_options_and_more.py +49 -0
- karrio/server/tracing/migrations/0007_tracingrecord_tracing_created_at_idx.py +19 -0
- karrio/server/tracing/migrations/__init__.py +0 -0
- karrio/server/tracing/models.py +82 -0
- karrio/server/tracing/tests.py +3 -0
- karrio/server/tracing/utils.py +109 -0
- karrio/server/user/__init__.py +0 -0
- karrio/server/user/admin.py +96 -0
- karrio/server/user/apps.py +7 -0
- karrio/server/user/forms.py +35 -0
- karrio/server/user/migrations/0001_initial.py +41 -0
- karrio/server/user/migrations/0002_token.py +29 -0
- karrio/server/user/migrations/0003_token_test_mode.py +20 -0
- karrio/server/user/migrations/0004_group.py +26 -0
- karrio/server/user/migrations/0005_token_label.py +21 -0
- karrio/server/user/migrations/0006_workspaceconfig.py +63 -0
- karrio/server/user/migrations/0007_user_metadata.py +25 -0
- karrio/server/user/migrations/__init__.py +0 -0
- karrio/server/user/models.py +218 -0
- karrio/server/user/serializers.py +47 -0
- karrio/server/user/templates/registration/login.html +108 -0
- karrio/server/user/templates/registration/registration_confirm_email.html +10 -0
- karrio/server/user/templates/registration/registration_confirm_email.txt +3 -0
- karrio/server/user/tests.py +3 -0
- karrio/server/user/urls.py +10 -0
- karrio/server/user/utils.py +60 -0
- karrio/server/user/views.py +9 -0
- karrio_server_core-2025.5.dist-info/METADATA +32 -0
- karrio_server_core-2025.5.dist-info/RECORD +213 -0
- karrio_server_core-2025.5.dist-info/WHEEL +5 -0
- karrio_server_core-2025.5.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,1011 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
import typing
|
|
3
|
+
import datetime
|
|
4
|
+
|
|
5
|
+
from django.db.models import Q
|
|
6
|
+
from django.conf import settings
|
|
7
|
+
from rest_framework import status
|
|
8
|
+
from rest_framework.exceptions import NotFound
|
|
9
|
+
from karrio.server.core.logging import logger
|
|
10
|
+
|
|
11
|
+
import karrio.lib as lib
|
|
12
|
+
import karrio.sdk as karrio
|
|
13
|
+
import karrio.server.core.utils as utils
|
|
14
|
+
import karrio.server.core.models as core
|
|
15
|
+
import karrio.server.core.datatypes as datatypes
|
|
16
|
+
import karrio.server.core.dataunits as dataunits
|
|
17
|
+
import karrio.server.core.exceptions as exceptions
|
|
18
|
+
import karrio.server.providers.models as providers
|
|
19
|
+
import karrio.server.serializers as base_serializers
|
|
20
|
+
import karrio.server.core.serializers as serializers
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Carriers:
|
|
24
|
+
@staticmethod
|
|
25
|
+
def list(context=None, **kwargs) -> typing.List[providers.Carrier]:
|
|
26
|
+
list_filter = kwargs.copy()
|
|
27
|
+
user_filter = core.get_access_filter(context) if context is not None else []
|
|
28
|
+
|
|
29
|
+
test_mode = list_filter.get("test_mode") or getattr(context, "test_mode", None)
|
|
30
|
+
system_only = list_filter.get("system_only") is True
|
|
31
|
+
active_key = lib.identity(
|
|
32
|
+
"active_orgs__id" if settings.MULTI_ORGANIZATIONS else "active_users__id"
|
|
33
|
+
)
|
|
34
|
+
access_id = getattr(
|
|
35
|
+
getattr(context, "org" if settings.MULTI_ORGANIZATIONS else "user", None),
|
|
36
|
+
"id",
|
|
37
|
+
None,
|
|
38
|
+
)
|
|
39
|
+
creator_filter = lib.identity(
|
|
40
|
+
Q(
|
|
41
|
+
created_by__id=context.user.id,
|
|
42
|
+
**(dict(org=None) if settings.MULTI_ORGANIZATIONS else {}),
|
|
43
|
+
)
|
|
44
|
+
if getattr(context, "user", None) is not None
|
|
45
|
+
else Q()
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
_user_carriers = providers.Carrier.user_carriers.filter(
|
|
49
|
+
user_filter if len(user_filter) > 0 else Q() | creator_filter
|
|
50
|
+
)
|
|
51
|
+
_system_carriers = providers.Carrier.system_carriers.filter(
|
|
52
|
+
Q(
|
|
53
|
+
**{
|
|
54
|
+
"active": True,
|
|
55
|
+
**({active_key: access_id} if access_id is not None else {}),
|
|
56
|
+
}
|
|
57
|
+
)
|
|
58
|
+
)
|
|
59
|
+
_queryset = lib.identity(
|
|
60
|
+
_system_carriers if system_only else _user_carriers | _system_carriers
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# Check if the test filter is specified then set it otherwise return all carriers live and test mode
|
|
64
|
+
if test_mode is not None:
|
|
65
|
+
_queryset = _queryset.filter(test_mode=test_mode)
|
|
66
|
+
|
|
67
|
+
# Check if the active flag is specified and return all active carrier is active is not set to false
|
|
68
|
+
if list_filter.get("active") is not None:
|
|
69
|
+
active = False if list_filter["active"] is False else True
|
|
70
|
+
_queryset = _queryset.filter(Q(active=active))
|
|
71
|
+
|
|
72
|
+
# Check if a specific carrier_id is provided, to add it to the query
|
|
73
|
+
if "carrier_id" in list_filter:
|
|
74
|
+
_queryset = _queryset.filter(carrier_id=list_filter["carrier_id"])
|
|
75
|
+
|
|
76
|
+
# Check if a specific carrier_id is provided, to add it to the query
|
|
77
|
+
if "capability" in list_filter:
|
|
78
|
+
_queryset = _queryset.filter(
|
|
79
|
+
capabilities__icontains=list_filter["capability"]
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
# Check if a metadata key is provided, to add it to the query
|
|
83
|
+
if "metadata_key" in list_filter:
|
|
84
|
+
_queryset = _queryset.filter(metadata__has_key=list_filter["metadata_key"])
|
|
85
|
+
|
|
86
|
+
# Check if a metadata value is provided, to add it to the query
|
|
87
|
+
if "metadata_value" in list_filter:
|
|
88
|
+
_value = list_filter["metadata_value"]
|
|
89
|
+
_queryset = _queryset.filter(
|
|
90
|
+
id__in=[
|
|
91
|
+
_["id"]
|
|
92
|
+
for _ in _queryset.values("id", "metadata")
|
|
93
|
+
if _value in (_.get("metadata") or {}).values()
|
|
94
|
+
]
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
# Check if a list of carrier_ids are provided, to add the list to the query
|
|
98
|
+
if any(list_filter.get("carrier_ids", [])):
|
|
99
|
+
_queryset = _queryset.filter(carrier_id__in=list_filter["carrier_ids"])
|
|
100
|
+
|
|
101
|
+
if any(list_filter.get("services", [])):
|
|
102
|
+
carrier_names = [
|
|
103
|
+
name
|
|
104
|
+
for name, services in dataunits.contextual_reference(context)[
|
|
105
|
+
"services"
|
|
106
|
+
].items()
|
|
107
|
+
if any(
|
|
108
|
+
service in list_filter["services"] for service in services.keys()
|
|
109
|
+
)
|
|
110
|
+
]
|
|
111
|
+
|
|
112
|
+
if len(carrier_names) > 0:
|
|
113
|
+
_queryset = _queryset.filter(carrier_code__in=carrier_names)
|
|
114
|
+
if "carrier_name" in list_filter:
|
|
115
|
+
carrier_name = list_filter["carrier_name"]
|
|
116
|
+
_queryset = _queryset.filter(carrier_code=carrier_name)
|
|
117
|
+
|
|
118
|
+
carriers = _queryset.distinct()
|
|
119
|
+
|
|
120
|
+
# Raise an error if no carrier is found
|
|
121
|
+
if list_filter.get("raise_not_found") and len(carriers) == 0:
|
|
122
|
+
raise NotFound("No active carrier connection found to process the request")
|
|
123
|
+
|
|
124
|
+
return carriers
|
|
125
|
+
|
|
126
|
+
@staticmethod
|
|
127
|
+
def first(**kwargs) -> providers.Carrier:
|
|
128
|
+
return next(iter(Carriers.list(**kwargs)), None)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class Address:
|
|
132
|
+
@staticmethod
|
|
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
|
+
)
|
|
151
|
+
|
|
152
|
+
request = karrio.Address.validate(
|
|
153
|
+
lib.to_object(datatypes.AddressValidationRequest, payload)
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
# The request is wrapped in utils.identity to simplify mocking in tests.
|
|
157
|
+
return utils.identity(lambda: request.from_(provider.gateway).parse())
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class Shipments:
|
|
161
|
+
@staticmethod
|
|
162
|
+
@utils.with_telemetry("shipment_create")
|
|
163
|
+
@utils.require_selected_rate
|
|
164
|
+
def create(
|
|
165
|
+
payload: dict,
|
|
166
|
+
carrier: providers.Carrier = None,
|
|
167
|
+
selected_rate: typing.Union[datatypes.Rate, dict] = None,
|
|
168
|
+
resolve_tracking_url: typing.Callable[[str, str], str] = None,
|
|
169
|
+
context: base_serializers.Context = None,
|
|
170
|
+
**kwargs,
|
|
171
|
+
) -> datatypes.Shipment:
|
|
172
|
+
selected_rate = lib.to_object(
|
|
173
|
+
datatypes.Rate,
|
|
174
|
+
lib.to_dict(selected_rate),
|
|
175
|
+
)
|
|
176
|
+
carrier = carrier or Carriers.first(
|
|
177
|
+
carrier_id=selected_rate.carrier_id,
|
|
178
|
+
test_mode=selected_rate.test_mode,
|
|
179
|
+
services=[selected_rate.service],
|
|
180
|
+
context=context,
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
if carrier is None:
|
|
184
|
+
raise NotFound("No active carrier connection found to process the request")
|
|
185
|
+
|
|
186
|
+
payload = {
|
|
187
|
+
**lib.to_dict(payload),
|
|
188
|
+
"options": {
|
|
189
|
+
**(selected_rate.meta or {}),
|
|
190
|
+
**(payload.get("options") or {}),
|
|
191
|
+
},
|
|
192
|
+
}
|
|
193
|
+
request = lib.to_object(
|
|
194
|
+
datatypes.ShipmentRequest,
|
|
195
|
+
{
|
|
196
|
+
**lib.to_dict(payload),
|
|
197
|
+
"service": selected_rate.service,
|
|
198
|
+
},
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
# The request is wrapped in utils.identity to simplify mocking in tests.
|
|
202
|
+
shipment, messages = utils.identity(
|
|
203
|
+
lambda: karrio.Shipment.create(request).from_(carrier.gateway).parse()
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
if shipment is None:
|
|
207
|
+
raise exceptions.APIException(
|
|
208
|
+
detail=messages,
|
|
209
|
+
status_code=status.HTTP_424_FAILED_DEPENDENCY,
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
def process_meta(parent) -> dict:
|
|
213
|
+
service_name = utils.upper(
|
|
214
|
+
(parent.meta or {}).get("service_name") or selected_rate.service
|
|
215
|
+
)
|
|
216
|
+
rate_provider = (
|
|
217
|
+
(parent.meta or {}).get("rate_provider") or carrier.carrier_name
|
|
218
|
+
).lower()
|
|
219
|
+
custom_carrier_name = carrier.credentials.get("custom_carrier_name")
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
**(parent.meta or {}),
|
|
223
|
+
"ext": carrier.ext,
|
|
224
|
+
"carrier": rate_provider,
|
|
225
|
+
"service_name": service_name,
|
|
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
|
+
),
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
def process_selected_rate() -> dict:
|
|
235
|
+
estimated_delivery = lib.failsafe(
|
|
236
|
+
lambda: (
|
|
237
|
+
getattr(shipment.selected_rate, "estimated_delivery", None)
|
|
238
|
+
or getattr(selected_rate, "estimated_delivery", None)
|
|
239
|
+
)
|
|
240
|
+
)
|
|
241
|
+
transit_days = lib.failsafe(
|
|
242
|
+
lambda: (
|
|
243
|
+
getattr(shipment.selected_rate, "transit_days", None)
|
|
244
|
+
or getattr(selected_rate, "transit_days", None)
|
|
245
|
+
)
|
|
246
|
+
)
|
|
247
|
+
rate = lib.identity(
|
|
248
|
+
{
|
|
249
|
+
**lib.to_dict(shipment.selected_rate),
|
|
250
|
+
"id": f"rat_{uuid.uuid4().hex}",
|
|
251
|
+
"test_mode": carrier.test_mode,
|
|
252
|
+
"estimated_delivery": estimated_delivery,
|
|
253
|
+
"transit_days": transit_days,
|
|
254
|
+
}
|
|
255
|
+
if shipment.selected_rate is not None
|
|
256
|
+
else lib.to_dict(selected_rate)
|
|
257
|
+
)
|
|
258
|
+
return lib.to_dict(
|
|
259
|
+
{
|
|
260
|
+
**rate,
|
|
261
|
+
"meta": process_meta(shipment.selected_rate or selected_rate),
|
|
262
|
+
}
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
def process_tracking_url(rate: datatypes.Rate) -> str:
|
|
266
|
+
rate_provider = (rate.get("meta") or {}).get("rate_provider")
|
|
267
|
+
if (rate_provider not in dataunits.CARRIER_NAMES) and (
|
|
268
|
+
(shipment.meta or {}).get("tracking_url") is not None
|
|
269
|
+
):
|
|
270
|
+
return shipment.meta["tracking_url"]
|
|
271
|
+
|
|
272
|
+
if resolve_tracking_url is not None:
|
|
273
|
+
url = resolve_tracking_url(
|
|
274
|
+
shipment.tracking_number, rate_provider or rate.carrier_name
|
|
275
|
+
)
|
|
276
|
+
return utils.app_tracking_query_params(url, carrier)
|
|
277
|
+
|
|
278
|
+
return ""
|
|
279
|
+
|
|
280
|
+
def process_parcel_refs(parcels: typing.List[dict]) -> list:
|
|
281
|
+
references = (shipment.meta or {}).get("tracking_numbers") or [
|
|
282
|
+
shipment.tracking_number
|
|
283
|
+
]
|
|
284
|
+
|
|
285
|
+
return [
|
|
286
|
+
{
|
|
287
|
+
**lib.to_dict(parcel),
|
|
288
|
+
"reference_number": (
|
|
289
|
+
references[index]
|
|
290
|
+
if len(references) > index
|
|
291
|
+
else parcel.get("reference_number")
|
|
292
|
+
),
|
|
293
|
+
}
|
|
294
|
+
for index, parcel in enumerate(parcels)
|
|
295
|
+
]
|
|
296
|
+
|
|
297
|
+
shipment_rate = process_selected_rate()
|
|
298
|
+
|
|
299
|
+
return lib.to_object(
|
|
300
|
+
datatypes.Shipment,
|
|
301
|
+
{
|
|
302
|
+
"id": f"shp_{uuid.uuid4().hex}",
|
|
303
|
+
**payload,
|
|
304
|
+
**lib.to_dict(shipment),
|
|
305
|
+
"test_mode": carrier.test_mode,
|
|
306
|
+
"selected_rate": shipment_rate,
|
|
307
|
+
"service": shipment_rate["service"],
|
|
308
|
+
"selected_rate_id": shipment_rate["id"],
|
|
309
|
+
"parcels": process_parcel_refs(payload["parcels"]),
|
|
310
|
+
"tracking_url": process_tracking_url(shipment_rate),
|
|
311
|
+
"status": serializers.ShipmentStatus.purchased.value,
|
|
312
|
+
"created_at": datetime.datetime.now().strftime(
|
|
313
|
+
"%Y-%m-%d %H:%M:%S.%f%z"
|
|
314
|
+
),
|
|
315
|
+
"meta": process_meta(shipment),
|
|
316
|
+
"messages": messages,
|
|
317
|
+
},
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
@staticmethod
|
|
321
|
+
@utils.with_telemetry("shipment_cancel")
|
|
322
|
+
def cancel(
|
|
323
|
+
payload: dict, carrier: providers.Carrier = None, **carrier_filters
|
|
324
|
+
) -> datatypes.ConfirmationResponse:
|
|
325
|
+
carrier_id = lib.identity(
|
|
326
|
+
dict(carrier_id=payload.pop("carrier_id"))
|
|
327
|
+
if any(payload.get("carrier_id") or "")
|
|
328
|
+
else {}
|
|
329
|
+
)
|
|
330
|
+
carrier = carrier or Carriers.first(
|
|
331
|
+
**{
|
|
332
|
+
**dict(active=True, capability="shipping", raise_not_found=True),
|
|
333
|
+
**carrier_id,
|
|
334
|
+
**carrier_filters,
|
|
335
|
+
}
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
if carrier is None:
|
|
339
|
+
raise NotFound("No active carrier connection found to process the request")
|
|
340
|
+
|
|
341
|
+
request = karrio.Shipment.cancel(
|
|
342
|
+
lib.to_object(datatypes.ShipmentCancelRequest, payload)
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
# The request call is wrapped in utils.identity to simplify mocking in tests
|
|
346
|
+
confirmation, messages = lib.identity(
|
|
347
|
+
utils.identity(lambda: request.from_(carrier.gateway).parse())
|
|
348
|
+
if "cancel_shipment" in carrier.gateway.proxy_methods
|
|
349
|
+
else (
|
|
350
|
+
datatypes.Confirmation(
|
|
351
|
+
carrier_name=carrier.gateway.settings.carrier_name,
|
|
352
|
+
carrier_id=carrier.gateway.settings.carrier_id,
|
|
353
|
+
success=True,
|
|
354
|
+
operation="Safe cancellation allowed",
|
|
355
|
+
),
|
|
356
|
+
[],
|
|
357
|
+
)
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
if confirmation is None:
|
|
361
|
+
raise exceptions.APIException(
|
|
362
|
+
detail=messages,
|
|
363
|
+
status_code=status.HTTP_424_FAILED_DEPENDENCY,
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
return datatypes.ConfirmationResponse(
|
|
367
|
+
confirmation=confirmation,
|
|
368
|
+
messages=messages,
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
@staticmethod
|
|
372
|
+
@utils.with_telemetry("tracking_fetch")
|
|
373
|
+
def track(
|
|
374
|
+
payload: dict,
|
|
375
|
+
carrier: providers.Carrier = None,
|
|
376
|
+
raise_on_error: bool = True,
|
|
377
|
+
**carrier_filters,
|
|
378
|
+
) -> datatypes.TrackingResponse:
|
|
379
|
+
carrier = carrier or Carriers.first(
|
|
380
|
+
**{
|
|
381
|
+
**dict(active=True, capability="tracking", raise_not_found=True),
|
|
382
|
+
**carrier_filters,
|
|
383
|
+
}
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
if carrier is None:
|
|
387
|
+
raise NotFound("No active carrier connection found to process the request")
|
|
388
|
+
|
|
389
|
+
request = karrio.Tracking.fetch(
|
|
390
|
+
lib.to_object(datatypes.TrackingRequest, payload)
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
# The request call is wrapped in utils.identity to simplify mocking in tests
|
|
394
|
+
results, messages = utils.identity(
|
|
395
|
+
lambda: request.from_(carrier.gateway).parse()
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
if not any(results or []) and (
|
|
399
|
+
raise_on_error or utils.is_sdk_message(messages)
|
|
400
|
+
):
|
|
401
|
+
raise exceptions.APIException(
|
|
402
|
+
detail=messages,
|
|
403
|
+
status_code=status.HTTP_404_NOT_FOUND,
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
result = next(iter(results or []), None)
|
|
407
|
+
tracking_number = payload["tracking_numbers"][0]
|
|
408
|
+
details = result or datatypes.TrackingDetails(
|
|
409
|
+
carrier_id=carrier.carrier_id,
|
|
410
|
+
carrier_name=carrier.carrier_name,
|
|
411
|
+
tracking_number=tracking_number,
|
|
412
|
+
events=[
|
|
413
|
+
datatypes.TrackingEvent(
|
|
414
|
+
date=datetime.datetime.now().strftime("%Y-%m-%d"),
|
|
415
|
+
description="Awaiting update from carrier...",
|
|
416
|
+
code="UNKNOWN",
|
|
417
|
+
time=datetime.datetime.now().strftime("%H:%M"),
|
|
418
|
+
)
|
|
419
|
+
],
|
|
420
|
+
delivered=False,
|
|
421
|
+
)
|
|
422
|
+
options = {
|
|
423
|
+
**(payload.get("options") or {}),
|
|
424
|
+
tracking_number: {
|
|
425
|
+
**(details.meta or {}),
|
|
426
|
+
**(payload.get("options") or {}).get(tracking_number, {}),
|
|
427
|
+
},
|
|
428
|
+
}
|
|
429
|
+
meta = {
|
|
430
|
+
"ext": carrier.ext,
|
|
431
|
+
"carrier": carrier.carrier_name,
|
|
432
|
+
**(details.meta or {}),
|
|
433
|
+
}
|
|
434
|
+
info = {
|
|
435
|
+
"carrier_tracking_link": utils.get_carrier_tracking_link(
|
|
436
|
+
carrier, tracking_number
|
|
437
|
+
),
|
|
438
|
+
"source": "api",
|
|
439
|
+
**(lib.to_dict(details.info or {})),
|
|
440
|
+
**(lib.to_dict(payload.get("info") or {})),
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
return datatypes.TrackingResponse(
|
|
444
|
+
tracking=lib.to_object(
|
|
445
|
+
datatypes.Tracking,
|
|
446
|
+
{
|
|
447
|
+
**lib.to_dict(details),
|
|
448
|
+
"id": f"trk_{uuid.uuid4().hex}",
|
|
449
|
+
"test_mode": carrier.test_mode,
|
|
450
|
+
"status": utils.compute_tracking_status(result).value,
|
|
451
|
+
"options": options,
|
|
452
|
+
"meta": meta,
|
|
453
|
+
"info": info,
|
|
454
|
+
},
|
|
455
|
+
),
|
|
456
|
+
messages=messages,
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
class Pickups:
|
|
461
|
+
@staticmethod
|
|
462
|
+
@utils.with_telemetry("pickup_schedule")
|
|
463
|
+
def schedule(
|
|
464
|
+
payload: dict, carrier: providers.Carrier = None, **carrier_filters
|
|
465
|
+
) -> datatypes.PickupResponse:
|
|
466
|
+
carrier = carrier or Carriers.first(
|
|
467
|
+
**{
|
|
468
|
+
**dict(active=True, capability="pickup", raise_not_found=True),
|
|
469
|
+
**carrier_filters,
|
|
470
|
+
}
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
if carrier is None:
|
|
474
|
+
raise NotFound("No active carrier connection found to process the request")
|
|
475
|
+
|
|
476
|
+
request = karrio.Pickup.schedule(
|
|
477
|
+
datatypes.PickupRequest(**lib.to_dict(payload))
|
|
478
|
+
)
|
|
479
|
+
|
|
480
|
+
# The request call is wrapped in utils.identity to simplify mocking in tests
|
|
481
|
+
pickup, messages = utils.identity(
|
|
482
|
+
lambda: request.from_(carrier.gateway).parse()
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
if pickup is None:
|
|
486
|
+
raise exceptions.APIException(
|
|
487
|
+
detail=messages,
|
|
488
|
+
status_code=status.HTTP_424_FAILED_DEPENDENCY,
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
def process_meta(parent) -> dict:
|
|
492
|
+
return {
|
|
493
|
+
**(parent.meta or {}),
|
|
494
|
+
"ext": carrier.ext,
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
return datatypes.PickupResponse(
|
|
498
|
+
pickup=datatypes.Pickup(
|
|
499
|
+
**{
|
|
500
|
+
**payload,
|
|
501
|
+
**lib.to_dict(pickup),
|
|
502
|
+
"id": f"pck_{uuid.uuid4().hex}",
|
|
503
|
+
"test_mode": carrier.test_mode,
|
|
504
|
+
"meta": process_meta(pickup),
|
|
505
|
+
"messages": messages,
|
|
506
|
+
}
|
|
507
|
+
),
|
|
508
|
+
messages=messages,
|
|
509
|
+
)
|
|
510
|
+
|
|
511
|
+
@staticmethod
|
|
512
|
+
@utils.with_telemetry("pickup_update")
|
|
513
|
+
def update(
|
|
514
|
+
payload: dict, carrier: providers.Carrier = None, **carrier_filters
|
|
515
|
+
) -> datatypes.PickupResponse:
|
|
516
|
+
carrier = carrier or Carriers.first(
|
|
517
|
+
**{
|
|
518
|
+
**dict(active=True, capability="pickup", raise_not_found=True),
|
|
519
|
+
**carrier_filters,
|
|
520
|
+
}
|
|
521
|
+
)
|
|
522
|
+
|
|
523
|
+
if carrier is None:
|
|
524
|
+
raise NotFound("No active carrier connection found to process the request")
|
|
525
|
+
|
|
526
|
+
request = karrio.Pickup.update(
|
|
527
|
+
datatypes.PickupUpdateRequest(**lib.to_dict(payload))
|
|
528
|
+
)
|
|
529
|
+
|
|
530
|
+
# The request call is wrapped in utils.identity to simplify mocking in tests
|
|
531
|
+
pickup, messages = utils.identity(
|
|
532
|
+
lambda: request.from_(carrier.gateway).parse()
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
if pickup is None:
|
|
536
|
+
raise exceptions.APIException(
|
|
537
|
+
detail=messages,
|
|
538
|
+
status_code=status.HTTP_424_FAILED_DEPENDENCY,
|
|
539
|
+
)
|
|
540
|
+
|
|
541
|
+
return datatypes.PickupResponse(
|
|
542
|
+
pickup=datatypes.Pickup(
|
|
543
|
+
**{
|
|
544
|
+
**payload,
|
|
545
|
+
**lib.to_dict(pickup),
|
|
546
|
+
"test_mode": carrier.test_mode,
|
|
547
|
+
}
|
|
548
|
+
),
|
|
549
|
+
messages=messages,
|
|
550
|
+
)
|
|
551
|
+
|
|
552
|
+
@staticmethod
|
|
553
|
+
@utils.with_telemetry("pickup_cancel")
|
|
554
|
+
def cancel(
|
|
555
|
+
payload: dict, carrier: providers.Carrier = None, **carrier_filters
|
|
556
|
+
) -> datatypes.ConfirmationResponse:
|
|
557
|
+
carrier = carrier or Carriers.first(
|
|
558
|
+
**{
|
|
559
|
+
**dict(active=True, capability="pickup"),
|
|
560
|
+
**carrier_filters,
|
|
561
|
+
}
|
|
562
|
+
)
|
|
563
|
+
|
|
564
|
+
if carrier is None:
|
|
565
|
+
raise NotFound("No active carrier connection found to process the request")
|
|
566
|
+
|
|
567
|
+
request = karrio.Pickup.cancel(
|
|
568
|
+
datatypes.PickupCancelRequest(**lib.to_dict(payload))
|
|
569
|
+
)
|
|
570
|
+
|
|
571
|
+
# The request call is wrapped in utils.identity to simplify mocking in tests
|
|
572
|
+
confirmation, messages = lib.identity(
|
|
573
|
+
utils.identity(lambda: request.from_(carrier.gateway).parse())
|
|
574
|
+
if "cancel_shipment" in carrier.gateway.proxy_methods
|
|
575
|
+
else (
|
|
576
|
+
datatypes.Confirmation(
|
|
577
|
+
carrier_name=carrier.gateway.settings.carrier_name,
|
|
578
|
+
carrier_id=carrier.gateway.settings.carrier_id,
|
|
579
|
+
success=True,
|
|
580
|
+
operation="Safe cancellation allowed",
|
|
581
|
+
),
|
|
582
|
+
[],
|
|
583
|
+
)
|
|
584
|
+
)
|
|
585
|
+
|
|
586
|
+
if confirmation is None:
|
|
587
|
+
raise exceptions.APIException(
|
|
588
|
+
detail=messages,
|
|
589
|
+
status_code=status.HTTP_424_FAILED_DEPENDENCY,
|
|
590
|
+
)
|
|
591
|
+
|
|
592
|
+
return datatypes.ConfirmationResponse(
|
|
593
|
+
confirmation=confirmation, messages=messages
|
|
594
|
+
)
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
@utils.post_processing(methods=["fetch"])
|
|
598
|
+
class Rates:
|
|
599
|
+
post_process_functions: typing.List[typing.Callable] = []
|
|
600
|
+
|
|
601
|
+
@staticmethod
|
|
602
|
+
@utils.with_telemetry("rates_fetch")
|
|
603
|
+
def fetch(
|
|
604
|
+
payload: dict,
|
|
605
|
+
carriers: typing.List[providers.Carrier] = None,
|
|
606
|
+
raise_on_error: bool = True,
|
|
607
|
+
**carrier_filters,
|
|
608
|
+
) -> datatypes.RateResponse:
|
|
609
|
+
services = payload.get("services", [])
|
|
610
|
+
carrier_ids = payload.get("carrier_ids", [])
|
|
611
|
+
shipper_country_code = payload["shipper"].get("country_code")
|
|
612
|
+
carriers = carriers or Carriers.list(
|
|
613
|
+
**{
|
|
614
|
+
"active": True,
|
|
615
|
+
"capability": "rating",
|
|
616
|
+
"carrier_ids": carrier_ids,
|
|
617
|
+
"services": services,
|
|
618
|
+
**carrier_filters,
|
|
619
|
+
}
|
|
620
|
+
)
|
|
621
|
+
|
|
622
|
+
gateways = utils.filter_rate_carrier_compatible_gateways(
|
|
623
|
+
carriers, carrier_ids, shipper_country_code
|
|
624
|
+
)
|
|
625
|
+
|
|
626
|
+
if raise_on_error and len(gateways) == 0:
|
|
627
|
+
raise NotFound("No active carrier connection found to process the request")
|
|
628
|
+
|
|
629
|
+
request = karrio.Rating.fetch(lib.to_object(datatypes.RateRequest, payload))
|
|
630
|
+
|
|
631
|
+
# The request call is wrapped in utils.identity to simplify mocking in tests
|
|
632
|
+
rates, messages = utils.identity(lambda: request.from_(*gateways).parse())
|
|
633
|
+
|
|
634
|
+
if raise_on_error and not any(rates) and any(messages):
|
|
635
|
+
raise exceptions.APIException(
|
|
636
|
+
detail=messages,
|
|
637
|
+
status_code=status.HTTP_424_FAILED_DEPENDENCY,
|
|
638
|
+
)
|
|
639
|
+
|
|
640
|
+
def process_rate(rate: datatypes.Rate) -> datatypes.Rate:
|
|
641
|
+
carrier = next((c for c in carriers if c.carrier_id == rate.carrier_id))
|
|
642
|
+
rate_provider = (
|
|
643
|
+
(rate.meta or {}).get("rate_provider")
|
|
644
|
+
or getattr(carrier, "custom_carrier_name", None)
|
|
645
|
+
or rate.carrier_name
|
|
646
|
+
).lower()
|
|
647
|
+
service_name = utils.upper(
|
|
648
|
+
(rate.meta or {}).get("service_name") or rate.service
|
|
649
|
+
)
|
|
650
|
+
|
|
651
|
+
meta = {
|
|
652
|
+
**(rate.meta or {}),
|
|
653
|
+
"ext": carrier.ext,
|
|
654
|
+
"carrier": rate_provider,
|
|
655
|
+
"service_name": service_name,
|
|
656
|
+
"rate_provider": rate_provider, # TODO: deprecate rate_provider
|
|
657
|
+
"carrier_connection_id": carrier.id,
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
return lib.to_object(
|
|
661
|
+
datatypes.Rate,
|
|
662
|
+
{
|
|
663
|
+
**lib.to_dict(rate),
|
|
664
|
+
"id": f"rat_{uuid.uuid4().hex}",
|
|
665
|
+
"test_mode": carrier.test_mode,
|
|
666
|
+
"meta": meta,
|
|
667
|
+
},
|
|
668
|
+
)
|
|
669
|
+
|
|
670
|
+
formated_rates: typing.List[datatypes.Rate] = sorted(
|
|
671
|
+
map(process_rate, rates), key=lambda rate: rate.total_charge
|
|
672
|
+
)
|
|
673
|
+
|
|
674
|
+
return lib.to_object(
|
|
675
|
+
datatypes.RateResponse, dict(rates=formated_rates, messages=messages)
|
|
676
|
+
)
|
|
677
|
+
|
|
678
|
+
|
|
679
|
+
class Documents:
|
|
680
|
+
@staticmethod
|
|
681
|
+
@utils.with_telemetry("document_upload")
|
|
682
|
+
def upload(
|
|
683
|
+
payload: dict,
|
|
684
|
+
carrier: providers.Carrier = None,
|
|
685
|
+
**carrier_filters,
|
|
686
|
+
) -> datatypes.DocumentUploadResponse:
|
|
687
|
+
carrier = carrier or Carriers.first(
|
|
688
|
+
**{
|
|
689
|
+
**dict(active=True, raise_not_found=True),
|
|
690
|
+
**carrier_filters,
|
|
691
|
+
}
|
|
692
|
+
)
|
|
693
|
+
|
|
694
|
+
if "upload_document" not in carrier.gateway.proxy_methods:
|
|
695
|
+
raise exceptions.APIException(
|
|
696
|
+
detail=f"trade document upload is not supported by carrier: '{carrier.carrier_id}'",
|
|
697
|
+
status_code=status.HTTP_406_NOT_ACCEPTABLE,
|
|
698
|
+
)
|
|
699
|
+
|
|
700
|
+
request = karrio.Document.upload(
|
|
701
|
+
lib.to_object(datatypes.DocumentUploadRequest, payload)
|
|
702
|
+
)
|
|
703
|
+
|
|
704
|
+
# The request is wrapped in utils.identity to simplify mocking in tests.
|
|
705
|
+
upload, messages = utils.identity(
|
|
706
|
+
lambda: request.from_(carrier.gateway).parse()
|
|
707
|
+
)
|
|
708
|
+
|
|
709
|
+
if upload is None:
|
|
710
|
+
raise exceptions.APIException(
|
|
711
|
+
detail=messages,
|
|
712
|
+
status_code=status.HTTP_424_FAILED_DEPENDENCY,
|
|
713
|
+
)
|
|
714
|
+
|
|
715
|
+
return lib.to_object(
|
|
716
|
+
datatypes.DocumentUploadResponse,
|
|
717
|
+
{
|
|
718
|
+
**payload,
|
|
719
|
+
**lib.to_dict(upload),
|
|
720
|
+
"test_mode": carrier.test_mode,
|
|
721
|
+
"id": f"sdoc_{uuid.uuid4().hex}",
|
|
722
|
+
"messages": lib.to_dict(messages),
|
|
723
|
+
},
|
|
724
|
+
)
|
|
725
|
+
|
|
726
|
+
|
|
727
|
+
class Manifests:
|
|
728
|
+
@staticmethod
|
|
729
|
+
@utils.with_telemetry("manifest_create")
|
|
730
|
+
def create(
|
|
731
|
+
payload: dict, carrier: providers.Carrier = None, **carrier_filters
|
|
732
|
+
) -> datatypes.ManifestResponse:
|
|
733
|
+
carrier = carrier or Carriers.first(
|
|
734
|
+
**{
|
|
735
|
+
**dict(active=True, capability="manifest", raise_not_found=True),
|
|
736
|
+
**carrier_filters,
|
|
737
|
+
}
|
|
738
|
+
)
|
|
739
|
+
|
|
740
|
+
if carrier is None:
|
|
741
|
+
raise NotFound("No active carrier connection found to process the request")
|
|
742
|
+
|
|
743
|
+
request = karrio.Manifest.create(
|
|
744
|
+
lib.to_object(datatypes.ManifestRequest, lib.to_dict(payload))
|
|
745
|
+
)
|
|
746
|
+
|
|
747
|
+
# The request call is wrapped in utils.identity to simplify mocking in tests
|
|
748
|
+
manifest, messages = utils.identity(
|
|
749
|
+
lambda: request.from_(carrier.gateway).parse()
|
|
750
|
+
)
|
|
751
|
+
|
|
752
|
+
if manifest is None:
|
|
753
|
+
raise exceptions.APIException(
|
|
754
|
+
detail=messages,
|
|
755
|
+
status_code=status.HTTP_424_FAILED_DEPENDENCY,
|
|
756
|
+
)
|
|
757
|
+
|
|
758
|
+
def process_meta(parent) -> dict:
|
|
759
|
+
return {
|
|
760
|
+
**(parent.meta or {}),
|
|
761
|
+
"ext": carrier.ext,
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
return datatypes.ManifestResponse(
|
|
765
|
+
manifest=datatypes.Manifest(
|
|
766
|
+
**{
|
|
767
|
+
**payload,
|
|
768
|
+
**lib.to_dict(manifest),
|
|
769
|
+
"id": f"manf_{uuid.uuid4().hex}",
|
|
770
|
+
"test_mode": carrier.test_mode,
|
|
771
|
+
"meta": process_meta(manifest),
|
|
772
|
+
"messages": messages,
|
|
773
|
+
}
|
|
774
|
+
),
|
|
775
|
+
messages=messages,
|
|
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
|
+
)
|