karrio-server-core 2025.5rc1__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.
Potentially problematic release.
This version of karrio-server-core might be problematic. Click here for more details.
- 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 +313 -0
- karrio/server/core/context_processors.py +12 -0
- karrio/server/core/datatypes.py +369 -0
- karrio/server/core/dataunits.py +156 -0
- karrio/server/core/exceptions.py +200 -0
- karrio/server/core/fields.py +12 -0
- karrio/server/core/filters.py +823 -0
- karrio/server/core/gateway.py +720 -0
- karrio/server/core/management/commands/cli.py +19 -0
- karrio/server/core/management/commands/create_oauth_client.py +41 -0
- karrio/server/core/middleware.py +95 -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/__init__.py +0 -0
- karrio/server/core/models/__init__.py +48 -0
- karrio/server/core/models/base.py +70 -0
- karrio/server/core/models/entity.py +22 -0
- karrio/server/core/models/metafield.py +144 -0
- karrio/server/core/models/third_party.py +21 -0
- karrio/server/core/oauth_validators.py +171 -0
- karrio/server/core/permissions.py +37 -0
- karrio/server/core/renderers.py +11 -0
- karrio/server/core/router.py +3 -0
- karrio/server/core/serializers.py +1898 -0
- karrio/server/core/signals.py +57 -0
- karrio/server/core/tests.py +98 -0
- karrio/server/core/urls.py +12 -0
- karrio/server/core/utils.py +479 -0
- karrio/server/core/validators.py +416 -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 +74 -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 +134 -0
- karrio/server/iam/serializers.py +39 -0
- karrio/server/iam/signals.py +20 -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/extension/__init__.py +1 -0
- karrio/server/providers/extension/models/__init__.py +1 -0
- karrio/server/providers/extension/models/allied_express.py +22 -0
- karrio/server/providers/extension/models/allied_express_local.py +22 -0
- karrio/server/providers/extension/models/amazon_shipping.py +27 -0
- karrio/server/providers/extension/models/aramex.py +25 -0
- karrio/server/providers/extension/models/asendia_us.py +21 -0
- karrio/server/providers/extension/models/australiapost.py +20 -0
- karrio/server/providers/extension/models/boxknight.py +19 -0
- karrio/server/providers/extension/models/bpost.py +21 -0
- karrio/server/providers/extension/models/canadapost.py +21 -0
- karrio/server/providers/extension/models/canpar.py +19 -0
- karrio/server/providers/extension/models/chronopost.py +22 -0
- karrio/server/providers/extension/models/colissimo.py +22 -0
- karrio/server/providers/extension/models/dhl_express.py +23 -0
- karrio/server/providers/extension/models/dhl_parcel_de.py +25 -0
- karrio/server/providers/extension/models/dhl_poland.py +22 -0
- karrio/server/providers/extension/models/dhl_universal.py +19 -0
- karrio/server/providers/extension/models/dicom.py +20 -0
- karrio/server/providers/extension/models/dpd.py +37 -0
- karrio/server/providers/extension/models/dpdhl.py +26 -0
- karrio/server/providers/extension/models/easypost.py +20 -0
- karrio/server/providers/extension/models/eshipper.py +21 -0
- karrio/server/providers/extension/models/fedex.py +25 -0
- karrio/server/providers/extension/models/fedex_ws.py +24 -0
- karrio/server/providers/extension/models/freightcom.py +21 -0
- karrio/server/providers/extension/models/generic.py +35 -0
- karrio/server/providers/extension/models/geodis.py +22 -0
- karrio/server/providers/extension/models/hay_post.py +22 -0
- karrio/server/providers/extension/models/laposte.py +19 -0
- karrio/server/providers/extension/models/locate2u.py +22 -0
- karrio/server/providers/extension/models/nationex.py +22 -0
- karrio/server/providers/extension/models/purolator.py +21 -0
- karrio/server/providers/extension/models/roadie.py +18 -0
- karrio/server/providers/extension/models/royalmail.py +19 -0
- karrio/server/providers/extension/models/sendle.py +22 -0
- karrio/server/providers/extension/models/tge.py +63 -0
- karrio/server/providers/extension/models/tnt.py +23 -0
- karrio/server/providers/extension/models/ups.py +23 -0
- karrio/server/providers/extension/models/usps.py +23 -0
- karrio/server/providers/extension/models/usps_international.py +23 -0
- karrio/server/providers/extension/models/usps_wt.py +24 -0
- karrio/server/providers/extension/models/usps_wt_international.py +24 -0
- karrio/server/providers/extension/models/zoom2u.py +23 -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/__init__.py +0 -0
- karrio/server/providers/models/__init__.py +17 -0
- karrio/server/providers/models/carrier.py +309 -0
- karrio/server/providers/models/config.py +30 -0
- karrio/server/providers/models/service.py +62 -0
- karrio/server/providers/models/sheet.py +60 -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 +277 -0
- karrio/server/providers/signals.py +27 -0
- karrio/server/providers/tests.py +3 -0
- karrio/server/providers/urls.py +11 -0
- karrio/server/providers/views/__init__.py +0 -0
- karrio/server/providers/views/carriers.py +269 -0
- karrio/server/providers/views/connections.py +176 -0
- karrio/server/samples.py +352 -0
- karrio/server/serializers/__init__.py +2 -0
- karrio/server/serializers/abstract.py +506 -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/__init__.py +0 -0
- karrio/server/tracing/models.py +80 -0
- karrio/server/tracing/tests.py +3 -0
- karrio/server/tracing/utils.py +112 -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/__init__.py +0 -0
- karrio/server/user/models.py +203 -0
- karrio/server/user/serializers.py +46 -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.5rc1.dist-info/METADATA +32 -0
- karrio_server_core-2025.5rc1.dist-info/RECORD +241 -0
- karrio_server_core-2025.5rc1.dist-info/WHEEL +5 -0
- karrio_server_core-2025.5rc1.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from django.conf import settings
|
|
3
|
+
from django.dispatch import receiver
|
|
4
|
+
from constance import config
|
|
5
|
+
from constance.signals import config_updated
|
|
6
|
+
from django.core.signals import request_started
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def register_signals():
|
|
12
|
+
config_updated.connect(constance_updated)
|
|
13
|
+
# Defer config initialization until after Django is fully loaded
|
|
14
|
+
request_started.connect(initialize_settings)
|
|
15
|
+
|
|
16
|
+
logger.info("karrio.core signals registered...")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def initialize_settings(sender=None, **kwargs):
|
|
20
|
+
# Only run once
|
|
21
|
+
if not getattr(initialize_settings, 'has_run', False):
|
|
22
|
+
try:
|
|
23
|
+
update_settings(config)
|
|
24
|
+
initialize_settings.has_run = True
|
|
25
|
+
except Exception as e:
|
|
26
|
+
logger.error(f"Failed to initialize settings: {e}")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@receiver(config_updated)
|
|
30
|
+
def constance_updated(sender, key, old_value, new_value, **kwargs):
|
|
31
|
+
logger.info(f"Updated config {key} to {new_value}")
|
|
32
|
+
update_settings(sender)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def update_settings(current):
|
|
36
|
+
CONSTANCE_CONFIG_KEYS = [
|
|
37
|
+
key for key in settings.CONSTANCE_CONFIG.keys() if hasattr(settings, key)
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
for key in CONSTANCE_CONFIG_KEYS:
|
|
41
|
+
try:
|
|
42
|
+
setattr(settings, key, getattr(current, key))
|
|
43
|
+
except Exception as e:
|
|
44
|
+
logger.error(f"Failed to update setting {key}: {e}")
|
|
45
|
+
|
|
46
|
+
# Check EMAIL_ENABLED after all settings are updated
|
|
47
|
+
try:
|
|
48
|
+
settings.EMAIL_ENABLED = all(
|
|
49
|
+
cfg is not None and cfg != ""
|
|
50
|
+
for cfg in [
|
|
51
|
+
current.EMAIL_HOST,
|
|
52
|
+
current.EMAIL_HOST_USER,
|
|
53
|
+
]
|
|
54
|
+
)
|
|
55
|
+
except Exception as e:
|
|
56
|
+
logger.error(f"Failed to set EMAIL_ENABLED: {e}")
|
|
57
|
+
settings.EMAIL_ENABLED = False
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from django.contrib.auth import get_user_model
|
|
3
|
+
from rest_framework.test import APITestCase as BaseAPITestCase, APIClient
|
|
4
|
+
|
|
5
|
+
from karrio.server.user.models import Token
|
|
6
|
+
import karrio.server.iam.permissions as iam
|
|
7
|
+
import karrio.server.providers.models as providers
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
iam.setup_groups()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class APITestCase(BaseAPITestCase):
|
|
14
|
+
def setUp(self) -> None:
|
|
15
|
+
self.maxDiff = None
|
|
16
|
+
logging.basicConfig(level=logging.DEBUG)
|
|
17
|
+
|
|
18
|
+
# Setup user and API Token.
|
|
19
|
+
self.user = get_user_model().objects.create_superuser(
|
|
20
|
+
"admin@example.com", "test"
|
|
21
|
+
)
|
|
22
|
+
self.token = Token.objects.create(user=self.user, test_mode=True)
|
|
23
|
+
|
|
24
|
+
# Setup API client.
|
|
25
|
+
self.client = APIClient()
|
|
26
|
+
self.client.credentials(HTTP_AUTHORIZATION="Token " + self.token.key)
|
|
27
|
+
|
|
28
|
+
# Setup test carrier connections.
|
|
29
|
+
self.carrier = providers.Carrier.objects.create(
|
|
30
|
+
carrier_code="canadapost",
|
|
31
|
+
carrier_id="canadapost",
|
|
32
|
+
test_mode=True,
|
|
33
|
+
active=True,
|
|
34
|
+
created_by=self.user,
|
|
35
|
+
credentials=dict(
|
|
36
|
+
username="6e93d53968881714",
|
|
37
|
+
customer_number="2004381",
|
|
38
|
+
contract_id="42708517",
|
|
39
|
+
password="0bfa9fcb9853d1f51ee57a",
|
|
40
|
+
),
|
|
41
|
+
)
|
|
42
|
+
self.ups_carrier = providers.Carrier.objects.create(
|
|
43
|
+
carrier_code="ups",
|
|
44
|
+
carrier_id="ups_package",
|
|
45
|
+
test_mode=True,
|
|
46
|
+
active=True,
|
|
47
|
+
created_by=self.user,
|
|
48
|
+
credentials=dict(
|
|
49
|
+
client_id="test",
|
|
50
|
+
client_secret="test",
|
|
51
|
+
account_number="000000",
|
|
52
|
+
),
|
|
53
|
+
)
|
|
54
|
+
self.fedex_carrier = providers.Carrier.objects.create(
|
|
55
|
+
carrier_code="fedex",
|
|
56
|
+
carrier_id="fedex_express",
|
|
57
|
+
test_mode=True,
|
|
58
|
+
active=True,
|
|
59
|
+
created_by=self.user,
|
|
60
|
+
credentials=dict(
|
|
61
|
+
api_key="test",
|
|
62
|
+
secret_key="password",
|
|
63
|
+
account_number="000000",
|
|
64
|
+
track_api_key="test",
|
|
65
|
+
track_secret_key="password",
|
|
66
|
+
),
|
|
67
|
+
)
|
|
68
|
+
self.dhl_carrier = providers.Carrier.objects.create(
|
|
69
|
+
carrier_code="dhl_express",
|
|
70
|
+
carrier_id="dhl_express",
|
|
71
|
+
test_mode=True,
|
|
72
|
+
active=True,
|
|
73
|
+
created_by=self.user,
|
|
74
|
+
credentials=dict(
|
|
75
|
+
site_id="test",
|
|
76
|
+
password="password",
|
|
77
|
+
account_number="000000",
|
|
78
|
+
)
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
def getJWTToken(self, email: str, password: str) -> str:
|
|
82
|
+
url = reverse("jwt-obtain-pair")
|
|
83
|
+
data = dict(
|
|
84
|
+
email=email,
|
|
85
|
+
password=password,
|
|
86
|
+
)
|
|
87
|
+
response = self.client.post(url, data)
|
|
88
|
+
|
|
89
|
+
return response.data.get("access")
|
|
90
|
+
|
|
91
|
+
def assertResponseNoErrors(self, response):
|
|
92
|
+
is_ok = f"{response.status_code}".startswith("2")
|
|
93
|
+
|
|
94
|
+
if is_ok is False or response.data.get("errors") is not None:
|
|
95
|
+
print(response.data)
|
|
96
|
+
|
|
97
|
+
self.assertTrue(is_ok)
|
|
98
|
+
assert response.data.get("errors") is None
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""
|
|
2
|
+
karrio server core module urls
|
|
3
|
+
"""
|
|
4
|
+
from django.urls import include, path
|
|
5
|
+
from karrio.server.core.views import metadata, router
|
|
6
|
+
|
|
7
|
+
app_name = "karrio.server.core"
|
|
8
|
+
urlpatterns = [
|
|
9
|
+
path("", metadata.view, name="metadata"),
|
|
10
|
+
path("v1/", include(router.urls), name="references"),
|
|
11
|
+
path("status/", include("health_check.urls")),
|
|
12
|
+
]
|
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import typing
|
|
3
|
+
import inspect
|
|
4
|
+
import logging
|
|
5
|
+
import functools
|
|
6
|
+
from string import Template
|
|
7
|
+
from concurrent import futures
|
|
8
|
+
from datetime import timedelta, datetime
|
|
9
|
+
from typing import TypeVar, Union, Callable, Any, List, Optional
|
|
10
|
+
|
|
11
|
+
from django.conf import settings
|
|
12
|
+
from django.utils.translation import gettext_lazy as _
|
|
13
|
+
import django_email_verification.confirm as confirm
|
|
14
|
+
import rest_framework_simplejwt.tokens as jwt
|
|
15
|
+
import rest_framework.status as status
|
|
16
|
+
|
|
17
|
+
import karrio.lib as lib
|
|
18
|
+
from karrio.core.utils import DP, DF
|
|
19
|
+
from karrio.server.core import datatypes, serializers, exceptions
|
|
20
|
+
|
|
21
|
+
T = TypeVar("T")
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def identity(value: Union[Any, Callable]) -> Any:
|
|
26
|
+
"""
|
|
27
|
+
:param value: function or value desired to be wrapped
|
|
28
|
+
:return: value or callable return
|
|
29
|
+
"""
|
|
30
|
+
return value() if callable(value) else value
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def failsafe(callable: Callable[[], T], warning: str = None) -> T:
|
|
34
|
+
"""This higher order function wraps a callable in a try..except
|
|
35
|
+
scope to capture any exception raised.
|
|
36
|
+
Only use it when you are running something unstable that you
|
|
37
|
+
don't mind if it fails.
|
|
38
|
+
"""
|
|
39
|
+
try:
|
|
40
|
+
return callable()
|
|
41
|
+
except Exception as e:
|
|
42
|
+
if warning:
|
|
43
|
+
logger.warning(Template(warning).substitute(error=e))
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def run_async(callable: Callable[[], Any]) -> futures.Future:
|
|
48
|
+
"""This higher order function initiate the execution
|
|
49
|
+
of a callable in a non-blocking thread and return a
|
|
50
|
+
handle for a future response.
|
|
51
|
+
"""
|
|
52
|
+
return futures.ThreadPoolExecutor(max_workers=1).submit(callable)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def error_wrapper(func):
|
|
56
|
+
@functools.wraps(func)
|
|
57
|
+
def wrapper(*args, **kwargs):
|
|
58
|
+
try:
|
|
59
|
+
return func(*args, **kwargs)
|
|
60
|
+
except Exception as e:
|
|
61
|
+
logger.exception(e)
|
|
62
|
+
raise e
|
|
63
|
+
|
|
64
|
+
return wrapper
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def async_wrapper(func):
|
|
68
|
+
@functools.wraps(func)
|
|
69
|
+
def wrapper(*args, run_synchronous: bool = False, **kwargs):
|
|
70
|
+
def _run():
|
|
71
|
+
return func(*args, **kwargs)
|
|
72
|
+
|
|
73
|
+
if run_synchronous:
|
|
74
|
+
return _run()
|
|
75
|
+
|
|
76
|
+
return run_async(_run)
|
|
77
|
+
|
|
78
|
+
return wrapper
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def tenant_aware(func):
|
|
82
|
+
@functools.wraps(func)
|
|
83
|
+
def wrapper(*args, **kwargs):
|
|
84
|
+
if settings.MULTI_TENANTS:
|
|
85
|
+
import django_tenants.utils as tenant_utils
|
|
86
|
+
|
|
87
|
+
schema = kwargs.get("schema") or "public"
|
|
88
|
+
|
|
89
|
+
with tenant_utils.schema_context(schema):
|
|
90
|
+
return func(*args, **kwargs)
|
|
91
|
+
else:
|
|
92
|
+
return func(*args, **kwargs)
|
|
93
|
+
|
|
94
|
+
return wrapper
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def run_on_all_tenants(func):
|
|
98
|
+
@functools.wraps(func)
|
|
99
|
+
def wrapper(*args, **kwargs):
|
|
100
|
+
if settings.MULTI_TENANTS:
|
|
101
|
+
import django_tenants.utils as tenant_utils
|
|
102
|
+
|
|
103
|
+
tenants = tenant_utils.get_tenant_model().objects.exclude(
|
|
104
|
+
schema_name="public"
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
for tenant in tenants:
|
|
108
|
+
with tenant_utils.tenant_context(tenant):
|
|
109
|
+
func(*args, **kwargs, schema=tenant.schema_name)
|
|
110
|
+
else:
|
|
111
|
+
func(*args, **kwargs)
|
|
112
|
+
|
|
113
|
+
return wrapper
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def disable_for_loaddata(signal_handler):
|
|
117
|
+
@functools.wraps(signal_handler)
|
|
118
|
+
def wrapper(*args, **kwargs):
|
|
119
|
+
if is_system_loading_data():
|
|
120
|
+
return
|
|
121
|
+
|
|
122
|
+
signal_handler(*args, **kwargs)
|
|
123
|
+
|
|
124
|
+
return wrapper
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def skip_on_loadata(func):
|
|
128
|
+
@functools.wraps(func)
|
|
129
|
+
def wrapper(*args, **kwargs):
|
|
130
|
+
if "loaddata" in sys.argv:
|
|
131
|
+
return
|
|
132
|
+
|
|
133
|
+
return func(*args, **kwargs)
|
|
134
|
+
|
|
135
|
+
return wrapper
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def skip_on_commands(
|
|
139
|
+
commands: typing.List[str] = ["loaddata", "migrate", "makemigrations"]
|
|
140
|
+
):
|
|
141
|
+
def _decorator(func):
|
|
142
|
+
@functools.wraps(func)
|
|
143
|
+
def wrapper(*args, **kwargs):
|
|
144
|
+
if any(cmd in sys.argv for cmd in commands):
|
|
145
|
+
return
|
|
146
|
+
|
|
147
|
+
return func(*args, **kwargs)
|
|
148
|
+
|
|
149
|
+
return wrapper
|
|
150
|
+
|
|
151
|
+
return _decorator
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def email_setup_required(func):
|
|
155
|
+
@functools.wraps(func)
|
|
156
|
+
def wrapper(*args, **kwargs):
|
|
157
|
+
if not settings.EMAIL_ENABLED:
|
|
158
|
+
raise Exception(_("The email service is not configured."))
|
|
159
|
+
|
|
160
|
+
return func(*args, **kwargs)
|
|
161
|
+
|
|
162
|
+
return wrapper
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def post_processing(methods: List[str] = None):
|
|
166
|
+
def class_wrapper(klass):
|
|
167
|
+
setattr(
|
|
168
|
+
klass,
|
|
169
|
+
"post_process_functions",
|
|
170
|
+
getattr(klass, "post_process_functions") or [],
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
for name in methods:
|
|
174
|
+
method = getattr(klass, name)
|
|
175
|
+
|
|
176
|
+
def wrapper(*args, **kwargs):
|
|
177
|
+
result = method(*args, **kwargs)
|
|
178
|
+
processes = klass.post_process_functions
|
|
179
|
+
context = kwargs.get("context")
|
|
180
|
+
|
|
181
|
+
return functools.reduce(
|
|
182
|
+
lambda cummulated_result, process: process(
|
|
183
|
+
context, cummulated_result
|
|
184
|
+
),
|
|
185
|
+
processes,
|
|
186
|
+
result,
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
setattr(klass, name, wrapper)
|
|
190
|
+
|
|
191
|
+
return klass
|
|
192
|
+
|
|
193
|
+
return class_wrapper
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def upper(value_str: Optional[str]) -> Optional[str]:
|
|
197
|
+
if value_str is None:
|
|
198
|
+
return None
|
|
199
|
+
|
|
200
|
+
return value_str.upper().replace("_", " ")
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def compute_tracking_status(
|
|
204
|
+
details: Optional[datatypes.Tracking] = None,
|
|
205
|
+
) -> serializers.TrackerStatus:
|
|
206
|
+
if details is None:
|
|
207
|
+
return serializers.TrackerStatus.pending
|
|
208
|
+
elif details.delivered:
|
|
209
|
+
return serializers.TrackerStatus.delivered
|
|
210
|
+
elif (len(details.events) == 0) or (
|
|
211
|
+
len(details.events) == 1 and details.events[0].code == "CREATED"
|
|
212
|
+
):
|
|
213
|
+
return serializers.TrackerStatus.pending
|
|
214
|
+
|
|
215
|
+
if (
|
|
216
|
+
any(details.status or "")
|
|
217
|
+
and serializers.TrackerStatus.map(details.status).value is not None
|
|
218
|
+
):
|
|
219
|
+
return serializers.TrackerStatus.map(details.status)
|
|
220
|
+
|
|
221
|
+
return serializers.TrackerStatus.in_transit
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def is_sdk_message(
|
|
225
|
+
message: Optional[Union[datatypes.Message, List[datatypes.Message]]]
|
|
226
|
+
) -> bool:
|
|
227
|
+
msg = next(iter(message), None) if isinstance(message, list) else message
|
|
228
|
+
|
|
229
|
+
return "SHIPPING_SDK_" in str(getattr(msg, "code", ""))
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def filter_rate_carrier_compatible_gateways(
|
|
233
|
+
carriers: List, carrier_ids: List[str], shipper_country_code: Optional[str] = None
|
|
234
|
+
) -> List:
|
|
235
|
+
"""
|
|
236
|
+
This function filters the carriers based on the capability to "rating"
|
|
237
|
+
and if no explicit carrier list is provided, it will filter out any
|
|
238
|
+
carrier that does not support the shipper's country code.
|
|
239
|
+
"""
|
|
240
|
+
_gateways = [
|
|
241
|
+
carrier.gateway
|
|
242
|
+
for carrier in carriers
|
|
243
|
+
if (
|
|
244
|
+
# If no carrier list is provided, and gateway has "rating" capability.
|
|
245
|
+
("rating" in carrier.gateway.capabilities and len(carrier_ids) > 0)
|
|
246
|
+
# If a carrier list is provided, and gateway is in the list.
|
|
247
|
+
or (
|
|
248
|
+
# the gateway has "rating" capability.
|
|
249
|
+
"rating" in carrier.gateway.capabilities
|
|
250
|
+
# and no explicit carrier list is provided.
|
|
251
|
+
and len(carrier_ids) == 0
|
|
252
|
+
# and the shipper country code is provided.
|
|
253
|
+
and shipper_country_code is not None
|
|
254
|
+
and (
|
|
255
|
+
carrier.gateway.settings.account_country_code
|
|
256
|
+
== shipper_country_code
|
|
257
|
+
or carrier.gateway.settings.account_country_code is None
|
|
258
|
+
)
|
|
259
|
+
)
|
|
260
|
+
)
|
|
261
|
+
]
|
|
262
|
+
|
|
263
|
+
return ({_.settings.carrier_id: _ for _ in _gateways}).values()
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def is_system_loading_data() -> bool:
|
|
267
|
+
try:
|
|
268
|
+
for fr in inspect.stack():
|
|
269
|
+
if inspect.getmodulename(fr[1]) == "loaddata":
|
|
270
|
+
return True
|
|
271
|
+
except:
|
|
272
|
+
pass
|
|
273
|
+
|
|
274
|
+
return False
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
@email_setup_required
|
|
278
|
+
def send_email(
|
|
279
|
+
emails: List[str],
|
|
280
|
+
subject: str,
|
|
281
|
+
email_template: str,
|
|
282
|
+
context: dict = {},
|
|
283
|
+
text_template: str = None,
|
|
284
|
+
**kwargs,
|
|
285
|
+
):
|
|
286
|
+
sender = confirm._get_validated_field("EMAIL_FROM_ADDRESS")
|
|
287
|
+
html = confirm.render_to_string(email_template, context)
|
|
288
|
+
text = confirm.render_to_string(text_template or email_template, context)
|
|
289
|
+
|
|
290
|
+
msg = confirm.EmailMultiAlternatives(subject, text, sender, emails)
|
|
291
|
+
msg.attach_alternative(html, "text/html")
|
|
292
|
+
msg.send()
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
class ConfirmationToken(jwt.Token):
|
|
296
|
+
token_type = "confirmation"
|
|
297
|
+
lifetime = timedelta(hours=2)
|
|
298
|
+
|
|
299
|
+
@classmethod
|
|
300
|
+
def for_data(cls, user, data: dict) -> str:
|
|
301
|
+
token = super().for_user(user)
|
|
302
|
+
|
|
303
|
+
for k, v in data.items():
|
|
304
|
+
token[k] = v
|
|
305
|
+
|
|
306
|
+
return token
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def app_tracking_query_params(url: str, carrier) -> str:
|
|
310
|
+
hub_flag = f"&hub={carrier.carrier_name}" if carrier.gateway.is_hub else ""
|
|
311
|
+
|
|
312
|
+
return f"{url}{hub_flag}"
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def default_tracking_event(
|
|
316
|
+
event_at: datetime = None,
|
|
317
|
+
code: str = None,
|
|
318
|
+
description: str = None,
|
|
319
|
+
):
|
|
320
|
+
return [
|
|
321
|
+
DP.to_dict(
|
|
322
|
+
datatypes.TrackingEvent(
|
|
323
|
+
date=DF.fdate(event_at or datetime.now()),
|
|
324
|
+
description=(description or "Label created and ready for shipment"),
|
|
325
|
+
location="",
|
|
326
|
+
code=(code or "CREATED"),
|
|
327
|
+
time=DF.ftime(event_at or datetime.now()),
|
|
328
|
+
)
|
|
329
|
+
)
|
|
330
|
+
]
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def get_carrier_tracking_link(carrier, tracking_number: str):
|
|
334
|
+
tracking_url = getattr(carrier.gateway.settings, "tracking_url", None)
|
|
335
|
+
|
|
336
|
+
return tracking_url.format(tracking_number) if tracking_url is not None else None
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def process_events(
|
|
340
|
+
response_events: typing.List[datatypes.TrackingEvent],
|
|
341
|
+
current_events: typing.List[dict],
|
|
342
|
+
) -> typing.List[dict]:
|
|
343
|
+
"""Merge new tracking events with existing ones, avoiding duplicates by comparing event hashes.
|
|
344
|
+
Latest events are kept at the top of the list."""
|
|
345
|
+
if not any(response_events):
|
|
346
|
+
return current_events
|
|
347
|
+
|
|
348
|
+
new_events = lib.to_dict(response_events)
|
|
349
|
+
if not any(current_events):
|
|
350
|
+
return sorted(
|
|
351
|
+
new_events,
|
|
352
|
+
key=lambda e: f"{e.get('date', '')} {e.get('time', '')}",
|
|
353
|
+
reverse=True,
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
# Create hash for comparison using lib.to_json
|
|
357
|
+
event_hashes = {lib.to_json(event): event for event in current_events}
|
|
358
|
+
|
|
359
|
+
for event in new_events:
|
|
360
|
+
event_hash = lib.to_json(event)
|
|
361
|
+
if event_hash not in event_hashes:
|
|
362
|
+
event_hashes[event_hash] = event
|
|
363
|
+
|
|
364
|
+
# Sort events by date and time in descending order (latest first)
|
|
365
|
+
return sorted(
|
|
366
|
+
event_hashes.values(),
|
|
367
|
+
key=lambda e: f"{e.get('date', '')} {e.get('time', '')}",
|
|
368
|
+
reverse=True,
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def apply_rate_selection(payload: typing.Union[dict, typing.Any], **kwargs):
|
|
373
|
+
data = kwargs.get("data") or kwargs
|
|
374
|
+
get = lambda key, default=None: lib.identity(
|
|
375
|
+
payload.get(key, data.get(key, default)) if isinstance(payload, dict)
|
|
376
|
+
else getattr(payload, key, data.get(key, default))
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
ctx = kwargs.get("context")
|
|
380
|
+
rates = get("rates") or data.get("rates", [])
|
|
381
|
+
options = get("options") or data.get("options", {})
|
|
382
|
+
service = get("service") or data.get("service", None)
|
|
383
|
+
rate_id = get("selected_rate_id") or data.get("selected_rate_id", None)
|
|
384
|
+
selected_rate = get("selected_rate") or data.get("selected_rate", None)
|
|
385
|
+
apply_shipping_rules = lib.identity(
|
|
386
|
+
getattr(settings, "SHIPPING_RULES", False)
|
|
387
|
+
and options.get("apply_shipping_rules", False)
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
if selected_rate:
|
|
391
|
+
kwargs.update(selected_rate=selected_rate)
|
|
392
|
+
return kwargs
|
|
393
|
+
|
|
394
|
+
# Select by id or service if provided
|
|
395
|
+
if rate_id or service:
|
|
396
|
+
kwargs.update(selected_rate=next(
|
|
397
|
+
(
|
|
398
|
+
rate for rate in rates
|
|
399
|
+
if (rate_id and rate.get("id") == rate_id)
|
|
400
|
+
or (service and rate.get("service") == service)
|
|
401
|
+
),
|
|
402
|
+
None,
|
|
403
|
+
))
|
|
404
|
+
return kwargs
|
|
405
|
+
|
|
406
|
+
# Apply shipping rules if enabled and no selected rate is provided
|
|
407
|
+
if apply_shipping_rules:
|
|
408
|
+
# Import rules engine only when needed
|
|
409
|
+
import karrio.server.automation.models as automation_models
|
|
410
|
+
import karrio.server.automation.services.rules_engine as engine
|
|
411
|
+
|
|
412
|
+
# Get active shipping rules
|
|
413
|
+
active_rules = list(
|
|
414
|
+
automation_models.ShippingRule
|
|
415
|
+
.access_by(ctx)
|
|
416
|
+
.filter(is_active=True)
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
# Always run rule evaluation for activity tracking
|
|
420
|
+
if active_rules:
|
|
421
|
+
_, rule_selected_rate, rule_activity = engine.process_shipping_rules(
|
|
422
|
+
shipment=payload,
|
|
423
|
+
rules=active_rules,
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
kwargs.update(
|
|
427
|
+
selected_rate=rule_selected_rate,
|
|
428
|
+
rule_activity=rule_activity,
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
return kwargs
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def require_selected_rate(func):
|
|
435
|
+
"""
|
|
436
|
+
Decorator for rate selection process.
|
|
437
|
+
- Checks if shipping rules are enabled
|
|
438
|
+
- Evaluates and applies rules to modify service if needed
|
|
439
|
+
- Augments response metadata with applied rules
|
|
440
|
+
"""
|
|
441
|
+
|
|
442
|
+
@functools.wraps(func)
|
|
443
|
+
def wrapper(payload, **kwargs):
|
|
444
|
+
|
|
445
|
+
kwargs = apply_rate_selection(payload, **kwargs)
|
|
446
|
+
|
|
447
|
+
if kwargs.get("selected_rate") is None:
|
|
448
|
+
raise exceptions.APIException(
|
|
449
|
+
"The service you selected is not available for this shipment.",
|
|
450
|
+
code="service_unavailable",
|
|
451
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
# Execute original function
|
|
455
|
+
result = func(payload, **kwargs)
|
|
456
|
+
|
|
457
|
+
if isinstance(result, datatypes.Shipment) and kwargs.get("rule_activity"):
|
|
458
|
+
return lib.to_object(
|
|
459
|
+
datatypes.Shipment,
|
|
460
|
+
{
|
|
461
|
+
**lib.to_dict(result),
|
|
462
|
+
"meta": {
|
|
463
|
+
**(result.meta or {}),
|
|
464
|
+
**({"rule_activity": kwargs.get("rule_activity")}),
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
if hasattr(result, "save") and kwargs.get("rule_activity"):
|
|
470
|
+
result.meta = {
|
|
471
|
+
**(result.meta or {}),
|
|
472
|
+
**({"rule_activity": kwargs.get("rule_activity")}),
|
|
473
|
+
}
|
|
474
|
+
result.save()
|
|
475
|
+
return result
|
|
476
|
+
|
|
477
|
+
return result
|
|
478
|
+
|
|
479
|
+
return wrapper
|