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.
Files changed (213) hide show
  1. karrio/server/conf.py +54 -0
  2. karrio/server/core/__init__.py +3 -0
  3. karrio/server/core/admin.py +1 -0
  4. karrio/server/core/apps.py +10 -0
  5. karrio/server/core/authentication.py +347 -0
  6. karrio/server/core/config.py +31 -0
  7. karrio/server/core/context_processors.py +12 -0
  8. karrio/server/core/datatypes.py +394 -0
  9. karrio/server/core/dataunits.py +187 -0
  10. karrio/server/core/exceptions.py +404 -0
  11. karrio/server/core/fields.py +12 -0
  12. karrio/server/core/filters.py +837 -0
  13. karrio/server/core/gateway.py +1011 -0
  14. karrio/server/core/logging.py +403 -0
  15. karrio/server/core/management/commands/cli.py +19 -0
  16. karrio/server/core/management/commands/create_oauth_client.py +41 -0
  17. karrio/server/core/management/commands/runserver.py +5 -0
  18. karrio/server/core/middleware.py +197 -0
  19. karrio/server/core/migrations/0001_initial.py +28 -0
  20. karrio/server/core/migrations/0002_apilogindex.py +69 -0
  21. karrio/server/core/migrations/0003_apilogindex_test_mode.py +62 -0
  22. karrio/server/core/migrations/0004_metafield.py +74 -0
  23. karrio/server/core/migrations/0005_alter_metafield_type_alter_metafield_value.py +23 -0
  24. karrio/server/core/migrations/0006_add_api_log_requested_at_index.py +22 -0
  25. karrio/server/core/migrations/__init__.py +0 -0
  26. karrio/server/core/models/__init__.py +48 -0
  27. karrio/server/core/models/base.py +103 -0
  28. karrio/server/core/models/entity.py +24 -0
  29. karrio/server/core/models/metafield.py +144 -0
  30. karrio/server/core/models/third_party.py +21 -0
  31. karrio/server/core/oauth_validators.py +170 -0
  32. karrio/server/core/permissions.py +36 -0
  33. karrio/server/core/renderers.py +11 -0
  34. karrio/server/core/router.py +3 -0
  35. karrio/server/core/serializers.py +1971 -0
  36. karrio/server/core/signals.py +55 -0
  37. karrio/server/core/telemetry.py +573 -0
  38. karrio/server/core/tests.py +99 -0
  39. karrio/server/core/tests_resource_token.py +411 -0
  40. karrio/server/core/urls.py +12 -0
  41. karrio/server/core/utils.py +1025 -0
  42. karrio/server/core/validators.py +264 -0
  43. karrio/server/core/views/__init__.py +2 -0
  44. karrio/server/core/views/api.py +133 -0
  45. karrio/server/core/views/metadata.py +44 -0
  46. karrio/server/core/views/oauth.py +75 -0
  47. karrio/server/core/views/references.py +82 -0
  48. karrio/server/core/views/schema.py +310 -0
  49. karrio/server/filters/__init__.py +2 -0
  50. karrio/server/filters/abstract.py +26 -0
  51. karrio/server/iam/__init__.py +0 -0
  52. karrio/server/iam/admin.py +3 -0
  53. karrio/server/iam/apps.py +21 -0
  54. karrio/server/iam/migrations/0001_initial.py +33 -0
  55. karrio/server/iam/migrations/__init__.py +0 -0
  56. karrio/server/iam/models.py +48 -0
  57. karrio/server/iam/permissions.py +155 -0
  58. karrio/server/iam/serializers.py +54 -0
  59. karrio/server/iam/signals.py +18 -0
  60. karrio/server/iam/tests.py +3 -0
  61. karrio/server/iam/views.py +3 -0
  62. karrio/server/openapi.py +75 -0
  63. karrio/server/providers/__init__.py +1 -0
  64. karrio/server/providers/admin.py +364 -0
  65. karrio/server/providers/apps.py +10 -0
  66. karrio/server/providers/management/commands/migrate_rate_sheets.py +101 -0
  67. karrio/server/providers/migrations/0001_initial.py +140 -0
  68. karrio/server/providers/migrations/0002_carrier_active.py +18 -0
  69. karrio/server/providers/migrations/0003_auto_20201230_0820.py +24 -0
  70. karrio/server/providers/migrations/0004_auto_20210212_0554.py +178 -0
  71. karrio/server/providers/migrations/0005_auto_20210212_0555.py +18 -0
  72. karrio/server/providers/migrations/0006_australiapostsettings.py +29 -0
  73. karrio/server/providers/migrations/0007_auto_20210213_0206.py +21 -0
  74. karrio/server/providers/migrations/0008_auto_20210214_0409.py +30 -0
  75. karrio/server/providers/migrations/0009_auto_20210308_0302.py +18 -0
  76. karrio/server/providers/migrations/0010_auto_20210409_0852.py +32 -0
  77. karrio/server/providers/migrations/0011_auto_20210409_0853.py +21 -0
  78. karrio/server/providers/migrations/0012_alter_carrier_options.py +17 -0
  79. karrio/server/providers/migrations/0013_tntsettings.py +30 -0
  80. karrio/server/providers/migrations/0014_auto_20210612_1608.py +46 -0
  81. karrio/server/providers/migrations/0015_auto_20210615_1601.py +28 -0
  82. karrio/server/providers/migrations/0016_alter_purolatorsettings_user_token.py +18 -0
  83. karrio/server/providers/migrations/0017_auto_20210805_0359.py +1293 -0
  84. karrio/server/providers/migrations/0018_alter_fedexsettings_user_key.py +18 -0
  85. karrio/server/providers/migrations/0019_dhlpolandsettings_servicelevel.py +65 -0
  86. karrio/server/providers/migrations/0020_genericsettings_labeltemplate.py +52 -0
  87. karrio/server/providers/migrations/0021_auto_20211231_2353.py +40 -0
  88. karrio/server/providers/migrations/0022_carrier_metadata.py +18 -0
  89. karrio/server/providers/migrations/0023_auto_20220124_1916.py +27 -0
  90. karrio/server/providers/migrations/0024_alter_genericsettings_custom_carrier_name.py +19 -0
  91. karrio/server/providers/migrations/0025_alter_servicelevel_service_code.py +19 -0
  92. karrio/server/providers/migrations/0026_auto_20220208_0132.py +59 -0
  93. karrio/server/providers/migrations/0027_auto_20220304_1340.py +29 -0
  94. karrio/server/providers/migrations/0028_auto_20220323_1500.py +33 -0
  95. karrio/server/providers/migrations/0029_easypostsettings.py +27 -0
  96. karrio/server/providers/migrations/0030_amazonmwssettings.py +29 -0
  97. karrio/server/providers/migrations/0031_delete_amazonmwssettings.py +18 -0
  98. karrio/server/providers/migrations/0032_alter_carrier_test.py +18 -0
  99. karrio/server/providers/migrations/0033_auto_20220708_1350.py +22 -0
  100. karrio/server/providers/migrations/0034_amazonmwssettings_dpdhlsettings.py +47 -0
  101. karrio/server/providers/migrations/0035_alter_carrier_capabilities.py +43 -0
  102. karrio/server/providers/migrations/0036_upsfreightsettings.py +31 -0
  103. karrio/server/providers/migrations/0037_chronopostsettings.py +29 -0
  104. karrio/server/providers/migrations/0038_alter_genericsettings_label_template.py +19 -0
  105. karrio/server/providers/migrations/0039_auto_20220906_0612.py +23 -0
  106. karrio/server/providers/migrations/0040_dpdhlsettings_services.py +18 -0
  107. karrio/server/providers/migrations/0041_auto_20221105_0705.py +38 -0
  108. karrio/server/providers/migrations/0042_auto_20221215_1642.py +23 -0
  109. karrio/server/providers/migrations/0043_alter_genericsettings_account_number_and_more.py +39 -0
  110. karrio/server/providers/migrations/0044_carrier_carrier_capabilities.py +64 -0
  111. karrio/server/providers/migrations/0045_alter_carrier_active_alter_carrier_carrier_id.py +31 -0
  112. karrio/server/providers/migrations/0046_remove_dpdhlsettings_signature_and_more.py +41 -0
  113. karrio/server/providers/migrations/0047_dpdsettings.py +286 -0
  114. karrio/server/providers/migrations/0048_servicelevel_min_weight_servicelevel_transit_days_and_more.py +64 -0
  115. karrio/server/providers/migrations/0049_boxknightsettings_geodissettings_lapostesettings_and_more.py +156 -0
  116. karrio/server/providers/migrations/0050_carrier_is_system_alter_carrier_metadata_and_more.py +106 -0
  117. karrio/server/providers/migrations/0051_rename_username_upssettings_client_id_and_more.py +31 -0
  118. karrio/server/providers/migrations/0052_alter_upssettings_account_number_and_more.py +20 -0
  119. karrio/server/providers/migrations/0053_locate2usettings.py +281 -0
  120. karrio/server/providers/migrations/0054_zoom2usettings.py +280 -0
  121. karrio/server/providers/migrations/0055_rename_amazonmwssettings_amazonshippingsettings_and_more.py +44 -0
  122. karrio/server/providers/migrations/0056_asendiaussettings_geodissettings_code_client_and_more.py +75 -0
  123. karrio/server/providers/migrations/0057_alter_servicelevel_weight_unit_belgianpostsettings.py +51 -0
  124. karrio/server/providers/migrations/0058_alliedexpresssettings.py +38 -0
  125. karrio/server/providers/migrations/0059_ratesheet.py +81 -0
  126. karrio/server/providers/migrations/0060_belgianpostsettings_rate_sheet_and_more.py +73 -0
  127. karrio/server/providers/migrations/0061_alliedexpresssettings_service_type.py +17 -0
  128. karrio/server/providers/migrations/0062_sendlesettings_account_country_code.py +257 -0
  129. karrio/server/providers/migrations/0063_servicelevel_metadata.py +25 -0
  130. karrio/server/providers/migrations/0064_alliedexpresslocalsettings.py +43 -0
  131. karrio/server/providers/migrations/0065_servicelevel_carrier_service_code_and_more.py +66 -0
  132. karrio/server/providers/migrations/0066_rename_fedexsettings_fedexwssettings_and_more.py +28 -0
  133. karrio/server/providers/migrations/0067_fedexsettings.py +283 -0
  134. karrio/server/providers/migrations/0068_fedexsettings_track_api_key_and_more.py +38 -0
  135. karrio/server/providers/migrations/0069_alter_canadapostsettings_contract_id_and_more.py +23 -0
  136. karrio/server/providers/migrations/0070_tgesettings_alter_carrier_capabilities.py +65 -0
  137. karrio/server/providers/migrations/0071_alter_tgesettings_my_toll_token.py +18 -0
  138. karrio/server/providers/migrations/0072_rename_eshippersettings_eshipperxmlsettings_and_more.py +28 -0
  139. karrio/server/providers/migrations/0073_delete_eshipperxmlsettings.py +41 -0
  140. karrio/server/providers/migrations/0074_eshippersettings.py +38 -0
  141. karrio/server/providers/migrations/0075_haypostsettings.py +40 -0
  142. karrio/server/providers/migrations/0076_rename_customer_registration_id_uspsinternationalsettings_account_number_and_more.py +125 -0
  143. karrio/server/providers/migrations/0077_uspswtinternationalsettings_uspswtsettings_and_more.py +165 -0
  144. karrio/server/providers/migrations/0078_auto_20240813_1552.py +120 -0
  145. karrio/server/providers/migrations/0079_alter_carrier_options_alter_ratesheet_created_by.py +31 -0
  146. karrio/server/providers/migrations/0080_alter_aramexsettings_account_country_code_and_more.py +3025 -0
  147. karrio/server/providers/migrations/0081_remove_alliedexpresssettings_carrier_ptr_and_more.py +338 -0
  148. karrio/server/providers/migrations/0082_add_zone_identifiers.py +50 -0
  149. karrio/server/providers/migrations/0083_add_optimized_rate_sheet_structure.py +33 -0
  150. karrio/server/providers/migrations/0084_alter_servicelevel_currency.py +168 -0
  151. karrio/server/providers/migrations/0085_populate_dhl_parcel_de_oauth_credentials.py +82 -0
  152. karrio/server/providers/migrations/0086_rename_dhl_parcel_de_customer_number_to_billing_number.py +71 -0
  153. karrio/server/providers/migrations/__init__.py +0 -0
  154. karrio/server/providers/models/__init__.py +16 -0
  155. karrio/server/providers/models/carrier.py +387 -0
  156. karrio/server/providers/models/config.py +30 -0
  157. karrio/server/providers/models/service.py +192 -0
  158. karrio/server/providers/models/sheet.py +287 -0
  159. karrio/server/providers/models/template.py +39 -0
  160. karrio/server/providers/models/utils.py +58 -0
  161. karrio/server/providers/router.py +3 -0
  162. karrio/server/providers/serializers/__init__.py +3 -0
  163. karrio/server/providers/serializers/base.py +538 -0
  164. karrio/server/providers/signals.py +25 -0
  165. karrio/server/providers/templates/providers/oauth_callback.html +105 -0
  166. karrio/server/providers/tests/__init__.py +5 -0
  167. karrio/server/providers/tests/test_connections.py +895 -0
  168. karrio/server/providers/urls.py +11 -0
  169. karrio/server/providers/views/__init__.py +0 -0
  170. karrio/server/providers/views/carriers.py +267 -0
  171. karrio/server/providers/views/connections.py +496 -0
  172. karrio/server/samples.py +352 -0
  173. karrio/server/serializers/__init__.py +2 -0
  174. karrio/server/serializers/abstract.py +602 -0
  175. karrio/server/tracing/__init__.py +0 -0
  176. karrio/server/tracing/admin.py +63 -0
  177. karrio/server/tracing/apps.py +8 -0
  178. karrio/server/tracing/migrations/0001_initial.py +41 -0
  179. karrio/server/tracing/migrations/0002_auto_20220710_1307.py +22 -0
  180. karrio/server/tracing/migrations/0003_auto_20221105_0317.py +43 -0
  181. karrio/server/tracing/migrations/0004_tracingrecord_carrier_account_idx.py +24 -0
  182. karrio/server/tracing/migrations/0005_optimise_tracingrecord_request_log_idx.py +25 -0
  183. karrio/server/tracing/migrations/0006_alter_tracingrecord_options_and_more.py +49 -0
  184. karrio/server/tracing/migrations/0007_tracingrecord_tracing_created_at_idx.py +19 -0
  185. karrio/server/tracing/migrations/__init__.py +0 -0
  186. karrio/server/tracing/models.py +82 -0
  187. karrio/server/tracing/tests.py +3 -0
  188. karrio/server/tracing/utils.py +109 -0
  189. karrio/server/user/__init__.py +0 -0
  190. karrio/server/user/admin.py +96 -0
  191. karrio/server/user/apps.py +7 -0
  192. karrio/server/user/forms.py +35 -0
  193. karrio/server/user/migrations/0001_initial.py +41 -0
  194. karrio/server/user/migrations/0002_token.py +29 -0
  195. karrio/server/user/migrations/0003_token_test_mode.py +20 -0
  196. karrio/server/user/migrations/0004_group.py +26 -0
  197. karrio/server/user/migrations/0005_token_label.py +21 -0
  198. karrio/server/user/migrations/0006_workspaceconfig.py +63 -0
  199. karrio/server/user/migrations/0007_user_metadata.py +25 -0
  200. karrio/server/user/migrations/__init__.py +0 -0
  201. karrio/server/user/models.py +218 -0
  202. karrio/server/user/serializers.py +47 -0
  203. karrio/server/user/templates/registration/login.html +108 -0
  204. karrio/server/user/templates/registration/registration_confirm_email.html +10 -0
  205. karrio/server/user/templates/registration/registration_confirm_email.txt +3 -0
  206. karrio/server/user/tests.py +3 -0
  207. karrio/server/user/urls.py +10 -0
  208. karrio/server/user/utils.py +60 -0
  209. karrio/server/user/views.py +9 -0
  210. karrio_server_core-2025.5.dist-info/METADATA +32 -0
  211. karrio_server_core-2025.5.dist-info/RECORD +213 -0
  212. karrio_server_core-2025.5.dist-info/WHEEL +5 -0
  213. 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,5 @@
1
+ from django.core.management.commands.runserver import Command as RunserverCommand
2
+
3
+ class Command(RunserverCommand):
4
+ default_port = "5002"
5
+ default_addr = "0.0.0.0"
@@ -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
+ ]