fabric-chaincode-python 2.5.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. fabric_chaincode_python-2.5.2.dist-info/METADATA +101 -0
  2. fabric_chaincode_python-2.5.2.dist-info/RECORD +40 -0
  3. fabric_chaincode_python-2.5.2.dist-info/WHEEL +5 -0
  4. fabric_chaincode_python-2.5.2.dist-info/licenses/LICENSE +201 -0
  5. fabric_chaincode_python-2.5.2.dist-info/licenses/NOTICE +18 -0
  6. fabric_chaincode_python-2.5.2.dist-info/top_level.txt +1 -0
  7. src/__init__.py +5 -0
  8. src/fabric_contract_api/__init__.py +51 -0
  9. src/fabric_contract_api/contractapi/__init__.py +29 -0
  10. src/fabric_contract_api/contractapi/contract.py +141 -0
  11. src/fabric_contract_api/contractapi/contract_chaincode.py +552 -0
  12. src/fabric_contract_api/contractapi/system_contract.py +45 -0
  13. src/fabric_contract_api/contractapi/transaction_context.py +160 -0
  14. src/fabric_contract_api/contractapi/utils/__init__.py +4 -0
  15. src/fabric_contract_api/contractapi/utils/undefined_interface.py +33 -0
  16. src/fabric_contract_api/internal/__init__.py +16 -0
  17. src/fabric_contract_api/internal/contract_function.py +453 -0
  18. src/fabric_contract_api/internal/transaction_handler.py +131 -0
  19. src/fabric_contract_api/internal/types.py +180 -0
  20. src/fabric_contract_api/internal/types_handler.py +224 -0
  21. src/fabric_contract_api/internal/utils.py +59 -0
  22. src/fabric_contract_api/metadata/__init__.py +32 -0
  23. src/fabric_contract_api/metadata/metadata.py +456 -0
  24. src/fabric_contract_api/metadata/schema.py +238 -0
  25. src/fabric_contract_api/serializer/__init__.py +5 -0
  26. src/fabric_contract_api/serializer/json_transaction_serializer.py +363 -0
  27. src/fabric_contract_api/serializer/transaction_serializer.py +45 -0
  28. src/fabric_shim/__init__.py +2 -0
  29. src/fabric_shim/chaincode.py +8 -0
  30. src/fabric_shim/handler.py +336 -0
  31. src/fabric_shim/interfaces.py +134 -0
  32. src/fabric_shim/iterators.py +107 -0
  33. src/fabric_shim/logger.py +1 -0
  34. src/fabric_shim/logging.py +63 -0
  35. src/fabric_shim/msg_queue_handler.py +91 -0
  36. src/fabric_shim/response.py +49 -0
  37. src/fabric_shim/server.py +155 -0
  38. src/fabric_shim/stub.py +342 -0
  39. src/fabric_shim/utils.py +41 -0
  40. src/version.py +3 -0
@@ -0,0 +1,336 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ import datetime
3
+ from typing import AsyncIterable
4
+ import asyncio
5
+ import grpc
6
+
7
+ from fabric_protos.peer import chaincode_shim_pb2 as ccshim_pb2
8
+ from fabric_protos.peer import chaincode_pb2 as cc_pb2
9
+ from fabric_protos.peer import proposal_response_pb2 as pr_pb
10
+ from src.fabric_shim.stub import ChaincodeStub
11
+ from src.fabric_shim.msg_queue_handler import MsgQueueHandler, QueueMessage
12
+ from src.fabric_shim.response import new_error_msg, ResponseCode
13
+ from src.fabric_shim.utils import generate_logging_prefix
14
+ from src.fabric_shim.logging import LOGGER
15
+ from src.fabric_shim.interfaces import Chaincode
16
+
17
+
18
+ class STATES:
19
+ CREATED = "created" # start state
20
+ ESTABLISHED = "established" # connection established
21
+ READY = "ready" # ready for requests
22
+
23
+
24
+ MIN_UNICODE_RUNE_VALUE = '\u0000' # U + 0000
25
+ MAX_UNICODE_RUNE_VALUE = '\u0010FFFF' # U+10FFFF - maximum (and unallocated) code point
26
+ COMPOSITEKEY_NS = '\x00'
27
+ EMPTY_KEY_SUBSTITUTE = '\x01'
28
+
29
+ class Handler:
30
+ def __init__(self, cc_id: str, cc: Chaincode) -> None:
31
+ self.chaincode_id = cc_pb2.ChaincodeID()
32
+ self.chaincode_id.name = cc_id
33
+ self.chaincode = cc
34
+ self.msg_queue_handler = None
35
+ self.context = None
36
+ self.state = STATES.CREATED
37
+ self._pending_tasks = set()
38
+
39
+ def _track_task(self, coro):
40
+ """Schedule message handling while surfacing task failures in logs."""
41
+ task = asyncio.create_task(coro)
42
+ self._pending_tasks.add(task)
43
+
44
+ def _done_callback(done_task):
45
+ self._pending_tasks.discard(done_task)
46
+ try:
47
+ exc = done_task.exception()
48
+ except asyncio.CancelledError:
49
+ return
50
+ if exc is not None:
51
+ LOGGER.exception('Unhandled exception while processing peer message', exc_info=exc)
52
+
53
+ task.add_done_callback(_done_callback)
54
+
55
+ async def handle_stub_interaction(self, msg, action="Invoke"):
56
+ """handle_message calls the Init | Invoke function of the associated chaincode."""
57
+ # Get the function and args from Payload
58
+ cc_input = cc_pb2.ChaincodeInput()
59
+ cc_input.ParseFromString(msg.payload)
60
+
61
+ stub = ChaincodeStub(self, msg.channel_id, msg.txid, cc_input, msg.proposal)
62
+
63
+ # Normalize action and call appropriate chaincode method
64
+ action_normalized = (action or "").lower()
65
+ if action_normalized == 'init':
66
+ method = 'Init'
67
+ resp: pr_pb.Response = await self.chaincode.init(stub)
68
+ else:
69
+ method = 'Invoke'
70
+ resp: pr_pb.Response = await self.chaincode.invoke(stub)
71
+
72
+ # check that a response object has been returned otherwise assume an error.
73
+
74
+ if not resp or not resp.status:
75
+ err_msg = '%s Calling chaincode %s() has not called success or error.' \
76
+ % (generate_logging_prefix(msg.channel_id, msg.txid), method)
77
+ LOGGER.info(err_msg)
78
+
79
+ resp = pr_pb.Response(
80
+ status=ResponseCode.ERROR,
81
+ message=err_msg
82
+ )
83
+
84
+ LOGGER.info('%s Calling chaincode %s(), response status: %s'
85
+ % (generate_logging_prefix(msg.channel_id, msg.txid), method, resp.status))
86
+
87
+ if resp.status >= ResponseCode.ERROR:
88
+ err_msg = '%s Calling chaincode %s() returned error response [%s]. Sending COMPLETED message back to peer' \
89
+ % (generate_logging_prefix(msg.channel_id, msg.txid), method, resp.message)
90
+ LOGGER.info(err_msg)
91
+
92
+ next_state_msg = ccshim_pb2.ChaincodeMessage(
93
+ type=ccshim_pb2.ChaincodeMessage.COMPLETED,
94
+ payload=resp.SerializeToString(),
95
+ txid=msg.txid,
96
+ channel_id=msg.channel_id
97
+ )
98
+ else:
99
+ LOGGER.info('%s Calling chaincode %s() succeeded. Sending COMPLETED message back to peer'
100
+ % (generate_logging_prefix(msg.channel_id, msg.txid), method))
101
+
102
+ next_state_msg = ccshim_pb2.ChaincodeMessage(
103
+ type=ccshim_pb2.ChaincodeMessage.COMPLETED,
104
+ payload=resp.SerializeToString(),
105
+ txid=msg.txid,
106
+ channel_id=msg.channel_id
107
+ )
108
+
109
+ await self.context.write(next_state_msg)
110
+
111
+ async def handle_message_ready(self, msg):
112
+ """handle_message_ready handles messages received from the peer when the handler is in the "ready" state."""
113
+ if msg.type == ccshim_pb2.ChaincodeMessage.RESPONSE or msg.type == ccshim_pb2.ChaincodeMessage.ERROR:
114
+ LOGGER.info("+++ ERROR or RESPONSE +++")
115
+ await self.msg_queue_handler.handle_msg_response(msg)
116
+ elif msg.type == ccshim_pb2.ChaincodeMessage.INIT:
117
+ LOGGER.info("+++ call INIT +++")
118
+ await self.handle_stub_interaction(msg, "Init")
119
+ return
120
+ elif msg.type == ccshim_pb2.ChaincodeMessage.TRANSACTION:
121
+ LOGGER.info("+++ call INVOKE +++")
122
+ await self.handle_stub_interaction(msg, "Invoke")
123
+ return
124
+ else:
125
+ await self.context.write(new_error_msg(msg, self.state))
126
+
127
+ async def handle_message_established(self, msg):
128
+ """
129
+ handle_message_established handles messages received from the peer when the handler is in the "established" state.
130
+ """
131
+ if msg.type != ccshim_pb2.ChaincodeMessage.READY:
132
+ LOGGER.error(f'Chaincode is in "established" state, can only process messages of type "ready", '
133
+ f'but received "{msg.type}"')
134
+ await self.context.write(new_error_msg(msg, self.state))
135
+ else:
136
+ LOGGER.info('Successfully established communication with peer node. State transferred to "ready"')
137
+ self.state = STATES.READY
138
+
139
+ async def handle_message_created(self, msg):
140
+ """handle_message_created handles messages received from the peer when the handler is in the "created" state."""
141
+ if msg.type != ccshim_pb2.ChaincodeMessage.REGISTERED:
142
+ LOGGER.error(f'Chaincode is in "created" state, can only process messages of type "registered", '
143
+ f'but received "{msg.type}"')
144
+ await self.context.write(new_error_msg(msg, self.state))
145
+ else:
146
+ LOGGER.info('Successfully registered with peer node. State transferred to "established"')
147
+ self.state = STATES.ESTABLISHED
148
+
149
+ async def handle_message(self, msg: ccshim_pb2.ChaincodeMessage):
150
+ """handle_message message handles loop for shim side of chaincode/peer stream."""
151
+ LOGGER.warning('-->> Look out!')
152
+
153
+ # TODO: ?
154
+ if msg.type == ccshim_pb2.ChaincodeMessage.KEEPALIVE:
155
+ LOGGER.info('-| KEEPALIVE')
156
+ return
157
+
158
+ if self.state == STATES.READY:
159
+ await self.handle_message_ready(msg)
160
+ elif self.state == STATES.ESTABLISHED:
161
+ await self.handle_message_established(msg)
162
+ elif self.state == STATES.CREATED:
163
+ await self.handle_message_created(msg)
164
+ else:
165
+ await self.context.write(new_error_msg(msg, self.state))
166
+
167
+ async def chat_with_peer(self, stream: AsyncIterable[ccshim_pb2.ChaincodeMessage], context: grpc.aio.ServicerContext):
168
+ """chat stream for peer-chaincode interactions post connection"""
169
+ self.state = STATES.CREATED
170
+
171
+ self.context = context
172
+ self.msg_queue_handler = MsgQueueHandler(self)
173
+
174
+ # Send the ChaincodeID during register.
175
+ cm = ccshim_pb2.ChaincodeMessage(
176
+ type=ccshim_pb2.ChaincodeMessage.REGISTER, payload=self.chaincode_id.SerializeToString())
177
+ cm.timestamp.FromDatetime(datetime.datetime.now())
178
+
179
+ # Register on the stream
180
+ await self.context.write(cm)
181
+
182
+ async for receive_message in stream:
183
+ LOGGER.info('Received message')
184
+ if receive_message is None:
185
+ err_str = "received nil message, ending chaincode stream"
186
+ LOGGER.error(err_str)
187
+ return ccshim_pb2.ChaincodeMessage(
188
+ type=ccshim_pb2.ChaincodeMessage.ERROR, payload=err_str.encode(encoding='utf-8'))
189
+ else:
190
+ # Keep handling asynchronous so the stream can continue receiving
191
+ # response frames needed by in-flight request futures.
192
+ self._track_task(self.handle_message(receive_message))
193
+
194
+ LOGGER.info(f'->>>> proposal {receive_message.proposal}')
195
+ LOGGER.info(f'->>>> payload {receive_message.payload}')
196
+ LOGGER.info(f'->>>> channel ID {receive_message.channel_id}')
197
+ LOGGER.info(f'->>>> Tx ID {receive_message.txid}')
198
+
199
+ if self._pending_tasks:
200
+ await asyncio.gather(*self._pending_tasks, return_exceptions=True)
201
+
202
+ async def handle_get_state(self, collection, key, channel_id, tx_id):
203
+ msg_pb = ccshim_pb2.GetState()
204
+ msg_pb.key = key
205
+ msg_pb.collection = collection
206
+ msg = ccshim_pb2.ChaincodeMessage(
207
+ type=ccshim_pb2.ChaincodeMessage.GET_STATE,
208
+ payload=msg_pb.SerializeToString(),
209
+ txid=tx_id,
210
+ channel_id=channel_id
211
+ )
212
+
213
+ result = await self.__ask_peer_and_listen(msg, 'GetState')
214
+ return result.payload
215
+
216
+ async def handle_put_state(self, collection, key, value, channel_id, tx_id):
217
+ msg_pb = ccshim_pb2.PutState()
218
+ msg_pb.key = key
219
+ msg_pb.value = value
220
+ msg_pb.collection = collection
221
+ msg = ccshim_pb2.ChaincodeMessage(
222
+ type=ccshim_pb2.ChaincodeMessage.PUT_STATE,
223
+ payload=msg_pb.SerializeToString(),
224
+ txid=tx_id,
225
+ channel_id=channel_id
226
+ )
227
+ return await self.__ask_peer_and_listen(msg, 'PutState')
228
+
229
+ async def handle_delete_state(self, collection, key, channel_id, tx_id):
230
+ msg_pb = ccshim_pb2.DelState()
231
+ msg_pb.key = key
232
+ msg_pb.collection = collection
233
+ msg = ccshim_pb2.ChaincodeMessage(
234
+ type=ccshim_pb2.ChaincodeMessage.DEL_STATE,
235
+ payload=msg_pb.SerializeToString(),
236
+ txid=tx_id,
237
+ channel_id=channel_id
238
+ )
239
+ return await self.__ask_peer_and_listen(msg, 'DeleteState')
240
+
241
+ async def handle_get_state_by_range(self, collection, start_key, end_key,
242
+ channel_id, tx_id, metadata=None):
243
+ """Send a GetStateByRange request to the peer.
244
+
245
+ ``metadata`` (if present) is a serialised :class:`QueryMetadata`
246
+ message — used for pagination.
247
+ """
248
+ msg_pb = ccshim_pb2.GetStateByRange()
249
+ # NB: the protobuf field names are camelCase (startKey/endKey), not
250
+ # snake_case.
251
+ msg_pb.startKey = start_key or ""
252
+ msg_pb.endKey = end_key or ""
253
+ msg_pb.collection = collection
254
+ if metadata is not None:
255
+ msg_pb.metadata = metadata
256
+ msg = ccshim_pb2.ChaincodeMessage(
257
+ type=ccshim_pb2.ChaincodeMessage.GET_STATE_BY_RANGE,
258
+ payload=msg_pb.SerializeToString(),
259
+ txid=tx_id,
260
+ channel_id=channel_id,
261
+ )
262
+ return await self.__ask_peer_and_listen(msg, 'GetStateByRange')
263
+
264
+ async def handle_get_query_result(self, collection, query, channel_id, tx_id,
265
+ metadata=None):
266
+ """Send a GetQueryResult (CouchDB rich query) request to the peer."""
267
+ msg_pb = ccshim_pb2.GetQueryResult()
268
+ msg_pb.query = query
269
+ msg_pb.collection = collection
270
+ if metadata is not None:
271
+ msg_pb.metadata = metadata
272
+ msg = ccshim_pb2.ChaincodeMessage(
273
+ type=ccshim_pb2.ChaincodeMessage.GET_QUERY_RESULT,
274
+ payload=msg_pb.SerializeToString(),
275
+ txid=tx_id,
276
+ channel_id=channel_id,
277
+ )
278
+ return await self.__ask_peer_and_listen(msg, 'GetQueryResult')
279
+
280
+ async def handle_get_history_for_key(self, key, channel_id, tx_id):
281
+ """Send a GetHistoryForKey request to the peer."""
282
+ msg_pb = ccshim_pb2.GetHistoryForKey()
283
+ msg_pb.key = key
284
+ msg = ccshim_pb2.ChaincodeMessage(
285
+ type=ccshim_pb2.ChaincodeMessage.GET_HISTORY_FOR_KEY,
286
+ payload=msg_pb.SerializeToString(),
287
+ txid=tx_id,
288
+ channel_id=channel_id,
289
+ )
290
+ return await self.__ask_peer_and_listen(msg, 'GetHistoryForKey')
291
+
292
+ async def handle_query_state_next(self, query_id, channel_id, tx_id):
293
+ """Fetch the next page of an in-flight paged query."""
294
+ msg_pb = ccshim_pb2.QueryStateNext()
295
+ msg_pb.id = query_id
296
+ msg = ccshim_pb2.ChaincodeMessage(
297
+ type=ccshim_pb2.ChaincodeMessage.QUERY_STATE_NEXT,
298
+ payload=msg_pb.SerializeToString(),
299
+ txid=tx_id,
300
+ channel_id=channel_id,
301
+ )
302
+ return await self.__ask_peer_and_listen(msg, 'QueryStateNext')
303
+
304
+ async def handle_query_state_close(self, query_id, channel_id, tx_id):
305
+ """Close an in-flight paged query."""
306
+ msg_pb = ccshim_pb2.QueryStateClose()
307
+ msg_pb.id = query_id
308
+ msg = ccshim_pb2.ChaincodeMessage(
309
+ type=ccshim_pb2.ChaincodeMessage.QUERY_STATE_CLOSE,
310
+ payload=msg_pb.SerializeToString(),
311
+ txid=tx_id,
312
+ channel_id=channel_id,
313
+ )
314
+ return await self.__ask_peer_and_listen(msg, 'QueryStateClose')
315
+
316
+ async def handle_invoke_chaincode(self, chaincode_name, args, channel_id, tx_id):
317
+ """Invoke another chaincode by name."""
318
+ ci = cc_pb2.ChaincodeInput()
319
+ for a in args:
320
+ ci.args.append(a.encode() if isinstance(a, str) else a)
321
+ msg = ccshim_pb2.ChaincodeMessage(
322
+ type=ccshim_pb2.ChaincodeMessage.INVOKE_CHAINCODE,
323
+ payload=ci.SerializeToString(),
324
+ txid=tx_id,
325
+ channel_id=channel_id,
326
+ )
327
+ return await self.__ask_peer_and_listen(msg, 'InvokeChaincode')
328
+
329
+ async def __ask_peer_and_listen(self, msg, action):
330
+ loop = asyncio.get_running_loop()
331
+ fut = loop.create_future()
332
+
333
+ message = QueueMessage(msg, action, fut)
334
+ await self.msg_queue_handler.queue_msg(message)
335
+
336
+ return await fut
@@ -0,0 +1,134 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ from abc import ABC, abstractmethod
3
+
4
+ from fabric_protos.peer import proposal_response_pb2 as pb
5
+
6
+
7
+ class ChaincodeStubInterface(ABC):
8
+ """
9
+ ChaincodeStubInterface is used by deployable chaincode apps to access and modify their ledgers
10
+ """
11
+ def get_function_and_parameters(self): # Get the chaincode calling method name and parameter list
12
+ pass
13
+
14
+ def get_txid(self): # Get the ID of the chaincode calling transaction
15
+ pass
16
+
17
+ def get_channel_id(self): # Get the channel ID of the chaincode calling transaction
18
+ pass
19
+
20
+ def get_creator(self): # Get the user ID of the chaincode calling transaction
21
+ pass
22
+
23
+ def get_transient(self): # Get the transient dataset of chaincode call transactions
24
+ pass
25
+
26
+ def get_tx_timestamp(self): # Get the timestamp of the chaincode calling transaction
27
+ pass
28
+
29
+ @abstractmethod
30
+ def get_state(self, key: str): #-> bytearray:
31
+ """
32
+ GetState returns the value of the specified `key` from the
33
+ ledger. Note that GetState doesn't read data from the writeset, which
34
+ has not been committed to the ledger. In other words, GetState doesn't
35
+ consider data modified by PutState that has not been committed.
36
+ If the key does not exist in the state database, (nil, nil) is returned.
37
+ """
38
+
39
+ def put_state(self, key: str, value): # update the state of the specified key on the ledger
40
+ pass
41
+
42
+ def delete_state(self, key: str): # delete the state of the specified key on the ledger
43
+ pass
44
+
45
+ def set_state_validation_parameter(self): # Set state validation parameters
46
+ pass
47
+
48
+ def get_state_validation_parameter(self): # Get state validation parameters
49
+ pass
50
+
51
+ def get_state_by_range(self): # Get the state of the keys in the specified range on the ledger
52
+ pass
53
+
54
+ def get_state_by_range_with_pagination(
55
+ self): # Pagination to get the state of the keys in the specified range on the ledger
56
+ pass
57
+
58
+ def get_query_result(self): # Get the node rich query result, which is only valid when couchdb is used
59
+ pass
60
+
61
+ def get_query_result_with_pagination(self): # Pagination to get node rich query results
62
+ pass
63
+
64
+ def get_history_for_key(self): # Get the update history of the specified key on the ledger
65
+ pass
66
+
67
+ def invoke_chaincode(self): # Invoke other chaincodes
68
+ pass
69
+
70
+ def set_event(self): # trigger chaincode event
71
+ pass
72
+
73
+ def create_composite_key(self): # Create a composite key
74
+ pass
75
+
76
+ # split composite key, return composite key type and composition attribute value
77
+ def split_composite_key(self):
78
+ pass
79
+
80
+ def get_state_by_partial_composite_key(self): # Query the ledger state using a partial composite key
81
+ pass
82
+
83
+ # Use partial composite key pagination to query ledger state
84
+ def get_state_by_partial_composite_key_with_pagination(self):
85
+ pass
86
+
87
+ def get_private_data(self): # Get the status of the specified key in the specified private dataset
88
+ pass
89
+
90
+ def get_private_data_hash(self): # Get the state hash of the specified key in the specified private dataset
91
+ pass
92
+
93
+ def put_private_data(self): # Update the state of the specified key in the specified private dataset
94
+ pass
95
+
96
+ def delete_private_data(self): # delete the specified key in the specified private data set
97
+ pass
98
+
99
+ def set_private_data_validation_parameter(self): # Set validation parameters for private data
100
+ pass
101
+
102
+ def get_private_data_validation_parameter(self): # Get the validation parameters for private data
103
+ pass
104
+
105
+ # Get the status of the keys of the specified range in the specified private dataset
106
+ def get_private_data_by_range(self):
107
+ pass
108
+
109
+ def get_private_data_by_partial_composite_key(self): # Query a private dataset with a partial composite key
110
+ pass
111
+
112
+ # Get rich query results for private datasets, only valid when couchdb is enabled
113
+ def get_private_data_query_result(self):
114
+ pass
115
+
116
+
117
+ class Chaincode(ABC):
118
+ @staticmethod
119
+ @abstractmethod
120
+ async def init(stub: ChaincodeStubInterface) -> pb.Response:
121
+ """
122
+ init is called during Instantiate transaction after the chaincode container has been established for the
123
+ first time, allowing the chaincode to initialize its internal data
124
+ """
125
+ pass
126
+
127
+ @staticmethod
128
+ @abstractmethod
129
+ async def invoke(stub: ChaincodeStubInterface) -> pb.Response:
130
+ """
131
+ invoke is called to update or query the ledger in a proposal transaction. Updated state variables are not
132
+ committed to the ledger until the transaction is committed.
133
+ """
134
+ pass
@@ -0,0 +1,107 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Asynchronous iterators for Fabric state / history / query results.
3
+
4
+ The contract API exposes three operations that return iterators over ledger
5
+ data:
6
+
7
+ * :func:`ChaincodeStub.get_state_by_range` / :func:`get_state_by_partial_composite_key`
8
+ → :class:`StateQueryIterator`
9
+ * :func:`ChaincodeStub.get_query_result` (CouchDB rich queries)
10
+ → :class:`QueryResultIterator` (same wire protocol as state queries)
11
+ * :func:`ChaincodeStub.get_history_for_key`
12
+ → :class:`HistoryQueryIterator` (yields :class:`KeyModification` records)
13
+
14
+ Each iterator transparently pages through the peer's responses by sending
15
+ ``QueryStateNext`` messages until the peer reports ``has_more == false``,
16
+ then closes the query with ``QueryStateClose`` when the iteration ends.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import asyncio
22
+ from typing import Any, AsyncIterator, List, Optional
23
+
24
+
25
+ class _QueryIteratorBase:
26
+ """Common base for paged query iterators."""
27
+
28
+ def __init__(self, handler, channel_id: str, tx_id: str,
29
+ initial_response, response_factory, parser):
30
+ self._handler = handler
31
+ self._channel_id = channel_id
32
+ self._tx_id = tx_id
33
+ self._response_factory = response_factory # callable(msg_bytes) -> QueryResponse
34
+ self._parser = parser # callable(QueryResultBytes) -> parsed value
35
+ self._buffer: List[Any] = []
36
+ self._id: Optional[str] = None
37
+ self._has_more: bool = False
38
+ self._closed: bool = False
39
+ self._consume(initial_response)
40
+
41
+ def _consume(self, raw_msg) -> None:
42
+ """Parse a peer QueryResponse message into buffer + pagination state."""
43
+ if raw_msg is None:
44
+ return
45
+ resp = self._response_factory(raw_msg.payload)
46
+ self._id = resp.id
47
+ for qrb in resp.results:
48
+ self._buffer.append(self._parser(qrb))
49
+ self._has_more = bool(resp.has_more)
50
+
51
+ async def __aiter__(self):
52
+ return self
53
+
54
+ def __aiter__(self):
55
+ return self
56
+
57
+ async def __anext__(self):
58
+ if self._buffer:
59
+ return self._buffer.pop(0)
60
+ if not self._has_more or self._id is None:
61
+ await self._close()
62
+ raise StopAsyncIteration
63
+ # Request the next page from the peer.
64
+ next_response = await self._handler.handle_query_state_next(
65
+ self._id, self._channel_id, self._tx_id,
66
+ )
67
+ self._consume(next_response)
68
+ if self._buffer:
69
+ return self._buffer.pop(0)
70
+ await self._close()
71
+ raise StopAsyncIteration
72
+
73
+ async def _close(self) -> None:
74
+ if self._closed or self._id is None:
75
+ self._closed = True
76
+ return
77
+ self._closed = True
78
+ try:
79
+ await self._handler.handle_query_state_close(
80
+ self._id, self._channel_id, self._tx_id,
81
+ )
82
+ except Exception:
83
+ # Best-effort close; iterators must not raise on close.
84
+ pass
85
+
86
+ async def aclose(self) -> None:
87
+ """Explicit close — safe to call multiple times."""
88
+ await self._close()
89
+
90
+
91
+ class StateQueryIterator(_QueryIteratorBase):
92
+ """Iterates over (key, value) byte pairs returned by GetStateByRange."""
93
+
94
+
95
+ class QueryResultIterator(_QueryIteratorBase):
96
+ """Iterates over rich-query (CouchDB) results."""
97
+
98
+
99
+ class HistoryQueryIterator(_QueryIteratorBase):
100
+ """Iterates over :class:`KeyModification` records returned by GetHistoryForKey."""
101
+
102
+
103
+ __all__ = [
104
+ "StateQueryIterator",
105
+ "QueryResultIterator",
106
+ "HistoryQueryIterator",
107
+ ]
@@ -0,0 +1 @@
1
+ # SPDX-License-Identifier: Apache-2.0
@@ -0,0 +1,63 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ import asyncio
3
+ import logging
4
+ import logging.handlers
5
+
6
+ try:
7
+ # Python 3.7 and newer, fast reentrant implementation
8
+ # without task tracking (not needed for that when logging)
9
+ from queue import SimpleQueue as Queue
10
+ except ImportError:
11
+ from queue import Queue
12
+ from typing import List
13
+
14
+ LOGGER = logging.getLogger("asyncio")
15
+
16
+
17
+ class LocalQueueHandler(logging.handlers.QueueHandler):
18
+ """
19
+ Network logging can block the event loop. It is recommended to use a separate thread for handling logs or use
20
+ non-blocking IO.
21
+ """
22
+
23
+ def emit(self, record: logging.LogRecord) -> None:
24
+ # Removed the call to self.prepare(), handle task cancellation
25
+ try:
26
+ self.enqueue(record)
27
+ except asyncio.CancelledError:
28
+ raise
29
+ except Exception:
30
+ self.handleError(record)
31
+
32
+
33
+ def setup_logging_queue() -> List[logging.Handler]:
34
+ """Move log handlers to a separate thread.
35
+
36
+ Replace handlers on the root logger with a LocalQueueHandler,
37
+ and start a logging.QueueListener holding the original
38
+ handlers.
39
+
40
+ """
41
+ queue = Queue()
42
+ logging.basicConfig(level=logging.DEBUG, format='%(asctime)s %(levelname)s %(message)s')
43
+
44
+ handlers: List[logging.Handler] = []
45
+
46
+ handler = LocalQueueHandler(queue)
47
+ LOGGER.addHandler(handler)
48
+ for h in LOGGER.handlers[:]:
49
+ if h is not handler:
50
+ LOGGER.removeHandler(h)
51
+ handlers.append(h)
52
+
53
+ listener = logging.handlers.QueueListener(
54
+ queue, *handlers, respect_handler_level=True
55
+ )
56
+ # Keep a global reference to the listener so it is not garbage collected
57
+ global _QUEUE_LISTENER
58
+ _QUEUE_LISTENER = listener
59
+ listener.start()
60
+
61
+
62
+ setup_logging_queue()
63
+ # logging.basicConfig(level=logging.DEBUG, format='%(asctime)s %(levelname)s %(message)s')