karrio-server-core 2025.5rc12__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 (112) hide show
  1. karrio/server/core/authentication.py +59 -25
  2. karrio/server/core/config.py +31 -0
  3. karrio/server/core/datatypes.py +30 -4
  4. karrio/server/core/dataunits.py +53 -22
  5. karrio/server/core/exceptions.py +287 -17
  6. karrio/server/core/filters.py +14 -0
  7. karrio/server/core/gateway.py +285 -10
  8. karrio/server/core/logging.py +403 -0
  9. karrio/server/core/management/commands/runserver.py +5 -0
  10. karrio/server/core/middleware.py +104 -2
  11. karrio/server/core/migrations/0006_add_api_log_requested_at_index.py +22 -0
  12. karrio/server/core/models/base.py +34 -1
  13. karrio/server/core/oauth_validators.py +2 -3
  14. karrio/server/core/permissions.py +1 -2
  15. karrio/server/core/serializers.py +183 -10
  16. karrio/server/core/signals.py +22 -28
  17. karrio/server/core/telemetry.py +573 -0
  18. karrio/server/core/tests/__init__.py +27 -0
  19. karrio/server/core/{tests.py → tests/base.py} +6 -7
  20. karrio/server/core/tests/test_exception_level.py +159 -0
  21. karrio/server/core/tests/test_resource_token.py +593 -0
  22. karrio/server/core/utils.py +688 -38
  23. karrio/server/core/validators.py +144 -222
  24. karrio/server/core/views/oauth.py +13 -12
  25. karrio/server/core/views/references.py +2 -2
  26. karrio/server/iam/apps.py +1 -4
  27. karrio/server/iam/migrations/0002_setup_carrier_permission_groups.py +103 -0
  28. karrio/server/iam/migrations/0003_remove_permission_groups.py +91 -0
  29. karrio/server/iam/permissions.py +7 -134
  30. karrio/server/iam/serializers.py +17 -2
  31. karrio/server/iam/signals.py +2 -4
  32. karrio/server/providers/admin.py +1 -1
  33. karrio/server/providers/management/commands/migrate_rate_sheets.py +101 -0
  34. karrio/server/providers/migrations/0082_add_zone_identifiers.py +50 -0
  35. karrio/server/providers/migrations/0083_add_optimized_rate_sheet_structure.py +33 -0
  36. karrio/server/providers/migrations/0084_alter_servicelevel_currency.py +168 -0
  37. karrio/server/providers/migrations/0085_populate_dhl_parcel_de_oauth_credentials.py +82 -0
  38. karrio/server/providers/migrations/0086_rename_dhl_parcel_de_customer_number_to_billing_number.py +71 -0
  39. karrio/server/providers/migrations/0087_alter_carrier_capabilities.py +38 -0
  40. karrio/server/providers/migrations/0088_servicelevel_surcharges.py +24 -0
  41. karrio/server/providers/migrations/0089_servicelevel_cost_max_volume.py +31 -0
  42. karrio/server/providers/migrations/0090_ratesheet_surcharges_servicelevel_zone_surcharge_ids.py +47 -0
  43. karrio/server/providers/migrations/0091_migrate_legacy_zones_surcharges.py +154 -0
  44. karrio/server/providers/models/__init__.py +1 -2
  45. karrio/server/providers/models/carrier.py +103 -18
  46. karrio/server/providers/models/service.py +188 -1
  47. karrio/server/providers/models/sheet.py +371 -0
  48. karrio/server/providers/serializers/base.py +263 -2
  49. karrio/server/providers/signals.py +2 -4
  50. karrio/server/providers/templates/providers/oauth_callback.html +105 -0
  51. karrio/server/providers/tests/__init__.py +5 -0
  52. karrio/server/providers/tests/test_connections.py +895 -0
  53. karrio/server/providers/views/carriers.py +1 -3
  54. karrio/server/providers/views/connections.py +322 -2
  55. karrio/server/samples.py +1 -1
  56. karrio/server/serializers/abstract.py +116 -21
  57. karrio/server/tracing/migrations/0007_tracingrecord_tracing_created_at_idx.py +19 -0
  58. karrio/server/tracing/models.py +2 -0
  59. karrio/server/tracing/utils.py +5 -8
  60. karrio/server/user/migrations/0007_user_metadata.py +25 -0
  61. karrio/server/user/models.py +38 -23
  62. karrio/server/user/serializers.py +1 -0
  63. karrio/server/user/templates/registration/registration_confirm_email.html +1 -1
  64. {karrio_server_core-2025.5rc12.dist-info → karrio_server_core-2026.1.1.dist-info}/METADATA +2 -2
  65. {karrio_server_core-2025.5rc12.dist-info → karrio_server_core-2026.1.1.dist-info}/RECORD +67 -86
  66. karrio/server/providers/extension/__init__.py +0 -1
  67. karrio/server/providers/extension/models/__init__.py +0 -1
  68. karrio/server/providers/extension/models/allied_express.py +0 -22
  69. karrio/server/providers/extension/models/allied_express_local.py +0 -22
  70. karrio/server/providers/extension/models/amazon_shipping.py +0 -27
  71. karrio/server/providers/extension/models/aramex.py +0 -25
  72. karrio/server/providers/extension/models/asendia_us.py +0 -21
  73. karrio/server/providers/extension/models/australiapost.py +0 -20
  74. karrio/server/providers/extension/models/boxknight.py +0 -19
  75. karrio/server/providers/extension/models/bpost.py +0 -21
  76. karrio/server/providers/extension/models/canadapost.py +0 -21
  77. karrio/server/providers/extension/models/canpar.py +0 -19
  78. karrio/server/providers/extension/models/chronopost.py +0 -22
  79. karrio/server/providers/extension/models/colissimo.py +0 -22
  80. karrio/server/providers/extension/models/dhl_express.py +0 -23
  81. karrio/server/providers/extension/models/dhl_parcel_de.py +0 -25
  82. karrio/server/providers/extension/models/dhl_poland.py +0 -22
  83. karrio/server/providers/extension/models/dhl_universal.py +0 -19
  84. karrio/server/providers/extension/models/dicom.py +0 -20
  85. karrio/server/providers/extension/models/dpd.py +0 -37
  86. karrio/server/providers/extension/models/dpdhl.py +0 -26
  87. karrio/server/providers/extension/models/easypost.py +0 -20
  88. karrio/server/providers/extension/models/eshipper.py +0 -21
  89. karrio/server/providers/extension/models/fedex.py +0 -25
  90. karrio/server/providers/extension/models/fedex_ws.py +0 -24
  91. karrio/server/providers/extension/models/freightcom.py +0 -21
  92. karrio/server/providers/extension/models/generic.py +0 -35
  93. karrio/server/providers/extension/models/geodis.py +0 -22
  94. karrio/server/providers/extension/models/hay_post.py +0 -22
  95. karrio/server/providers/extension/models/laposte.py +0 -19
  96. karrio/server/providers/extension/models/locate2u.py +0 -22
  97. karrio/server/providers/extension/models/nationex.py +0 -22
  98. karrio/server/providers/extension/models/purolator.py +0 -21
  99. karrio/server/providers/extension/models/roadie.py +0 -18
  100. karrio/server/providers/extension/models/royalmail.py +0 -19
  101. karrio/server/providers/extension/models/sendle.py +0 -22
  102. karrio/server/providers/extension/models/tge.py +0 -63
  103. karrio/server/providers/extension/models/tnt.py +0 -23
  104. karrio/server/providers/extension/models/ups.py +0 -23
  105. karrio/server/providers/extension/models/usps.py +0 -23
  106. karrio/server/providers/extension/models/usps_international.py +0 -23
  107. karrio/server/providers/extension/models/usps_wt.py +0 -24
  108. karrio/server/providers/extension/models/usps_wt_international.py +0 -24
  109. karrio/server/providers/extension/models/zoom2u.py +0 -23
  110. karrio/server/providers/tests.py +0 -3
  111. {karrio_server_core-2025.5rc12.dist-info → karrio_server_core-2026.1.1.dist-info}/WHEEL +0 -0
  112. {karrio_server_core-2025.5rc12.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,19 +251,40 @@ 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):
244
- auth = pydoc.locate(authenticator)().authenticate(request)
245
-
246
- if auth is not None:
247
- user, token = auth
248
- request.user = user
249
- request.token = token
254
+ if (
255
+ not hasattr(request, "user")
256
+ or request.user is None
257
+ or not getattr(request.user, "is_authenticated", False)
258
+ ):
259
+ logger.debug(f"Trying authenticator: {authenticator}")
260
+ try:
261
+ auth_instance = pydoc.locate(authenticator)()
262
+ auth = auth_instance.authenticate(request)
263
+
264
+ if auth is not None:
265
+ user, token = auth
266
+ request.user = user
267
+ request.token = token
268
+ except AttributeError as e:
269
+ # Skip SessionAuthentication if it fails with _request attribute error
270
+ if "'WSGIRequest' object has no attribute '_request'" in str(e):
271
+ logger.debug(
272
+ f"Skipping {authenticator} - incompatible with middleware context"
273
+ )
274
+ else:
275
+ raise
276
+ except exceptions.AuthenticationFailed:
277
+ # Silently skip authentication failures - let the next authenticator try
278
+ logger.debug(f"Authentication failed with {authenticator}, trying next")
279
+ pass
250
280
 
251
281
  return request
252
282
 
253
283
  try:
284
+ logger.debug(f"Auth classes to try: {AUTHENTICATION_CLASSES}")
254
285
  return functools.reduce(authenticate, AUTHENTICATION_CLASSES, request)
255
- except Exception:
286
+ except Exception as e:
287
+ logger.debug(f"Authentication error: {e}")
256
288
  return request
257
289
 
258
290
 
@@ -266,13 +298,15 @@ def get_request_org(request, user, org_id: str = None, default_org=None):
266
298
 
267
299
  if default_org is not None:
268
300
  org = default_org
269
- elif user and hasattr(user, 'id') and user.id:
301
+ elif user and hasattr(user, "id") and user.id:
270
302
  orgs = Organization.objects.filter(users__id=user.id)
271
- org = (
272
- orgs.filter(id=org_id).first()
273
- if org_id is not None and orgs.filter(id=org_id).exists()
274
- else orgs.filter(is_active=True).first()
275
- )
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()
276
310
  else:
277
311
  org = None
278
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 typing
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()
@@ -33,16 +33,25 @@ NON_HUBS_CARRIERS = [
33
33
 
34
34
 
35
35
  def contextual_metadata(request: Request):
36
- _host: str = typing.cast(
37
- str,
38
- (
39
- request.build_absolute_uri(
40
- reverse("karrio.server.core:metadata", kwargs={})
41
- )
42
- if hasattr(request, "build_absolute_uri")
43
- else "/"
44
- ),
45
- )
36
+ # Detect HTTPS from headers (for proxied environments like Caddy/ALB)
37
+ is_https = False
38
+ if hasattr(request, 'META'):
39
+ # Check X-Forwarded-Proto header (set by load balancers/proxies)
40
+ forwarded_proto = request.META.get('HTTP_X_FORWARDED_PROTO', '').lower()
41
+ # Check if request is secure (Django's built-in HTTPS detection)
42
+ is_secure = getattr(request, 'is_secure', lambda: False)()
43
+ is_https = forwarded_proto == 'https' or is_secure
44
+
45
+ if hasattr(request, "build_absolute_uri"):
46
+ _host: str = request.build_absolute_uri(
47
+ reverse("karrio.server.core:metadata", kwargs={})
48
+ )
49
+ # Override protocol if we detected HTTPS but build_absolute_uri returned HTTP
50
+ if is_https and _host.startswith('http://'):
51
+ _host = _host.replace('http://', 'https://', 1)
52
+ else:
53
+ _host = "/"
54
+
46
55
  host = _host[:-1] if _host[-1] == "/" else _host
47
56
  name = lib.identity(
48
57
  getattr(conf.settings.tenant, "name", conf.settings.APP_NAME)
@@ -55,6 +64,30 @@ def contextual_metadata(request: Request):
55
64
  else getattr(config, "APP_WEBSITE", None) or conf.settings.APP_WEBSITE
56
65
  )
57
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
+
58
91
  return {
59
92
  "VERSION": conf.settings.VERSION,
60
93
  "APP_NAME": name,
@@ -63,10 +96,7 @@ def contextual_metadata(request: Request):
63
96
  "ADMIN": f"{host}/admin",
64
97
  "GRAPHQL": f"{host}/graphql",
65
98
  "OPENAPI": f"{host}/openapi",
66
- **{
67
- flag: getattr(conf.settings, flag, None)
68
- for flag, _ in conf.settings.FEATURE_FLAGS
69
- },
99
+ **feature_flags,
70
100
  }
71
101
 
72
102
 
@@ -93,30 +123,31 @@ def contextual_reference(request: Request = None, reduced: bool = True):
93
123
  }
94
124
 
95
125
  def _get_generic_carriers():
126
+ # Get all carriers, then filter by extension instead of hardcoded slug
96
127
  system_custom_carriers = [
97
- c for c in gateway.Carriers.list(system_only=True, carrier_name="generic")
128
+ c for c in gateway.Carriers.list(system_only=True)
129
+ if c.ext == "generic"
98
130
  ]
99
131
  custom_carriers = [
100
132
  c
101
133
  for c in (
102
- gateway.Carriers.list(context=request, carrier_name="generic").exclude(
103
- is_system=True
104
- )
134
+ gateway.Carriers.list(context=request).exclude(is_system=True)
105
135
  if is_authenticated
106
136
  else []
107
137
  )
138
+ if c.ext == "generic"
108
139
  ]
109
140
 
110
141
  extra_carriers = {
111
- f"{c.credentials.get('custom_carrier_name') or 'generic'}": c.display_name
142
+ c.carrier_code: c.display_name
112
143
  for c in custom_carriers
113
144
  }
114
145
  system_carriers = {
115
- f"{c.credentials.get('custom_carrier_name') or 'generic'}": c.display_name
146
+ c.carrier_code: c.display_name
116
147
  for c in system_custom_carriers
117
148
  }
118
149
  extra_services = {
119
- f"{c.credentials.get('custom_carrier_name') or 'generic'}": {
150
+ c.carrier_code: {
120
151
  s.service_code: s.service_code
121
152
  for s in c.services
122
153
  or [