django-cfg 1.4.119__py3-none-any.whl → 1.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.

Potentially problematic release.


This version of django-cfg might be problematic. Click here for more details.

Files changed (84) hide show
  1. django_cfg/__init__.py +8 -4
  2. django_cfg/apps/centrifugo/admin/centrifugo_log.py +33 -71
  3. django_cfg/apps/grpc/__init__.py +9 -0
  4. django_cfg/apps/grpc/admin/__init__.py +11 -0
  5. django_cfg/apps/grpc/admin/config.py +89 -0
  6. django_cfg/apps/grpc/admin/grpc_request_log.py +252 -0
  7. django_cfg/apps/grpc/apps.py +28 -0
  8. django_cfg/apps/grpc/auth/__init__.py +9 -0
  9. django_cfg/apps/grpc/auth/jwt_auth.py +295 -0
  10. django_cfg/apps/grpc/interceptors/__init__.py +19 -0
  11. django_cfg/apps/grpc/interceptors/errors.py +241 -0
  12. django_cfg/apps/grpc/interceptors/logging.py +270 -0
  13. django_cfg/apps/grpc/interceptors/metrics.py +306 -0
  14. django_cfg/apps/grpc/interceptors/request_logger.py +515 -0
  15. django_cfg/apps/grpc/management/__init__.py +1 -0
  16. django_cfg/apps/grpc/management/commands/__init__.py +0 -0
  17. django_cfg/apps/grpc/management/commands/rungrpc.py +302 -0
  18. django_cfg/apps/grpc/managers/__init__.py +10 -0
  19. django_cfg/apps/grpc/managers/grpc_request_log.py +310 -0
  20. django_cfg/apps/grpc/migrations/0001_initial.py +69 -0
  21. django_cfg/apps/grpc/migrations/0002_rename_django_cfg__service_4c4a8e_idx_django_cfg__service_584308_idx_and_more.py +38 -0
  22. django_cfg/apps/grpc/migrations/__init__.py +0 -0
  23. django_cfg/apps/grpc/models/__init__.py +9 -0
  24. django_cfg/apps/grpc/models/grpc_request_log.py +219 -0
  25. django_cfg/apps/grpc/serializers/__init__.py +23 -0
  26. django_cfg/apps/grpc/serializers/health.py +18 -0
  27. django_cfg/apps/grpc/serializers/requests.py +18 -0
  28. django_cfg/apps/grpc/serializers/services.py +50 -0
  29. django_cfg/apps/grpc/serializers/stats.py +22 -0
  30. django_cfg/apps/grpc/services/__init__.py +16 -0
  31. django_cfg/apps/grpc/services/base.py +375 -0
  32. django_cfg/apps/grpc/services/discovery.py +415 -0
  33. django_cfg/apps/grpc/urls.py +23 -0
  34. django_cfg/apps/grpc/utils/__init__.py +13 -0
  35. django_cfg/apps/grpc/utils/proto_gen.py +423 -0
  36. django_cfg/apps/grpc/views/__init__.py +9 -0
  37. django_cfg/apps/grpc/views/monitoring.py +497 -0
  38. django_cfg/apps/maintenance/admin/api_key_admin.py +7 -8
  39. django_cfg/apps/maintenance/admin/site_admin.py +5 -4
  40. django_cfg/apps/payments/admin/balance_admin.py +26 -36
  41. django_cfg/apps/payments/admin/payment_admin.py +65 -85
  42. django_cfg/apps/payments/admin/withdrawal_admin.py +65 -100
  43. django_cfg/apps/tasks/admin/task_log.py +20 -47
  44. django_cfg/apps/urls.py +7 -1
  45. django_cfg/config.py +106 -0
  46. django_cfg/core/base/config_model.py +6 -0
  47. django_cfg/core/builders/apps_builder.py +3 -0
  48. django_cfg/core/generation/integration_generators/grpc_generator.py +318 -0
  49. django_cfg/core/generation/orchestrator.py +10 -0
  50. django_cfg/models/api/grpc/__init__.py +59 -0
  51. django_cfg/models/api/grpc/config.py +364 -0
  52. django_cfg/modules/base.py +15 -0
  53. django_cfg/modules/django_admin/__init__.py +2 -0
  54. django_cfg/modules/django_admin/base/pydantic_admin.py +2 -2
  55. django_cfg/modules/django_admin/config/__init__.py +2 -0
  56. django_cfg/modules/django_admin/config/field_config.py +24 -0
  57. django_cfg/modules/django_admin/utils/__init__.py +41 -3
  58. django_cfg/modules/django_admin/utils/badges/__init__.py +13 -0
  59. django_cfg/modules/django_admin/utils/{badges.py → badges/status_badges.py} +3 -3
  60. django_cfg/modules/django_admin/utils/displays/__init__.py +13 -0
  61. django_cfg/modules/django_admin/utils/{displays.py → displays/data_displays.py} +2 -2
  62. django_cfg/modules/django_admin/utils/html/__init__.py +26 -0
  63. django_cfg/modules/django_admin/utils/html/badges.py +47 -0
  64. django_cfg/modules/django_admin/utils/html/base.py +167 -0
  65. django_cfg/modules/django_admin/utils/html/code.py +87 -0
  66. django_cfg/modules/django_admin/utils/html/composition.py +198 -0
  67. django_cfg/modules/django_admin/utils/html/formatting.py +231 -0
  68. django_cfg/modules/django_admin/utils/html/keyvalue.py +219 -0
  69. django_cfg/modules/django_admin/utils/html/markdown_integration.py +108 -0
  70. django_cfg/modules/django_admin/utils/html/progress.py +127 -0
  71. django_cfg/modules/django_admin/utils/html_builder.py +55 -408
  72. django_cfg/modules/django_admin/utils/markdown/__init__.py +21 -0
  73. django_cfg/modules/django_admin/widgets/registry.py +42 -0
  74. django_cfg/modules/django_unfold/navigation.py +28 -0
  75. django_cfg/pyproject.toml +3 -5
  76. django_cfg/registry/modules.py +6 -0
  77. {django_cfg-1.4.119.dist-info → django_cfg-1.5.1.dist-info}/METADATA +10 -1
  78. {django_cfg-1.4.119.dist-info → django_cfg-1.5.1.dist-info}/RECORD +83 -34
  79. django_cfg/modules/django_admin/utils/CODE_BLOCK_DOCS.md +0 -396
  80. /django_cfg/modules/django_admin/utils/{mermaid_plugin.py → markdown/mermaid_plugin.py} +0 -0
  81. /django_cfg/modules/django_admin/utils/{markdown_renderer.py → markdown/renderer.py} +0 -0
  82. {django_cfg-1.4.119.dist-info → django_cfg-1.5.1.dist-info}/WHEEL +0 -0
  83. {django_cfg-1.4.119.dist-info → django_cfg-1.5.1.dist-info}/entry_points.txt +0 -0
  84. {django_cfg-1.4.119.dist-info → django_cfg-1.5.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,167 @@
1
+ """
2
+ Basic HTML elements for Django Admin.
3
+
4
+ Provides fundamental HTML building blocks: icons, spans, text, divs, links, and empty placeholders.
5
+ """
6
+
7
+ from typing import Any, Optional
8
+
9
+ from django.utils.html import escape, format_html
10
+ from django.utils.safestring import SafeString
11
+
12
+
13
+ class BaseElements:
14
+ """Basic HTML building blocks."""
15
+
16
+ @staticmethod
17
+ def icon(icon_name: str, size: str = "xs", css_class: str = "") -> SafeString:
18
+ """
19
+ Render Material Icon.
20
+
21
+ Args:
22
+ icon_name: Icon name from Icons class
23
+ size: xs, sm, base, lg, xl
24
+ css_class: Additional CSS classes
25
+ """
26
+ size_classes = {
27
+ 'xs': 'text-xs',
28
+ 'sm': 'text-sm',
29
+ 'base': 'text-base',
30
+ 'lg': 'text-lg',
31
+ 'xl': 'text-xl'
32
+ }
33
+ size_class = size_classes.get(size, 'text-xs')
34
+ classes = f"material-symbols-outlined {size_class}"
35
+ if css_class:
36
+ classes += f" {css_class}"
37
+
38
+ return format_html('<span class="{}">{}</span>', classes, icon_name)
39
+
40
+ @staticmethod
41
+ def span(text: Any, css_class: str = "") -> SafeString:
42
+ """
43
+ Render text in span with optional CSS class.
44
+
45
+ Args:
46
+ text: Text to display
47
+ css_class: CSS classes
48
+ """
49
+ if css_class:
50
+ return format_html('<span class="{}">{}</span>', css_class, escape(str(text)))
51
+ return format_html('<span>{}</span>', escape(str(text)))
52
+
53
+ @staticmethod
54
+ def text(
55
+ content: Any,
56
+ variant: Optional[str] = None,
57
+ size: Optional[str] = None,
58
+ weight: Optional[str] = None,
59
+ muted: bool = False
60
+ ) -> SafeString:
61
+ """
62
+ Render styled text with semantic variants.
63
+
64
+ Args:
65
+ content: Text content (can be SafeString from other html methods)
66
+ variant: Color variant - 'success', 'warning', 'danger', 'info', 'primary'
67
+ size: Size - 'xs', 'sm', 'base', 'lg', 'xl', '2xl'
68
+ weight: Font weight - 'normal', 'medium', 'semibold', 'bold'
69
+ muted: Use muted/subtle color
70
+
71
+ Usage:
72
+ # Success text
73
+ html.text("$1,234.56", variant="success", size="lg")
74
+
75
+ # Muted small text
76
+ html.text("(12.5%)", muted=True, size="sm")
77
+
78
+ # Combined with other methods
79
+ total = html.number(1234.56, prefix="$")
80
+ html.text(total, variant="success", size="lg")
81
+
82
+ Returns:
83
+ SafeString with styled text
84
+ """
85
+ classes = []
86
+
87
+ # Variant colors
88
+ if variant:
89
+ variant_classes = {
90
+ 'success': 'text-success-600 dark:text-success-400',
91
+ 'warning': 'text-warning-600 dark:text-warning-400',
92
+ 'danger': 'text-danger-600 dark:text-danger-400',
93
+ 'info': 'text-info-600 dark:text-info-400',
94
+ 'primary': 'text-primary-600 dark:text-primary-400',
95
+ }
96
+ classes.append(variant_classes.get(variant, ''))
97
+
98
+ # Muted
99
+ if muted:
100
+ classes.append('text-font-subtle-light dark:text-font-subtle-dark')
101
+
102
+ # Size
103
+ if size:
104
+ size_classes = {
105
+ 'xs': 'text-xs',
106
+ 'sm': 'text-sm',
107
+ 'base': 'text-base',
108
+ 'lg': 'text-lg',
109
+ 'xl': 'text-xl',
110
+ '2xl': 'text-2xl',
111
+ }
112
+ classes.append(size_classes.get(size, ''))
113
+
114
+ # Weight
115
+ if weight:
116
+ weight_classes = {
117
+ 'normal': 'font-normal',
118
+ 'medium': 'font-medium',
119
+ 'semibold': 'font-semibold',
120
+ 'bold': 'font-bold',
121
+ }
122
+ classes.append(weight_classes.get(weight, ''))
123
+
124
+ css_class = ' '.join(filter(None, classes))
125
+
126
+ if css_class:
127
+ return format_html('<span class="{}">{}</span>', css_class, content)
128
+ return format_html('<span>{}</span>', content)
129
+
130
+ @staticmethod
131
+ def div(content: Any, css_class: str = "") -> SafeString:
132
+ """
133
+ Render content in div with optional CSS class.
134
+
135
+ Args:
136
+ content: Content to display (can be SafeString)
137
+ css_class: CSS classes
138
+ """
139
+ if css_class:
140
+ return format_html('<div class="{}">{}</div>', css_class, content)
141
+ return format_html('<div>{}</div>', content)
142
+
143
+ @staticmethod
144
+ def link(url: str, text: str, css_class: str = "", target: str = "") -> SafeString:
145
+ """
146
+ Render link.
147
+
148
+ Args:
149
+ url: URL
150
+ text: Link text
151
+ css_class: CSS classes
152
+ target: Target attribute (_blank, _self, etc)
153
+ """
154
+ if target:
155
+ return format_html(
156
+ '<a href="{}" class="{}" target="{}">{}</a>',
157
+ url, css_class, target, escape(text)
158
+ )
159
+ return format_html('<a href="{}" class="{}">{}</a>', url, css_class, escape(text))
160
+
161
+ @staticmethod
162
+ def empty(text: str = "—") -> SafeString:
163
+ """Render empty/placeholder value."""
164
+ return format_html(
165
+ '<span class="text-font-subtle-light dark:text-font-subtle-dark">{}</span>',
166
+ escape(text)
167
+ )
@@ -0,0 +1,87 @@
1
+ """
2
+ Code display elements for Django Admin.
3
+
4
+ Provides inline code and code block rendering with syntax highlighting support.
5
+ """
6
+
7
+ from typing import Any, Optional
8
+
9
+ from django.utils.html import escape, format_html
10
+ from django.utils.safestring import SafeString
11
+
12
+
13
+ class CodeElements:
14
+ """Code display elements."""
15
+
16
+ @staticmethod
17
+ def code(text: Any, css_class: str = "") -> SafeString:
18
+ """
19
+ Render inline code.
20
+
21
+ Args:
22
+ text: Code text
23
+ css_class: Additional CSS classes
24
+
25
+ Usage:
26
+ html.code("/path/to/file")
27
+ html.code("command --arg value")
28
+ """
29
+ base_classes = "font-mono text-xs bg-base-100 dark:bg-base-800 px-1.5 py-0.5 rounded"
30
+ classes = f"{base_classes} {css_class}".strip()
31
+
32
+ return format_html(
33
+ '<code class="{}">{}</code>',
34
+ classes,
35
+ escape(str(text))
36
+ )
37
+
38
+ @staticmethod
39
+ def code_block(
40
+ text: Any,
41
+ language: Optional[str] = None,
42
+ max_height: Optional[str] = None,
43
+ variant: str = "default"
44
+ ) -> SafeString:
45
+ """
46
+ Render code block with optional syntax highlighting and scrolling.
47
+
48
+ Args:
49
+ text: Code content
50
+ language: Programming language (json, python, bash, etc.) - for future syntax highlighting
51
+ max_height: Max height with scrolling (e.g., "400px", "20rem")
52
+ variant: Color variant - default, warning, danger, success, info
53
+
54
+ Usage:
55
+ html.code_block(json.dumps(data, indent=2), language="json")
56
+ html.code_block(stdout, max_height="400px")
57
+ html.code_block(stderr, max_height="400px", variant="warning")
58
+ """
59
+ # Variant-specific styles
60
+ variant_classes = {
61
+ 'default': 'bg-base-50 dark:bg-base-900 border-base-200 dark:border-base-700',
62
+ 'warning': 'bg-warning-50 dark:bg-warning-900/20 border-warning-200 dark:border-warning-700',
63
+ 'danger': 'bg-danger-50 dark:bg-danger-900/20 border-danger-200 dark:border-danger-700',
64
+ 'success': 'bg-success-50 dark:bg-success-900/20 border-success-200 dark:border-success-700',
65
+ 'info': 'bg-info-50 dark:bg-info-900/20 border-info-200 dark:border-info-700',
66
+ }
67
+
68
+ variant_class = variant_classes.get(variant, variant_classes['default'])
69
+
70
+ # Base styles
71
+ base_classes = f"font-mono text-xs whitespace-pre-wrap break-words border rounded-md p-3 {variant_class}"
72
+
73
+ # Add max-height and overflow if specified
74
+ style = ""
75
+ if max_height:
76
+ style = f'style="max-height: {max_height}; overflow-y: auto;"'
77
+
78
+ # Add language class for potential syntax highlighting
79
+ lang_class = f"language-{language}" if language else ""
80
+
81
+ return format_html(
82
+ '<pre class="{} {}" {}><code>{}</code></pre>',
83
+ base_classes,
84
+ lang_class,
85
+ style,
86
+ escape(str(text))
87
+ )
@@ -0,0 +1,198 @@
1
+ """
2
+ Composition elements for Django Admin.
3
+
4
+ Provides methods for composing multiple elements together: inline, icon_text, header.
5
+ """
6
+
7
+ from typing import Any, Optional, Union
8
+
9
+ from django.utils.html import escape, format_html
10
+ from django.utils.safestring import SafeString
11
+
12
+
13
+ class CompositionElements:
14
+ """Element composition utilities."""
15
+
16
+ @staticmethod
17
+ def icon(icon_name: str, size: str = "xs", css_class: str = "") -> SafeString:
18
+ """
19
+ Render Material Icon (helper for internal use).
20
+
21
+ Args:
22
+ icon_name: Icon name
23
+ size: Icon size
24
+ css_class: Additional CSS classes
25
+ """
26
+ from .base import BaseElements
27
+ return BaseElements.icon(icon_name, size, css_class)
28
+
29
+ @staticmethod
30
+ def icon_text(icon_or_text: Union[str, Any], text: Any = None,
31
+ icon_size: str = "xs", separator: str = " ") -> SafeString:
32
+ """
33
+ Render icon with text or emoji with text.
34
+
35
+ Args:
36
+ icon_or_text: Icon from Icons class, emoji, or text if text param is None
37
+ text: Optional text to display after icon
38
+ icon_size: Icon size (xs, sm, base, lg, xl)
39
+ separator: Separator between icon and text
40
+
41
+ Usage:
42
+ html.icon_text(Icons.EDIT, 5) # Icon with number
43
+ html.icon_text("📝", 5) # Emoji with number
44
+ html.icon_text("Active") # Just text
45
+ """
46
+ if text is None:
47
+ # Just text
48
+ return format_html('<span>{}</span>', escape(str(icon_or_text)))
49
+
50
+ # Check if it's a Material Icon (from Icons class) or emoji
51
+ icon_str = str(icon_or_text)
52
+
53
+ # Detect if it's emoji by checking for non-ASCII characters
54
+ is_emoji = any(ord(c) > 127 for c in icon_str)
55
+
56
+ if is_emoji or icon_str in ['📝', '💬', '🛒', '👤', '📧', '🔔', '⚙️', '🔧', '📊', '🎯']:
57
+ # Emoji
58
+ icon_html = escape(icon_str)
59
+ else:
60
+ # Material Icon
61
+ icon_html = CompositionElements.icon(icon_str, size=icon_size)
62
+
63
+ return format_html('{}{}<span>{}</span>', icon_html, separator, escape(str(text)))
64
+
65
+ @staticmethod
66
+ def inline(*items, separator: str = " | ",
67
+ size: str = "small", css_class: str = "") -> SafeString:
68
+ """
69
+ Render items inline with separator.
70
+
71
+ Args:
72
+ *items: Variable number of SafeString/str items to join (filters out None values)
73
+ separator: Separator between items
74
+ size: small, medium, large
75
+ css_class: Additional CSS classes
76
+
77
+ Usage:
78
+ html.inline(
79
+ html.icon_text(Icons.EDIT, 5),
80
+ html.icon_text(Icons.CHAT, 10),
81
+ separator=" | "
82
+ )
83
+ """
84
+ # Filter out None values
85
+ filtered_items = [item for item in items if item is not None]
86
+
87
+ if not filtered_items:
88
+ return format_html('<span class="text-font-subtle-light dark:text-font-subtle-dark">—</span>')
89
+
90
+ size_classes = {
91
+ 'small': 'text-xs',
92
+ 'medium': 'text-sm',
93
+ 'large': 'text-base'
94
+ }
95
+ size_class = size_classes.get(size, 'text-xs')
96
+
97
+ classes = size_class
98
+ if css_class:
99
+ classes += f" {css_class}"
100
+
101
+ # Convert items to strings, keeping SafeString as-is
102
+ from django.utils.safestring import SafeString, mark_safe
103
+ processed_items = []
104
+ for item in filtered_items:
105
+ if isinstance(item, (SafeString, str)):
106
+ processed_items.append(item)
107
+ else:
108
+ processed_items.append(escape(str(item)))
109
+
110
+ # Join with separator
111
+ joined = mark_safe(separator.join(str(item) for item in processed_items))
112
+
113
+ return format_html('<span class="{}">{}</span>', classes, joined)
114
+
115
+ @staticmethod
116
+ def header(
117
+ title: str,
118
+ subtitle: Optional[str] = None,
119
+ initials: Optional[str] = None,
120
+ avatar_variant: str = "primary"
121
+ ) -> SafeString:
122
+ """
123
+ Render header with avatar/initials, title and subtitle.
124
+
125
+ Creates a horizontal layout with circular avatar badge and text content.
126
+ Common pattern for displaying users, accounts, entities with identity.
127
+
128
+ Args:
129
+ title: Main title text
130
+ subtitle: Optional subtitle text (smaller, muted)
131
+ initials: Optional initials for avatar (e.g., "AB", "JD")
132
+ avatar_variant: Color variant for avatar badge (primary, success, info, etc.)
133
+
134
+ Usage:
135
+ # User with avatar
136
+ html.header(
137
+ title="John Doe",
138
+ subtitle="john@example.com • Admin",
139
+ initials="JD"
140
+ )
141
+
142
+ # Account with info
143
+ html.header(
144
+ title="Trading Account",
145
+ subtitle="Binance • SPOT • user@email.com",
146
+ initials="TA",
147
+ avatar_variant="success"
148
+ )
149
+
150
+ # Simple title only
151
+ html.header(title="Item Name")
152
+
153
+ Returns:
154
+ SafeString with header component HTML
155
+ """
156
+ # Avatar/initials badge
157
+ avatar_html = ""
158
+ if initials:
159
+ avatar_html = format_html(
160
+ '<span class="inline-flex items-center justify-center rounded-full w-8 h-8 text-xs font-semibold {} mr-3">{}</span>',
161
+ CompositionElements._get_avatar_classes(avatar_variant),
162
+ escape(initials)
163
+ )
164
+
165
+ # Title
166
+ title_html = format_html(
167
+ '<div class="font-medium text-sm text-font-default-light dark:text-font-default-dark">{}</div>',
168
+ escape(title)
169
+ )
170
+
171
+ # Subtitle
172
+ subtitle_html = ""
173
+ if subtitle:
174
+ subtitle_html = format_html(
175
+ '<div class="text-xs text-font-subtle-light dark:text-font-subtle-dark mt-0.5">{}</div>',
176
+ escape(subtitle)
177
+ )
178
+
179
+ # Combine
180
+ return format_html(
181
+ '<div class="flex items-center">{}<div>{}{}</div></div>',
182
+ avatar_html,
183
+ title_html,
184
+ subtitle_html
185
+ )
186
+
187
+ @staticmethod
188
+ def _get_avatar_classes(variant: str) -> str:
189
+ """Get CSS classes for avatar badge variant."""
190
+ variant_classes = {
191
+ 'success': 'bg-success-100 text-success-800 dark:bg-success-900 dark:text-success-200',
192
+ 'warning': 'bg-warning-100 text-warning-800 dark:bg-warning-900 dark:text-warning-200',
193
+ 'danger': 'bg-danger-100 text-danger-800 dark:bg-danger-900 dark:text-danger-200',
194
+ 'info': 'bg-info-100 text-info-800 dark:bg-info-900 dark:text-info-200',
195
+ 'primary': 'bg-primary-100 text-primary-800 dark:bg-primary-900 dark:text-primary-200',
196
+ 'secondary': 'bg-base-100 text-font-default-light dark:bg-base-800 dark:text-font-default-dark',
197
+ }
198
+ return variant_classes.get(variant, variant_classes['primary'])
@@ -0,0 +1,231 @@
1
+ """
2
+ Formatting elements for Django Admin.
3
+
4
+ Provides number and UUID formatting utilities.
5
+ """
6
+
7
+ from typing import Any, Optional
8
+
9
+ from django.utils.html import escape, format_html
10
+ from django.utils.safestring import SafeString
11
+
12
+
13
+ class FormattingElements:
14
+ """Formatting utilities for numbers and UUIDs."""
15
+
16
+ @staticmethod
17
+ def empty(text: str = "—") -> SafeString:
18
+ """Helper for empty values."""
19
+ from .base import BaseElements
20
+ return BaseElements.empty(text)
21
+
22
+ @staticmethod
23
+ def number(
24
+ value: Any,
25
+ precision: int = 8,
26
+ thousands_separator: bool = True,
27
+ strip_zeros: bool = True,
28
+ min_threshold: Optional[float] = None,
29
+ compact: bool = False,
30
+ prefix: str = "",
31
+ suffix: str = "",
32
+ css_class: str = ""
33
+ ) -> SafeString:
34
+ """
35
+ Format numeric values with smart precision handling.
36
+
37
+ Handles:
38
+ - Trailing zeros removal (20.000000 → 20)
39
+ - Scientific notation (0E-18 → 0)
40
+ - Thousands separators (1000000 → 1,000,000)
41
+ - Very small numbers (0.0000001 → < 0.00000001)
42
+ - Compact notation with K/M/B/T suffixes (1500000 → 1.5M)
43
+ - Negative values
44
+
45
+ Args:
46
+ value: Numeric value (int, float, Decimal, str)
47
+ precision: Number of decimal places (default: 8)
48
+ thousands_separator: Add thousands separator (default: True)
49
+ strip_zeros: Remove trailing zeros (default: True)
50
+ min_threshold: Show "< threshold" for very small numbers
51
+ compact: Use compact notation with K/M/B/T suffixes (default: False)
52
+ prefix: Text before number (e.g., "$", "BTC ")
53
+ suffix: Text after number (e.g., " USD", "%")
54
+ css_class: Additional CSS classes
55
+
56
+ Usage:
57
+ # Crypto balance
58
+ html.number(0.00012345, precision=8) # "0.00012345"
59
+ html.number(20.000000, precision=8) # "20"
60
+ html.number(1000000.5) # "1,000,000.5"
61
+
62
+ # Currency
63
+ html.number(1234.56, precision=2, prefix="$") # "$1,234.56"
64
+
65
+ # Compact notation
66
+ html.number(1500, compact=True, prefix="$") # "$1.5K"
67
+ html.number(2500000, compact=True, prefix="$") # "$2.5M"
68
+ html.number(3500000000, compact=True, prefix="$") # "$3.5B"
69
+ html.number(1200000000000, compact=True, prefix="$") # "$1.2T"
70
+
71
+ # Very small numbers
72
+ html.number(0.0000001, precision=8, min_threshold=1e-8) # "< 0.00000001"
73
+
74
+ # Scientific notation handling
75
+ html.number("0E-18", precision=8) # "0"
76
+
77
+ Returns:
78
+ SafeString with formatted number
79
+ """
80
+ from decimal import Decimal, InvalidOperation
81
+
82
+ # Handle None/empty
83
+ if value is None or value == "":
84
+ return FormattingElements.empty("—")
85
+
86
+ # Convert to Decimal for precise calculations
87
+ try:
88
+ if isinstance(value, str):
89
+ # Handle scientific notation in strings
90
+ decimal_value = Decimal(value)
91
+ elif isinstance(value, (int, float)):
92
+ decimal_value = Decimal(str(value))
93
+ elif isinstance(value, Decimal):
94
+ decimal_value = value
95
+ else:
96
+ return FormattingElements.empty(str(value))
97
+ except (InvalidOperation, ValueError):
98
+ return FormattingElements.empty(str(value))
99
+
100
+ # Check if value is effectively zero (scientific notation like 0E-18)
101
+ if decimal_value == 0:
102
+ formatted = "0"
103
+ else:
104
+ # Check min_threshold for very small positive numbers
105
+ if min_threshold and 0 < abs(decimal_value) < min_threshold:
106
+ threshold_str = f"{min_threshold:.{precision}f}"
107
+ if strip_zeros:
108
+ threshold_str = threshold_str.rstrip('0').rstrip('.')
109
+ return format_html(
110
+ '<span class="{}">{}< {}{}</span>',
111
+ css_class,
112
+ escape(prefix),
113
+ threshold_str,
114
+ escape(suffix)
115
+ )
116
+
117
+ # Compact notation with K/M/B/T suffixes
118
+ if compact:
119
+ abs_value = abs(float(decimal_value))
120
+ is_negative = decimal_value < 0
121
+ compact_suffix = ""
122
+
123
+ # Determine divisor and suffix
124
+ if abs_value >= 1_000_000_000_000: # Trillion
125
+ divided_value = abs_value / 1_000_000_000_000
126
+ compact_suffix = "T"
127
+ elif abs_value >= 1_000_000_000: # Billion
128
+ divided_value = abs_value / 1_000_000_000
129
+ compact_suffix = "B"
130
+ elif abs_value >= 1_000_000: # Million
131
+ divided_value = abs_value / 1_000_000
132
+ compact_suffix = "M"
133
+ elif abs_value >= 1_000: # Thousand
134
+ divided_value = abs_value / 1_000
135
+ compact_suffix = "K"
136
+ else:
137
+ # Below 1000, use normal formatting
138
+ divided_value = abs_value
139
+ compact_suffix = ""
140
+
141
+ # Format with precision (use 1 decimal for compact)
142
+ compact_precision = 1 if compact_suffix else precision
143
+ formatted = f"{divided_value:.{compact_precision}f}"
144
+
145
+ # Strip trailing zeros if requested
146
+ if strip_zeros:
147
+ formatted = formatted.rstrip('0').rstrip('.')
148
+
149
+ # Add negative sign back
150
+ if is_negative:
151
+ formatted = f"-{formatted}"
152
+
153
+ # Add compact suffix
154
+ formatted += compact_suffix
155
+ else:
156
+ # Format with precision
157
+ formatted = f"{decimal_value:.{precision}f}"
158
+
159
+ # Strip trailing zeros if requested (only for non-compact)
160
+ if strip_zeros:
161
+ formatted = formatted.rstrip('0').rstrip('.')
162
+
163
+ # Add thousands separator (only for non-compact)
164
+ if thousands_separator:
165
+ parts = formatted.split('.')
166
+ integer_part = parts[0]
167
+ decimal_part = parts[1] if len(parts) > 1 else None
168
+
169
+ # Handle negative sign
170
+ is_negative = integer_part.startswith('-')
171
+ if is_negative:
172
+ integer_part = integer_part[1:]
173
+
174
+ # Add commas
175
+ integer_part = f"{int(integer_part):,}"
176
+
177
+ # Restore negative sign
178
+ if is_negative:
179
+ integer_part = f"-{integer_part}"
180
+
181
+ # Rebuild number
182
+ formatted = integer_part
183
+ if decimal_part:
184
+ formatted += f".{decimal_part}"
185
+
186
+ # Add prefix/suffix
187
+ result = f"{prefix}{formatted}{suffix}"
188
+
189
+ # Wrap in span with CSS class if provided
190
+ if css_class:
191
+ return format_html('<span class="{}">{}</span>', css_class, result)
192
+
193
+ return format_html('<span>{}</span>', result)
194
+
195
+ @staticmethod
196
+ def uuid_short(uuid_value: Any, length: int = 6, show_tooltip: bool = True) -> SafeString:
197
+ """
198
+ Shorten UUID to first N characters with optional tooltip.
199
+
200
+ Args:
201
+ uuid_value: UUID string or UUID object
202
+ length: Number of characters to show (default: 6)
203
+ show_tooltip: Show full UUID on hover (default: True)
204
+
205
+ Usage:
206
+ html.uuid_short(obj.id) # "a1b2c3..."
207
+ html.uuid_short(obj.id, length=8) # "a1b2c3d4..."
208
+ html.uuid_short(obj.id, show_tooltip=False) # Just short version
209
+
210
+ Returns:
211
+ SafeString with shortened UUID
212
+ """
213
+ uuid_str = str(uuid_value)
214
+
215
+ # Remove dashes for cleaner display
216
+ uuid_clean = uuid_str.replace('-', '')
217
+
218
+ # Take first N characters
219
+ short_uuid = uuid_clean[:length]
220
+
221
+ if show_tooltip:
222
+ return format_html(
223
+ '<code class="font-mono text-xs bg-base-100 dark:bg-base-800 px-1.5 py-0.5 rounded cursor-help" title="{}">{}</code>',
224
+ uuid_str,
225
+ short_uuid
226
+ )
227
+
228
+ return format_html(
229
+ '<code class="font-mono text-xs bg-base-100 dark:bg-base-800 px-1.5 py-0.5 rounded">{}</code>',
230
+ short_uuid
231
+ )