karrio-server 2025.5rc36__py3-none-any.whl → 2025.5.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/VERSION CHANGED
@@ -1 +1 @@
1
- 2025.5rc36
1
+ 2025.5.1
@@ -35,33 +35,192 @@ 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
+ SENTRY_TRACES_SAMPLE_RATE = config("SENTRY_TRACES_SAMPLE_RATE", default=1.0, cast=float)
68
+ SENTRY_PROFILES_SAMPLE_RATE = config("SENTRY_PROFILES_SAMPLE_RATE", default=1.0, cast=float)
69
+ SENTRY_SEND_PII = config("SENTRY_SEND_PII", default=True, cast=bool)
70
+ SENTRY_DEBUG = config("SENTRY_DEBUG", default=False, cast=bool)
71
+
72
+
73
+ def _sentry_before_send(event, hint):
74
+ """Pre-process events before sending to Sentry.
75
+
76
+ This hook allows us to:
77
+ - Scrub sensitive data (API keys, tokens, passwords)
78
+ - Add custom tags
79
+ - Filter out certain events
80
+ """
81
+ # Scrub sensitive data from request bodies
82
+ if "request" in event:
83
+ request_data = event["request"]
84
+
85
+ # Scrub headers
86
+ if "headers" in request_data:
87
+ sensitive_headers = ["authorization", "x-api-key", "cookie", "x-csrf-token"]
88
+ for header in sensitive_headers:
89
+ if header in request_data["headers"]:
90
+ request_data["headers"][header] = "[Filtered]"
91
+
92
+ # Scrub POST data
93
+ if "data" in request_data and isinstance(request_data["data"], dict):
94
+ sensitive_fields = [
95
+ "password", "secret", "token", "api_key", "apikey",
96
+ "access_token", "refresh_token", "client_secret",
97
+ "account_number", "meter_number", "license_key",
98
+ ]
99
+ for field in sensitive_fields:
100
+ for key in list(request_data["data"].keys()):
101
+ if field.lower() in key.lower():
102
+ request_data["data"][key] = "[Filtered]"
103
+
104
+ return event
105
+
106
+
107
+ def _sentry_before_send_transaction(event, hint):
108
+ """Pre-process transactions before sending to Sentry.
109
+
110
+ This hook allows us to:
111
+ - Filter out noisy transactions (health checks, static files)
112
+ - Add custom tags
113
+ """
114
+ transaction_name = event.get("transaction", "")
115
+
116
+ # Filter out health check and monitoring endpoints
117
+ noisy_endpoints = [
118
+ "/health",
119
+ "/ready",
120
+ "/live",
121
+ "/_health",
122
+ "/favicon.ico",
123
+ "/static/",
124
+ "/robots.txt",
125
+ ]
126
+
127
+ for endpoint in noisy_endpoints:
128
+ if transaction_name.startswith(endpoint):
129
+ return None # Drop this transaction
130
+
131
+ return event
132
+
41
133
 
42
134
  if SENTRY_DSN:
43
135
  # Build integrations list
44
- integrations = [DjangoIntegration()]
136
+ integrations = [
137
+ DjangoIntegration(
138
+ transaction_style="url", # Use URL patterns for transaction names
139
+ middleware_spans=True, # Create spans for middleware
140
+ signals_spans=True, # Create spans for Django signals
141
+ ),
142
+ ]
143
+
144
+ # Add PostHog integration if available
45
145
  if POSTHOG_KEY and PostHogIntegration is not None:
46
146
  integrations.append(PostHogIntegration())
47
147
 
148
+ # Try to add Redis integration if Redis is configured
149
+ try:
150
+ from sentry_sdk.integrations.redis import RedisIntegration
151
+ if config("REDIS_URL", default=None) or config("REDIS_HOST", default=None):
152
+ integrations.append(RedisIntegration())
153
+ except ImportError:
154
+ pass
155
+
156
+ # Try to add Huey integration for background tasks
157
+ try:
158
+ from sentry_sdk.integrations.huey import HueyIntegration
159
+ integrations.append(HueyIntegration())
160
+ except ImportError:
161
+ pass
162
+
163
+ # Try to add httpx integration for async HTTP clients
164
+ try:
165
+ from sentry_sdk.integrations.httpx import HttpxIntegration
166
+ integrations.append(HttpxIntegration())
167
+ except Exception:
168
+ pass # httpx may not be installed
169
+
170
+ # Try to add Strawberry GraphQL integration
171
+ try:
172
+ from sentry_sdk.integrations.strawberry import StrawberryIntegration
173
+ integrations.append(StrawberryIntegration(async_execution=False))
174
+ except Exception:
175
+ pass # strawberry integration may not be available
176
+
48
177
  sentry_sdk.init(
49
178
  dsn=SENTRY_DSN,
50
179
  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
180
+
181
+ # Environment and release tracking
182
+ environment=SENTRY_ENVIRONMENT,
183
+ release=SENTRY_RELEASE,
184
+
185
+ # Performance monitoring
186
+ traces_sample_rate=SENTRY_TRACES_SAMPLE_RATE,
187
+ profile_session_sample_rate=SENTRY_PROFILES_SAMPLE_RATE,
59
188
  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
189
+
190
+ # Privacy settings
191
+ send_default_pii=SENTRY_SEND_PII,
192
+
193
+ # Logging integration
64
194
  enable_logs=True,
195
+
196
+ # Debug mode
197
+ debug=SENTRY_DEBUG,
198
+
199
+ # Event processing hooks
200
+ before_send=_sentry_before_send,
201
+ before_send_transaction=_sentry_before_send_transaction,
202
+
203
+ # Additional options
204
+ max_breadcrumbs=50, # Keep last 50 breadcrumbs for context
205
+ attach_stacktrace=True, # Attach stack traces to messages
206
+ include_source_context=True, # Include source code in stack traces
207
+ include_local_variables=True, # Include local variables in stack traces
208
+
209
+ # Set custom tags
210
+ _experiments={
211
+ "record_sql_params": True, # Record SQL query parameters
212
+ },
213
+ )
214
+
215
+ # Set default tags that will be applied to all events
216
+ sentry_sdk.set_tag("service", "karrio-api")
217
+ sentry_sdk.set_tag("framework", "django")
218
+
219
+ import logging
220
+ logger = logging.getLogger(__name__)
221
+ logger.info(
222
+ f"Sentry initialized: env={SENTRY_ENVIRONMENT}, "
223
+ f"traces_sample_rate={SENTRY_TRACES_SAMPLE_RATE}"
65
224
  )
66
225
 
67
226
 
@@ -191,3 +350,141 @@ if OTEL_ENABLED and OTEL_EXPORTER_OTLP_ENDPOINT:
191
350
  # Log that OpenTelemetry is enabled
192
351
  logger = logging.getLogger(__name__)
193
352
  logger.info(f"OpenTelemetry enabled: Service={OTEL_SERVICE_NAME}, Endpoint={OTEL_EXPORTER_OTLP_ENDPOINT}")
353
+
354
+
355
+ # =============================================================================
356
+ # Datadog Configuration
357
+ # =============================================================================
358
+ #
359
+ # Datadog provides full-stack observability including APM, infrastructure
360
+ # monitoring, logs, and more. When DD_TRACE_ENABLED is set, the following
361
+ # features are enabled:
362
+ #
363
+ # 1. APM Tracing: Distributed tracing for all requests and background tasks
364
+ # 2. Metrics: Custom metrics via DogStatsD
365
+ # 3. Logs: Automatic trace ID injection into logs
366
+ # 4. Profiling: Continuous profiling (optional)
367
+ #
368
+ # Environment Variables:
369
+ # DD_TRACE_ENABLED - Enable Datadog tracing (default: false)
370
+ # DD_SERVICE - Service name (default: karrio-api)
371
+ # DD_ENV - Environment name (default: from ENV or "production")
372
+ # DD_VERSION - Application version (default: VERSION)
373
+ # DD_AGENT_HOST - Datadog agent host (default: localhost)
374
+ # DD_TRACE_AGENT_PORT - Datadog agent port (default: 8126)
375
+ # DD_DOGSTATSD_PORT - DogStatsD port for metrics (default: 8125)
376
+ # DD_TRACE_SAMPLE_RATE - Sampling rate 0.0-1.0 (default: 1.0)
377
+ # DD_PROFILING_ENABLED - Enable continuous profiling (default: false)
378
+ # DD_LOGS_INJECTION - Inject trace IDs into logs (default: true)
379
+ #
380
+ # =============================================================================
381
+
382
+ DD_TRACE_ENABLED = config("DD_TRACE_ENABLED", default=False, cast=bool)
383
+ DATADOG_ENABLED = config("DATADOG_ENABLED", default=False, cast=bool) # Alias
384
+
385
+ # Use either DD_TRACE_ENABLED or DATADOG_ENABLED
386
+ _datadog_enabled = DD_TRACE_ENABLED or DATADOG_ENABLED
387
+
388
+ if _datadog_enabled:
389
+ # Datadog configuration
390
+ DD_SERVICE = config("DD_SERVICE", default="karrio-api")
391
+ DD_ENV = config("DD_ENV", default=config("ENV", default="production"))
392
+ DD_VERSION = config("DD_VERSION", default=config("VERSION", default="unknown"))
393
+ DD_AGENT_HOST = config("DD_AGENT_HOST", default="localhost")
394
+ DD_TRACE_AGENT_PORT = config("DD_TRACE_AGENT_PORT", default=8126, cast=int)
395
+ DD_DOGSTATSD_PORT = config("DD_DOGSTATSD_PORT", default=8125, cast=int)
396
+ DD_TRACE_SAMPLE_RATE = config("DD_TRACE_SAMPLE_RATE", default=1.0, cast=float)
397
+ DD_PROFILING_ENABLED = config("DD_PROFILING_ENABLED", default=False, cast=bool)
398
+ DD_LOGS_INJECTION = config("DD_LOGS_INJECTION", default=True, cast=bool)
399
+
400
+ try:
401
+ import ddtrace
402
+ from ddtrace import config as dd_config, tracer, patch_all
403
+
404
+ # Configure tracer
405
+ ddtrace.config.service = DD_SERVICE
406
+ ddtrace.config.env = DD_ENV
407
+ ddtrace.config.version = DD_VERSION
408
+
409
+ # Configure Django integration
410
+ dd_config.django["service_name"] = DD_SERVICE
411
+ dd_config.django["cache_service_name"] = f"{DD_SERVICE}-cache"
412
+ dd_config.django["database_service_name"] = f"{DD_SERVICE}-db"
413
+ dd_config.django["trace_query_string"] = True
414
+ dd_config.django["analytics_enabled"] = True
415
+
416
+ # Configure trace sampling
417
+ tracer.configure(
418
+ hostname=DD_AGENT_HOST,
419
+ port=DD_TRACE_AGENT_PORT,
420
+ )
421
+
422
+ # Set global sample rate
423
+ from ddtrace.sampler import DatadogSampler
424
+ tracer.configure(sampler=DatadogSampler(default_sample_rate=DD_TRACE_SAMPLE_RATE))
425
+
426
+ # Enable log injection
427
+ if DD_LOGS_INJECTION:
428
+ ddtrace.patch(logging=True)
429
+
430
+ # Patch all supported libraries
431
+ patch_all(
432
+ django=True,
433
+ redis=True,
434
+ psycopg=True,
435
+ requests=True,
436
+ httpx=True,
437
+ logging=DD_LOGS_INJECTION,
438
+ )
439
+
440
+ # Patch Huey for background task tracing
441
+ try:
442
+ from ddtrace import patch
443
+ patch(huey=True)
444
+ except Exception:
445
+ pass # Huey integration may not be available in all ddtrace versions
446
+
447
+ # Enable profiling if configured
448
+ if DD_PROFILING_ENABLED:
449
+ try:
450
+ import ddtrace.profiling.auto # noqa: F401
451
+ except ImportError:
452
+ pass
453
+
454
+ # Configure DogStatsD for metrics
455
+ try:
456
+ from datadog import initialize, statsd
457
+
458
+ initialize(
459
+ statsd_host=DD_AGENT_HOST,
460
+ statsd_port=DD_DOGSTATSD_PORT,
461
+ )
462
+
463
+ # Set default tags for all metrics
464
+ statsd.constant_tags = [
465
+ f"service:{DD_SERVICE}",
466
+ f"env:{DD_ENV}",
467
+ f"version:{DD_VERSION}",
468
+ ]
469
+
470
+ except ImportError:
471
+ pass # datadog package not installed, metrics won't work
472
+
473
+ import logging
474
+ logger = logging.getLogger(__name__)
475
+ logger.info(
476
+ f"Datadog APM initialized: service={DD_SERVICE}, env={DD_ENV}, "
477
+ f"agent={DD_AGENT_HOST}:{DD_TRACE_AGENT_PORT}"
478
+ )
479
+
480
+ except ImportError:
481
+ import logging
482
+ logger = logging.getLogger(__name__)
483
+ logger.warning(
484
+ "Datadog tracing enabled but ddtrace package not installed. "
485
+ "Install with: pip install ddtrace"
486
+ )
487
+ except Exception as e:
488
+ import logging
489
+ logger = logging.getLogger(__name__)
490
+ 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.1
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=tGn2vY99q8a82QkInZS50BCOgMKISav_zxLuzLB71_A,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=mcQqchavxkbDTWYqUwaiJLSd8JD3cWx5c-Nr8iOeFN8,18916
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.1.dist-info/METADATA,sha256=vzpN8lPECnPqc16li5kgjxg1CYnZDcJdCXwBkANc1jg,4303
79
+ karrio_server-2025.5.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
80
+ karrio_server-2025.5.1.dist-info/entry_points.txt,sha256=c2eftt6MpJjyp0OFv1OmO9nUYSDemt9fGq_RDdvpGLw,55
81
+ karrio_server-2025.5.1.dist-info/top_level.txt,sha256=D1D7x8R3cTfjF_15mfiO7wCQ5QMtuM4x8GaPr7z5i78,12
82
+ karrio_server-2025.5.1.dist-info/RECORD,,