DefStream-RabbitMQ 0.1.26__116-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.
@@ -0,0 +1,277 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ from DefStream.Broker import (
4
+ Sendable,
5
+ Callable,
6
+ RemoteError,
7
+ validate_priority,
8
+ )
9
+
10
+ from .Serializer import (
11
+ Encoder,
12
+ Decoder,
13
+ )
14
+ from copy import deepcopy
15
+ from logging import getLogger
16
+ from math import (
17
+ ceil,
18
+ isfinite,
19
+ )
20
+
21
+ from pika import (
22
+ BlockingConnection,
23
+ BasicProperties,
24
+ ConnectionParameters,
25
+ )
26
+
27
+ from time import (
28
+ time,
29
+ monotonic,
30
+ )
31
+ from uuid import uuid4
32
+
33
+ from typing import (
34
+ Any,
35
+ Dict,
36
+ Optional,
37
+ )
38
+
39
+ __all__ = (
40
+ 'Exchange',
41
+ )
42
+
43
+
44
+ class Exchange(
45
+ Sendable,
46
+ Callable,
47
+ ):
48
+ """Exchange of Event Broker for RabbitMQ"""
49
+ logger = getLogger('Broker.RabbitMQ.Exchange')
50
+
51
+ def __init__(
52
+ self,
53
+ connection: BlockingConnection,
54
+ encode: Encoder,
55
+ name: str,
56
+ decode: Optional[Decoder] = None,
57
+ rpc_parameters: Optional[ConnectionParameters] = None,
58
+ ):
59
+ self.connection = connection
60
+ self.encode = encode
61
+ try:
62
+ self.channel = self.connection.channel()
63
+ except Exception as error:
64
+ self.logger.error('Exchange channel creation failed: exchange=%s error_type=%s', name, type(error).__name__)
65
+ raise
66
+ self.exchange = name
67
+ self.decode = decode
68
+ self.rpc_parameters = rpc_parameters
69
+ return
70
+
71
+ def __del__(self):
72
+ channel = getattr(self, 'channel', None)
73
+ try:
74
+ if channel and channel.is_open:
75
+ channel.close()
76
+ self.logger.debug('Exchange channel cleanup completed: exchange=%s', self.exchange)
77
+ except Exception as error:
78
+ self.logger.error('Exchange channel cleanup failed: error_type=%s', type(error).__name__)
79
+ raise
80
+ return
81
+
82
+ def __str__(self): return self.exchange
83
+
84
+ def send(
85
+ self,
86
+ body,
87
+ event: Optional[str] = None,
88
+ headers: Dict = {},
89
+ priority: Optional[int] = None,
90
+ expiration: Optional[int] = None,
91
+ timestamp: Optional[int] = None,
92
+ persistent: bool = True,
93
+ id: Optional[str] = None,
94
+ ):
95
+ validate_priority(
96
+ priority,
97
+ 255,
98
+ )
99
+ if not timestamp: timestamp = int(time())
100
+ if not id: id = uuid4().hex
101
+ properties = BasicProperties(
102
+ type=event if event else '',
103
+ headers=headers,
104
+ content_type=self.encode.format,
105
+ content_encoding=self.encode.charset,
106
+ priority=priority,
107
+ timestamp=timestamp,
108
+ expiration=str(expiration) if expiration is not None else None,
109
+ message_id=id,
110
+ delivery_mode=2 if persistent else 1,
111
+ )
112
+ self.logger.debug('Publishing message: exchange=%s routing_key=%s message_id=%s priority=%s persistent=%s publisher_confirm=False', self.exchange, event, id, priority, persistent)
113
+ try:
114
+ self.channel.basic_publish(
115
+ exchange=self.exchange,
116
+ routing_key=event if event else '',
117
+ properties=properties,
118
+ body=self.encode(body),
119
+ )
120
+ except Exception as error:
121
+ self.logger.error('Message publication failed: exchange=%s routing_key=%s message_id=%s error_type=%s', self.exchange, event, id, type(error).__name__)
122
+ raise
123
+ self.logger.debug('Publish API call completed: exchange=%s message_id=%s does not imply Consumer processing completion', self.exchange, id)
124
+ return id
125
+
126
+ def call(
127
+ self,
128
+ body: Any,
129
+ timeout: float = 5,
130
+ *,
131
+ event: Optional[str] = None,
132
+ ) -> Any:
133
+ if self.rpc_parameters is None or self.decode is None:
134
+ raise RuntimeError('RPC requires decoder and connection parameters; obtain the exchange through Connection.exchange')
135
+ if isinstance(
136
+ timeout,
137
+ bool,
138
+ ) or not isinstance(
139
+ timeout,
140
+ (
141
+ int,
142
+ float,
143
+ ),
144
+ ) or not isfinite(timeout) or timeout <= 0:
145
+ raise ValueError('RPC timeout must be a finite positive number of seconds')
146
+ deadline = monotonic() + timeout
147
+ settings = deepcopy(self.rpc_parameters)
148
+ settings.connection_attempts = 1
149
+ settings.socket_timeout = timeout if settings.socket_timeout is None else min(
150
+ settings.socket_timeout,
151
+ timeout,
152
+ )
153
+ settings.stack_timeout = timeout
154
+ settings.blocked_connection_timeout = timeout
155
+ timed_out = False
156
+ self.logger.debug('Opening dedicated RPC connection: exchange=%s routing_key=%s timeout=%s', self.exchange, event, timeout)
157
+ try:
158
+ connection = BlockingConnection(settings)
159
+ except Exception as error:
160
+ self.logger.error('Dedicated RPC connection failed: exchange=%s error_type=%s', self.exchange, type(error).__name__)
161
+ raise
162
+ try:
163
+ channel = connection.channel()
164
+ reply_queue = channel.queue_declare(
165
+ queue='',
166
+ exclusive=True,
167
+ auto_delete=True,
168
+ ).method.queue
169
+ channel.confirm_delivery()
170
+ correlation_id = uuid4().hex
171
+ self.logger.debug('RPC reply queue created: exchange=%s reply_to=%s correlation_id=%s publisher_confirm=True', self.exchange, reply_queue, correlation_id)
172
+ responses = []
173
+
174
+ def receive(
175
+ channel,
176
+ method,
177
+ properties,
178
+ payload,
179
+ ):
180
+ if properties.correlation_id == correlation_id:
181
+ responses.append((
182
+ properties,
183
+ payload,
184
+ ))
185
+ self.logger.debug('RPC response received: exchange=%s correlation_id=%s', self.exchange, correlation_id)
186
+ else:
187
+ self.logger.debug('Ignoring RPC response with mismatched correlation_id: exchange=%s correlation_id=%s', self.exchange, properties.correlation_id)
188
+
189
+ channel.basic_consume(
190
+ reply_queue,
191
+ on_message_callback=receive,
192
+ auto_ack=True,
193
+ )
194
+ remaining = deadline - monotonic()
195
+ if remaining <= 0:
196
+ timed_out = True
197
+ self.logger.warning('RPC timed out before publication: exchange=%s routing_key=%s correlation_id=%s', self.exchange, event, correlation_id)
198
+ raise TimeoutError('RPC timed out before request publication')
199
+ self.logger.debug('Publishing RPC request: exchange=%s routing_key=%s correlation_id=%s', self.exchange, event, correlation_id)
200
+ channel.basic_publish(
201
+ exchange=self.exchange,
202
+ routing_key=event if event is not None else '',
203
+ body=self.encode(body),
204
+ properties=BasicProperties(
205
+ reply_to=reply_queue,
206
+ correlation_id=correlation_id,
207
+ content_type=self.encode.format,
208
+ content_encoding=self.encode.charset,
209
+ expiration=str(max(
210
+ 1,
211
+ ceil(remaining * 1000),
212
+ )),
213
+ ),
214
+ mandatory=True,
215
+ )
216
+ self.logger.debug('RPC request published: exchange=%s routing_key=%s correlation_id=%s', self.exchange, event, correlation_id)
217
+ while not responses:
218
+ remaining = deadline - monotonic()
219
+ if remaining <= 0:
220
+ timed_out = True
221
+ self.logger.warning('RPC response timed out after publication; delivered work is not cancelled: exchange=%s routing_key=%s correlation_id=%s', self.exchange, event, correlation_id)
222
+ raise TimeoutError(f'RPC response timed out for exchange {self.exchange!r} routing_key={event!r}')
223
+ # pika accepts a float time_limit; the bundled stub declares int only.
224
+ connection.process_data_events(time_limit=min( # pyright: ignore[reportArgumentType]
225
+ remaining,
226
+ 0.1,
227
+ ))
228
+ properties, payload = responses[0]
229
+ response = self.decode(
230
+ payload,
231
+ properties.content_type,
232
+ properties.content_encoding,
233
+ )
234
+ if not isinstance(
235
+ response,
236
+ dict,
237
+ ) or type(response.get('ok')) is not bool:
238
+ raise ValueError('Invalid RPC response envelope')
239
+ if response['ok']:
240
+ if 'result' not in response:
241
+ raise ValueError('RPC response has no result')
242
+ self.logger.debug('RPC response validated: exchange=%s routing_key=%s correlation_id=%s elapsed=%s', self.exchange, event, correlation_id, timeout - (deadline - monotonic()))
243
+ return response['result']
244
+ error = response.get('error')
245
+ if not isinstance(
246
+ error,
247
+ dict,
248
+ ) or not isinstance(
249
+ error.get('code'),
250
+ str,
251
+ ) or not isinstance(
252
+ error.get('message'),
253
+ str,
254
+ ):
255
+ raise ValueError('Invalid RPC error response')
256
+ self.logger.warning('RPC remote task error: exchange=%s routing_key=%s correlation_id=%s', self.exchange, event, correlation_id)
257
+ raise RemoteError(
258
+ error['code'],
259
+ error['message'],
260
+ )
261
+ except TimeoutError:
262
+ if not timed_out:
263
+ self.logger.error('RPC internal timeout error: exchange=%s routing_key=%s', self.exchange, event)
264
+ raise
265
+ except RemoteError:
266
+ raise
267
+ except Exception as error:
268
+ self.logger.error('RPC transmission or response handling failed: exchange=%s routing_key=%s error_type=%s', self.exchange, event, type(error).__name__)
269
+ raise
270
+ finally:
271
+ try:
272
+ if connection.is_open:
273
+ connection.close()
274
+ self.logger.debug('Dedicated RPC connection and reply queue cleanup completed: exchange=%s', self.exchange)
275
+ except Exception as error:
276
+ self.logger.error('Dedicated RPC connection cleanup failed: exchange=%s error_type=%s', self.exchange, type(error).__name__)
277
+ raise
@@ -0,0 +1,321 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ from DefStream.Broker import (
4
+ Sendable,
5
+ Callable,
6
+ Poppable,
7
+ RemoteError,
8
+ validate_priority,
9
+ )
10
+
11
+ from .Serializer import (
12
+ Encoder,
13
+ Decoder,
14
+ )
15
+ from .Event import Event
16
+ from copy import deepcopy
17
+ from logging import getLogger
18
+ from math import (
19
+ ceil,
20
+ isfinite,
21
+ )
22
+
23
+ from pika import (
24
+ BlockingConnection,
25
+ BasicProperties,
26
+ ConnectionParameters,
27
+ )
28
+
29
+ from time import (
30
+ time,
31
+ sleep,
32
+ monotonic,
33
+ )
34
+ from uuid import uuid4
35
+
36
+ from typing import (
37
+ Any,
38
+ Optional,
39
+ Dict,
40
+ )
41
+
42
+ __all__ = (
43
+ 'Queue',
44
+ )
45
+
46
+
47
+ class Queue(
48
+ Sendable,
49
+ Callable,
50
+ Poppable,
51
+ ):
52
+ """Queue of Event Broker for RabbitMQ"""
53
+ logger = getLogger('Broker.RabbitMQ.Queue')
54
+
55
+ def __init__(
56
+ self,
57
+ connection: BlockingConnection,
58
+ encode: Encoder,
59
+ decode: Decoder,
60
+ name: str,
61
+ rpc_parameters: Optional[ConnectionParameters] = None,
62
+ ):
63
+ self.connection = connection
64
+ self.encode = encode
65
+ self.decode = decode
66
+ try:
67
+ self.channel = self.connection.channel()
68
+ except Exception as error:
69
+ self.logger.error('Queue channel creation failed: queue=%s error_type=%s', name, type(error).__name__)
70
+ raise
71
+ self.queue = name
72
+ self.event = None
73
+ self.rpc_parameters = rpc_parameters
74
+ return
75
+
76
+ def __del__(self):
77
+ channel = getattr(self, 'channel', None)
78
+ try:
79
+ if channel and channel.is_open:
80
+ channel.close()
81
+ self.logger.debug('Queue channel cleanup completed: queue=%s', self.queue)
82
+ except Exception as error:
83
+ self.logger.error('Queue channel cleanup failed: error_type=%s', type(error).__name__)
84
+ raise
85
+ return
86
+
87
+ def __str__(self): return self.queue
88
+
89
+ def send(
90
+ self,
91
+ body,
92
+ event: Optional[str] = None,
93
+ headers: Dict = {},
94
+ priority: Optional[int] = None,
95
+ expiration: Optional[int] = None,
96
+ timestamp: Optional[int] = None,
97
+ persistent: bool = True,
98
+ id: Optional[str] = None,
99
+ reply_to: Optional[str] = None,
100
+ correlation_id: Optional[str] = None,
101
+ ):
102
+ validate_priority(
103
+ priority,
104
+ 255,
105
+ )
106
+ if not timestamp: timestamp = int(time())
107
+ if not id: id = uuid4().hex
108
+ properties = BasicProperties(
109
+ type=event if event else '',
110
+ headers=headers,
111
+ content_type=self.encode.format,
112
+ content_encoding=self.encode.charset,
113
+ priority=priority,
114
+ timestamp=timestamp,
115
+ expiration=str(expiration) if expiration is not None else None,
116
+ message_id=id,
117
+ delivery_mode=2 if persistent else 1,
118
+ reply_to=reply_to,
119
+ correlation_id=correlation_id,
120
+ )
121
+ self.logger.debug('Publishing message: queue=%s message_id=%s priority=%s persistent=%s publisher_confirm=False', self.queue, id, priority, persistent)
122
+ try:
123
+ self.channel.basic_publish(
124
+ exchange='',
125
+ routing_key=self.queue,
126
+ properties=properties,
127
+ body=self.encode(body)
128
+ )
129
+ except Exception as error:
130
+ self.logger.error('Message publication failed: queue=%s message_id=%s error_type=%s', self.queue, id, type(error).__name__)
131
+ raise
132
+ self.logger.debug('Publish API call completed: queue=%s message_id=%s does not imply Consumer processing completion', self.queue, id)
133
+ return id
134
+
135
+ def call(
136
+ self,
137
+ body: Any,
138
+ timeout: float = 5,
139
+ *,
140
+ priority: Optional[int] = None,
141
+ ) -> Any:
142
+ validate_priority(
143
+ priority,
144
+ 255,
145
+ )
146
+ if self.rpc_parameters is None:
147
+ raise RuntimeError('RPC requires connection parameters; obtain the queue through Connection.queue')
148
+ if isinstance(
149
+ timeout,
150
+ bool,
151
+ ) or not isinstance(
152
+ timeout,
153
+ (
154
+ int,
155
+ float,
156
+ ),
157
+ ) or not isfinite(timeout) or timeout <= 0:
158
+ raise ValueError('RPC timeout must be a finite positive number of seconds')
159
+ deadline = monotonic() + timeout
160
+ settings = deepcopy(self.rpc_parameters)
161
+ settings.connection_attempts = 1
162
+ settings.socket_timeout = timeout if settings.socket_timeout is None else min(
163
+ settings.socket_timeout,
164
+ timeout,
165
+ )
166
+ settings.stack_timeout = timeout
167
+ settings.blocked_connection_timeout = timeout
168
+ timed_out = False
169
+ self.logger.debug('Opening dedicated RPC connection: queue=%s timeout=%s', self.queue, timeout)
170
+ try:
171
+ connection = BlockingConnection(settings)
172
+ except Exception as error:
173
+ self.logger.error('Dedicated RPC connection failed: queue=%s error_type=%s', self.queue, type(error).__name__)
174
+ raise
175
+ try:
176
+ channel = connection.channel()
177
+ reply_queue = channel.queue_declare(
178
+ queue=f'{self.queue}.{uuid4().hex}',
179
+ exclusive=True,
180
+ auto_delete=True,
181
+ ).method.queue
182
+ channel.confirm_delivery()
183
+ correlation_id = uuid4().hex
184
+ self.logger.debug('RPC reply queue created: queue=%s reply_to=%s correlation_id=%s publisher_confirm=True', self.queue, reply_queue, correlation_id)
185
+ responses = []
186
+
187
+ def receive(
188
+ channel,
189
+ method,
190
+ properties,
191
+ payload,
192
+ ):
193
+ if properties.correlation_id == correlation_id:
194
+ responses.append((
195
+ properties,
196
+ payload,
197
+ ))
198
+ self.logger.debug('RPC response received: queue=%s correlation_id=%s', self.queue, correlation_id)
199
+ else:
200
+ self.logger.debug('Ignoring RPC response with mismatched correlation_id: queue=%s correlation_id=%s', self.queue, properties.correlation_id)
201
+
202
+ channel.basic_consume(
203
+ reply_queue,
204
+ on_message_callback=receive,
205
+ auto_ack=True,
206
+ )
207
+ remaining = deadline - monotonic()
208
+ if remaining <= 0:
209
+ timed_out = True
210
+ self.logger.warning('RPC timed out before publication: queue=%s correlation_id=%s', self.queue, correlation_id)
211
+ raise TimeoutError('RPC timed out before request publication')
212
+ self.logger.debug('Publishing RPC request: queue=%s correlation_id=%s priority=%s', self.queue, correlation_id, priority)
213
+ channel.basic_publish(
214
+ exchange='',
215
+ routing_key=self.queue,
216
+ body=self.encode(body),
217
+ properties=BasicProperties(
218
+ priority=priority,
219
+ reply_to=reply_queue,
220
+ correlation_id=correlation_id,
221
+ content_type=self.encode.format,
222
+ content_encoding=self.encode.charset,
223
+ expiration=str(max(
224
+ 1,
225
+ ceil(remaining * 1000),
226
+ )),
227
+ ),
228
+ mandatory=True,
229
+ )
230
+ self.logger.debug('RPC request published: queue=%s correlation_id=%s', self.queue, correlation_id)
231
+ while not responses:
232
+ remaining = deadline - monotonic()
233
+ if remaining <= 0:
234
+ timed_out = True
235
+ self.logger.warning('RPC response timed out after publication; delivered work is not cancelled: queue=%s correlation_id=%s', self.queue, correlation_id)
236
+ raise TimeoutError(f'RPC response timed out for queue {self.queue!r}')
237
+ # pika accepts a float time_limit; the bundled stub declares int only.
238
+ connection.process_data_events(time_limit=min( # pyright: ignore[reportArgumentType]
239
+ remaining,
240
+ 0.1,
241
+ ))
242
+ properties, payload = responses[0]
243
+ response = self.decode(
244
+ payload,
245
+ properties.content_type,
246
+ properties.content_encoding,
247
+ )
248
+ if not isinstance(
249
+ response,
250
+ dict,
251
+ ) or type(response.get('ok')) is not bool:
252
+ raise ValueError('Invalid RPC response envelope')
253
+ if response['ok']:
254
+ if 'result' not in response:
255
+ raise ValueError('RPC response has no result')
256
+ self.logger.debug('RPC response validated: queue=%s correlation_id=%s elapsed=%s', self.queue, correlation_id, timeout - (deadline - monotonic()))
257
+ return response['result']
258
+ error = response.get('error')
259
+ if not isinstance(
260
+ error,
261
+ dict,
262
+ ) or not isinstance(
263
+ error.get('code'),
264
+ str,
265
+ ) or not isinstance(
266
+ error.get('message'),
267
+ str,
268
+ ):
269
+ raise ValueError('Invalid RPC error response')
270
+ self.logger.warning('RPC remote task error: queue=%s correlation_id=%s', self.queue, correlation_id)
271
+ raise RemoteError(
272
+ error['code'],
273
+ error['message'],
274
+ )
275
+ except TimeoutError:
276
+ if not timed_out:
277
+ self.logger.error('RPC internal timeout error: queue=%s', self.queue)
278
+ raise
279
+ except RemoteError:
280
+ raise
281
+ except Exception as error:
282
+ self.logger.error('RPC transmission or response handling failed: queue=%s error_type=%s', self.queue, type(error).__name__)
283
+ raise
284
+ finally:
285
+ try:
286
+ if connection.is_open:
287
+ connection.close()
288
+ self.logger.debug('Dedicated RPC connection and reply queue cleanup completed: queue=%s', self.queue)
289
+ except Exception as error:
290
+ self.logger.error('Dedicated RPC connection cleanup failed: queue=%s error_type=%s', self.queue, type(error).__name__)
291
+ raise
292
+
293
+ def pop(self, timeout: Optional[int] = None) -> Optional[Event]:
294
+ self.logger.debug('Waiting for a single message: queue=%s timeout_ms=%s', self.queue, timeout)
295
+ channel = self.channel
296
+ try:
297
+ channel.basic_qos(0, 1, False)
298
+ deadline = time() + (timeout / 1000) if timeout else None
299
+ while True:
300
+ method, properties, body = channel.basic_get(queue=self.queue, auto_ack=False)
301
+ if method:
302
+ # pika returns properties and body together with a GetOk method frame.
303
+ assert properties is not None and body is not None
304
+ event = Event(
305
+ channel,
306
+ self.queue,
307
+ None,
308
+ method.delivery_tag,
309
+ properties,
310
+ self.decode(body, properties.content_type, properties.content_encoding),
311
+ )
312
+ self.logger.debug('Single message received and decoded: queue=%s delivery_tag=%s message_id=%s', self.queue, method.delivery_tag, properties.message_id)
313
+ return event
314
+ if deadline and time() >= deadline:
315
+ break
316
+ sleep(0.1)
317
+ except Exception as error:
318
+ self.logger.error('Single-message reception failed: queue=%s error_type=%s', self.queue, type(error).__name__)
319
+ raise
320
+ self.logger.debug('Single-message wait ended: queue=%s', self.queue)
321
+ return
@@ -0,0 +1,75 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ from abc import (
4
+ ABCMeta,
5
+ abstractmethod,
6
+ )
7
+
8
+ from json import (
9
+ dumps,
10
+ loads,
11
+ )
12
+ from typing import (
13
+ Any,
14
+ Optional,
15
+ )
16
+
17
+ __all__ = (
18
+ 'Encoder',
19
+ 'Decoder',
20
+ 'JavaScriptObjectNotationEncoder',
21
+ 'JavaScriptObjectNotationDecoder',
22
+ )
23
+
24
+ class Encoder(metaclass=ABCMeta):
25
+ """Encoder Interface"""
26
+ @abstractmethod
27
+ def __call__(self, obj: Any) -> bytes:
28
+ raise NotImplementedError('{} must be implemented __call__'.format(self.__class__.__name__))
29
+
30
+ @property
31
+ @abstractmethod
32
+ def format(self) -> str:
33
+ raise NotImplementedError('{} must be implemented format'.format(self.__class__.__name__))
34
+
35
+ @property
36
+ @abstractmethod
37
+ def charset(self) -> str:
38
+ raise NotImplementedError('{} must be implemented charset'.format(self.__class__.__name__))
39
+
40
+
41
+ class Decoder(metaclass=ABCMeta):
42
+ """Decoder Interface"""
43
+ @abstractmethod
44
+ def __call__(
45
+ self,
46
+ obj: bytes,
47
+ format: Optional[str],
48
+ charset: Optional[str],
49
+ ) -> Any:
50
+ raise NotImplementedError('{} must be implemented __call__'.format(self.__class__.__name__))
51
+
52
+
53
+ class JavaScriptObjectNotationEncoder(Encoder):
54
+ """JavaScriptObjectNotation Encoder"""
55
+ def __call__(self, obj: Any) -> bytes:
56
+ return dumps(obj, ensure_ascii=False).encode(self.charset)
57
+
58
+ @property
59
+ def format(self) -> str: return 'application/json'
60
+
61
+ @property
62
+ def charset(self) -> str: return 'utf-8'
63
+
64
+
65
+ class JavaScriptObjectNotationDecoder(Decoder):
66
+ """JavaScriptObjectNotation Decoder"""
67
+ def __call__(
68
+ self,
69
+ obj: bytes,
70
+ format: Optional[str],
71
+ charset: Optional[str],
72
+ ) -> Any:
73
+ if format != 'application/json': raise ValueError('format is not support')
74
+ if charset is None: raise ValueError('charset is required')
75
+ return loads(obj.decode(charset))