karrio-server-core 2025.5__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/conf.py +54 -0
- karrio/server/core/__init__.py +3 -0
- karrio/server/core/admin.py +1 -0
- karrio/server/core/apps.py +10 -0
- karrio/server/core/authentication.py +347 -0
- karrio/server/core/config.py +31 -0
- karrio/server/core/context_processors.py +12 -0
- karrio/server/core/datatypes.py +394 -0
- karrio/server/core/dataunits.py +187 -0
- karrio/server/core/exceptions.py +404 -0
- karrio/server/core/fields.py +12 -0
- karrio/server/core/filters.py +837 -0
- karrio/server/core/gateway.py +1011 -0
- karrio/server/core/logging.py +403 -0
- karrio/server/core/management/commands/cli.py +19 -0
- karrio/server/core/management/commands/create_oauth_client.py +41 -0
- karrio/server/core/management/commands/runserver.py +5 -0
- karrio/server/core/middleware.py +197 -0
- karrio/server/core/migrations/0001_initial.py +28 -0
- karrio/server/core/migrations/0002_apilogindex.py +69 -0
- karrio/server/core/migrations/0003_apilogindex_test_mode.py +62 -0
- karrio/server/core/migrations/0004_metafield.py +74 -0
- karrio/server/core/migrations/0005_alter_metafield_type_alter_metafield_value.py +23 -0
- karrio/server/core/migrations/0006_add_api_log_requested_at_index.py +22 -0
- karrio/server/core/migrations/__init__.py +0 -0
- karrio/server/core/models/__init__.py +48 -0
- karrio/server/core/models/base.py +103 -0
- karrio/server/core/models/entity.py +24 -0
- karrio/server/core/models/metafield.py +144 -0
- karrio/server/core/models/third_party.py +21 -0
- karrio/server/core/oauth_validators.py +170 -0
- karrio/server/core/permissions.py +36 -0
- karrio/server/core/renderers.py +11 -0
- karrio/server/core/router.py +3 -0
- karrio/server/core/serializers.py +1971 -0
- karrio/server/core/signals.py +55 -0
- karrio/server/core/telemetry.py +573 -0
- karrio/server/core/tests.py +99 -0
- karrio/server/core/tests_resource_token.py +411 -0
- karrio/server/core/urls.py +12 -0
- karrio/server/core/utils.py +1025 -0
- karrio/server/core/validators.py +264 -0
- karrio/server/core/views/__init__.py +2 -0
- karrio/server/core/views/api.py +133 -0
- karrio/server/core/views/metadata.py +44 -0
- karrio/server/core/views/oauth.py +75 -0
- karrio/server/core/views/references.py +82 -0
- karrio/server/core/views/schema.py +310 -0
- karrio/server/filters/__init__.py +2 -0
- karrio/server/filters/abstract.py +26 -0
- karrio/server/iam/__init__.py +0 -0
- karrio/server/iam/admin.py +3 -0
- karrio/server/iam/apps.py +21 -0
- karrio/server/iam/migrations/0001_initial.py +33 -0
- karrio/server/iam/migrations/__init__.py +0 -0
- karrio/server/iam/models.py +48 -0
- karrio/server/iam/permissions.py +155 -0
- karrio/server/iam/serializers.py +54 -0
- karrio/server/iam/signals.py +18 -0
- karrio/server/iam/tests.py +3 -0
- karrio/server/iam/views.py +3 -0
- karrio/server/openapi.py +75 -0
- karrio/server/providers/__init__.py +1 -0
- karrio/server/providers/admin.py +364 -0
- karrio/server/providers/apps.py +10 -0
- karrio/server/providers/management/commands/migrate_rate_sheets.py +101 -0
- karrio/server/providers/migrations/0001_initial.py +140 -0
- karrio/server/providers/migrations/0002_carrier_active.py +18 -0
- karrio/server/providers/migrations/0003_auto_20201230_0820.py +24 -0
- karrio/server/providers/migrations/0004_auto_20210212_0554.py +178 -0
- karrio/server/providers/migrations/0005_auto_20210212_0555.py +18 -0
- karrio/server/providers/migrations/0006_australiapostsettings.py +29 -0
- karrio/server/providers/migrations/0007_auto_20210213_0206.py +21 -0
- karrio/server/providers/migrations/0008_auto_20210214_0409.py +30 -0
- karrio/server/providers/migrations/0009_auto_20210308_0302.py +18 -0
- karrio/server/providers/migrations/0010_auto_20210409_0852.py +32 -0
- karrio/server/providers/migrations/0011_auto_20210409_0853.py +21 -0
- karrio/server/providers/migrations/0012_alter_carrier_options.py +17 -0
- karrio/server/providers/migrations/0013_tntsettings.py +30 -0
- karrio/server/providers/migrations/0014_auto_20210612_1608.py +46 -0
- karrio/server/providers/migrations/0015_auto_20210615_1601.py +28 -0
- karrio/server/providers/migrations/0016_alter_purolatorsettings_user_token.py +18 -0
- karrio/server/providers/migrations/0017_auto_20210805_0359.py +1293 -0
- karrio/server/providers/migrations/0018_alter_fedexsettings_user_key.py +18 -0
- karrio/server/providers/migrations/0019_dhlpolandsettings_servicelevel.py +65 -0
- karrio/server/providers/migrations/0020_genericsettings_labeltemplate.py +52 -0
- karrio/server/providers/migrations/0021_auto_20211231_2353.py +40 -0
- karrio/server/providers/migrations/0022_carrier_metadata.py +18 -0
- karrio/server/providers/migrations/0023_auto_20220124_1916.py +27 -0
- karrio/server/providers/migrations/0024_alter_genericsettings_custom_carrier_name.py +19 -0
- karrio/server/providers/migrations/0025_alter_servicelevel_service_code.py +19 -0
- karrio/server/providers/migrations/0026_auto_20220208_0132.py +59 -0
- karrio/server/providers/migrations/0027_auto_20220304_1340.py +29 -0
- karrio/server/providers/migrations/0028_auto_20220323_1500.py +33 -0
- karrio/server/providers/migrations/0029_easypostsettings.py +27 -0
- karrio/server/providers/migrations/0030_amazonmwssettings.py +29 -0
- karrio/server/providers/migrations/0031_delete_amazonmwssettings.py +18 -0
- karrio/server/providers/migrations/0032_alter_carrier_test.py +18 -0
- karrio/server/providers/migrations/0033_auto_20220708_1350.py +22 -0
- karrio/server/providers/migrations/0034_amazonmwssettings_dpdhlsettings.py +47 -0
- karrio/server/providers/migrations/0035_alter_carrier_capabilities.py +43 -0
- karrio/server/providers/migrations/0036_upsfreightsettings.py +31 -0
- karrio/server/providers/migrations/0037_chronopostsettings.py +29 -0
- karrio/server/providers/migrations/0038_alter_genericsettings_label_template.py +19 -0
- karrio/server/providers/migrations/0039_auto_20220906_0612.py +23 -0
- karrio/server/providers/migrations/0040_dpdhlsettings_services.py +18 -0
- karrio/server/providers/migrations/0041_auto_20221105_0705.py +38 -0
- karrio/server/providers/migrations/0042_auto_20221215_1642.py +23 -0
- karrio/server/providers/migrations/0043_alter_genericsettings_account_number_and_more.py +39 -0
- karrio/server/providers/migrations/0044_carrier_carrier_capabilities.py +64 -0
- karrio/server/providers/migrations/0045_alter_carrier_active_alter_carrier_carrier_id.py +31 -0
- karrio/server/providers/migrations/0046_remove_dpdhlsettings_signature_and_more.py +41 -0
- karrio/server/providers/migrations/0047_dpdsettings.py +286 -0
- karrio/server/providers/migrations/0048_servicelevel_min_weight_servicelevel_transit_days_and_more.py +64 -0
- karrio/server/providers/migrations/0049_boxknightsettings_geodissettings_lapostesettings_and_more.py +156 -0
- karrio/server/providers/migrations/0050_carrier_is_system_alter_carrier_metadata_and_more.py +106 -0
- karrio/server/providers/migrations/0051_rename_username_upssettings_client_id_and_more.py +31 -0
- karrio/server/providers/migrations/0052_alter_upssettings_account_number_and_more.py +20 -0
- karrio/server/providers/migrations/0053_locate2usettings.py +281 -0
- karrio/server/providers/migrations/0054_zoom2usettings.py +280 -0
- karrio/server/providers/migrations/0055_rename_amazonmwssettings_amazonshippingsettings_and_more.py +44 -0
- karrio/server/providers/migrations/0056_asendiaussettings_geodissettings_code_client_and_more.py +75 -0
- karrio/server/providers/migrations/0057_alter_servicelevel_weight_unit_belgianpostsettings.py +51 -0
- karrio/server/providers/migrations/0058_alliedexpresssettings.py +38 -0
- karrio/server/providers/migrations/0059_ratesheet.py +81 -0
- karrio/server/providers/migrations/0060_belgianpostsettings_rate_sheet_and_more.py +73 -0
- karrio/server/providers/migrations/0061_alliedexpresssettings_service_type.py +17 -0
- karrio/server/providers/migrations/0062_sendlesettings_account_country_code.py +257 -0
- karrio/server/providers/migrations/0063_servicelevel_metadata.py +25 -0
- karrio/server/providers/migrations/0064_alliedexpresslocalsettings.py +43 -0
- karrio/server/providers/migrations/0065_servicelevel_carrier_service_code_and_more.py +66 -0
- karrio/server/providers/migrations/0066_rename_fedexsettings_fedexwssettings_and_more.py +28 -0
- karrio/server/providers/migrations/0067_fedexsettings.py +283 -0
- karrio/server/providers/migrations/0068_fedexsettings_track_api_key_and_more.py +38 -0
- karrio/server/providers/migrations/0069_alter_canadapostsettings_contract_id_and_more.py +23 -0
- karrio/server/providers/migrations/0070_tgesettings_alter_carrier_capabilities.py +65 -0
- karrio/server/providers/migrations/0071_alter_tgesettings_my_toll_token.py +18 -0
- karrio/server/providers/migrations/0072_rename_eshippersettings_eshipperxmlsettings_and_more.py +28 -0
- karrio/server/providers/migrations/0073_delete_eshipperxmlsettings.py +41 -0
- karrio/server/providers/migrations/0074_eshippersettings.py +38 -0
- karrio/server/providers/migrations/0075_haypostsettings.py +40 -0
- karrio/server/providers/migrations/0076_rename_customer_registration_id_uspsinternationalsettings_account_number_and_more.py +125 -0
- karrio/server/providers/migrations/0077_uspswtinternationalsettings_uspswtsettings_and_more.py +165 -0
- karrio/server/providers/migrations/0078_auto_20240813_1552.py +120 -0
- karrio/server/providers/migrations/0079_alter_carrier_options_alter_ratesheet_created_by.py +31 -0
- karrio/server/providers/migrations/0080_alter_aramexsettings_account_country_code_and_more.py +3025 -0
- karrio/server/providers/migrations/0081_remove_alliedexpresssettings_carrier_ptr_and_more.py +338 -0
- karrio/server/providers/migrations/0082_add_zone_identifiers.py +50 -0
- karrio/server/providers/migrations/0083_add_optimized_rate_sheet_structure.py +33 -0
- karrio/server/providers/migrations/0084_alter_servicelevel_currency.py +168 -0
- 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/__init__.py +0 -0
- karrio/server/providers/models/__init__.py +16 -0
- karrio/server/providers/models/carrier.py +387 -0
- karrio/server/providers/models/config.py +30 -0
- karrio/server/providers/models/service.py +192 -0
- karrio/server/providers/models/sheet.py +287 -0
- karrio/server/providers/models/template.py +39 -0
- karrio/server/providers/models/utils.py +58 -0
- karrio/server/providers/router.py +3 -0
- karrio/server/providers/serializers/__init__.py +3 -0
- karrio/server/providers/serializers/base.py +538 -0
- karrio/server/providers/signals.py +25 -0
- 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/urls.py +11 -0
- karrio/server/providers/views/__init__.py +0 -0
- karrio/server/providers/views/carriers.py +267 -0
- karrio/server/providers/views/connections.py +496 -0
- karrio/server/samples.py +352 -0
- karrio/server/serializers/__init__.py +2 -0
- karrio/server/serializers/abstract.py +602 -0
- karrio/server/tracing/__init__.py +0 -0
- karrio/server/tracing/admin.py +63 -0
- karrio/server/tracing/apps.py +8 -0
- karrio/server/tracing/migrations/0001_initial.py +41 -0
- karrio/server/tracing/migrations/0002_auto_20220710_1307.py +22 -0
- karrio/server/tracing/migrations/0003_auto_20221105_0317.py +43 -0
- karrio/server/tracing/migrations/0004_tracingrecord_carrier_account_idx.py +24 -0
- karrio/server/tracing/migrations/0005_optimise_tracingrecord_request_log_idx.py +25 -0
- karrio/server/tracing/migrations/0006_alter_tracingrecord_options_and_more.py +49 -0
- karrio/server/tracing/migrations/0007_tracingrecord_tracing_created_at_idx.py +19 -0
- karrio/server/tracing/migrations/__init__.py +0 -0
- karrio/server/tracing/models.py +82 -0
- karrio/server/tracing/tests.py +3 -0
- karrio/server/tracing/utils.py +109 -0
- karrio/server/user/__init__.py +0 -0
- karrio/server/user/admin.py +96 -0
- karrio/server/user/apps.py +7 -0
- karrio/server/user/forms.py +35 -0
- karrio/server/user/migrations/0001_initial.py +41 -0
- karrio/server/user/migrations/0002_token.py +29 -0
- karrio/server/user/migrations/0003_token_test_mode.py +20 -0
- karrio/server/user/migrations/0004_group.py +26 -0
- karrio/server/user/migrations/0005_token_label.py +21 -0
- karrio/server/user/migrations/0006_workspaceconfig.py +63 -0
- karrio/server/user/migrations/0007_user_metadata.py +25 -0
- karrio/server/user/migrations/__init__.py +0 -0
- karrio/server/user/models.py +218 -0
- karrio/server/user/serializers.py +47 -0
- karrio/server/user/templates/registration/login.html +108 -0
- karrio/server/user/templates/registration/registration_confirm_email.html +10 -0
- karrio/server/user/templates/registration/registration_confirm_email.txt +3 -0
- karrio/server/user/tests.py +3 -0
- karrio/server/user/urls.py +10 -0
- karrio/server/user/utils.py +60 -0
- karrio/server/user/views.py +9 -0
- karrio_server_core-2025.5.dist-info/METADATA +32 -0
- karrio_server_core-2025.5.dist-info/RECORD +213 -0
- karrio_server_core-2025.5.dist-info/WHEEL +5 -0
- karrio_server_core-2025.5.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Django-integrated Loguru logging configuration for Karrio Server.
|
|
3
|
+
|
|
4
|
+
This module provides seamless integration between Django's logging system
|
|
5
|
+
and Loguru, allowing you to use Loguru's powerful features while maintaining
|
|
6
|
+
compatibility with Django's ecosystem.
|
|
7
|
+
|
|
8
|
+
Usage in Django settings:
|
|
9
|
+
# In settings/base.py, after LOGGING configuration
|
|
10
|
+
from karrio.server.core.logging import setup_django_loguru
|
|
11
|
+
setup_django_loguru()
|
|
12
|
+
|
|
13
|
+
Usage in code:
|
|
14
|
+
from karrio.server.core.logging import logger
|
|
15
|
+
|
|
16
|
+
logger.info("User logged in", user_id=user.id)
|
|
17
|
+
logger.error("Payment failed", error=str(e), order_id=order.id)
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import os
|
|
21
|
+
import sys
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from loguru import logger as _logger
|
|
24
|
+
from typing import Optional
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# Remove default handler
|
|
28
|
+
_logger.remove()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class DjangoLoguruHandler:
|
|
32
|
+
"""
|
|
33
|
+
Custom handler that integrates Loguru with Django's logging system.
|
|
34
|
+
Preserves Django's context and request information.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(self):
|
|
38
|
+
self.logger = _logger
|
|
39
|
+
|
|
40
|
+
def write(self, message):
|
|
41
|
+
"""Write method for Django compatibility."""
|
|
42
|
+
self.logger.opt(depth=6, colors=True).info(message)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def get_django_log_config():
|
|
46
|
+
"""
|
|
47
|
+
Get Django-specific log configuration from Django settings.
|
|
48
|
+
Falls back to environment variables if Django settings are not available.
|
|
49
|
+
"""
|
|
50
|
+
try:
|
|
51
|
+
from django.conf import settings
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
"level": getattr(settings, "LOG_LEVEL", "INFO"),
|
|
55
|
+
"log_file": getattr(settings, "LOG_FILE_NAME", None),
|
|
56
|
+
"log_dir": getattr(settings, "LOG_FILE_DIR", None),
|
|
57
|
+
"debug": getattr(settings, "DEBUG", False),
|
|
58
|
+
}
|
|
59
|
+
except Exception:
|
|
60
|
+
# Fallback to environment variables
|
|
61
|
+
return {
|
|
62
|
+
"level": os.getenv("LOG_LEVEL", "INFO"),
|
|
63
|
+
"log_file": os.getenv("LOG_FILE_NAME"),
|
|
64
|
+
"log_dir": os.getenv("LOG_DIR"),
|
|
65
|
+
"debug": os.getenv("DEBUG_MODE", "False").lower() in ("true", "1", "yes"),
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def get_log_format(debug: bool = False) -> str:
|
|
70
|
+
"""Get the log format string appropriate for Django."""
|
|
71
|
+
if debug:
|
|
72
|
+
return (
|
|
73
|
+
"<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | "
|
|
74
|
+
"<level>{level: <8}</level> | "
|
|
75
|
+
"<cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> | "
|
|
76
|
+
"<level>{message}</level> | "
|
|
77
|
+
"{extra}"
|
|
78
|
+
)
|
|
79
|
+
else:
|
|
80
|
+
return (
|
|
81
|
+
"<green>{time:YYYY-MM-DD HH:mm:ss}</green> | "
|
|
82
|
+
"<level>{level: <8}</level> | "
|
|
83
|
+
"<cyan>{name}</cyan> | "
|
|
84
|
+
"<level>{message}</level>"
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _is_sentry_enabled() -> bool:
|
|
89
|
+
"""Check if Sentry is configured and enabled."""
|
|
90
|
+
try:
|
|
91
|
+
from django.conf import settings
|
|
92
|
+
return bool(getattr(settings, "SENTRY_DSN", None))
|
|
93
|
+
except Exception:
|
|
94
|
+
return False
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _sentry_sink(message):
|
|
98
|
+
"""Loguru sink that sends logs to Sentry.
|
|
99
|
+
|
|
100
|
+
- ERROR and CRITICAL level logs are sent as Sentry events
|
|
101
|
+
- WARNING level logs are added as Sentry breadcrumbs
|
|
102
|
+
- INFO and DEBUG logs are ignored (too noisy for Sentry)
|
|
103
|
+
"""
|
|
104
|
+
try:
|
|
105
|
+
import sentry_sdk
|
|
106
|
+
|
|
107
|
+
record = message.record
|
|
108
|
+
level = record["level"].name
|
|
109
|
+
log_message = record["message"]
|
|
110
|
+
|
|
111
|
+
# Build extra context from record
|
|
112
|
+
extra = dict(record.get("extra", {}))
|
|
113
|
+
extra["logger"] = record["name"]
|
|
114
|
+
extra["function"] = record["function"]
|
|
115
|
+
extra["line"] = record["line"]
|
|
116
|
+
extra["file"] = record["file"].name if record["file"] else None
|
|
117
|
+
|
|
118
|
+
if level in ("ERROR", "CRITICAL"):
|
|
119
|
+
# Send as Sentry event
|
|
120
|
+
exception = record.get("exception")
|
|
121
|
+
if exception:
|
|
122
|
+
# If there's an exception, capture it
|
|
123
|
+
exc_type, exc_value, exc_tb = exception.value
|
|
124
|
+
if exc_value:
|
|
125
|
+
with sentry_sdk.push_scope() as scope:
|
|
126
|
+
scope.set_context("loguru", extra)
|
|
127
|
+
scope.set_tag("log_level", level)
|
|
128
|
+
sentry_sdk.capture_exception(exc_value)
|
|
129
|
+
else:
|
|
130
|
+
# No exception, send as message
|
|
131
|
+
with sentry_sdk.push_scope() as scope:
|
|
132
|
+
scope.set_context("loguru", extra)
|
|
133
|
+
scope.set_tag("log_level", level)
|
|
134
|
+
sentry_sdk.capture_message(
|
|
135
|
+
log_message,
|
|
136
|
+
level="error" if level == "ERROR" else "fatal"
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
elif level == "WARNING":
|
|
140
|
+
# Add as breadcrumb for context
|
|
141
|
+
sentry_sdk.add_breadcrumb(
|
|
142
|
+
message=log_message,
|
|
143
|
+
category="loguru",
|
|
144
|
+
level="warning",
|
|
145
|
+
data=extra,
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
except ImportError:
|
|
149
|
+
# Sentry not installed
|
|
150
|
+
pass
|
|
151
|
+
except Exception:
|
|
152
|
+
# Fail silently - don't let logging errors break the app
|
|
153
|
+
pass
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def setup_django_loguru(
|
|
157
|
+
level: Optional[str] = None,
|
|
158
|
+
log_file: Optional[str] = None,
|
|
159
|
+
intercept_django: bool = True,
|
|
160
|
+
serialize: bool = False,
|
|
161
|
+
enqueue: bool = True,
|
|
162
|
+
):
|
|
163
|
+
"""
|
|
164
|
+
Set up Loguru for Django with optimal configuration.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
level: Log level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
|
|
168
|
+
log_file: Path to log file (overrides Django settings)
|
|
169
|
+
intercept_django: Whether to intercept Django's logging (recommended)
|
|
170
|
+
serialize: Whether to serialize logs as JSON
|
|
171
|
+
enqueue: Whether to use async logging (recommended for Django)
|
|
172
|
+
|
|
173
|
+
This function should be called in Django settings after LOGGING configuration.
|
|
174
|
+
"""
|
|
175
|
+
# Remove all existing handlers
|
|
176
|
+
_logger.remove()
|
|
177
|
+
|
|
178
|
+
# Get configuration from Django settings or environment
|
|
179
|
+
config = get_django_log_config()
|
|
180
|
+
log_level = level or config["level"]
|
|
181
|
+
debug_mode = config["debug"]
|
|
182
|
+
log_format = get_log_format(debug_mode)
|
|
183
|
+
|
|
184
|
+
# Add console handler with colors
|
|
185
|
+
_logger.add(
|
|
186
|
+
sys.stderr,
|
|
187
|
+
format=log_format,
|
|
188
|
+
level=log_level,
|
|
189
|
+
colorize=True,
|
|
190
|
+
diagnose=debug_mode,
|
|
191
|
+
backtrace=True,
|
|
192
|
+
enqueue=enqueue,
|
|
193
|
+
serialize=serialize,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
# Add file handler if configured
|
|
197
|
+
log_file_path = log_file or config.get("log_file")
|
|
198
|
+
if log_file_path:
|
|
199
|
+
# Ensure directory exists
|
|
200
|
+
log_dir = Path(log_file_path).parent
|
|
201
|
+
log_dir.mkdir(parents=True, exist_ok=True)
|
|
202
|
+
|
|
203
|
+
_logger.add(
|
|
204
|
+
log_file_path,
|
|
205
|
+
format=log_format,
|
|
206
|
+
level=log_level,
|
|
207
|
+
rotation="500 MB",
|
|
208
|
+
retention="10 days",
|
|
209
|
+
compression="zip",
|
|
210
|
+
diagnose=debug_mode,
|
|
211
|
+
backtrace=True,
|
|
212
|
+
enqueue=enqueue,
|
|
213
|
+
serialize=serialize,
|
|
214
|
+
)
|
|
215
|
+
_logger.info(f"Django file logging enabled: {log_file_path}")
|
|
216
|
+
|
|
217
|
+
# Add Sentry handler if Sentry is configured
|
|
218
|
+
if _is_sentry_enabled():
|
|
219
|
+
_logger.add(
|
|
220
|
+
_sentry_sink,
|
|
221
|
+
level="WARNING", # Only WARNING and above go to Sentry
|
|
222
|
+
format="{message}", # Simple format for Sentry
|
|
223
|
+
enqueue=True, # Async to not block
|
|
224
|
+
backtrace=True,
|
|
225
|
+
diagnose=False, # Don't include verbose diagnostics in Sentry
|
|
226
|
+
)
|
|
227
|
+
_logger.info("Sentry logging handler enabled")
|
|
228
|
+
|
|
229
|
+
# Intercept Django's standard logging
|
|
230
|
+
if intercept_django:
|
|
231
|
+
intercept_standard_logging()
|
|
232
|
+
|
|
233
|
+
# Configure third-party library loggers
|
|
234
|
+
configure_third_party_loggers(debug_mode)
|
|
235
|
+
|
|
236
|
+
_logger.info(f"Loguru configured for Django (level: {log_level})")
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def intercept_standard_logging():
|
|
240
|
+
"""
|
|
241
|
+
Intercept all standard library logging and route it through Loguru.
|
|
242
|
+
|
|
243
|
+
This ensures that Django and all third-party libraries using standard
|
|
244
|
+
logging benefit from Loguru's features and consistent formatting.
|
|
245
|
+
"""
|
|
246
|
+
import logging
|
|
247
|
+
|
|
248
|
+
class InterceptHandler(logging.Handler):
|
|
249
|
+
"""
|
|
250
|
+
Handler that intercepts standard logging and forwards to Loguru.
|
|
251
|
+
"""
|
|
252
|
+
|
|
253
|
+
def emit(self, record: logging.LogRecord) -> None:
|
|
254
|
+
# Get corresponding Loguru level
|
|
255
|
+
try:
|
|
256
|
+
level = _logger.level(record.levelname).name
|
|
257
|
+
except ValueError:
|
|
258
|
+
level = record.levelno
|
|
259
|
+
|
|
260
|
+
# Find caller from where the logged message originated
|
|
261
|
+
frame, depth = sys._getframe(6), 6
|
|
262
|
+
while frame and frame.f_code.co_filename == logging.__file__:
|
|
263
|
+
frame = frame.f_back
|
|
264
|
+
depth += 1
|
|
265
|
+
|
|
266
|
+
# Add extra context from Django if available
|
|
267
|
+
extra = {}
|
|
268
|
+
if hasattr(record, "request"):
|
|
269
|
+
extra["request_id"] = getattr(record.request, "id", None)
|
|
270
|
+
extra["user"] = getattr(record.request, "user", None)
|
|
271
|
+
|
|
272
|
+
_logger.opt(depth=depth, exception=record.exc_info).bind(**extra).log(
|
|
273
|
+
level, record.getMessage()
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
# Configure root logger to use our interceptor
|
|
277
|
+
logging.root.handlers = [InterceptHandler()]
|
|
278
|
+
logging.root.setLevel(0)
|
|
279
|
+
|
|
280
|
+
# Update all existing loggers
|
|
281
|
+
for name in logging.root.manager.loggerDict.keys():
|
|
282
|
+
logging.getLogger(name).handlers = []
|
|
283
|
+
logging.getLogger(name).propagate = True
|
|
284
|
+
|
|
285
|
+
_logger.info("Standard logging interception enabled")
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def configure_third_party_loggers(debug_mode: bool = False):
|
|
289
|
+
"""
|
|
290
|
+
Configure logging levels for third-party libraries to reduce noise.
|
|
291
|
+
|
|
292
|
+
In production, we suppress verbose warnings from libraries that don't provide
|
|
293
|
+
useful context. In development, we keep them at WARNING level for debugging.
|
|
294
|
+
"""
|
|
295
|
+
import logging
|
|
296
|
+
|
|
297
|
+
# Configure jstruct logger
|
|
298
|
+
# Note: jstruct logs "unknown arguments" warnings which are handled more verbosely
|
|
299
|
+
# by our own code in karrio.core.utils.dict.DICTPARSE.to_object
|
|
300
|
+
jstruct_logger = logging.getLogger("jstruct.utils")
|
|
301
|
+
|
|
302
|
+
if debug_mode:
|
|
303
|
+
# In debug mode, let our enhanced logging handle unknown arguments
|
|
304
|
+
# Suppress jstruct's basic warnings to avoid duplicates
|
|
305
|
+
jstruct_logger.setLevel(logging.ERROR)
|
|
306
|
+
else:
|
|
307
|
+
# In production, completely silence jstruct warnings
|
|
308
|
+
# (they're typically not actionable in production)
|
|
309
|
+
jstruct_logger.setLevel(logging.ERROR)
|
|
310
|
+
|
|
311
|
+
# Configure WeasyPrint/CSS parsing loggers to suppress CSS warnings
|
|
312
|
+
# WeasyPrint uses cssutils/tinycss2 which emit verbose CSS parsing warnings
|
|
313
|
+
# These warnings are typically not actionable and clutter the logs
|
|
314
|
+
css_loggers = [
|
|
315
|
+
"weasyprint",
|
|
316
|
+
"weasyprint.css",
|
|
317
|
+
"weasyprint.css.validation",
|
|
318
|
+
"weasyprint.html",
|
|
319
|
+
"cssutils",
|
|
320
|
+
"cssutils.css",
|
|
321
|
+
"tinycss2",
|
|
322
|
+
]
|
|
323
|
+
|
|
324
|
+
for logger_name in css_loggers:
|
|
325
|
+
css_logger = logging.getLogger(logger_name)
|
|
326
|
+
# Suppress CSS parsing warnings - they're typically not useful
|
|
327
|
+
# Only show ERROR level and above
|
|
328
|
+
css_logger.setLevel(logging.ERROR)
|
|
329
|
+
# Disable propagation to prevent warnings from bubbling up to parent loggers
|
|
330
|
+
css_logger.propagate = False
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def get_request_context_logger(request):
|
|
334
|
+
"""
|
|
335
|
+
Get a logger bound with request context for structured logging.
|
|
336
|
+
|
|
337
|
+
Usage in Django views:
|
|
338
|
+
from karrio.server.core.logging import get_request_context_logger
|
|
339
|
+
|
|
340
|
+
def my_view(request):
|
|
341
|
+
logger = get_request_context_logger(request)
|
|
342
|
+
logger.info("Processing request")
|
|
343
|
+
"""
|
|
344
|
+
return _logger.bind(
|
|
345
|
+
request_id=getattr(request, "id", None),
|
|
346
|
+
user_id=getattr(request.user, "id", None) if hasattr(request, "user") else None,
|
|
347
|
+
path=request.path if hasattr(request, "path") else None,
|
|
348
|
+
method=request.method if hasattr(request, "method") else None,
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
# Create middleware for automatic request logging
|
|
353
|
+
class LoguruRequestLoggingMiddleware:
|
|
354
|
+
"""
|
|
355
|
+
Django middleware that adds request/response logging with Loguru.
|
|
356
|
+
|
|
357
|
+
Add to MIDDLEWARE in Django settings:
|
|
358
|
+
'karrio.server.core.logging.LoguruRequestLoggingMiddleware',
|
|
359
|
+
"""
|
|
360
|
+
|
|
361
|
+
def __init__(self, get_response):
|
|
362
|
+
self.get_response = get_response
|
|
363
|
+
|
|
364
|
+
def __call__(self, request):
|
|
365
|
+
# Bind request context
|
|
366
|
+
request_logger = get_request_context_logger(request)
|
|
367
|
+
|
|
368
|
+
# Log request
|
|
369
|
+
request_logger.info(
|
|
370
|
+
f"Request started: {request.method} {request.path}",
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
# Process request
|
|
374
|
+
response = self.get_response(request)
|
|
375
|
+
|
|
376
|
+
# Log response
|
|
377
|
+
request_logger.info(
|
|
378
|
+
f"Request finished: {request.method} {request.path} - Status: {response.status_code}",
|
|
379
|
+
status_code=response.status_code,
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
return response
|
|
383
|
+
|
|
384
|
+
def process_exception(self, request, exception):
|
|
385
|
+
"""Log exceptions with full context."""
|
|
386
|
+
request_logger = get_request_context_logger(request)
|
|
387
|
+
request_logger.exception(
|
|
388
|
+
f"Request exception: {request.method} {request.path}",
|
|
389
|
+
exception_type=type(exception).__name__,
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
# Export the configured logger
|
|
394
|
+
logger = _logger
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
__all__ = [
|
|
398
|
+
"logger",
|
|
399
|
+
"setup_django_loguru",
|
|
400
|
+
"intercept_standard_logging",
|
|
401
|
+
"get_request_context_logger",
|
|
402
|
+
"LoguruRequestLoggingMiddleware",
|
|
403
|
+
]
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from django.core.management.base import BaseCommand
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
class Command(BaseCommand):
|
|
5
|
+
help = "Run kcli commands from the Django CLI."
|
|
6
|
+
|
|
7
|
+
def run_from_argv(self, argv):
|
|
8
|
+
# Remove the management command name ("kcli") from argv
|
|
9
|
+
kcli_args = argv[2:]
|
|
10
|
+
try:
|
|
11
|
+
from kcli.__main__ import app
|
|
12
|
+
# Call the Typer app with the forwarded arguments
|
|
13
|
+
app(prog_name="karrio cli", args=kcli_args)
|
|
14
|
+
except SystemExit as e:
|
|
15
|
+
# Typer uses SystemExit for normal CLI exit, so suppress traceback
|
|
16
|
+
sys.exit(e.code)
|
|
17
|
+
except ImportError:
|
|
18
|
+
self.stderr.write(self.style.ERROR("Could not import kcli CLI app."))
|
|
19
|
+
sys.exit(1)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from django.core.management.base import BaseCommand
|
|
2
|
+
from oauth2_provider.models import Application
|
|
3
|
+
from django.contrib.auth import get_user_model
|
|
4
|
+
|
|
5
|
+
User = get_user_model()
|
|
6
|
+
|
|
7
|
+
class Command(BaseCommand):
|
|
8
|
+
help = 'Creates an OAuth2 client application'
|
|
9
|
+
|
|
10
|
+
def add_arguments(self, parser):
|
|
11
|
+
parser.add_argument('--name', required=True)
|
|
12
|
+
parser.add_argument('--client_id', required=True)
|
|
13
|
+
parser.add_argument('--client_secret', required=True)
|
|
14
|
+
parser.add_argument('--redirect_uri', required=True)
|
|
15
|
+
parser.add_argument('--user_email', required=True)
|
|
16
|
+
|
|
17
|
+
def handle(self, *args, **options):
|
|
18
|
+
try:
|
|
19
|
+
user = User.objects.get(email=options['user_email'])
|
|
20
|
+
except User.DoesNotExist:
|
|
21
|
+
self.stdout.write(self.style.ERROR(f"User with email {options['user_email']} does not exist"))
|
|
22
|
+
return
|
|
23
|
+
|
|
24
|
+
# Check if application with this client_id already exists
|
|
25
|
+
app, created = Application.objects.update_or_create(
|
|
26
|
+
client_id=options['client_id'],
|
|
27
|
+
defaults={
|
|
28
|
+
'name': options['name'],
|
|
29
|
+
'user': user,
|
|
30
|
+
'client_type': Application.CLIENT_CONFIDENTIAL,
|
|
31
|
+
'authorization_grant_type': Application.GRANT_AUTHORIZATION_CODE,
|
|
32
|
+
'client_secret': options['client_secret'],
|
|
33
|
+
'redirect_uris': options['redirect_uri'],
|
|
34
|
+
'skip_authorization': True,
|
|
35
|
+
}
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
if created:
|
|
39
|
+
self.stdout.write(self.style.SUCCESS(f"Successfully created OAuth2 application: {options['name']}"))
|
|
40
|
+
else:
|
|
41
|
+
self.stdout.write(self.style.SUCCESS(f"Successfully updated OAuth2 application: {options['name']}"))
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import threading
|
|
3
|
+
from django.db.models import Q
|
|
4
|
+
from django.http import HttpResponse
|
|
5
|
+
from karrio.core.utils import Tracer
|
|
6
|
+
from karrio.server.conf import settings
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class CreatorAccess:
|
|
10
|
+
def __call__(self, context, key: str = "created_by", **kwargs) -> Q:
|
|
11
|
+
user_key = f"{key}_id"
|
|
12
|
+
user = getattr(context, "user", None)
|
|
13
|
+
|
|
14
|
+
return Q(**{user_key: getattr(user, "id", None)})
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class WideAccess:
|
|
18
|
+
def __call__(self, *args, **kwargs) -> Q:
|
|
19
|
+
return Q()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class UserToken:
|
|
23
|
+
def __call__(self, context, **kwargs) -> dict:
|
|
24
|
+
return dict(user=getattr(context, "user", context))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class SessionContext:
|
|
28
|
+
"""Middleware that manages request context, tracing, and telemetry.
|
|
29
|
+
|
|
30
|
+
This middleware:
|
|
31
|
+
1. Creates a Tracer instance for each request
|
|
32
|
+
2. Injects telemetry (Sentry) if configured
|
|
33
|
+
3. Stores the request in thread-local storage for access throughout the request lifecycle
|
|
34
|
+
4. Saves tracing records after the response is generated
|
|
35
|
+
5. Sets up user context for telemetry
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
_threadmap: dict = {}
|
|
39
|
+
|
|
40
|
+
def __init__(self, get_response):
|
|
41
|
+
self.get_response = get_response
|
|
42
|
+
# One-time configuration and initialization.
|
|
43
|
+
|
|
44
|
+
def __call__(self, request):
|
|
45
|
+
import time
|
|
46
|
+
|
|
47
|
+
# Code to be executed for each request before
|
|
48
|
+
# the view (and later middleware) are called.
|
|
49
|
+
|
|
50
|
+
# Create tracer with telemetry injection
|
|
51
|
+
tracer = Tracer()
|
|
52
|
+
self._inject_telemetry(tracer, request)
|
|
53
|
+
request.tracer = tracer
|
|
54
|
+
|
|
55
|
+
self._threadmap[threading.get_ident()] = request
|
|
56
|
+
|
|
57
|
+
# Track request timing
|
|
58
|
+
start_time = time.time()
|
|
59
|
+
|
|
60
|
+
response = self.get_response(request)
|
|
61
|
+
|
|
62
|
+
# Record request metrics
|
|
63
|
+
self._record_request_metrics(request, response, start_time)
|
|
64
|
+
|
|
65
|
+
# Code to be executed for each request/response after
|
|
66
|
+
# the view is called.
|
|
67
|
+
try:
|
|
68
|
+
self._save_tracing_records(request, schema=settings.schema)
|
|
69
|
+
del self._threadmap[threading.get_ident()]
|
|
70
|
+
except KeyError:
|
|
71
|
+
pass
|
|
72
|
+
|
|
73
|
+
return response
|
|
74
|
+
|
|
75
|
+
def _inject_telemetry(self, tracer: Tracer, request):
|
|
76
|
+
"""Inject telemetry into tracer if Sentry is configured.
|
|
77
|
+
|
|
78
|
+
This method conditionally imports and sets up SentryTelemetry
|
|
79
|
+
only when SENTRY_DSN is configured, ensuring zero overhead
|
|
80
|
+
when Sentry is not in use.
|
|
81
|
+
"""
|
|
82
|
+
try:
|
|
83
|
+
from karrio.server.core.telemetry import get_telemetry_for_request
|
|
84
|
+
|
|
85
|
+
telemetry = get_telemetry_for_request()
|
|
86
|
+
tracer.set_telemetry(telemetry)
|
|
87
|
+
|
|
88
|
+
# Set user context if authenticated
|
|
89
|
+
user = getattr(request, "user", None)
|
|
90
|
+
if user and getattr(user, "is_authenticated", False):
|
|
91
|
+
tracer.set_user(
|
|
92
|
+
user_id=str(user.id) if hasattr(user, "id") else None,
|
|
93
|
+
email=getattr(user, "email", None),
|
|
94
|
+
username=getattr(user, "username", None),
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
# Set request context tags
|
|
98
|
+
tracer.set_tag("http.method", request.method)
|
|
99
|
+
tracer.set_tag("http.path", request.path)
|
|
100
|
+
|
|
101
|
+
# Set test_mode tag if available
|
|
102
|
+
test_mode = getattr(request, "test_mode", None)
|
|
103
|
+
if test_mode is not None:
|
|
104
|
+
tracer.set_tag("test_mode", str(test_mode).lower())
|
|
105
|
+
|
|
106
|
+
# Set org context for multi-tenant deployments
|
|
107
|
+
org = getattr(request, "org", None)
|
|
108
|
+
if org:
|
|
109
|
+
tracer.set_tag("org_id", str(org.id) if hasattr(org, "id") else str(org))
|
|
110
|
+
|
|
111
|
+
except ImportError:
|
|
112
|
+
# Telemetry module not available, continue with NoOpTelemetry
|
|
113
|
+
pass
|
|
114
|
+
except Exception:
|
|
115
|
+
# Any other error, continue with NoOpTelemetry
|
|
116
|
+
pass
|
|
117
|
+
|
|
118
|
+
def _record_request_metrics(self, request, response, start_time):
|
|
119
|
+
"""Record HTTP request metrics to telemetry."""
|
|
120
|
+
import time
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
from karrio.server.core.telemetry import get_telemetry_for_request
|
|
124
|
+
|
|
125
|
+
telemetry = get_telemetry_for_request()
|
|
126
|
+
duration_ms = (time.time() - start_time) * 1000
|
|
127
|
+
|
|
128
|
+
# Common tags for all metrics
|
|
129
|
+
tags = {
|
|
130
|
+
"method": request.method,
|
|
131
|
+
"path": request.path,
|
|
132
|
+
"status_code": str(response.status_code),
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
# Add test_mode tag if available
|
|
136
|
+
test_mode = getattr(request, "test_mode", None)
|
|
137
|
+
if test_mode is not None:
|
|
138
|
+
tags["test_mode"] = str(test_mode).lower()
|
|
139
|
+
|
|
140
|
+
# Record request count
|
|
141
|
+
telemetry.record_metric("karrio.http.request", 1, tags=tags, metric_type="counter")
|
|
142
|
+
|
|
143
|
+
# Record response time distribution
|
|
144
|
+
telemetry.record_metric("karrio.http.duration", duration_ms, unit="millisecond", tags=tags, metric_type="distribution")
|
|
145
|
+
|
|
146
|
+
# Record error count for 4xx/5xx responses
|
|
147
|
+
if response.status_code >= 400:
|
|
148
|
+
error_tags = {**tags, "error_class": "client" if response.status_code < 500 else "server"}
|
|
149
|
+
telemetry.record_metric("karrio.http.error", 1, tags=error_tags, metric_type="counter")
|
|
150
|
+
|
|
151
|
+
except Exception:
|
|
152
|
+
pass # Don't let metrics recording break the request
|
|
153
|
+
|
|
154
|
+
def _save_tracing_records(self, request, schema: str = None):
|
|
155
|
+
from karrio.server.tracing.utils import save_tracing_records
|
|
156
|
+
|
|
157
|
+
save_tracing_records(request, schema=schema)
|
|
158
|
+
|
|
159
|
+
@classmethod
|
|
160
|
+
def get_current_request(cls):
|
|
161
|
+
return cls._threadmap.get(threading.get_ident())
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class NonHtmlDebugToolbarMiddleware:
|
|
165
|
+
"""
|
|
166
|
+
The Django Debug Toolbar usually only works for views that return HTML.
|
|
167
|
+
This middleware wraps any non-HTML response in HTML if the request
|
|
168
|
+
has a 'debug' query parameter (e.g. https://api.karrio.io/foo?debug)
|
|
169
|
+
Special handling for json (pretty printing) and
|
|
170
|
+
binary data (only show data length)
|
|
171
|
+
"""
|
|
172
|
+
|
|
173
|
+
def __init__(self, get_response):
|
|
174
|
+
self.get_response = get_response
|
|
175
|
+
|
|
176
|
+
def __call__(self, request):
|
|
177
|
+
response = self.get_response(request)
|
|
178
|
+
|
|
179
|
+
if request.GET.get("debug") == "":
|
|
180
|
+
if response["Content-Type"] == "application/octet-stream":
|
|
181
|
+
new_content = (
|
|
182
|
+
"<html><body>Binary Data, "
|
|
183
|
+
"Length: {}</body></html>".format(len(response.content))
|
|
184
|
+
)
|
|
185
|
+
response = HttpResponse(new_content)
|
|
186
|
+
elif response["Content-Type"] != "text/html":
|
|
187
|
+
content = response.content
|
|
188
|
+
try:
|
|
189
|
+
json_ = json.loads(content)
|
|
190
|
+
content = json.dumps(json_, sort_keys=True, indent=2)
|
|
191
|
+
except ValueError:
|
|
192
|
+
pass
|
|
193
|
+
response = HttpResponse(
|
|
194
|
+
"<html><body><pre>{}" "</pre></body></html>".format(content)
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
return response
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Generated by Django 3.2.3 on 2021-05-18 10:53
|
|
2
|
+
|
|
3
|
+
from django.db import migrations
|
|
4
|
+
import karrio.server.core.models.base
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Migration(migrations.Migration):
|
|
8
|
+
|
|
9
|
+
initial = True
|
|
10
|
+
|
|
11
|
+
dependencies = [
|
|
12
|
+
('rest_framework_tracking', '0011_auto_20201117_2016'),
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
operations = [
|
|
16
|
+
migrations.CreateModel(
|
|
17
|
+
name='APILog',
|
|
18
|
+
fields=[
|
|
19
|
+
],
|
|
20
|
+
options={
|
|
21
|
+
'ordering': ['-requested_at'],
|
|
22
|
+
'proxy': True,
|
|
23
|
+
'indexes': [],
|
|
24
|
+
'constraints': [],
|
|
25
|
+
},
|
|
26
|
+
bases=('rest_framework_tracking.apirequestlog', karrio.server.core.models.base.ControlledAccessModel),
|
|
27
|
+
),
|
|
28
|
+
]
|