karrio-server 2025.5rc36__py3-none-any.whl → 2025.5.2__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/VERSION CHANGED
@@ -1 +1 @@
1
- 2025.5rc36
1
+ 2025.5.2
@@ -35,33 +35,188 @@ if POSTHOG_KEY:
35
35
  posthog.host = POSTHOG_HOST
36
36
 
37
37
 
38
- # Sentry
38
+ # =============================================================================
39
+ # Sentry Configuration
40
+ # =============================================================================
41
+ #
42
+ # Sentry provides error tracking, performance monitoring, and distributed
43
+ # tracing for Karrio. When SENTRY_DSN is configured, the following features
44
+ # are enabled:
45
+ #
46
+ # 1. Error Tracking: All unhandled exceptions are captured with full context
47
+ # 2. Performance Monitoring: API endpoints and gateway operations are traced
48
+ # 3. Distributed Tracing: Carrier API calls are linked to parent transactions
49
+ # 4. Metrics: Custom metrics for carrier operations (rates, shipments, etc.)
50
+ # 5. Logging Integration: ERROR/CRITICAL logs are sent to Sentry
51
+ #
52
+ # Environment Variables:
53
+ # SENTRY_DSN - Sentry Data Source Name (required to enable)
54
+ # SENTRY_ENVIRONMENT - Environment name (default: from ENV or "production")
55
+ # SENTRY_RELEASE - Release version (default: VERSION)
56
+ # SENTRY_TRACES_SAMPLE_RATE - Transaction sampling rate 0.0-1.0 (default: 1.0)
57
+ # SENTRY_PROFILES_SAMPLE_RATE - Profile sampling rate 0.0-1.0 (default: 1.0)
58
+ # SENTRY_SEND_PII - Send personally identifiable information (default: true)
59
+ # SENTRY_DEBUG - Enable Sentry debug mode (default: false)
60
+ #
61
+ # =============================================================================
62
+
39
63
  sentry_sdk.utils.MAX_STRING_LENGTH = 4096
40
64
  SENTRY_DSN = config("SENTRY_DSN", default=None)
65
+ SENTRY_ENVIRONMENT = config("SENTRY_ENVIRONMENT", default=config("ENV", default="production"))
66
+ SENTRY_RELEASE = config("SENTRY_RELEASE", default=config("VERSION", default=None))
67
+ # Lower default sample rates for better performance (was 1.0/100%)
68
+ SENTRY_TRACES_SAMPLE_RATE = config("SENTRY_TRACES_SAMPLE_RATE", default=0.1, cast=float) # 10% of transactions
69
+ SENTRY_PROFILES_SAMPLE_RATE = config("SENTRY_PROFILES_SAMPLE_RATE", default=0.0, cast=float) # Disabled by default
70
+ SENTRY_SEND_PII = config("SENTRY_SEND_PII", default=True, cast=bool)
71
+ SENTRY_DEBUG = config("SENTRY_DEBUG", default=False, cast=bool)
72
+
73
+
74
+ def _sentry_before_send(event, hint):
75
+ """Pre-process events before sending to Sentry.
76
+
77
+ This hook allows us to:
78
+ - Scrub sensitive data (API keys, tokens, passwords)
79
+ - Add custom tags
80
+ - Filter out certain events
81
+ """
82
+ # Scrub sensitive data from request bodies
83
+ if "request" in event:
84
+ request_data = event["request"]
85
+
86
+ # Scrub headers
87
+ if "headers" in request_data:
88
+ sensitive_headers = ["authorization", "x-api-key", "cookie", "x-csrf-token"]
89
+ for header in sensitive_headers:
90
+ if header in request_data["headers"]:
91
+ request_data["headers"][header] = "[Filtered]"
92
+
93
+ # Scrub POST data
94
+ if "data" in request_data and isinstance(request_data["data"], dict):
95
+ sensitive_fields = [
96
+ "password", "secret", "token", "api_key", "apikey",
97
+ "access_token", "refresh_token", "client_secret",
98
+ "account_number", "meter_number", "license_key",
99
+ ]
100
+ for field in sensitive_fields:
101
+ for key in list(request_data["data"].keys()):
102
+ if field.lower() in key.lower():
103
+ request_data["data"][key] = "[Filtered]"
104
+
105
+ return event
106
+
107
+
108
+ def _sentry_before_send_transaction(event, hint):
109
+ """Pre-process transactions before sending to Sentry.
110
+
111
+ This hook allows us to:
112
+ - Filter out noisy transactions (health checks, static files)
113
+ - Add custom tags
114
+ """
115
+ transaction_name = event.get("transaction", "")
116
+
117
+ # Filter out health check and monitoring endpoints
118
+ noisy_endpoints = [
119
+ "/health",
120
+ "/ready",
121
+ "/live",
122
+ "/_health",
123
+ "/favicon.ico",
124
+ "/static/",
125
+ "/robots.txt",
126
+ ]
127
+
128
+ for endpoint in noisy_endpoints:
129
+ if transaction_name.startswith(endpoint):
130
+ return None # Drop this transaction
131
+
132
+ return event
133
+
41
134
 
42
135
  if SENTRY_DSN:
43
136
  # Build integrations list
44
- integrations = [DjangoIntegration()]
137
+ integrations = [
138
+ DjangoIntegration(
139
+ transaction_style="url", # Use URL patterns for transaction names
140
+ middleware_spans=False, # Disabled for performance (was True)
141
+ signals_spans=False, # Disabled for performance (was True)
142
+ ),
143
+ ]
144
+
145
+ # Add PostHog integration if available
45
146
  if POSTHOG_KEY and PostHogIntegration is not None:
46
147
  integrations.append(PostHogIntegration())
47
148
 
149
+ # Try to add Redis integration if Redis is configured
150
+ try:
151
+ from sentry_sdk.integrations.redis import RedisIntegration
152
+ if config("REDIS_URL", default=None) or config("REDIS_HOST", default=None):
153
+ integrations.append(RedisIntegration())
154
+ except ImportError:
155
+ pass
156
+
157
+ # Try to add Huey integration for background tasks
158
+ try:
159
+ from sentry_sdk.integrations.huey import HueyIntegration
160
+ integrations.append(HueyIntegration())
161
+ except ImportError:
162
+ pass
163
+
164
+ # Try to add httpx integration for async HTTP clients
165
+ try:
166
+ from sentry_sdk.integrations.httpx import HttpxIntegration
167
+ integrations.append(HttpxIntegration())
168
+ except Exception:
169
+ pass # httpx may not be installed
170
+
171
+ # Try to add Strawberry GraphQL integration
172
+ try:
173
+ from sentry_sdk.integrations.strawberry import StrawberryIntegration
174
+ integrations.append(StrawberryIntegration(async_execution=False))
175
+ except Exception:
176
+ pass # strawberry integration may not be available
177
+
48
178
  sentry_sdk.init(
49
179
  dsn=SENTRY_DSN,
50
180
  integrations=integrations,
51
- # Set traces_sample_rate to 1.0 to capture 100%
52
- # of transactions for tracing.
53
- traces_sample_rate=1.0,
54
- # Set profile_session_sample_rate to 1.0 to profile 100%
55
- # of profile sessions.
56
- profile_session_sample_rate=1.0,
57
- # Set profile_lifecycle to "trace" to automatically
58
- # run the profiler on when there is an active transaction
59
- profile_lifecycle="trace",
60
- # If you wish to associate users to errors (assuming you are using
61
- # django.contrib.auth) you may enable sending PII data.
62
- send_default_pii=True,
63
- # Enable sending logs to Sentry
181
+
182
+ # Environment and release tracking
183
+ environment=SENTRY_ENVIRONMENT,
184
+ release=SENTRY_RELEASE,
185
+
186
+ # Performance monitoring (lower sample rates for better performance)
187
+ traces_sample_rate=SENTRY_TRACES_SAMPLE_RATE,
188
+ # Only enable profiling if explicitly configured (disabled by default)
189
+ **({"profile_session_sample_rate": SENTRY_PROFILES_SAMPLE_RATE, "profile_lifecycle": "trace"} if SENTRY_PROFILES_SAMPLE_RATE > 0 else {}),
190
+
191
+ # Privacy settings
192
+ send_default_pii=SENTRY_SEND_PII,
193
+
194
+ # Logging integration
64
195
  enable_logs=True,
196
+
197
+ # Debug mode
198
+ debug=SENTRY_DEBUG,
199
+
200
+ # Event processing hooks
201
+ before_send=_sentry_before_send,
202
+ before_send_transaction=_sentry_before_send_transaction,
203
+
204
+ # Additional options (reduced for performance)
205
+ max_breadcrumbs=25, # Reduced from 50 for lower memory usage
206
+ attach_stacktrace=True, # Attach stack traces to messages
207
+ include_source_context=False, # Disabled for performance (was True)
208
+ include_local_variables=False, # Disabled for performance (was True)
209
+ )
210
+
211
+ # Set default tags that will be applied to all events
212
+ sentry_sdk.set_tag("service", "karrio-api")
213
+ sentry_sdk.set_tag("framework", "django")
214
+
215
+ import logging
216
+ logger = logging.getLogger(__name__)
217
+ logger.info(
218
+ f"Sentry initialized: env={SENTRY_ENVIRONMENT}, "
219
+ f"traces_sample_rate={SENTRY_TRACES_SAMPLE_RATE}"
65
220
  )
66
221
 
67
222
 
@@ -191,3 +346,141 @@ if OTEL_ENABLED and OTEL_EXPORTER_OTLP_ENDPOINT:
191
346
  # Log that OpenTelemetry is enabled
192
347
  logger = logging.getLogger(__name__)
193
348
  logger.info(f"OpenTelemetry enabled: Service={OTEL_SERVICE_NAME}, Endpoint={OTEL_EXPORTER_OTLP_ENDPOINT}")
349
+
350
+
351
+ # =============================================================================
352
+ # Datadog Configuration
353
+ # =============================================================================
354
+ #
355
+ # Datadog provides full-stack observability including APM, infrastructure
356
+ # monitoring, logs, and more. When DD_TRACE_ENABLED is set, the following
357
+ # features are enabled:
358
+ #
359
+ # 1. APM Tracing: Distributed tracing for all requests and background tasks
360
+ # 2. Metrics: Custom metrics via DogStatsD
361
+ # 3. Logs: Automatic trace ID injection into logs
362
+ # 4. Profiling: Continuous profiling (optional)
363
+ #
364
+ # Environment Variables:
365
+ # DD_TRACE_ENABLED - Enable Datadog tracing (default: false)
366
+ # DD_SERVICE - Service name (default: karrio-api)
367
+ # DD_ENV - Environment name (default: from ENV or "production")
368
+ # DD_VERSION - Application version (default: VERSION)
369
+ # DD_AGENT_HOST - Datadog agent host (default: localhost)
370
+ # DD_TRACE_AGENT_PORT - Datadog agent port (default: 8126)
371
+ # DD_DOGSTATSD_PORT - DogStatsD port for metrics (default: 8125)
372
+ # DD_TRACE_SAMPLE_RATE - Sampling rate 0.0-1.0 (default: 1.0)
373
+ # DD_PROFILING_ENABLED - Enable continuous profiling (default: false)
374
+ # DD_LOGS_INJECTION - Inject trace IDs into logs (default: true)
375
+ #
376
+ # =============================================================================
377
+
378
+ DD_TRACE_ENABLED = config("DD_TRACE_ENABLED", default=False, cast=bool)
379
+ DATADOG_ENABLED = config("DATADOG_ENABLED", default=False, cast=bool) # Alias
380
+
381
+ # Use either DD_TRACE_ENABLED or DATADOG_ENABLED
382
+ _datadog_enabled = DD_TRACE_ENABLED or DATADOG_ENABLED
383
+
384
+ if _datadog_enabled:
385
+ # Datadog configuration
386
+ DD_SERVICE = config("DD_SERVICE", default="karrio-api")
387
+ DD_ENV = config("DD_ENV", default=config("ENV", default="production"))
388
+ DD_VERSION = config("DD_VERSION", default=config("VERSION", default="unknown"))
389
+ DD_AGENT_HOST = config("DD_AGENT_HOST", default="localhost")
390
+ DD_TRACE_AGENT_PORT = config("DD_TRACE_AGENT_PORT", default=8126, cast=int)
391
+ DD_DOGSTATSD_PORT = config("DD_DOGSTATSD_PORT", default=8125, cast=int)
392
+ DD_TRACE_SAMPLE_RATE = config("DD_TRACE_SAMPLE_RATE", default=1.0, cast=float)
393
+ DD_PROFILING_ENABLED = config("DD_PROFILING_ENABLED", default=False, cast=bool)
394
+ DD_LOGS_INJECTION = config("DD_LOGS_INJECTION", default=True, cast=bool)
395
+
396
+ try:
397
+ import ddtrace
398
+ from ddtrace import config as dd_config, tracer, patch_all
399
+
400
+ # Configure tracer
401
+ ddtrace.config.service = DD_SERVICE
402
+ ddtrace.config.env = DD_ENV
403
+ ddtrace.config.version = DD_VERSION
404
+
405
+ # Configure Django integration
406
+ dd_config.django["service_name"] = DD_SERVICE
407
+ dd_config.django["cache_service_name"] = f"{DD_SERVICE}-cache"
408
+ dd_config.django["database_service_name"] = f"{DD_SERVICE}-db"
409
+ dd_config.django["trace_query_string"] = True
410
+ dd_config.django["analytics_enabled"] = True
411
+
412
+ # Configure trace sampling
413
+ tracer.configure(
414
+ hostname=DD_AGENT_HOST,
415
+ port=DD_TRACE_AGENT_PORT,
416
+ )
417
+
418
+ # Set global sample rate
419
+ from ddtrace.sampler import DatadogSampler
420
+ tracer.configure(sampler=DatadogSampler(default_sample_rate=DD_TRACE_SAMPLE_RATE))
421
+
422
+ # Enable log injection
423
+ if DD_LOGS_INJECTION:
424
+ ddtrace.patch(logging=True)
425
+
426
+ # Patch all supported libraries
427
+ patch_all(
428
+ django=True,
429
+ redis=True,
430
+ psycopg=True,
431
+ requests=True,
432
+ httpx=True,
433
+ logging=DD_LOGS_INJECTION,
434
+ )
435
+
436
+ # Patch Huey for background task tracing
437
+ try:
438
+ from ddtrace import patch
439
+ patch(huey=True)
440
+ except Exception:
441
+ pass # Huey integration may not be available in all ddtrace versions
442
+
443
+ # Enable profiling if configured
444
+ if DD_PROFILING_ENABLED:
445
+ try:
446
+ import ddtrace.profiling.auto # noqa: F401
447
+ except ImportError:
448
+ pass
449
+
450
+ # Configure DogStatsD for metrics
451
+ try:
452
+ from datadog import initialize, statsd
453
+
454
+ initialize(
455
+ statsd_host=DD_AGENT_HOST,
456
+ statsd_port=DD_DOGSTATSD_PORT,
457
+ )
458
+
459
+ # Set default tags for all metrics
460
+ statsd.constant_tags = [
461
+ f"service:{DD_SERVICE}",
462
+ f"env:{DD_ENV}",
463
+ f"version:{DD_VERSION}",
464
+ ]
465
+
466
+ except ImportError:
467
+ pass # datadog package not installed, metrics won't work
468
+
469
+ import logging
470
+ logger = logging.getLogger(__name__)
471
+ logger.info(
472
+ f"Datadog APM initialized: service={DD_SERVICE}, env={DD_ENV}, "
473
+ f"agent={DD_AGENT_HOST}:{DD_TRACE_AGENT_PORT}"
474
+ )
475
+
476
+ except ImportError:
477
+ import logging
478
+ logger = logging.getLogger(__name__)
479
+ logger.warning(
480
+ "Datadog tracing enabled but ddtrace package not installed. "
481
+ "Install with: pip install ddtrace"
482
+ )
483
+ except Exception as e:
484
+ import logging
485
+ logger = logging.getLogger(__name__)
486
+ logger.warning(f"Failed to initialize Datadog APM: {e}")
@@ -46,6 +46,7 @@ urlpatterns = [
46
46
  for (subpath, urls, namespace) in settings.NAMESPACED_URLS
47
47
  ],
48
48
  path("", include("karrio.server.urls.jwt")),
49
+ path("", include("karrio.server.urls.tokens")),
49
50
  path("", include("karrio.server.user.urls")),
50
51
  *[path("", include(urls)) for urls in settings.KARRIO_URLS],
51
52
  path("admin/", admin.site.urls, name="app_admin"),
@@ -0,0 +1,237 @@
1
+ """Generic Resource Access Token API.
2
+
3
+ This module provides a generic endpoint for generating limited-access tokens
4
+ for various resources (documents, exports, etc.).
5
+ """
6
+
7
+ from datetime import datetime, timezone
8
+ from django.urls import path
9
+ from rest_framework import serializers, status
10
+ from rest_framework.views import APIView
11
+ from rest_framework.request import Request
12
+ from rest_framework.response import Response
13
+ from rest_framework.permissions import IsAuthenticated
14
+
15
+ import karrio.server.openapi as openapi
16
+ from karrio.server.core.utils import ResourceAccessToken
17
+
18
+ ENDPOINT_ID = "&&" # Unique endpoint ID for OpenAPI operation IDs
19
+
20
+
21
+ # Serializers
22
+ class ResourceTokenRequest(serializers.Serializer):
23
+ resource_type = serializers.ChoiceField(
24
+ choices=[
25
+ ("shipment", "Shipment"),
26
+ ("manifest", "Manifest"),
27
+ ("order", "Order"),
28
+ ("template", "Template"),
29
+ ("document", "Document"),
30
+ ],
31
+ help_text="The type of resource to grant access to.",
32
+ )
33
+ resource_ids = serializers.ListField(
34
+ child=serializers.CharField(),
35
+ min_length=1,
36
+ help_text="List of resource IDs to grant access to.",
37
+ )
38
+ access = serializers.ListField(
39
+ child=serializers.ChoiceField(
40
+ choices=[
41
+ ("label", "Label"),
42
+ ("invoice", "Invoice"),
43
+ ("manifest", "Manifest"),
44
+ ("render", "Render template"),
45
+ ("batch_labels", "Batch labels"),
46
+ ("batch_invoices", "Batch invoices"),
47
+ ("batch_manifests", "Batch manifests"),
48
+ ]
49
+ ),
50
+ min_length=1,
51
+ help_text="List of access permissions to grant.",
52
+ )
53
+ format = serializers.ChoiceField(
54
+ choices=[
55
+ ("pdf", "PDF"),
56
+ ("png", "PNG"),
57
+ ("zpl", "ZPL"),
58
+ ("gif", "GIF"),
59
+ ],
60
+ required=False,
61
+ allow_null=True,
62
+ help_text="Document format (optional).",
63
+ )
64
+ expires_in = serializers.IntegerField(
65
+ required=False,
66
+ min_value=60,
67
+ max_value=3600,
68
+ default=300,
69
+ help_text="Token expiration time in seconds (60-3600, default: 300).",
70
+ )
71
+
72
+
73
+ class ResourceTokenResponse(serializers.Serializer):
74
+ token = serializers.CharField(help_text="The JWT access token.")
75
+ expires_at = serializers.DateTimeField(help_text="Token expiration timestamp.")
76
+ resource_urls = serializers.DictField(
77
+ child=serializers.CharField(),
78
+ help_text="Map of resource IDs to their access URLs with token.",
79
+ )
80
+
81
+
82
+ # URL builders for different resource types
83
+ def _build_shipment_urls(resource_ids: list, access: list, format_ext: str, token: str) -> dict:
84
+ access_type = access[0] if access else "label"
85
+ return {rid: f"/v1/shipments/{rid}/{access_type}.{format_ext}?token={token}" for rid in resource_ids}
86
+
87
+
88
+ def _build_manifest_urls(resource_ids: list, format_ext: str, token: str) -> dict:
89
+ return {rid: f"/v1/manifests/{rid}/manifest.{format_ext}?token={token}" for rid in resource_ids}
90
+
91
+
92
+ def _build_order_urls(resource_ids: list, access: list, format_ext: str, token: str) -> dict:
93
+ order_ids = ",".join(resource_ids)
94
+ access_type = access[0] if access else "batch_labels"
95
+ doc_type = "invoice" if access_type == "batch_invoices" else "label"
96
+ return {"batch": f"/documents/orders/{doc_type}.{format_ext}?orders={order_ids}&token={token}"}
97
+
98
+
99
+ def _build_template_urls(resource_ids: list, token: str) -> dict:
100
+ from karrio.server.documents.models import DocumentTemplate
101
+
102
+ # Query templates to get their slugs
103
+ templates = DocumentTemplate.objects.filter(pk__in=resource_ids).values("pk", "slug")
104
+ template_map = {t["pk"]: t["slug"] for t in templates}
105
+
106
+ return {
107
+ rid: f"/documents/templates/{rid}.{template_map.get(rid, 'doc')}?token={token}"
108
+ for rid in resource_ids
109
+ }
110
+
111
+
112
+ def _build_document_urls(resource_ids: list, access: list, format_ext: str, token: str) -> dict:
113
+ access_type = access[0] if access else "batch_labels"
114
+ ids = ",".join(resource_ids)
115
+ url_map = {
116
+ "batch_labels": f"/documents/shipments/label.{format_ext}?shipments={ids}&token={token}",
117
+ "batch_invoices": f"/documents/shipments/invoice.{format_ext}?shipments={ids}&token={token}",
118
+ "batch_manifests": f"/documents/manifests/manifest.{format_ext}?manifests={ids}&token={token}",
119
+ }
120
+ return {"batch": url_map.get(access_type, url_map["batch_labels"])}
121
+
122
+
123
+ def build_resource_urls(
124
+ resource_type: str,
125
+ resource_ids: list,
126
+ access: list,
127
+ format: str,
128
+ token: str,
129
+ ) -> dict:
130
+ """Build resource URLs with token for each resource ID."""
131
+ format_ext = (format or "pdf").lower()
132
+
133
+ builders = {
134
+ "shipment": lambda: _build_shipment_urls(resource_ids, access, format_ext, token),
135
+ "manifest": lambda: _build_manifest_urls(resource_ids, format_ext, token),
136
+ "order": lambda: _build_order_urls(resource_ids, access, format_ext, token),
137
+ "template": lambda: _build_template_urls(resource_ids, token),
138
+ "document": lambda: _build_document_urls(resource_ids, access, format_ext, token),
139
+ }
140
+
141
+ return builders.get(resource_type, lambda: {})()
142
+
143
+
144
+
145
+ class ResourceTokenView(APIView):
146
+ """Generate limited-access tokens for resources."""
147
+
148
+ permission_classes = [IsAuthenticated]
149
+
150
+ @openapi.extend_schema(
151
+ tags=["Auth"],
152
+ operation_id=f"{ENDPOINT_ID}generate_resource_token",
153
+ summary="Generate resource access token",
154
+ description="""
155
+ Generate a short-lived JWT token for accessing specific resources.
156
+
157
+ This endpoint is used to create secure, time-limited access tokens for
158
+ resources like shipment labels, manifests, and document templates.
159
+
160
+ **Use cases:**
161
+ - Generate a token to allow document preview in a new browser window
162
+ - Create shareable links for documents with automatic expiration
163
+ - Enable secure document downloads without exposing API keys
164
+
165
+ **Token lifetime:** Default 5 minutes, configurable up to 1 hour.
166
+ """,
167
+ request=ResourceTokenRequest,
168
+ responses={
169
+ 201: ResourceTokenResponse,
170
+ 400: openapi.OpenApiTypes.OBJECT,
171
+ 401: openapi.OpenApiTypes.OBJECT,
172
+ },
173
+ )
174
+ def post(self, request: Request) -> Response:
175
+ serializer = ResourceTokenRequest(data=request.data)
176
+ serializer.is_valid(raise_exception=True)
177
+
178
+ data = serializer.validated_data
179
+ resource_type = data["resource_type"]
180
+ resource_ids = data["resource_ids"]
181
+ access = data["access"]
182
+ format = data.get("format")
183
+ expires_in = data.get("expires_in", 300)
184
+
185
+ # Get org_id for multi-tenant environments
186
+ org_id = None
187
+ if hasattr(request, "org") and request.org:
188
+ org_id = request.org.id
189
+
190
+ # Get test_mode from request context
191
+ test_mode = getattr(request, "test_mode", None)
192
+
193
+ # Generate the token
194
+ token = ResourceAccessToken.for_resource(
195
+ user=request.user,
196
+ resource_type=resource_type,
197
+ resource_ids=resource_ids,
198
+ access=access,
199
+ format=format,
200
+ org_id=org_id,
201
+ test_mode=test_mode,
202
+ expires_in=expires_in,
203
+ )
204
+
205
+ token_string = str(token)
206
+
207
+ # Calculate expiration time
208
+ expires_at = datetime.fromtimestamp(token["exp"], tz=timezone.utc)
209
+
210
+ # Build resource URLs
211
+ resource_urls = build_resource_urls(
212
+ resource_type=resource_type,
213
+ resource_ids=resource_ids,
214
+ access=access,
215
+ format=format,
216
+ token=token_string,
217
+ )
218
+
219
+ response = Response(
220
+ {
221
+ "token": token_string,
222
+ "expires_at": expires_at.isoformat(),
223
+ "resource_urls": resource_urls,
224
+ },
225
+ status=status.HTTP_201_CREATED,
226
+ )
227
+
228
+ # Prevent caching of tokens
229
+ response["Cache-Control"] = "no-store"
230
+ response["CDN-Cache-Control"] = "no-store"
231
+
232
+ return response
233
+
234
+
235
+ urlpatterns = [
236
+ path("api/tokens", ResourceTokenView.as_view(), name="resource-tokens"),
237
+ ]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: karrio_server
3
- Version: 2025.5rc36
3
+ Version: 2025.5.2
4
4
  Summary: Multi-carrier shipping API
5
5
  Author-email: karrio <hello@karrio.io>
6
6
  License-Expression: Apache-2.0
@@ -1,4 +1,4 @@
1
- karrio/server/VERSION,sha256=Qj4LbCG5JrZWZYr6ZKaSeFnW7FJKakK6UeX5NouGaCI,10
1
+ karrio/server/VERSION,sha256=8L9wkqLEVONURWyo_epXbN2o-JqokXuSi6umO6-stkc,8
2
2
  karrio/server/__init__.py,sha256=iOEMwnlORWezdO8-2vxBIPSR37D7JGjluZ8f55vzxls,81
3
3
  karrio/server/__main__.py,sha256=hy2-Zb2wSVe_Pu6zWZ-BhiM4rBaGZ3D16HSuhudygqg,632
4
4
  karrio/server/asgi.py,sha256=LsZYMWo8U9zURVPdHnvUsziOhMjdCdQoD2-gMJbS2U0,462
@@ -6,7 +6,7 @@ karrio/server/workers.py,sha256=wOlWmXC7zRJV86IfbQnfUZsnCIvvWtXzFHH_EIkg1J0,179
6
6
  karrio/server/wsgi.py,sha256=SpWqkEYlMsj89_znZ8p8IjH3EgTVRWRq_9eS8t64dMw,403
7
7
  karrio/server/lib/otel_huey.py,sha256=6MP6vX6b6x6RPF2K1m8B8L8S9GK1Q3vANmLidsxh65k,5428
8
8
  karrio/server/settings/__init__.py,sha256=iw-NBcReOnDYpnvSEBdYDfV7jC0040jYdupnmSdElec,866
9
- karrio/server/settings/apm.py,sha256=229sQB2TeE00fSAZHjzVmkttggOjlxTcIDoLTmSjFYY,7705
9
+ karrio/server/settings/apm.py,sha256=s_KEiOrhoh3RIONIAFlDzrr1RKnRSO0PL98T7M1JElw,19106
10
10
  karrio/server/settings/base.py,sha256=ZD0bd949IpHdFkI5WSuQ2W8QIoRZx11DHwdVwUqVlMM,22931
11
11
  karrio/server/settings/cache.py,sha256=0o6XLXjtRw3MU2iXQh7Ge9RDXyekIQcXy3Fp54in4Zo,3923
12
12
  karrio/server/settings/constance.py,sha256=IaSk5XmJzFBkvN2XzkZBaLcD97QDF1oO7231y58xoHY,7783
@@ -72,10 +72,11 @@ karrio/server/static/karrio/js/karrio.js,sha256=wuM0bdyAsFGMPPZjrbP6EDChdxLEK-nE
72
72
  karrio/server/static/karrio/js/karrio.js.map,sha256=dlVHo5i-7vPIMdNXuiwg51WsamHsksoHJa8iWKkfWrs,328313
73
73
  karrio/server/templates/admin/base_site.html,sha256=kbcdvehXZ1EHaw07JL7fSZmjrnVMKsnydjbTKa7Ondg,466
74
74
  karrio/server/templates/openapi/openapi.html,sha256=3ApCZ5pE6Wjv7CJllVbqD2WiDQuLy-BFS-IIHurXhBY,1133
75
- karrio/server/urls/__init__.py,sha256=Ah-XqaqRsfecQgCGRHjxmXe8O7a0avq5ocU90tkVwQI,1998
75
+ karrio/server/urls/__init__.py,sha256=SHmmXmVn9Hbf7eZ2wcL10hG84yQPBrcVaCsvi5cdrrA,2062
76
76
  karrio/server/urls/jwt.py,sha256=QN2L-EpUEQCF2UGYPu_VVlA49Fc0BtcY7Ov3-xpp7_U,6772
77
- karrio_server-2025.5rc36.dist-info/METADATA,sha256=u4YI4lpCnJxN1wI-YwdciZMUhC7RDDHikx6d0A6L8FU,4305
78
- karrio_server-2025.5rc36.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
79
- karrio_server-2025.5rc36.dist-info/entry_points.txt,sha256=c2eftt6MpJjyp0OFv1OmO9nUYSDemt9fGq_RDdvpGLw,55
80
- karrio_server-2025.5rc36.dist-info/top_level.txt,sha256=D1D7x8R3cTfjF_15mfiO7wCQ5QMtuM4x8GaPr7z5i78,12
81
- karrio_server-2025.5rc36.dist-info/RECORD,,
77
+ karrio/server/urls/tokens.py,sha256=5slk9F0HPgV3dRnR9UipAl-R8837eiGqKJ31QnCdLnU,8215
78
+ karrio_server-2025.5.2.dist-info/METADATA,sha256=aaA0696VOtDYR1RiCOi-99H1utZCDyH8KueFg5RC_2E,4303
79
+ karrio_server-2025.5.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
80
+ karrio_server-2025.5.2.dist-info/entry_points.txt,sha256=c2eftt6MpJjyp0OFv1OmO9nUYSDemt9fGq_RDdvpGLw,55
81
+ karrio_server-2025.5.2.dist-info/top_level.txt,sha256=D1D7x8R3cTfjF_15mfiO7wCQ5QMtuM4x8GaPr7z5i78,12
82
+ karrio_server-2025.5.2.dist-info/RECORD,,