django-cfg 1.5.20__py3-none-any.whl → 1.5.29__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 (88) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/integrations/centrifugo/__init__.py +2 -0
  3. django_cfg/apps/integrations/centrifugo/services/client/client.py +1 -1
  4. django_cfg/apps/integrations/centrifugo/services/logging.py +47 -0
  5. django_cfg/apps/integrations/centrifugo/views/admin_api.py +29 -32
  6. django_cfg/apps/integrations/centrifugo/views/testing_api.py +31 -37
  7. django_cfg/apps/integrations/centrifugo/views/wrapper.py +25 -23
  8. django_cfg/apps/integrations/grpc/auth/api_key_auth.py +11 -10
  9. django_cfg/apps/integrations/grpc/management/commands/generate_protos.py +1 -1
  10. django_cfg/apps/integrations/grpc/management/commands/rungrpc.py +21 -36
  11. django_cfg/apps/integrations/grpc/managers/grpc_request_log.py +84 -0
  12. django_cfg/apps/integrations/grpc/managers/grpc_server_status.py +126 -3
  13. django_cfg/apps/integrations/grpc/models/grpc_api_key.py +7 -1
  14. django_cfg/apps/integrations/grpc/models/grpc_server_status.py +22 -3
  15. django_cfg/apps/integrations/grpc/services/__init__.py +102 -17
  16. django_cfg/apps/integrations/grpc/services/centrifugo/bridge.py +469 -0
  17. django_cfg/apps/integrations/grpc/{centrifugo → services/centrifugo}/demo.py +1 -1
  18. django_cfg/apps/integrations/grpc/{centrifugo → services/centrifugo}/test_publish.py +4 -4
  19. django_cfg/apps/integrations/grpc/services/client/__init__.py +26 -0
  20. django_cfg/apps/integrations/grpc/services/commands/IMPLEMENTATION.md +456 -0
  21. django_cfg/apps/integrations/grpc/services/commands/README.md +252 -0
  22. django_cfg/apps/integrations/grpc/services/commands/__init__.py +93 -0
  23. django_cfg/apps/integrations/grpc/services/commands/base.py +243 -0
  24. django_cfg/apps/integrations/grpc/services/commands/examples/__init__.py +22 -0
  25. django_cfg/apps/integrations/grpc/services/commands/examples/base_client.py +228 -0
  26. django_cfg/apps/integrations/grpc/services/commands/examples/client.py +272 -0
  27. django_cfg/apps/integrations/grpc/services/commands/examples/config.py +177 -0
  28. django_cfg/apps/integrations/grpc/services/commands/examples/start.py +125 -0
  29. django_cfg/apps/integrations/grpc/services/commands/examples/stop.py +101 -0
  30. django_cfg/apps/integrations/grpc/services/commands/registry.py +170 -0
  31. django_cfg/apps/integrations/grpc/services/discovery/__init__.py +39 -0
  32. django_cfg/apps/integrations/grpc/services/{discovery.py → discovery/discovery.py} +62 -55
  33. django_cfg/apps/integrations/grpc/services/{service_registry.py → discovery/registry.py} +215 -5
  34. django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/metrics.py +3 -3
  35. django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/request_logger.py +10 -13
  36. django_cfg/apps/integrations/grpc/services/management/__init__.py +37 -0
  37. django_cfg/apps/integrations/grpc/services/monitoring/__init__.py +38 -0
  38. django_cfg/apps/integrations/grpc/services/{monitoring_service.py → monitoring/monitoring.py} +2 -2
  39. django_cfg/apps/integrations/grpc/services/{testing_service.py → monitoring/testing.py} +5 -5
  40. django_cfg/apps/integrations/grpc/services/rendering/__init__.py +27 -0
  41. django_cfg/apps/integrations/grpc/services/{chart_generator.py → rendering/charts.py} +1 -1
  42. django_cfg/apps/integrations/grpc/services/routing/__init__.py +59 -0
  43. django_cfg/apps/integrations/grpc/services/routing/config.py +76 -0
  44. django_cfg/apps/integrations/grpc/services/routing/router.py +430 -0
  45. django_cfg/apps/integrations/grpc/services/streaming/__init__.py +117 -0
  46. django_cfg/apps/integrations/grpc/services/streaming/config.py +451 -0
  47. django_cfg/apps/integrations/grpc/services/streaming/service.py +651 -0
  48. django_cfg/apps/integrations/grpc/services/streaming/types.py +367 -0
  49. django_cfg/apps/integrations/grpc/utils/__init__.py +58 -1
  50. django_cfg/apps/integrations/grpc/utils/converters.py +565 -0
  51. django_cfg/apps/integrations/grpc/utils/handlers.py +242 -0
  52. django_cfg/apps/integrations/grpc/utils/proto_gen.py +1 -1
  53. django_cfg/apps/integrations/grpc/utils/streaming_logger.py +55 -8
  54. django_cfg/apps/integrations/grpc/views/charts.py +1 -1
  55. django_cfg/apps/integrations/grpc/views/config.py +1 -1
  56. django_cfg/core/base/config_model.py +11 -0
  57. django_cfg/core/builders/middleware_builder.py +5 -0
  58. django_cfg/management/commands/pool_status.py +153 -0
  59. django_cfg/middleware/pool_cleanup.py +261 -0
  60. django_cfg/models/api/grpc/config.py +2 -2
  61. django_cfg/models/infrastructure/database/config.py +16 -0
  62. django_cfg/models/infrastructure/database/converters.py +2 -0
  63. django_cfg/modules/django_admin/utils/html/composition.py +57 -13
  64. django_cfg/modules/django_admin/utils/html_builder.py +1 -0
  65. django_cfg/modules/django_client/core/groups/manager.py +25 -18
  66. django_cfg/modules/django_client/management/commands/generate_client.py +9 -5
  67. django_cfg/modules/django_logging/django_logger.py +58 -19
  68. django_cfg/pyproject.toml +3 -3
  69. django_cfg/static/frontend/admin.zip +0 -0
  70. django_cfg/templates/admin/index.html +0 -39
  71. django_cfg/utils/pool_monitor.py +320 -0
  72. django_cfg/utils/smart_defaults.py +233 -7
  73. {django_cfg-1.5.20.dist-info → django_cfg-1.5.29.dist-info}/METADATA +75 -5
  74. {django_cfg-1.5.20.dist-info → django_cfg-1.5.29.dist-info}/RECORD +87 -59
  75. django_cfg/apps/integrations/grpc/centrifugo/bridge.py +0 -277
  76. /django_cfg/apps/integrations/grpc/{centrifugo → services/centrifugo}/__init__.py +0 -0
  77. /django_cfg/apps/integrations/grpc/{centrifugo → services/centrifugo}/config.py +0 -0
  78. /django_cfg/apps/integrations/grpc/{centrifugo → services/centrifugo}/transformers.py +0 -0
  79. /django_cfg/apps/integrations/grpc/services/{grpc_client.py → client/client.py} +0 -0
  80. /django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/__init__.py +0 -0
  81. /django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/centrifugo.py +0 -0
  82. /django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/errors.py +0 -0
  83. /django_cfg/apps/integrations/grpc/{interceptors → services/interceptors}/logging.py +0 -0
  84. /django_cfg/apps/integrations/grpc/services/{config_helper.py → management/config_helper.py} +0 -0
  85. /django_cfg/apps/integrations/grpc/services/{proto_files_manager.py → management/proto_manager.py} +0 -0
  86. {django_cfg-1.5.20.dist-info → django_cfg-1.5.29.dist-info}/WHEEL +0 -0
  87. {django_cfg-1.5.20.dist-info → django_cfg-1.5.29.dist-info}/entry_points.txt +0 -0
  88. {django_cfg-1.5.20.dist-info → django_cfg-1.5.29.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,565 @@
1
+ """
2
+ Universal protobuf conversion utilities with Pydantic v2 configuration.
3
+
4
+ This module provides a configurable mixin for converting between:
5
+ - Python datetime ↔ Protobuf Timestamp
6
+ - Python dict ↔ Protobuf Struct
7
+ - Protobuf messages ↔ JSON dicts
8
+
9
+ **Design Principles**:
10
+ - 100% Pydantic v2 configuration
11
+ - Type-safe conversion methods
12
+ - Async support for Django ORM
13
+ - Zero business logic (pure conversion utilities)
14
+
15
+ **Usage Example**:
16
+ ```python
17
+ from django_cfg.apps.integrations.grpc.utils.converters import (
18
+ ProtobufConverterMixin,
19
+ ConverterConfig,
20
+ )
21
+
22
+ class MyService(ProtobufConverterMixin):
23
+ def __init__(self):
24
+ self.converter_config = ConverterConfig(
25
+ preserving_proto_field_name=True,
26
+ use_integers_for_enums=False,
27
+ )
28
+
29
+ async def handle_message(self, message_pb):
30
+ # Protobuf → dict
31
+ data = self.protobuf_to_dict(message_pb)
32
+
33
+ # Create timestamp
34
+ ts = self.datetime_to_timestamp(timezone.now())
35
+
36
+ # Dict → struct
37
+ struct = self.dict_to_struct({'key': 'value'})
38
+ ```
39
+
40
+ Created: 2025-11-07
41
+ Status: %%PRODUCTION%%
42
+ Phase: Phase 1 - Universal Components
43
+ """
44
+
45
+ from typing import Dict, Any, Optional
46
+ from datetime import datetime
47
+
48
+ from pydantic import BaseModel, Field
49
+ from google.protobuf.message import Message
50
+ from google.protobuf.timestamp_pb2 import Timestamp
51
+ from google.protobuf.struct_pb2 import Struct
52
+ from google.protobuf.json_format import MessageToDict, ParseDict
53
+
54
+
55
+ # ============================================================================
56
+ # Configuration
57
+ # ============================================================================
58
+
59
+ class ConverterConfig(BaseModel):
60
+ """
61
+ Pydantic configuration for protobuf conversion behavior.
62
+
63
+ **Parameters**:
64
+ preserving_proto_field_name: Use proto field names (not camelCase)
65
+ use_integers_for_enums: Use int values for enums (not string names)
66
+ including_default_value_fields: Include fields with default values
67
+ float_precision: Decimal places for float formatting (None = no rounding)
68
+
69
+ **Example - Production Config**:
70
+ ```python
71
+ config = ConverterConfig(
72
+ preserving_proto_field_name=True, # snake_case field names
73
+ use_integers_for_enums=False, # String enum names
74
+ including_default_value_fields=True,
75
+ )
76
+ ```
77
+
78
+ **Example - Development Config**:
79
+ ```python
80
+ config = ConverterConfig(
81
+ preserving_proto_field_name=False, # camelCase for JSON APIs
82
+ use_integers_for_enums=True, # Int enums
83
+ including_default_value_fields=False,
84
+ )
85
+ ```
86
+ """
87
+
88
+ preserving_proto_field_name: bool = Field(
89
+ default=True,
90
+ description="Use proto field names (snake_case) instead of JSON names (camelCase)",
91
+ )
92
+
93
+ use_integers_for_enums: bool = Field(
94
+ default=False,
95
+ description="Use integer values for enums instead of string names",
96
+ )
97
+
98
+ including_default_value_fields: bool = Field(
99
+ default=True,
100
+ description="Include fields with default values in output",
101
+ )
102
+
103
+ float_precision: Optional[int] = Field(
104
+ default=None,
105
+ ge=0,
106
+ le=15,
107
+ description="Decimal places for float formatting (None = no rounding)",
108
+ )
109
+
110
+ model_config = {
111
+ 'extra': 'forbid',
112
+ 'frozen': True,
113
+ }
114
+
115
+
116
+ # ============================================================================
117
+ # Mixin
118
+ # ============================================================================
119
+
120
+ class ProtobufConverterMixin:
121
+ """
122
+ Mixin providing protobuf conversion utilities.
123
+
124
+ **Configuration**:
125
+ Classes using this mixin should set `converter_config` attribute:
126
+ ```python
127
+ class MyService(ProtobufConverterMixin):
128
+ def __init__(self):
129
+ self.converter_config = ConverterConfig(
130
+ preserving_proto_field_name=True,
131
+ )
132
+ ```
133
+
134
+ If not set, uses default ConverterConfig().
135
+
136
+ **Methods**:
137
+ - datetime_to_timestamp: datetime → Timestamp
138
+ - timestamp_to_datetime: Timestamp → datetime
139
+ - dict_to_struct: dict → Struct
140
+ - struct_to_dict: Struct → dict
141
+ - protobuf_to_dict: Message → dict
142
+ - dict_to_protobuf: dict → Message
143
+
144
+ **Example Usage**:
145
+ ```python
146
+ class SignalService(ProtobufConverterMixin):
147
+ converter_config = ConverterConfig()
148
+
149
+ async def process_message(self, message_pb):
150
+ # Convert to dict for processing
151
+ data = self.protobuf_to_dict(message_pb)
152
+
153
+ # Create response timestamp
154
+ ts = self.datetime_to_timestamp(timezone.now())
155
+
156
+ # Build response
157
+ response = ResponseMessage(timestamp=ts)
158
+ return response
159
+ ```
160
+ """
161
+
162
+ # Class-level default config (can be overridden per instance)
163
+ converter_config: ConverterConfig = ConverterConfig()
164
+
165
+ # ------------------------------------------------------------------------
166
+ # Timestamp Conversions
167
+ # ------------------------------------------------------------------------
168
+
169
+ def datetime_to_timestamp(self, dt: Optional[datetime]) -> Optional[Timestamp]:
170
+ """
171
+ Convert Python datetime to Protobuf Timestamp.
172
+
173
+ Args:
174
+ dt: Python datetime object (timezone-aware recommended)
175
+
176
+ Returns:
177
+ Protobuf Timestamp or None if dt is None
178
+
179
+ Example:
180
+ ```python
181
+ from django.utils import timezone
182
+
183
+ ts = self.datetime_to_timestamp(timezone.now())
184
+ # Timestamp(seconds=1699380000, nanos=123456789)
185
+ ```
186
+
187
+ **Timezone Handling**:
188
+ - Naive datetime → assumes UTC
189
+ - Aware datetime → converts to UTC
190
+ """
191
+ if dt is None:
192
+ return None
193
+
194
+ ts = Timestamp()
195
+ ts.FromDatetime(dt)
196
+ return ts
197
+
198
+ def timestamp_to_datetime(self, ts: Optional[Timestamp]) -> Optional[datetime]:
199
+ """
200
+ Convert Protobuf Timestamp to Python datetime.
201
+
202
+ Args:
203
+ ts: Protobuf Timestamp
204
+
205
+ Returns:
206
+ Python datetime object (timezone-aware in UTC) or None
207
+
208
+ Example:
209
+ ```python
210
+ dt = self.timestamp_to_datetime(message.created_at)
211
+ # datetime(2024, 11, 7, 12, 30, tzinfo=UTC)
212
+ ```
213
+ """
214
+ if ts is None:
215
+ return None
216
+
217
+ return ts.ToDatetime()
218
+
219
+ # ------------------------------------------------------------------------
220
+ # Struct Conversions
221
+ # ------------------------------------------------------------------------
222
+
223
+ def dict_to_struct(self, data: Optional[Dict[str, Any]]) -> Optional[Struct]:
224
+ """
225
+ Convert Python dict to Protobuf Struct.
226
+
227
+ Args:
228
+ data: Python dictionary with JSON-compatible values
229
+
230
+ Returns:
231
+ Protobuf Struct or None if data is None
232
+
233
+ Example:
234
+ ```python
235
+ settings = {
236
+ 'exchange': 'binance',
237
+ 'pair': 'BTC/USDT',
238
+ 'timeframe': '1h',
239
+ }
240
+ struct = self.dict_to_struct(settings)
241
+ # Use in protobuf: message.settings.CopyFrom(struct)
242
+ ```
243
+
244
+ **Supported Types**:
245
+ - str, int, float, bool
246
+ - dict (nested)
247
+ - list (of supported types)
248
+
249
+ **Unsupported**:
250
+ - bytes
251
+ - datetime (convert to ISO string first)
252
+ - custom objects
253
+ """
254
+ if data is None:
255
+ return None
256
+
257
+ struct = Struct()
258
+ struct.update(data)
259
+ return struct
260
+
261
+ def struct_to_dict(self, struct: Optional[Struct]) -> Optional[Dict[str, Any]]:
262
+ """
263
+ Convert Protobuf Struct to Python dict.
264
+
265
+ Args:
266
+ struct: Protobuf Struct
267
+
268
+ Returns:
269
+ Python dictionary or None if struct is None
270
+
271
+ Example:
272
+ ```python
273
+ data = self.struct_to_dict(message.settings)
274
+ # {'exchange': 'binance', 'pair': 'BTC/USDT', ...}
275
+ ```
276
+ """
277
+ if struct is None:
278
+ return None
279
+
280
+ return dict(struct)
281
+
282
+ # ------------------------------------------------------------------------
283
+ # Message Conversions
284
+ # ------------------------------------------------------------------------
285
+
286
+ def protobuf_to_dict(
287
+ self,
288
+ message: Message,
289
+ custom_config: Optional[ConverterConfig] = None,
290
+ ) -> Dict[str, Any]:
291
+ """
292
+ Convert Protobuf message to JSON-serializable dict.
293
+
294
+ Uses MessageToDict with configuration from converter_config.
295
+
296
+ Args:
297
+ message: Protobuf message instance
298
+ custom_config: Optional override config (uses self.converter_config if None)
299
+
300
+ Returns:
301
+ JSON-serializable dictionary
302
+
303
+ Example:
304
+ ```python
305
+ data = self.protobuf_to_dict(heartbeat_message)
306
+ # {
307
+ # 'cpu_usage': 45.2,
308
+ # 'memory_usage': 60.1,
309
+ # 'status': 'RUNNING' # Enum as string
310
+ # }
311
+ ```
312
+
313
+ **Field Naming**:
314
+ - preserving_proto_field_name=True → 'cpu_usage' (snake_case)
315
+ - preserving_proto_field_name=False → 'cpuUsage' (camelCase)
316
+
317
+ **Enum Handling**:
318
+ - use_integers_for_enums=False → 'RUNNING' (string name)
319
+ - use_integers_for_enums=True → 2 (integer value)
320
+ """
321
+ config = custom_config or self.converter_config
322
+
323
+ result = MessageToDict(
324
+ message,
325
+ preserving_proto_field_name=config.preserving_proto_field_name,
326
+ use_integers_for_enums=config.use_integers_for_enums,
327
+ including_default_value_fields=config.including_default_value_fields,
328
+ )
329
+
330
+ # Apply float precision if configured
331
+ if config.float_precision is not None:
332
+ result = self._round_floats(result, config.float_precision)
333
+
334
+ return result
335
+
336
+ def dict_to_protobuf(
337
+ self,
338
+ data: Dict[str, Any],
339
+ message_class: type[Message],
340
+ custom_config: Optional[ConverterConfig] = None,
341
+ ) -> Message:
342
+ """
343
+ Convert dict to Protobuf message.
344
+
345
+ Uses ParseDict to populate protobuf message from dict.
346
+
347
+ Args:
348
+ data: Dictionary with message data
349
+ message_class: Protobuf message class to instantiate
350
+ custom_config: Optional override config
351
+
352
+ Returns:
353
+ Populated protobuf message instance
354
+
355
+ Example:
356
+ ```python
357
+ data = {
358
+ 'bot_id': 'bot_123',
359
+ 'status': 'RUNNING',
360
+ 'cpu_usage': 45.2,
361
+ }
362
+
363
+ message = self.dict_to_protobuf(data, HeartbeatUpdate)
364
+ # HeartbeatUpdate(bot_id='bot_123', status=RUNNING, cpu_usage=45.2)
365
+ ```
366
+
367
+ **Field Name Handling**:
368
+ Automatically handles both snake_case and camelCase field names.
369
+ """
370
+ config = custom_config or self.converter_config
371
+
372
+ # Create empty message instance
373
+ message = message_class()
374
+
375
+ # Populate from dict
376
+ ParseDict(data, message, ignore_unknown_fields=True)
377
+
378
+ return message
379
+
380
+ # ------------------------------------------------------------------------
381
+ # Utility Methods
382
+ # ------------------------------------------------------------------------
383
+
384
+ def _round_floats(self, data: Any, precision: int) -> Any:
385
+ """
386
+ Recursively round float values in nested dict/list structures.
387
+
388
+ Args:
389
+ data: Data structure to process
390
+ precision: Decimal places
391
+
392
+ Returns:
393
+ Data with rounded floats
394
+ """
395
+ if isinstance(data, float):
396
+ return round(data, precision)
397
+ elif isinstance(data, dict):
398
+ return {k: self._round_floats(v, precision) for k, v in data.items()}
399
+ elif isinstance(data, list):
400
+ return [self._round_floats(item, precision) for item in data]
401
+ else:
402
+ return data
403
+
404
+ # ------------------------------------------------------------------------
405
+ # Convenience Methods
406
+ # ------------------------------------------------------------------------
407
+
408
+ @staticmethod
409
+ def create_timestamp_now() -> Timestamp:
410
+ """
411
+ Create Timestamp for current UTC time.
412
+
413
+ Returns:
414
+ Timestamp for now
415
+
416
+ Example:
417
+ ```python
418
+ message = ResponseMessage(
419
+ timestamp=self.create_timestamp_now()
420
+ )
421
+ ```
422
+ """
423
+ ts = Timestamp()
424
+ ts.GetCurrentTime()
425
+ return ts
426
+
427
+ @staticmethod
428
+ def is_timestamp_valid(ts: Timestamp) -> bool:
429
+ """
430
+ Check if Timestamp is valid (not default/zero).
431
+
432
+ Args:
433
+ ts: Timestamp to check
434
+
435
+ Returns:
436
+ True if timestamp has non-zero value
437
+
438
+ Example:
439
+ ```python
440
+ if self.is_timestamp_valid(message.created_at):
441
+ dt = self.timestamp_to_datetime(message.created_at)
442
+ ```
443
+ """
444
+ return ts.seconds != 0 or ts.nanos != 0
445
+
446
+ def merge_dicts_to_struct(self, *dicts: Dict[str, Any]) -> Struct:
447
+ """
448
+ Merge multiple dicts and convert to Struct.
449
+
450
+ Later dicts override earlier ones.
451
+
452
+ Args:
453
+ *dicts: Variable number of dicts to merge
454
+
455
+ Returns:
456
+ Merged Struct
457
+
458
+ Example:
459
+ ```python
460
+ defaults = {'timeout': 30, 'retries': 3}
461
+ overrides = {'timeout': 60}
462
+
463
+ struct = self.merge_dicts_to_struct(defaults, overrides)
464
+ # Struct with timeout=60, retries=3
465
+ ```
466
+ """
467
+ merged = {}
468
+ for d in dicts:
469
+ if d:
470
+ merged.update(d)
471
+
472
+ return self.dict_to_struct(merged)
473
+
474
+
475
+ # ============================================================================
476
+ # Standalone Functions
477
+ # ============================================================================
478
+
479
+ def datetime_to_timestamp(dt: Optional[datetime]) -> Optional[Timestamp]:
480
+ """
481
+ Standalone function: Convert datetime to Timestamp.
482
+
483
+ Useful when not using mixin.
484
+
485
+ Args:
486
+ dt: Python datetime
487
+
488
+ Returns:
489
+ Protobuf Timestamp or None
490
+ """
491
+ if dt is None:
492
+ return None
493
+
494
+ ts = Timestamp()
495
+ ts.FromDatetime(dt)
496
+ return ts
497
+
498
+
499
+ def timestamp_to_datetime(ts: Optional[Timestamp]) -> Optional[datetime]:
500
+ """
501
+ Standalone function: Convert Timestamp to datetime.
502
+
503
+ Args:
504
+ ts: Protobuf Timestamp
505
+
506
+ Returns:
507
+ Python datetime or None
508
+ """
509
+ if ts is None:
510
+ return None
511
+
512
+ return ts.ToDatetime()
513
+
514
+
515
+ def dict_to_struct(data: Optional[Dict[str, Any]]) -> Optional[Struct]:
516
+ """
517
+ Standalone function: Convert dict to Struct.
518
+
519
+ Args:
520
+ data: Python dictionary
521
+
522
+ Returns:
523
+ Protobuf Struct or None
524
+ """
525
+ if data is None:
526
+ return None
527
+
528
+ struct = Struct()
529
+ struct.update(data)
530
+ return struct
531
+
532
+
533
+ def struct_to_dict(struct: Optional[Struct]) -> Optional[Dict[str, Any]]:
534
+ """
535
+ Standalone function: Convert Struct to dict.
536
+
537
+ Args:
538
+ struct: Protobuf Struct
539
+
540
+ Returns:
541
+ Python dictionary or None
542
+ """
543
+ if struct is None:
544
+ return None
545
+
546
+ return dict(struct)
547
+
548
+
549
+ # ============================================================================
550
+ # Exports
551
+ # ============================================================================
552
+
553
+ __all__ = [
554
+ # Configuration
555
+ 'ConverterConfig',
556
+
557
+ # Mixin
558
+ 'ProtobufConverterMixin',
559
+
560
+ # Standalone functions
561
+ 'datetime_to_timestamp',
562
+ 'timestamp_to_datetime',
563
+ 'dict_to_struct',
564
+ 'struct_to_dict',
565
+ ]