karrio-server-core 2025.5rc31__py3-none-any.whl → 2026.1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. karrio/server/core/authentication.py +38 -20
  2. karrio/server/core/config.py +31 -0
  3. karrio/server/core/datatypes.py +30 -4
  4. karrio/server/core/dataunits.py +26 -7
  5. karrio/server/core/exceptions.py +287 -17
  6. karrio/server/core/filters.py +14 -0
  7. karrio/server/core/gateway.py +284 -11
  8. karrio/server/core/logging.py +403 -0
  9. karrio/server/core/middleware.py +104 -2
  10. karrio/server/core/models/base.py +34 -1
  11. karrio/server/core/oauth_validators.py +2 -3
  12. karrio/server/core/permissions.py +1 -2
  13. karrio/server/core/serializers.py +154 -7
  14. karrio/server/core/signals.py +22 -28
  15. karrio/server/core/telemetry.py +573 -0
  16. karrio/server/core/tests/__init__.py +27 -0
  17. karrio/server/core/{tests.py → tests/base.py} +6 -7
  18. karrio/server/core/tests/test_exception_level.py +159 -0
  19. karrio/server/core/tests/test_resource_token.py +593 -0
  20. karrio/server/core/utils.py +688 -38
  21. karrio/server/core/validators.py +144 -222
  22. karrio/server/core/views/oauth.py +13 -12
  23. karrio/server/core/views/references.py +2 -2
  24. karrio/server/iam/apps.py +1 -4
  25. karrio/server/iam/migrations/0002_setup_carrier_permission_groups.py +103 -0
  26. karrio/server/iam/migrations/0003_remove_permission_groups.py +91 -0
  27. karrio/server/iam/permissions.py +7 -134
  28. karrio/server/iam/serializers.py +9 -3
  29. karrio/server/iam/signals.py +2 -4
  30. karrio/server/providers/admin.py +1 -1
  31. karrio/server/providers/migrations/0085_populate_dhl_parcel_de_oauth_credentials.py +82 -0
  32. karrio/server/providers/migrations/0086_rename_dhl_parcel_de_customer_number_to_billing_number.py +71 -0
  33. karrio/server/providers/migrations/0087_alter_carrier_capabilities.py +38 -0
  34. karrio/server/providers/migrations/0088_servicelevel_surcharges.py +24 -0
  35. karrio/server/providers/migrations/0089_servicelevel_cost_max_volume.py +31 -0
  36. karrio/server/providers/migrations/0090_ratesheet_surcharges_servicelevel_zone_surcharge_ids.py +47 -0
  37. karrio/server/providers/migrations/0091_migrate_legacy_zones_surcharges.py +154 -0
  38. karrio/server/providers/models/carrier.py +101 -29
  39. karrio/server/providers/models/service.py +182 -125
  40. karrio/server/providers/models/sheet.py +342 -198
  41. karrio/server/providers/serializers/base.py +263 -2
  42. karrio/server/providers/signals.py +2 -4
  43. karrio/server/providers/templates/providers/oauth_callback.html +105 -0
  44. karrio/server/providers/tests/__init__.py +5 -0
  45. karrio/server/providers/tests/test_connections.py +895 -0
  46. karrio/server/providers/views/carriers.py +1 -3
  47. karrio/server/providers/views/connections.py +322 -2
  48. karrio/server/serializers/abstract.py +112 -21
  49. karrio/server/tracing/utils.py +5 -8
  50. karrio/server/user/models.py +36 -34
  51. karrio/server/user/serializers.py +1 -0
  52. {karrio_server_core-2025.5rc31.dist-info → karrio_server_core-2026.1.1.dist-info}/METADATA +2 -2
  53. {karrio_server_core-2025.5rc31.dist-info → karrio_server_core-2026.1.1.dist-info}/RECORD +55 -38
  54. karrio/server/providers/tests.py +0 -3
  55. {karrio_server_core-2025.5rc31.dist-info → karrio_server_core-2026.1.1.dist-info}/WHEEL +0 -0
  56. {karrio_server_core-2025.5rc31.dist-info → karrio_server_core-2026.1.1.dist-info}/top_level.txt +0 -0
@@ -1,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 hasattr(request, 'user') and request.user and not request.user.is_anonymous:
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, 'oauth_user'):
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, 'application') and token.application:
164
- user = getattr(token.application, 'user', None)
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, 'application') and hasattr(token.application, 'oauth_app'):
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, 'created_by') and oauth_app.created_by:
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, 'organizations'):
179
- default_org = app_owner.organizations.filter(is_active=True).first()
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 not hasattr(request, 'user') or request.user is None or not request.user.is_authenticated:
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 not hasattr(request, 'user') or request.user is None or not getattr(request.user, 'is_authenticated', False):
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(f"Skipping {authenticator} - incompatible with middleware context")
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, 'id') and user.id:
301
+ elif user and hasattr(user, "id") and user.id:
286
302
  orgs = Organization.objects.filter(users__id=user.id)
287
- org = (
288
- orgs.filter(id=org_id).first()
289
- if org_id is not None and orgs.filter(id=org_id).exists()
290
- else orgs.filter(is_active=True).first()
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()
@@ -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
- ManifestDocument,
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
@@ -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