django-nativemojo 0.1.10__py3-none-any.whl → 0.1.15__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.
- django_nativemojo-0.1.15.dist-info/METADATA +136 -0
- {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.15.dist-info}/RECORD +105 -65
- mojo/__init__.py +1 -1
- mojo/apps/account/management/__init__.py +5 -0
- mojo/apps/account/management/commands/__init__.py +6 -0
- mojo/apps/account/management/commands/serializer_admin.py +531 -0
- mojo/apps/account/migrations/0004_user_avatar.py +20 -0
- mojo/apps/account/migrations/0005_group_last_activity.py +18 -0
- mojo/apps/account/models/group.py +25 -7
- mojo/apps/account/models/member.py +15 -4
- mojo/apps/account/models/user.py +197 -20
- mojo/apps/account/rest/group.py +1 -0
- mojo/apps/account/rest/user.py +6 -2
- mojo/apps/aws/rest/__init__.py +1 -0
- mojo/apps/aws/rest/s3.py +64 -0
- mojo/apps/fileman/README.md +8 -8
- mojo/apps/fileman/backends/base.py +76 -70
- mojo/apps/fileman/backends/filesystem.py +86 -86
- mojo/apps/fileman/backends/s3.py +200 -108
- mojo/apps/fileman/migrations/0001_initial.py +106 -0
- mojo/apps/fileman/migrations/0002_filemanager_parent_alter_filemanager_max_file_size.py +24 -0
- mojo/apps/fileman/migrations/0003_remove_file_fileman_fil_upload__c4bc35_idx_and_more.py +25 -0
- mojo/apps/fileman/migrations/0004_remove_file_original_filename_and_more.py +39 -0
- mojo/apps/fileman/migrations/0005_alter_file_upload_token.py +18 -0
- mojo/apps/fileman/migrations/0006_file_download_url_filemanager_forever_urls.py +23 -0
- mojo/apps/fileman/migrations/0007_remove_filemanager_forever_urls_and_more.py +22 -0
- mojo/apps/fileman/migrations/0008_file_category.py +18 -0
- mojo/apps/fileman/migrations/0009_rename_file_path_file_storage_file_path.py +18 -0
- mojo/apps/fileman/migrations/0010_filerendition.py +33 -0
- mojo/apps/fileman/migrations/0011_alter_filerendition_original_file.py +19 -0
- mojo/apps/fileman/models/__init__.py +1 -5
- mojo/apps/fileman/models/file.py +204 -58
- mojo/apps/fileman/models/manager.py +161 -31
- mojo/apps/fileman/models/rendition.py +118 -0
- mojo/apps/fileman/renderer/__init__.py +111 -0
- mojo/apps/fileman/renderer/audio.py +403 -0
- mojo/apps/fileman/renderer/base.py +205 -0
- mojo/apps/fileman/renderer/document.py +404 -0
- mojo/apps/fileman/renderer/image.py +222 -0
- mojo/apps/fileman/renderer/utils.py +297 -0
- mojo/apps/fileman/renderer/video.py +304 -0
- mojo/apps/fileman/rest/__init__.py +1 -18
- mojo/apps/fileman/rest/upload.py +22 -32
- mojo/apps/fileman/signals.py +58 -0
- mojo/apps/fileman/tasks.py +254 -0
- mojo/apps/fileman/utils/__init__.py +40 -16
- mojo/apps/incident/migrations/0005_incidenthistory.py +39 -0
- mojo/apps/incident/migrations/0006_alter_incident_state.py +18 -0
- mojo/apps/incident/models/__init__.py +1 -0
- mojo/apps/incident/models/history.py +36 -0
- mojo/apps/incident/models/incident.py +1 -1
- mojo/apps/incident/reporter.py +3 -1
- mojo/apps/incident/rest/event.py +7 -1
- mojo/apps/logit/migrations/0004_alter_log_level.py +18 -0
- mojo/apps/logit/models/log.py +4 -1
- mojo/apps/metrics/utils.py +2 -2
- mojo/apps/notify/handlers/ses/message.py +1 -1
- mojo/apps/notify/providers/aws.py +2 -2
- mojo/apps/tasks/__init__.py +34 -1
- mojo/apps/tasks/manager.py +200 -45
- mojo/apps/tasks/rest/tasks.py +24 -10
- mojo/apps/tasks/runner.py +283 -18
- mojo/apps/tasks/task.py +99 -0
- mojo/apps/tasks/tq_handlers.py +118 -0
- mojo/decorators/auth.py +6 -1
- mojo/decorators/http.py +7 -2
- mojo/helpers/aws/__init__.py +41 -0
- mojo/helpers/aws/ec2.py +804 -0
- mojo/helpers/aws/iam.py +748 -0
- mojo/helpers/aws/s3.py +451 -11
- mojo/helpers/aws/ses.py +483 -0
- mojo/helpers/aws/sns.py +461 -0
- mojo/helpers/crypto/__pycache__/hash.cpython-310.pyc +0 -0
- mojo/helpers/crypto/__pycache__/sign.cpython-310.pyc +0 -0
- mojo/helpers/crypto/__pycache__/utils.cpython-310.pyc +0 -0
- mojo/helpers/dates.py +18 -0
- mojo/helpers/response.py +6 -2
- mojo/helpers/settings/__init__.py +2 -0
- mojo/helpers/{settings.py → settings/helper.py} +1 -37
- mojo/helpers/settings/parser.py +132 -0
- mojo/middleware/logging.py +1 -1
- mojo/middleware/mojo.py +5 -0
- mojo/models/rest.py +261 -46
- mojo/models/secrets.py +13 -4
- mojo/serializers/__init__.py +100 -0
- mojo/serializers/advanced/README.md +363 -0
- mojo/serializers/advanced/__init__.py +247 -0
- mojo/serializers/advanced/formats/__init__.py +28 -0
- mojo/serializers/advanced/formats/csv.py +416 -0
- mojo/serializers/advanced/formats/excel.py +516 -0
- mojo/serializers/advanced/formats/json.py +239 -0
- mojo/serializers/advanced/formats/localizers.py +509 -0
- mojo/serializers/advanced/formats/response.py +485 -0
- mojo/serializers/advanced/serializer.py +568 -0
- mojo/serializers/manager.py +501 -0
- mojo/serializers/optimized.py +618 -0
- mojo/serializers/settings_example.py +322 -0
- mojo/serializers/{models.py → simple.py} +38 -15
- testit/helpers.py +21 -4
- django_nativemojo-0.1.10.dist-info/METADATA +0 -96
- mojo/apps/metrics/rest/db.py +0 -0
- mojo/helpers/aws/setup_email.py +0 -0
- mojo/ws4redis/README.md +0 -174
- mojo/ws4redis/__init__.py +0 -2
- mojo/ws4redis/client.py +0 -283
- mojo/ws4redis/connection.py +0 -327
- mojo/ws4redis/exceptions.py +0 -32
- mojo/ws4redis/redis.py +0 -183
- mojo/ws4redis/servers/base.py +0 -86
- mojo/ws4redis/servers/django.py +0 -171
- mojo/ws4redis/servers/uwsgi.py +0 -63
- mojo/ws4redis/settings.py +0 -45
- mojo/ws4redis/utf8validator.py +0 -128
- mojo/ws4redis/websocket.py +0 -403
- {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.15.dist-info}/LICENSE +0 -0
- {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.15.dist-info}/NOTICE +0 -0
- {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.15.dist-info}/WHEEL +0 -0
- /mojo/{ws4redis/servers → apps/aws}/__init__.py +0 -0
- /mojo/apps/{fileman/models/render.py → aws/models/__init__.py} +0 -0
- /mojo/apps/fileman/{rest/__init__ → migrations/__init__.py} +0 -0
@@ -0,0 +1,509 @@
|
|
1
|
+
from decimal import Decimal
|
2
|
+
from datetime import datetime, date, time
|
3
|
+
import locale
|
4
|
+
from mojo.helpers import logit
|
5
|
+
|
6
|
+
logger = logit.get_logger("localizers", "localizers.log")
|
7
|
+
|
8
|
+
# Registry of available localizers
|
9
|
+
LOCALIZER_REGISTRY = {}
|
10
|
+
|
11
|
+
|
12
|
+
def register_localizer(name, func):
|
13
|
+
"""
|
14
|
+
Register a localizer function.
|
15
|
+
|
16
|
+
:param name: Name to register the localizer under
|
17
|
+
:param func: Localizer function that takes (value, extra=None)
|
18
|
+
"""
|
19
|
+
LOCALIZER_REGISTRY[name] = func
|
20
|
+
logger.debug(f"Registered localizer: {name}")
|
21
|
+
|
22
|
+
|
23
|
+
def get_localizer(name):
|
24
|
+
"""
|
25
|
+
Get a localizer function by name.
|
26
|
+
|
27
|
+
:param name: Localizer name
|
28
|
+
:return: Localizer function or None
|
29
|
+
"""
|
30
|
+
return LOCALIZER_REGISTRY.get(name)
|
31
|
+
|
32
|
+
|
33
|
+
def list_localizers():
|
34
|
+
"""
|
35
|
+
Get list of all registered localizer names.
|
36
|
+
|
37
|
+
:return: List of localizer names
|
38
|
+
"""
|
39
|
+
return list(LOCALIZER_REGISTRY.keys())
|
40
|
+
|
41
|
+
|
42
|
+
# Currency localizers
|
43
|
+
def cents_to_currency(value, extra=None):
|
44
|
+
"""
|
45
|
+
Convert cents to currency format.
|
46
|
+
|
47
|
+
:param value: Value in cents
|
48
|
+
:param extra: Currency symbol (default: no symbol)
|
49
|
+
:return: Formatted currency string
|
50
|
+
"""
|
51
|
+
if value is None:
|
52
|
+
return "0.00"
|
53
|
+
|
54
|
+
try:
|
55
|
+
currency, cents = divmod(int(value), 100)
|
56
|
+
if extra:
|
57
|
+
return f"{extra}{currency}.{cents:02d}"
|
58
|
+
return f"{currency}.{cents:02d}"
|
59
|
+
except (ValueError, TypeError):
|
60
|
+
return str(value)
|
61
|
+
|
62
|
+
|
63
|
+
def cents_to_dollars(value, extra=None):
|
64
|
+
"""
|
65
|
+
Convert cents to dollar format.
|
66
|
+
|
67
|
+
:param value: Value in cents
|
68
|
+
:param extra: Not used
|
69
|
+
:return: Dollar-formatted string
|
70
|
+
"""
|
71
|
+
if value is None:
|
72
|
+
return "$0.00"
|
73
|
+
|
74
|
+
try:
|
75
|
+
dollars, cents = divmod(int(value), 100)
|
76
|
+
return f"${dollars}.{cents:02d}"
|
77
|
+
except (ValueError, TypeError):
|
78
|
+
return str(value)
|
79
|
+
|
80
|
+
|
81
|
+
def currency_format(value, extra="$"):
|
82
|
+
"""
|
83
|
+
Format value as currency with specified symbol.
|
84
|
+
|
85
|
+
:param value: Numeric value
|
86
|
+
:param extra: Currency symbol
|
87
|
+
:return: Formatted currency string
|
88
|
+
"""
|
89
|
+
if value is None:
|
90
|
+
return f"{extra}0.00"
|
91
|
+
|
92
|
+
try:
|
93
|
+
if isinstance(value, (int, float, Decimal)):
|
94
|
+
return f"{extra}{value:.2f}"
|
95
|
+
return f"{extra}{float(value):.2f}"
|
96
|
+
except (ValueError, TypeError):
|
97
|
+
return str(value)
|
98
|
+
|
99
|
+
|
100
|
+
# Date/time localizers
|
101
|
+
def date_format(value, extra="%Y-%m-%d"):
|
102
|
+
"""
|
103
|
+
Format date with specified format.
|
104
|
+
|
105
|
+
:param value: Date value
|
106
|
+
:param extra: Date format string
|
107
|
+
:return: Formatted date string
|
108
|
+
"""
|
109
|
+
if value is None:
|
110
|
+
return ""
|
111
|
+
|
112
|
+
try:
|
113
|
+
if isinstance(value, datetime):
|
114
|
+
return value.strftime(extra)
|
115
|
+
elif isinstance(value, date):
|
116
|
+
return value.strftime(extra)
|
117
|
+
elif isinstance(value, str):
|
118
|
+
# Try to parse string date
|
119
|
+
parsed_date = datetime.fromisoformat(value.replace('Z', '+00:00'))
|
120
|
+
return parsed_date.strftime(extra)
|
121
|
+
return str(value)
|
122
|
+
except (ValueError, AttributeError):
|
123
|
+
return str(value)
|
124
|
+
|
125
|
+
|
126
|
+
def datetime_format(value, extra="%Y-%m-%d %H:%M:%S"):
|
127
|
+
"""
|
128
|
+
Format datetime with specified format.
|
129
|
+
|
130
|
+
:param value: Datetime value
|
131
|
+
:param extra: Datetime format string
|
132
|
+
:return: Formatted datetime string
|
133
|
+
"""
|
134
|
+
if value is None:
|
135
|
+
return ""
|
136
|
+
|
137
|
+
try:
|
138
|
+
if isinstance(value, datetime):
|
139
|
+
return value.strftime(extra)
|
140
|
+
elif isinstance(value, str):
|
141
|
+
# Try to parse string datetime
|
142
|
+
parsed_datetime = datetime.fromisoformat(value.replace('Z', '+00:00'))
|
143
|
+
return parsed_datetime.strftime(extra)
|
144
|
+
return str(value)
|
145
|
+
except (ValueError, AttributeError):
|
146
|
+
return str(value)
|
147
|
+
|
148
|
+
|
149
|
+
def time_format(value, extra="%H:%M:%S"):
|
150
|
+
"""
|
151
|
+
Format time with specified format.
|
152
|
+
|
153
|
+
:param value: Time value
|
154
|
+
:param extra: Time format string
|
155
|
+
:return: Formatted time string
|
156
|
+
"""
|
157
|
+
if value is None:
|
158
|
+
return ""
|
159
|
+
|
160
|
+
try:
|
161
|
+
if isinstance(value, time):
|
162
|
+
return value.strftime(extra)
|
163
|
+
elif isinstance(value, datetime):
|
164
|
+
return value.time().strftime(extra)
|
165
|
+
return str(value)
|
166
|
+
except (ValueError, AttributeError):
|
167
|
+
return str(value)
|
168
|
+
|
169
|
+
|
170
|
+
def timezone_format(value, extra=None):
|
171
|
+
"""
|
172
|
+
Format datetime with timezone information.
|
173
|
+
|
174
|
+
:param value: Datetime value
|
175
|
+
:param extra: Timezone name (optional)
|
176
|
+
:return: Formatted datetime with timezone
|
177
|
+
"""
|
178
|
+
if value is None:
|
179
|
+
return ""
|
180
|
+
|
181
|
+
try:
|
182
|
+
# Try to import timezone helper
|
183
|
+
try:
|
184
|
+
from mojo.helpers import dates
|
185
|
+
if extra:
|
186
|
+
localized_value = dates.get_local_time(extra, value)
|
187
|
+
return localized_value.strftime("%Y-%m-%d %H:%M:%S %Z")
|
188
|
+
elif hasattr(value, 'strftime'):
|
189
|
+
return value.strftime("%Y-%m-%d %H:%M:%S %Z")
|
190
|
+
except ImportError:
|
191
|
+
logger.warning("mojo.helpers.dates not available for timezone formatting")
|
192
|
+
|
193
|
+
if hasattr(value, 'strftime'):
|
194
|
+
return value.strftime("%Y-%m-%d %H:%M:%S")
|
195
|
+
return str(value)
|
196
|
+
except Exception as e:
|
197
|
+
logger.warning(f"Timezone formatting failed: {e}")
|
198
|
+
return str(value)
|
199
|
+
|
200
|
+
|
201
|
+
# Number formatters
|
202
|
+
def number_format(value, extra="2"):
|
203
|
+
"""
|
204
|
+
Format number with specified decimal places.
|
205
|
+
|
206
|
+
:param value: Numeric value
|
207
|
+
:param extra: Number of decimal places (default: 2)
|
208
|
+
:return: Formatted number string
|
209
|
+
"""
|
210
|
+
if value is None:
|
211
|
+
return "0"
|
212
|
+
|
213
|
+
try:
|
214
|
+
decimal_places = int(extra) if extra else 2
|
215
|
+
if isinstance(value, (int, float, Decimal)):
|
216
|
+
return f"{float(value):.{decimal_places}f}"
|
217
|
+
return f"{float(value):.{decimal_places}f}"
|
218
|
+
except (ValueError, TypeError):
|
219
|
+
return str(value)
|
220
|
+
|
221
|
+
|
222
|
+
def percentage_format(value, extra="2"):
|
223
|
+
"""
|
224
|
+
Format value as percentage.
|
225
|
+
|
226
|
+
:param value: Numeric value (0.1 = 10%)
|
227
|
+
:param extra: Number of decimal places
|
228
|
+
:return: Formatted percentage string
|
229
|
+
"""
|
230
|
+
if value is None:
|
231
|
+
return "0%"
|
232
|
+
|
233
|
+
try:
|
234
|
+
decimal_places = int(extra) if extra else 2
|
235
|
+
percentage = float(value) * 100
|
236
|
+
return f"{percentage:.{decimal_places}f}%"
|
237
|
+
except (ValueError, TypeError):
|
238
|
+
return str(value)
|
239
|
+
|
240
|
+
|
241
|
+
def thousands_separator(value, extra=","):
|
242
|
+
"""
|
243
|
+
Add thousands separator to number.
|
244
|
+
|
245
|
+
:param value: Numeric value
|
246
|
+
:param extra: Separator character (default: comma)
|
247
|
+
:return: Formatted number with separators
|
248
|
+
"""
|
249
|
+
if value is None:
|
250
|
+
return "0"
|
251
|
+
|
252
|
+
try:
|
253
|
+
separator = extra if extra else ","
|
254
|
+
return f"{int(value):,}".replace(",", separator)
|
255
|
+
except (ValueError, TypeError):
|
256
|
+
return str(value)
|
257
|
+
|
258
|
+
|
259
|
+
# Text formatters
|
260
|
+
def title_case(value, extra=None):
|
261
|
+
"""
|
262
|
+
Convert text to title case.
|
263
|
+
|
264
|
+
:param value: Text value
|
265
|
+
:param extra: Not used
|
266
|
+
:return: Title case text
|
267
|
+
"""
|
268
|
+
if value is None:
|
269
|
+
return ""
|
270
|
+
|
271
|
+
return str(value).title()
|
272
|
+
|
273
|
+
|
274
|
+
def upper_case(value, extra=None):
|
275
|
+
"""
|
276
|
+
Convert text to upper case.
|
277
|
+
|
278
|
+
:param value: Text value
|
279
|
+
:param extra: Not used
|
280
|
+
:return: Upper case text
|
281
|
+
"""
|
282
|
+
if value is None:
|
283
|
+
return ""
|
284
|
+
|
285
|
+
return str(value).upper()
|
286
|
+
|
287
|
+
|
288
|
+
def lower_case(value, extra=None):
|
289
|
+
"""
|
290
|
+
Convert text to lower case.
|
291
|
+
|
292
|
+
:param value: Text value
|
293
|
+
:param extra: Not used
|
294
|
+
:return: Lower case text
|
295
|
+
"""
|
296
|
+
if value is None:
|
297
|
+
return ""
|
298
|
+
|
299
|
+
return str(value).lower()
|
300
|
+
|
301
|
+
|
302
|
+
def truncate_text(value, extra="50"):
|
303
|
+
"""
|
304
|
+
Truncate text to specified length.
|
305
|
+
|
306
|
+
:param value: Text value
|
307
|
+
:param extra: Maximum length (default: 50)
|
308
|
+
:return: Truncated text
|
309
|
+
"""
|
310
|
+
if value is None:
|
311
|
+
return ""
|
312
|
+
|
313
|
+
try:
|
314
|
+
max_length = int(extra) if extra else 50
|
315
|
+
text = str(value)
|
316
|
+
if len(text) <= max_length:
|
317
|
+
return text
|
318
|
+
return text[:max_length-3] + "..."
|
319
|
+
except (ValueError, TypeError):
|
320
|
+
return str(value)
|
321
|
+
|
322
|
+
|
323
|
+
# Boolean formatters
|
324
|
+
def yes_no(value, extra=None):
|
325
|
+
"""
|
326
|
+
Convert boolean to Yes/No.
|
327
|
+
|
328
|
+
:param value: Boolean value
|
329
|
+
:param extra: Not used
|
330
|
+
:return: "Yes" or "No"
|
331
|
+
"""
|
332
|
+
if value is None:
|
333
|
+
return "No"
|
334
|
+
|
335
|
+
return "Yes" if bool(value) else "No"
|
336
|
+
|
337
|
+
|
338
|
+
def true_false(value, extra=None):
|
339
|
+
"""
|
340
|
+
Convert boolean to True/False.
|
341
|
+
|
342
|
+
:param value: Boolean value
|
343
|
+
:param extra: Not used
|
344
|
+
:return: "True" or "False"
|
345
|
+
"""
|
346
|
+
if value is None:
|
347
|
+
return "False"
|
348
|
+
|
349
|
+
return "True" if bool(value) else "False"
|
350
|
+
|
351
|
+
|
352
|
+
def on_off(value, extra=None):
|
353
|
+
"""
|
354
|
+
Convert boolean to On/Off.
|
355
|
+
|
356
|
+
:param value: Boolean value
|
357
|
+
:param extra: Not used
|
358
|
+
:return: "On" or "Off"
|
359
|
+
"""
|
360
|
+
if value is None:
|
361
|
+
return "Off"
|
362
|
+
|
363
|
+
return "On" if bool(value) else "Off"
|
364
|
+
|
365
|
+
|
366
|
+
# List/collection formatters
|
367
|
+
def join_list(value, extra=", "):
|
368
|
+
"""
|
369
|
+
Join list items with separator.
|
370
|
+
|
371
|
+
:param value: List or iterable
|
372
|
+
:param extra: Separator string (default: ", ")
|
373
|
+
:return: Joined string
|
374
|
+
"""
|
375
|
+
if value is None:
|
376
|
+
return ""
|
377
|
+
|
378
|
+
try:
|
379
|
+
separator = extra if extra else ", "
|
380
|
+
if isinstance(value, (list, tuple)):
|
381
|
+
return separator.join(str(item) for item in value)
|
382
|
+
return str(value)
|
383
|
+
except Exception:
|
384
|
+
return str(value)
|
385
|
+
|
386
|
+
|
387
|
+
def list_count(value, extra=None):
|
388
|
+
"""
|
389
|
+
Return count of items in list.
|
390
|
+
|
391
|
+
:param value: List or iterable
|
392
|
+
:param extra: Not used
|
393
|
+
:return: Count as string
|
394
|
+
"""
|
395
|
+
if value is None:
|
396
|
+
return "0"
|
397
|
+
|
398
|
+
try:
|
399
|
+
if hasattr(value, '__len__'):
|
400
|
+
return str(len(value))
|
401
|
+
elif hasattr(value, 'count'):
|
402
|
+
return str(value.count())
|
403
|
+
return "1"
|
404
|
+
except Exception:
|
405
|
+
return "0"
|
406
|
+
|
407
|
+
|
408
|
+
# File size formatter
|
409
|
+
def file_size(value, extra="auto"):
|
410
|
+
"""
|
411
|
+
Format file size in human readable format.
|
412
|
+
|
413
|
+
:param value: File size in bytes
|
414
|
+
:param extra: Unit preference ("auto", "KB", "MB", "GB")
|
415
|
+
:return: Formatted file size
|
416
|
+
"""
|
417
|
+
if value is None:
|
418
|
+
return "0 B"
|
419
|
+
|
420
|
+
try:
|
421
|
+
size = float(value)
|
422
|
+
if extra != "auto":
|
423
|
+
unit = extra.upper()
|
424
|
+
if unit == "KB":
|
425
|
+
return f"{size/1024:.2f} KB"
|
426
|
+
elif unit == "MB":
|
427
|
+
return f"{size/(1024**2):.2f} MB"
|
428
|
+
elif unit == "GB":
|
429
|
+
return f"{size/(1024**3):.2f} GB"
|
430
|
+
|
431
|
+
# Auto-detect best unit
|
432
|
+
if size < 1024:
|
433
|
+
return f"{size:.0f} B"
|
434
|
+
elif size < 1024**2:
|
435
|
+
return f"{size/1024:.2f} KB"
|
436
|
+
elif size < 1024**3:
|
437
|
+
return f"{size/(1024**2):.2f} MB"
|
438
|
+
else:
|
439
|
+
return f"{size/(1024**3):.2f} GB"
|
440
|
+
except (ValueError, TypeError):
|
441
|
+
return str(value)
|
442
|
+
|
443
|
+
|
444
|
+
# Register all localizers
|
445
|
+
register_localizer('cents_to_currency', cents_to_currency)
|
446
|
+
register_localizer('cents_to_dollars', cents_to_dollars)
|
447
|
+
register_localizer('currency', currency_format)
|
448
|
+
register_localizer('date', date_format)
|
449
|
+
register_localizer('datetime', datetime_format)
|
450
|
+
register_localizer('time', time_format)
|
451
|
+
register_localizer('timezone', timezone_format)
|
452
|
+
register_localizer('number', number_format)
|
453
|
+
register_localizer('percentage', percentage_format)
|
454
|
+
register_localizer('thousands', thousands_separator)
|
455
|
+
register_localizer('title', title_case)
|
456
|
+
register_localizer('upper', upper_case)
|
457
|
+
register_localizer('lower', lower_case)
|
458
|
+
register_localizer('truncate', truncate_text)
|
459
|
+
register_localizer('yes_no', yes_no)
|
460
|
+
register_localizer('true_false', true_false)
|
461
|
+
register_localizer('on_off', on_off)
|
462
|
+
register_localizer('join', join_list)
|
463
|
+
register_localizer('count', list_count)
|
464
|
+
register_localizer('filesize', file_size)
|
465
|
+
|
466
|
+
|
467
|
+
# Custom localizer decorator
|
468
|
+
def localizer(name):
|
469
|
+
"""
|
470
|
+
Decorator to register a custom localizer.
|
471
|
+
|
472
|
+
Usage:
|
473
|
+
@localizer('my_formatter')
|
474
|
+
def my_custom_formatter(value, extra=None):
|
475
|
+
return f"Custom: {value}"
|
476
|
+
"""
|
477
|
+
def decorator(func):
|
478
|
+
register_localizer(name, func)
|
479
|
+
return func
|
480
|
+
return decorator
|
481
|
+
|
482
|
+
|
483
|
+
# Legacy support
|
484
|
+
def apply_localizer(value, localizer_config):
|
485
|
+
"""
|
486
|
+
Apply localizer based on configuration string.
|
487
|
+
|
488
|
+
:param value: Value to localize
|
489
|
+
:param localizer_config: Configuration string like "currency|$" or "date|%Y-%m-%d"
|
490
|
+
:return: Localized value
|
491
|
+
"""
|
492
|
+
if not localizer_config:
|
493
|
+
return value
|
494
|
+
|
495
|
+
try:
|
496
|
+
if '|' in localizer_config:
|
497
|
+
localizer_name, extra = localizer_config.split('|', 1)
|
498
|
+
else:
|
499
|
+
localizer_name, extra = localizer_config, None
|
500
|
+
|
501
|
+
localizer_func = get_localizer(localizer_name)
|
502
|
+
if localizer_func:
|
503
|
+
return localizer_func(value, extra)
|
504
|
+
else:
|
505
|
+
logger.warning(f"Unknown localizer: {localizer_name}")
|
506
|
+
return value
|
507
|
+
except Exception as e:
|
508
|
+
logger.error(f"Localizer error for '{localizer_config}': {e}")
|
509
|
+
return value
|