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
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import yaml # type: ignore
|
|
2
2
|
import pydoc
|
|
3
|
-
import logging
|
|
4
3
|
import functools
|
|
5
4
|
from django.db.utils import ProgrammingError
|
|
6
5
|
from django.conf import settings
|
|
@@ -23,8 +22,8 @@ from oauth2_provider.contrib.rest_framework import (
|
|
|
23
22
|
OAuth2Authentication as BaseOAuth2Authentication,
|
|
24
23
|
)
|
|
25
24
|
from django_otp.middleware import OTPMiddleware
|
|
25
|
+
from karrio.server.core.logging import logger
|
|
26
26
|
|
|
27
|
-
logger = logging.getLogger(__name__)
|
|
28
27
|
UserModel = get_user_model()
|
|
29
28
|
AUTHENTICATION_CLASSES = getattr(settings, "AUTHENTICATION_CLASSES", [])
|
|
30
29
|
|
|
@@ -155,13 +154,17 @@ class OAuth2Authentication(BaseOAuth2Authentication):
|
|
|
155
154
|
if user is None:
|
|
156
155
|
# For client credentials flow, the user might be None from the base class
|
|
157
156
|
# but our custom validator should have set it on the request
|
|
158
|
-
if
|
|
157
|
+
if (
|
|
158
|
+
hasattr(request, "user")
|
|
159
|
+
and request.user
|
|
160
|
+
and not request.user.is_anonymous
|
|
161
|
+
):
|
|
159
162
|
user = request.user
|
|
160
|
-
elif hasattr(request,
|
|
163
|
+
elif hasattr(request, "oauth_user"):
|
|
161
164
|
user = request.oauth_user
|
|
162
165
|
# If we still don't have a user, try to get it from the OAuth application
|
|
163
|
-
elif hasattr(token,
|
|
164
|
-
user = getattr(token.application,
|
|
166
|
+
elif hasattr(token, "application") and token.application:
|
|
167
|
+
user = getattr(token.application, "user", None)
|
|
165
168
|
|
|
166
169
|
# Set request context
|
|
167
170
|
request.user = user or request.user
|
|
@@ -170,13 +173,17 @@ class OAuth2Authentication(BaseOAuth2Authentication):
|
|
|
170
173
|
|
|
171
174
|
# Enhanced organization context for OAuth apps
|
|
172
175
|
default_org = None
|
|
173
|
-
if hasattr(token,
|
|
176
|
+
if hasattr(token, "application") and hasattr(
|
|
177
|
+
token.application, "oauth_app"
|
|
178
|
+
):
|
|
174
179
|
# If this is an OAuth app token, use the app owner's organization
|
|
175
180
|
oauth_app = token.application.oauth_app
|
|
176
|
-
if hasattr(oauth_app,
|
|
181
|
+
if hasattr(oauth_app, "created_by") and oauth_app.created_by:
|
|
177
182
|
app_owner = oauth_app.created_by
|
|
178
|
-
if hasattr(app_owner,
|
|
179
|
-
default_org = app_owner.organizations.filter(
|
|
183
|
+
if hasattr(app_owner, "organizations"):
|
|
184
|
+
default_org = app_owner.organizations.filter(
|
|
185
|
+
is_active=True
|
|
186
|
+
).first()
|
|
180
187
|
|
|
181
188
|
request.org = SimpleLazyObject(
|
|
182
189
|
functools.partial(
|
|
@@ -199,7 +206,11 @@ class AccessMixin(mixins.AccessMixin):
|
|
|
199
206
|
"""Verify that the current user is authenticated."""
|
|
200
207
|
|
|
201
208
|
def dispatch(self, request, *args, **kwargs):
|
|
202
|
-
if
|
|
209
|
+
if (
|
|
210
|
+
not hasattr(request, "user")
|
|
211
|
+
or request.user is None
|
|
212
|
+
or not request.user.is_authenticated
|
|
213
|
+
):
|
|
203
214
|
authenticate_user(request)
|
|
204
215
|
|
|
205
216
|
request.user = SimpleLazyObject(
|
|
@@ -240,7 +251,11 @@ class AuthenticationMiddleware(BaseAuthenticationMiddleware):
|
|
|
240
251
|
def authenticate_user(request):
|
|
241
252
|
def authenticate(request, authenticator):
|
|
242
253
|
# Check if user exists and is not authenticated
|
|
243
|
-
if
|
|
254
|
+
if (
|
|
255
|
+
not hasattr(request, "user")
|
|
256
|
+
or request.user is None
|
|
257
|
+
or not getattr(request.user, "is_authenticated", False)
|
|
258
|
+
):
|
|
244
259
|
logger.debug(f"Trying authenticator: {authenticator}")
|
|
245
260
|
try:
|
|
246
261
|
auth_instance = pydoc.locate(authenticator)()
|
|
@@ -250,11 +265,12 @@ def authenticate_user(request):
|
|
|
250
265
|
user, token = auth
|
|
251
266
|
request.user = user
|
|
252
267
|
request.token = token
|
|
253
|
-
logger.info(f"Authentication successful with: {authenticator}")
|
|
254
268
|
except AttributeError as e:
|
|
255
269
|
# Skip SessionAuthentication if it fails with _request attribute error
|
|
256
270
|
if "'WSGIRequest' object has no attribute '_request'" in str(e):
|
|
257
|
-
logger.debug(
|
|
271
|
+
logger.debug(
|
|
272
|
+
f"Skipping {authenticator} - incompatible with middleware context"
|
|
273
|
+
)
|
|
258
274
|
else:
|
|
259
275
|
raise
|
|
260
276
|
except exceptions.AuthenticationFailed:
|
|
@@ -282,13 +298,15 @@ def get_request_org(request, user, org_id: str = None, default_org=None):
|
|
|
282
298
|
|
|
283
299
|
if default_org is not None:
|
|
284
300
|
org = default_org
|
|
285
|
-
elif user and hasattr(user,
|
|
301
|
+
elif user and hasattr(user, "id") and user.id:
|
|
286
302
|
orgs = Organization.objects.filter(users__id=user.id)
|
|
287
|
-
org =
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
303
|
+
org = None
|
|
304
|
+
|
|
305
|
+
if org_id is not None:
|
|
306
|
+
org = orgs.filter(id=org_id).first()
|
|
307
|
+
|
|
308
|
+
if org is None:
|
|
309
|
+
org = orgs.filter(is_active=True).first()
|
|
292
310
|
else:
|
|
293
311
|
org = None
|
|
294
312
|
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Karrio server system configuration adapter."""
|
|
2
|
+
|
|
3
|
+
import typing
|
|
4
|
+
import karrio.lib as lib
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ConstanceConfig(lib.AbstractSystemConfig):
|
|
8
|
+
"""Django constance configuration adapter.
|
|
9
|
+
|
|
10
|
+
Provides access to runtime configuration values stored
|
|
11
|
+
in Django constance (database-backed settings).
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
def get(self, key: str, default: typing.Any = None) -> typing.Any:
|
|
15
|
+
from constance import config as constance_config
|
|
16
|
+
|
|
17
|
+
return getattr(constance_config, key, default)
|
|
18
|
+
|
|
19
|
+
def __getitem__(self, key: str) -> typing.Any:
|
|
20
|
+
from constance import config as constance_config
|
|
21
|
+
|
|
22
|
+
return getattr(constance_config, key)
|
|
23
|
+
|
|
24
|
+
def __contains__(self, key: str) -> bool:
|
|
25
|
+
from constance import config as constance_config
|
|
26
|
+
|
|
27
|
+
return hasattr(constance_config, key)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# Singleton instance for use across the server
|
|
31
|
+
config = ConstanceConfig()
|
karrio/server/core/datatypes.py
CHANGED
|
@@ -23,13 +23,24 @@ from karrio.core.models import (
|
|
|
23
23
|
PickupCancelRequest as BasePickupCancelRequest,
|
|
24
24
|
ConfirmationDetails as Confirmation,
|
|
25
25
|
TrackingEvent,
|
|
26
|
-
TrackingDetails,
|
|
27
26
|
TrackingInfo,
|
|
27
|
+
ManifestDocument,
|
|
28
|
+
TrackingDetails,
|
|
28
29
|
DocumentFile,
|
|
29
30
|
DocumentUploadRequest,
|
|
30
31
|
ManifestRequest,
|
|
31
32
|
ManifestDetails,
|
|
32
|
-
|
|
33
|
+
RequestPayload,
|
|
34
|
+
OAuthAuthorizePayload,
|
|
35
|
+
OAuthAuthorizeRequest,
|
|
36
|
+
WebhookEventDetails,
|
|
37
|
+
WebhookRegistrationDetails,
|
|
38
|
+
WebhookDeregistrationRequest,
|
|
39
|
+
WebhookRegistrationRequest,
|
|
40
|
+
DutiesCalculationRequest,
|
|
41
|
+
DutiesCalculationDetails,
|
|
42
|
+
InsuranceRequest,
|
|
43
|
+
InsuranceDetails,
|
|
33
44
|
)
|
|
34
45
|
|
|
35
46
|
|
|
@@ -106,7 +117,9 @@ class Address(BaseAddress):
|
|
|
106
117
|
residential: bool = False
|
|
107
118
|
|
|
108
119
|
address_line1: str = ""
|
|
109
|
-
address_line2: str =
|
|
120
|
+
address_line2: str = None
|
|
121
|
+
street_number: str = None
|
|
122
|
+
suite: str = None
|
|
110
123
|
|
|
111
124
|
federal_tax_id: str = None
|
|
112
125
|
state_tax_id: str = None
|
|
@@ -362,8 +375,21 @@ class ManifestResponse:
|
|
|
362
375
|
manifest: Manifest = jstruct.JStruct[Manifest]
|
|
363
376
|
|
|
364
377
|
|
|
378
|
+
@attr.s(auto_attribs=True)
|
|
379
|
+
class DutiesResponse:
|
|
380
|
+
messages: typing.List[Message] = jstruct.JList[Message]
|
|
381
|
+
duties: DutiesCalculationDetails = jstruct.JStruct[DutiesCalculationDetails]
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
@attr.s(auto_attribs=True)
|
|
385
|
+
class InsuranceResponse:
|
|
386
|
+
messages: typing.List[Message] = jstruct.JList[Message]
|
|
387
|
+
insurance: InsuranceDetails = jstruct.JStruct[InsuranceDetails]
|
|
388
|
+
|
|
389
|
+
|
|
365
390
|
@attr.s(auto_attribs=True)
|
|
366
391
|
class Error:
|
|
367
|
-
message: str = None
|
|
368
392
|
code: str = None
|
|
393
|
+
message: str = None
|
|
394
|
+
level: str = None
|
|
369
395
|
details: typing.Dict = None
|
karrio/server/core/dataunits.py
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import logging
|
|
2
1
|
from constance import config
|
|
3
2
|
from django.urls import reverse
|
|
4
3
|
from rest_framework.request import Request
|
|
@@ -7,6 +6,7 @@ import karrio.lib as lib
|
|
|
7
6
|
import karrio.references as ref
|
|
8
7
|
import karrio.core.units as units
|
|
9
8
|
import karrio.server.conf as conf
|
|
9
|
+
import karrio.server.core.utils as utils
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
PACKAGE_MAPPERS = ref.collect_providers_data()
|
|
@@ -31,8 +31,6 @@ NON_HUBS_CARRIERS = [
|
|
|
31
31
|
carrier_name for carrier_name in CARRIER_NAMES if carrier_name not in CARRIER_HUBS
|
|
32
32
|
]
|
|
33
33
|
|
|
34
|
-
LOGGER = logging.getLogger(__name__)
|
|
35
|
-
|
|
36
34
|
|
|
37
35
|
def contextual_metadata(request: Request):
|
|
38
36
|
# Detect HTTPS from headers (for proxied environments like Caddy/ALB)
|
|
@@ -66,6 +64,30 @@ def contextual_metadata(request: Request):
|
|
|
66
64
|
else getattr(config, "APP_WEBSITE", None) or conf.settings.APP_WEBSITE
|
|
67
65
|
)
|
|
68
66
|
|
|
67
|
+
# Batch fetch all feature flags
|
|
68
|
+
flag_names = [flag for flag, _ in conf.settings.FEATURE_FLAGS]
|
|
69
|
+
|
|
70
|
+
if conf.settings.MULTI_TENANTS:
|
|
71
|
+
# In multi-tenancy mode, feature flags come from tenant.feature_flags (JSON field)
|
|
72
|
+
# No N+1 issue since it's a single field access on an already-loaded tenant object
|
|
73
|
+
tenant = conf.settings.tenant
|
|
74
|
+
tenant_flags = getattr(tenant, "feature_flags", {}) if tenant else {}
|
|
75
|
+
feature_flags = {
|
|
76
|
+
flag: tenant_flags.get(flag, getattr(conf.settings, flag, None))
|
|
77
|
+
for flag in flag_names
|
|
78
|
+
}
|
|
79
|
+
else:
|
|
80
|
+
# In single-tenant mode, batch fetch from Constance to avoid N+1 queries
|
|
81
|
+
constance_values = utils.batch_get_constance_values(flag_names)
|
|
82
|
+
feature_flags = {
|
|
83
|
+
flag: (
|
|
84
|
+
constance_values.get(flag)
|
|
85
|
+
if flag in constance_values
|
|
86
|
+
else getattr(conf.settings, flag, None)
|
|
87
|
+
)
|
|
88
|
+
for flag in flag_names
|
|
89
|
+
}
|
|
90
|
+
|
|
69
91
|
return {
|
|
70
92
|
"VERSION": conf.settings.VERSION,
|
|
71
93
|
"APP_NAME": name,
|
|
@@ -74,10 +96,7 @@ def contextual_metadata(request: Request):
|
|
|
74
96
|
"ADMIN": f"{host}/admin",
|
|
75
97
|
"GRAPHQL": f"{host}/graphql",
|
|
76
98
|
"OPENAPI": f"{host}/openapi",
|
|
77
|
-
**
|
|
78
|
-
flag: getattr(conf.settings, flag, None)
|
|
79
|
-
for flag, _ in conf.settings.FEATURE_FLAGS
|
|
80
|
-
},
|
|
99
|
+
**feature_flags,
|
|
81
100
|
}
|
|
82
101
|
|
|
83
102
|
|