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,91 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ import asyncio
3
+ from src.fabric_shim.logging import LOGGER
4
+
5
+
6
+ class QueueMessage:
7
+ def __init__(self, msg, method, future: asyncio.Future) -> None:
8
+ self.msg = msg
9
+ self.method = method
10
+ self.future = future
11
+
12
+ def get_msg(self):
13
+ return self.msg
14
+
15
+ def get_msg_txContextId(self):
16
+ return self.msg.channel_id + self.msg.txid
17
+
18
+ def get_method(self):
19
+ return self.method
20
+
21
+ def success(self, response):
22
+ self.future.set_result(response)
23
+
24
+ def fail(self, err):
25
+ self.future.set_exception(Exception(err))
26
+
27
+
28
+ class MsgQueueHandler:
29
+ """This class handles queuing messages to be sent to the peer based on transaction id"""
30
+
31
+ def __init__(self, handler) -> None:
32
+ self.handler = handler
33
+ self.tx_queues = {}
34
+ self.context = handler.context
35
+
36
+ async def queue_msg(self, msg: QueueMessage):
37
+ """Queue a message to be sent to the peer"""
38
+ tx_context_id = msg.get_msg_txContextId()
39
+ msg_queue = self.tx_queues.setdefault(tx_context_id, [])
40
+ msg_queue.append(msg)
41
+ if len(msg_queue) == 1:
42
+ await self.__send_msg(tx_context_id)
43
+
44
+ async def __send_msg(self, tx_context_id):
45
+ """send the current message to the peer"""
46
+ msg: QueueMessage = self.__get_current_msg(tx_context_id)
47
+ if msg:
48
+ try:
49
+ await self.context.write(msg.get_msg())
50
+ except Exception as e:
51
+ msg.fail(e)
52
+
53
+ async def handle_msg_response(self, response):
54
+ tx_id = response.txid
55
+ channel_id = response.channel_id
56
+ tx_context_id = channel_id + tx_id
57
+ msg: QueueMessage = self.__get_current_msg(tx_context_id)
58
+
59
+ if msg:
60
+ try:
61
+ # parsed_response = parse_response(self.handler, response, msg.get_method())
62
+ msg.success(response)
63
+ except Exception as e:
64
+ msg.fail(e)
65
+ await self.__remove_current_and_send_next(tx_context_id)
66
+ else:
67
+ LOGGER.warning('No queued message found for tx_context_id %s', tx_context_id)
68
+
69
+ def __get_current_msg(self, tx_context_id):
70
+ """returns the message at the top of the queue for the particular transaction"""
71
+ msg_queue = self.tx_queues.get(tx_context_id)
72
+ if msg_queue:
73
+ return msg_queue[0]
74
+
75
+ LOGGER.debug('Failed to find a message for transaction context id %s', tx_context_id)
76
+ return None
77
+
78
+ async def __remove_current_and_send_next(self, tx_context_id):
79
+ """Remove the current message and send the next message in the queue if there is one"""
80
+ msg_queue = self.tx_queues.get(tx_context_id)
81
+ if msg_queue and len(msg_queue) > 0:
82
+ msg_queue.pop(0)
83
+ if len(msg_queue) != 0:
84
+ await self.__send_msg(tx_context_id)
85
+ else:
86
+ # remove empty queue to free memory
87
+ try:
88
+ del self.tx_queues[tx_context_id]
89
+ except KeyError:
90
+ pass
91
+
@@ -0,0 +1,49 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ from fabric_protos.peer import chaincode_shim_pb2 as ccshim_pb2
3
+ from fabric_protos.peer import proposal_response_pb2 as pb
4
+
5
+
6
+ class ResponseCode:
7
+ """
8
+ OK constant - status code less than 400, endorser will endorse it.
9
+ OK means init or invoke successfully.
10
+
11
+ ERRORTHRESHOLD constant - status code greater than or equal to 400 will be considered an error and rejected by
12
+ endorser.
13
+
14
+ ERROR constant - default error value
15
+ """
16
+ OK, ERRORTHRESHOLD, ERROR = 200, 400, 500
17
+
18
+
19
+ def new_error_msg(msg, state) -> ccshim_pb2.ChaincodeMessage:
20
+ err_str = f"[{msg.txid}] Chaincode h cannot handle message ({msg.type}) while in state: {state}"
21
+ return ccshim_pb2.ChaincodeMessage(type=ccshim_pb2.ChaincodeMessage.ERROR,
22
+ payload=err_str.encode(encoding='utf-8'), txid=msg.txid)
23
+
24
+
25
+ def success(payload):
26
+ """Build a successful Fabric Response.
27
+
28
+ Puts *payload* in the ``payload`` (bytes) field of the Response — this
29
+ is the field that ``peer chaincode query`` / ``invoke`` reads to print
30
+ the chaincode result. ``payload`` may be ``bytes``, ``str`` (will be
31
+ UTF-8 encoded) or ``None`` (treated as an empty payload).
32
+ """
33
+ if payload is None:
34
+ payload = b""
35
+ if isinstance(payload, str):
36
+ payload = payload.encode("utf-8")
37
+ elif isinstance(payload, bytearray):
38
+ payload = bytes(payload)
39
+ return pb.Response(status=ResponseCode.OK, payload=payload)
40
+
41
+
42
+ def error(message: str = ""):
43
+ """Build an error Fabric Response.
44
+
45
+ Puts *message* in the ``message`` (string) field of the Response — this
46
+ is what ``peer chaincode query`` / ``invoke`` prints to stderr when
47
+ the chaincode returns an error.
48
+ """
49
+ return pb.Response(status=ResponseCode.ERROR, message=message)
@@ -0,0 +1,155 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ import asyncio
3
+ import logging
4
+ import os
5
+
6
+ from typing import AsyncIterable, Type
7
+
8
+ import grpc
9
+ import queue
10
+
11
+ from src.fabric_shim.handler import Handler
12
+ from src.fabric_shim.interfaces import Chaincode
13
+ from src.fabric_shim.logging import LOGGER
14
+ from fabric_protos.peer import chaincode_shim_pb2_grpc as ccshim_grpc_pb2
15
+ from fabric_protos.peer import chaincode_shim_pb2 as ccshim_pb2
16
+
17
+ # Coroutines to be invoked when the event loop is shutting down.
18
+ _cleanup_coroutines = []
19
+
20
+ send_queue = queue.SimpleQueue()
21
+
22
+
23
+ class ChaincodeService(ccshim_grpc_pb2.ChaincodeServicer):
24
+ """Chaincode as a server - peer establishes a connection to the chaincode as a client
25
+ Currently only supports a stream connection.
26
+ """
27
+
28
+ def __init__(self, chaincode_id: str, chaincode: Chaincode):
29
+ self._ccid = chaincode_id
30
+ self._cc = chaincode
31
+
32
+ async def Connect(self, request_iterator: AsyncIterable[ccshim_pb2.ChaincodeMessage],
33
+ context: grpc.aio.ServicerContext) -> None: # Iterable[ccshim_pb2.ChaincodeMessage]:
34
+ try:
35
+ handler = Handler(self._ccid, self._cc)
36
+ await handler.chat_with_peer(request_iterator, context)
37
+ except asyncio.CancelledError:
38
+ LOGGER.info("Cancelling RPC due to exhausted resources.")
39
+ # context.abort()
40
+
41
+
42
+ def load_tls_config(key: bytes = None, cert: bytes = None, client_ca_certs: bytes = None) -> grpc.ServerCredentials:
43
+ """
44
+ load_tls_config loads the TLS configuration for the chaincode
45
+
46
+ Returns:
47
+ A grpc.ServerCredentials
48
+ """
49
+ # Accept either file paths (str) or raw bytes. Normalize to bytes.
50
+ def _read_bytes(v):
51
+ if v is None:
52
+ return None
53
+ if isinstance(v, (bytes, bytearray)):
54
+ return bytes(v)
55
+ if isinstance(v, str):
56
+ try:
57
+ with open(v, 'rb') as fh:
58
+ return fh.read()
59
+ except Exception:
60
+ return v.encode()
61
+ return v
62
+
63
+ key_b = _read_bytes(key)
64
+ cert_b = _read_bytes(cert)
65
+ ca_b = _read_bytes(client_ca_certs)
66
+
67
+ client_auth = True if ca_b else False
68
+ # Loading credentials
69
+ return grpc.ssl_server_credentials(((key_b, cert_b,),), ca_b, client_auth)
70
+
71
+
72
+ def _internal_server(**kwargs) -> grpc.aio.Server:
73
+ server = grpc.aio.server()
74
+
75
+ key = kwargs.pop("key", os.getenv('CORE_TLS_CLIENT_KEY_PATH'))
76
+ cert = kwargs.pop("cert", os.getenv('CORE_TLS_CLIENT_CERT_PATH'))
77
+ address = kwargs.get("address")
78
+
79
+ if not key or not cert:
80
+ port = server.add_insecure_port(address)
81
+ else:
82
+ client_ca_certs = kwargs.pop("client_ca_certs", os.getenv('CORE_PEER_TLS_ROOTCERT_FILE'))
83
+ server_credentials = load_tls_config(key, cert, client_ca_certs)
84
+ # Pass down credentials
85
+ port = server.add_secure_port(address, server_credentials)
86
+
87
+ ccid = kwargs.get("ccid")
88
+ cc = kwargs.pop("cc")
89
+
90
+ ccshim_grpc_pb2.add_ChaincodeServicer_to_server(ChaincodeService(ccid, cc), server)
91
+ logging.info('Server is listening at port :%d', port)
92
+ return server
93
+
94
+
95
+ async def _internal_start(server: grpc.aio.Server) -> None:
96
+ await server.start()
97
+
98
+ async def server_graceful_shutdown():
99
+ logging.info("Starting graceful shutdown...")
100
+ # Shuts down the server with 3 seconds of grace period. During the
101
+ # grace period, the server won't accept new connections and allow
102
+ # existing RPCs to continue within the grace period.
103
+ await server.stop(3)
104
+
105
+ _cleanup_coroutines.append(server_graceful_shutdown())
106
+ await server.wait_for_termination()
107
+
108
+
109
+ def start(cc: Type[Chaincode],
110
+ cc_id: str = None,
111
+ address: str = None,
112
+ key: bytes = None,
113
+ cert: bytes = None,
114
+ client_ca_certs: bytes = None):
115
+ """
116
+ start the server
117
+
118
+ address Is the listen address of the chaincode server.
119
+ key TLS Private key passed to chaincode server.
120
+ cert TLS Certificate passed to chaincode server. Note
121
+ that this argument is compatible with 'key' - if some
122
+ are missing, 'TLS disabled'.
123
+ client_ca_certs Set if connecting peer should be verified.
124
+ """
125
+ cc_id = os.getenv('CHAINCODE_ID', cc_id)
126
+ address = os.getenv('CHAINCODE_SERVER_ADDRESS', address)
127
+ if not cc_id:
128
+ raise Exception("cc_id must be specified")
129
+ if not address:
130
+ raise Exception("address must be specified")
131
+
132
+ if cc is None:
133
+ raise Exception("chaincode must be specified")
134
+
135
+ # Accept either a Chaincode subclass (class) or an instance implementing Chaincode
136
+ if isinstance(cc, type):
137
+ if not issubclass(cc, Chaincode):
138
+ raise Exception("chaincode must subclass Chaincode")
139
+ # instantiate the chaincode class
140
+ try:
141
+ cc = cc()
142
+ except Exception as e:
143
+ raise Exception(f"failed to instantiate chaincode class: {e}")
144
+ else:
145
+ if not isinstance(cc, Chaincode):
146
+ raise Exception("chaincode must be an instance of Chaincode")
147
+
148
+ server = _internal_server(ccid=cc_id, address=address, cc=cc, key=key, cert=cert, client_ca_certs=client_ca_certs)
149
+ loop = asyncio.get_event_loop()
150
+ try:
151
+ loop.run_until_complete(_internal_start(server))
152
+ finally:
153
+ if _cleanup_coroutines:
154
+ loop.run_until_complete(asyncio.gather(*_cleanup_coroutines))
155
+ loop.close()
@@ -0,0 +1,342 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ from src.fabric_shim.interfaces import ChaincodeStubInterface
3
+ from src.fabric_shim.utils import (
4
+ COMPOSITEKEY_NS,
5
+ MIN_UNICODE_RUNE_VALUE,
6
+ validate_composite_key_attribute,
7
+ )
8
+ from fabric_protos.peer import chaincode_pb2 as pb
9
+ from fabric_protos.common import common_pb2 as cm_pb
10
+ from fabric_protos.peer import proposal_pb2 as pr_pb
11
+ from fabric_protos.msp import identities_pb2 as id_pb
12
+ from fabric_protos.peer import chaincode_event_pb2 as e_pb
13
+ from collections.abc import Sequence
14
+ from src.fabric_shim.logging import LOGGER
15
+
16
+ VALIDATION_PARAMETER: str = 'VALIDATION_PARAMETER'
17
+
18
+
19
+ class ChaincodeStub(ChaincodeStubInterface):
20
+ """The stub encapsulates the APIs between the chaincode implementation and the Fabric peer"""
21
+
22
+ def __init__(self, client, channel_id, tx_id, cc_input, signed_proposal_pb):
23
+ self.client = client
24
+ self.channel_id = channel_id
25
+ self.tx_id = tx_id
26
+ self.cc_input = cc_input
27
+ self.signed_proposal_pb = signed_proposal_pb
28
+ self.creator = {}
29
+ self.tx_timestamp = None
30
+ self.proposal = None
31
+ self.validationParameterMetakey = VALIDATION_PARAMETER
32
+
33
+ if self.signed_proposal_pb:
34
+ # Only attempt to decode the signed proposal if it contains proposal_bytes
35
+ proposal_bytes = getattr(self.signed_proposal_pb, 'proposal_bytes', None)
36
+ if proposal_bytes:
37
+ decoded_sp = {
38
+ 'signature': getattr(self.signed_proposal_pb, 'signature', None)
39
+ }
40
+ proposal = pr_pb.Proposal.FromString(proposal_bytes)
41
+ decoded_sp['proposal'] = {}
42
+ self.proposal = proposal
43
+
44
+ if not proposal.header or len(proposal.header) == 0:
45
+ raise Exception('Proposal header is empty')
46
+
47
+ if not proposal.payload or len(proposal.payload) == 0:
48
+ raise Exception('Proposal payload is empty')
49
+
50
+ try:
51
+ header = cm_pb.Header.FromString(proposal.header)
52
+ # Use a dict for the decoded header fields rather than assigning
53
+ # the protobuf Header object directly (protobuf objects don't
54
+ # support item assignment). Keep the parsed `header` protobuf
55
+ # in a local variable for further extraction.
56
+ decoded_sp['proposal']['header'] = {}
57
+ decoded_sp['proposal']['header']['_raw_header'] = header
58
+ except Exception as e:
59
+ raise Exception('Could not extract the header from the proposal: ' + str(e))
60
+
61
+ try:
62
+ signature_header = cm_pb.SignatureHeader.FromString(header.signature_header)
63
+ decoded_sp['proposal']['header']['signatureHeader'] = \
64
+ {'nonce': signature_header.nonce, 'creator_u8': signature_header.creator}
65
+ except Exception as e:
66
+ raise Exception('Decoding SignatureHeader failed: ' + str(e))
67
+
68
+ try:
69
+ creator = id_pb.SerializedIdentity.FromString(signature_header.creator)
70
+ decoded_sp['proposal']['header']['signatureHeader']['creator'] = creator
71
+ self.creator = {'mspid': creator.mspid, 'idBytes': creator.id_bytes}
72
+ except Exception as e:
73
+ raise Exception('Decoding SerializedIdentity failed: ' + str(e))
74
+
75
+ try:
76
+ channel_header = cm_pb.ChannelHeader.FromString(header.channel_header)
77
+ decoded_sp['proposal']['header']['channelHeader'] = channel_header
78
+ self.tx_timestamp = channel_header.timestamp
79
+ except Exception as e:
80
+ raise Exception('Decoding ChannelHeader failed: ' + str(e))
81
+
82
+ try:
83
+ ccpp = pr_pb.ChaincodeProposalPayload.FromString(proposal.payload)
84
+ decoded_sp['proposal']['payload'] = ccpp
85
+ except Exception as e:
86
+ raise Exception('Decoding ChaincodeProposalPayload failed: %s' + str(e))
87
+
88
+ self.signed_proposal_pb = decoded_sp
89
+ else:
90
+ # No proposal bytes provided; treat as unsigned/no proposal for tests
91
+ self.signed_proposal_pb = None
92
+
93
+ def get_channel_id(self):
94
+ """Get the channel ID of the chaincode calling transaction"""
95
+ return self.channel_id
96
+
97
+ def get_tx_timestamp(self):
98
+ """Get the timestamp of the chaincode calling transaction"""
99
+ return self.tx_timestamp
100
+
101
+ def get_creator(self):
102
+ """Get the user ID of the chaincode calling transaction"""
103
+ return self.creator
104
+
105
+ def get_txid(self):
106
+ """Get the ID of the chaincode calling transaction"""
107
+ return self.tx_id
108
+
109
+ def get_function_and_parameters(self):
110
+ """Get function name and parameters of the chaincode calling transaction"""
111
+ args = [arg.decode() for arg in self.cc_input.args]
112
+ if len(args) == 0:
113
+ raise Exception('no function name provided in transaction payload')
114
+ function: str = args[0]
115
+ params = args[1:]
116
+
117
+ return function, params
118
+
119
+ async def get_state(self, key: str): #-> bytearray:
120
+ """Get asset state from ledger"""
121
+ LOGGER.info('get_state called with key:%s' % key)
122
+ # Access public data by setting the collection to empty string
123
+ collection = ''
124
+ return await self.client.handle_get_state(collection, key, self.channel_id, self.tx_id)
125
+
126
+ async def put_state(self, key: str, value):
127
+ """Put asset state to ledger"""
128
+ LOGGER.info('put_state called with key:%s and value:%s' % (key, value))
129
+ # Access public data by setting the collection to empty string
130
+ collection = ''
131
+ if isinstance(value, str):
132
+ value = bytes(value.encode())
133
+ return await self.client.handle_put_state(collection, key, value, self.channel_id, self.tx_id)
134
+
135
+ async def delete_state(self, key: str):
136
+ """Delete asset state from ledger"""
137
+ LOGGER.info('delete_state called with key:%s' % key)
138
+ # Access public data by setting the collection to empty string
139
+ collection = ''
140
+ return await self.client.handle_delete_state(collection, key, self.channel_id, self.tx_id)
141
+
142
+ async def get_state_by_range(self, start_key: str, end_key: str):
143
+ """Returns a range of keys from the ledger.
144
+
145
+ Returns an async iterator yielding :class:`KV` records (with
146
+ ``key`` and ``value`` byte fields). Both keys may be empty strings
147
+ to mean "unbounded".
148
+ """
149
+ collection = ''
150
+ from src.fabric_shim.iterators import StateQueryIterator
151
+ from fabric_protos.peer import chaincode_shim_pb2 as ccshim_pb2
152
+ from fabric_protos.ledger.queryresult import kv_query_result_pb2 as kv_pb
153
+
154
+ raw = await self.client.handle_get_state_by_range(
155
+ collection, start_key, end_key,
156
+ self.channel_id, self.tx_id,
157
+ )
158
+ return StateQueryIterator(
159
+ handler=self.client,
160
+ channel_id=self.channel_id,
161
+ tx_id=self.tx_id,
162
+ initial_response=raw,
163
+ response_factory=lambda payload: ccshim_pb2.QueryResponse.FromString(payload),
164
+ parser=lambda qrb: kv_pb.KV.FromString(qrb.resultBytes),
165
+ )
166
+
167
+ async def get_state_by_range_with_pagination(self, start_key: str, end_key: str,
168
+ page_size: int, bookmark: str = ""):
169
+ """Paginated version of :meth:`get_state_by_range`.
170
+
171
+ Returns an async iterator yielding :class:`KV` records. Use the
172
+ :meth:`aclose` method of the iterator when you want to stop early.
173
+ """
174
+ collection = ''
175
+ from src.fabric_shim.iterators import StateQueryIterator
176
+ from fabric_protos.peer import chaincode_shim_pb2 as ccshim_pb2
177
+ from fabric_protos.ledger.queryresult import kv_query_result_pb2 as kv_pb
178
+
179
+ meta = ccshim_pb2.QueryMetadata()
180
+ meta.pageSize = int(page_size)
181
+ meta.bookmark = bookmark or ""
182
+ raw = await self.client.handle_get_state_by_range(
183
+ collection, start_key, end_key,
184
+ self.channel_id, self.tx_id, metadata=meta.SerializeToString(),
185
+ )
186
+ return StateQueryIterator(
187
+ handler=self.client,
188
+ channel_id=self.channel_id,
189
+ tx_id=self.tx_id,
190
+ initial_response=raw,
191
+ response_factory=lambda payload: ccshim_pb2.QueryResponse.FromString(payload),
192
+ parser=lambda qrb: kv_pb.KV.FromString(qrb.resultBytes),
193
+ )
194
+
195
+ async def get_query_result(self, query: str):
196
+ """Run a CouchDB rich query against the world state.
197
+
198
+ Returns an async iterator yielding :class:`KV` records.
199
+ """
200
+ collection = ''
201
+ from src.fabric_shim.iterators import QueryResultIterator
202
+ from fabric_protos.peer import chaincode_shim_pb2 as ccshim_pb2
203
+ from fabric_protos.ledger.queryresult import kv_query_result_pb2 as kv_pb
204
+
205
+ raw = await self.client.handle_get_query_result(
206
+ collection, query, self.channel_id, self.tx_id,
207
+ )
208
+ return QueryResultIterator(
209
+ handler=self.client,
210
+ channel_id=self.channel_id,
211
+ tx_id=self.tx_id,
212
+ initial_response=raw,
213
+ response_factory=lambda payload: ccshim_pb2.QueryResponse.FromString(payload),
214
+ parser=lambda qrb: kv_pb.KV.FromString(qrb.resultBytes),
215
+ )
216
+
217
+ async def get_query_result_with_pagination(self, query: str, page_size: int,
218
+ bookmark: str = ""):
219
+ """Paginated version of :meth:`get_query_result`."""
220
+ collection = ''
221
+ from src.fabric_shim.iterators import QueryResultIterator
222
+ from fabric_protos.peer import chaincode_shim_pb2 as ccshim_pb2
223
+ from fabric_protos.ledger.queryresult import kv_query_result_pb2 as kv_pb
224
+
225
+ meta = ccshim_pb2.QueryMetadata()
226
+ meta.pageSize = int(page_size)
227
+ meta.bookmark = bookmark or ""
228
+ raw = await self.client.handle_get_query_result(
229
+ collection, query, self.channel_id, self.tx_id,
230
+ metadata=meta.SerializeToString(),
231
+ )
232
+ return QueryResultIterator(
233
+ handler=self.client,
234
+ channel_id=self.channel_id,
235
+ tx_id=self.tx_id,
236
+ initial_response=raw,
237
+ response_factory=lambda payload: ccshim_pb2.QueryResponse.FromString(payload),
238
+ parser=lambda qrb: kv_pb.KV.FromString(qrb.resultBytes),
239
+ )
240
+
241
+ async def get_history_for_key(self, key: str):
242
+ """Return the history of modifications for *key*.
243
+
244
+ Yields :class:`KeyModification` records (``tx_id``, ``value``,
245
+ ``timestamp``, ``is_delete``).
246
+ """
247
+ from src.fabric_shim.iterators import HistoryQueryIterator
248
+ from fabric_protos.peer import chaincode_shim_pb2 as ccshim_pb2
249
+ from fabric_protos.ledger.queryresult import kv_query_result_pb2 as kv_pb
250
+
251
+ raw = await self.client.handle_get_history_for_key(
252
+ key, self.channel_id, self.tx_id,
253
+ )
254
+ return HistoryQueryIterator(
255
+ handler=self.client,
256
+ channel_id=self.channel_id,
257
+ tx_id=self.tx_id,
258
+ initial_response=raw,
259
+ response_factory=lambda payload: ccshim_pb2.QueryResponse.FromString(payload),
260
+ parser=lambda qrb: kv_pb.KeyModification.FromString(qrb.resultBytes),
261
+ )
262
+
263
+ async def get_state_by_partial_composite_key(self, object_type: str,
264
+ attributes):
265
+ """Range query on the ledger using a partial composite key.
266
+
267
+ Builds a composite key prefix from *object_type* and *attributes*
268
+ and runs a range query from that prefix to the next code point.
269
+ """
270
+ full_key = self.create_composite_key(object_type, attributes)
271
+ # Range from the composite key to the same prefix with the maximum
272
+ # rune appended — this is the Fabric convention for "prefix" range
273
+ # queries against composite keys.
274
+ end_key = full_key + '\U0010FFFF'
275
+ return await self.get_state_by_range(full_key, end_key)
276
+
277
+ async def invoke_chaincode(self, chaincode_name: str, args, channel: str = ""):
278
+ """Invoke another chaincode by name.
279
+
280
+ ``args`` is a list of strings (or bytes). The return value is the
281
+ ``Response`` produced by the invoked chaincode.
282
+ """
283
+ from fabric_protos.peer import proposal_response_pb2 as pr_pb
284
+ # If a channel is supplied, build a fully-qualified name.
285
+ fully_qualified = f"{chaincode_name}/{channel}" if channel else chaincode_name
286
+ raw = await self.client.handle_invoke_chaincode(
287
+ fully_qualified, list(args), self.channel_id, self.tx_id,
288
+ )
289
+ # raw.payload is a serialised proposal Response
290
+ return pr_pb.Response.FromString(raw.payload)
291
+
292
+ def set_event(self, name: str, payload):
293
+ """Emit a chaincode event.
294
+
295
+ ``payload`` is stored on the stub and serialised into the final
296
+ ``COMPLETED`` message built by the handler.
297
+ """
298
+ if not isinstance(name, str) or not name:
299
+ raise Exception('event name must be a non-empty string')
300
+ if isinstance(payload, str):
301
+ payload = payload.encode()
302
+ elif payload is None:
303
+ payload = b''
304
+ # Lazily initialise the event list on the stub.
305
+ if not hasattr(self, '_events') or self._events is None:
306
+ self._events = []
307
+ from fabric_protos.peer import chaincode_event_pb2 as e_pb
308
+ evt = e_pb.ChaincodeEvent()
309
+ evt.chaincode_id = self.chaincode_id_name if hasattr(self, 'chaincode_id_name') else ""
310
+ evt.tx_id = self.tx_id
311
+ evt.event_name = name
312
+ evt.payload = bytes(payload)
313
+ self._events.append(evt)
314
+
315
+ def get_events(self):
316
+ """Internal: return the list of accumulated events (used by handler)."""
317
+ return list(getattr(self, '_events', []) or [])
318
+
319
+ def create_composite_key(self, object_type, attributes):
320
+ """Creates a composite key by combining the objectType string
321
+ and the given `attributes` to form a composite key"""
322
+ validate_composite_key_attribute(object_type)
323
+ if not isinstance(attributes, Sequence):
324
+ raise Exception('attributes must be an array')
325
+
326
+ composite_key = COMPOSITEKEY_NS + object_type + MIN_UNICODE_RUNE_VALUE
327
+ for attribute in attributes:
328
+ validate_composite_key_attribute(attribute)
329
+ composite_key = composite_key + attribute + MIN_UNICODE_RUNE_VALUE
330
+ return composite_key
331
+
332
+ def split_composite_key(self, composite_key):
333
+ object_type = None
334
+ attributes = []
335
+ if composite_key and len(composite_key) > 1 and composite_key[0] == COMPOSITEKEY_NS:
336
+ split_key = composite_key[1:].split(MIN_UNICODE_RUNE_VALUE)
337
+ object_type = split_key[0]
338
+ split_key.pop()
339
+ if len(split_key) > 1:
340
+ split_key.pop(0)
341
+ attributes = split_key
342
+ return object_type, attributes
@@ -0,0 +1,41 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # Auxiliary tools
3
+
4
+
5
+ def enum_type(*sequential, **named) -> type:
6
+ """enum other than class Enum. better performance
7
+
8
+ Typical usage example:
9
+ >> Numbers = enum('ZERO', 'ONE', 'TWO')
10
+ >> Numbers.ZERO
11
+ 0
12
+ or:
13
+ >> Numbers = enum_type(ONE=1, TWO=2, THREE='three')
14
+ >> Numbers.THREE
15
+ 'three'
16
+ Args:
17
+ sequential: Collects all the positional arguments in a tuple.
18
+ named: Collects all the keyword arguments in a dictionary.
19
+
20
+ """
21
+ enums = dict(zip(sequential, range(len(sequential))), **named)
22
+ return type('Enum', (), enums)
23
+
24
+
25
+ MIN_UNICODE_RUNE_VALUE = '\u0000'
26
+ COMPOSITEKEY_NS = '\x00'
27
+
28
+
29
+ def validate_composite_key_attribute(attr):
30
+ if attr is None or not isinstance(attr, str) or len(attr) == 0:
31
+ raise Exception('object type or attribute not a non-zero length string')
32
+
33
+
34
+ def validate_simple_keys(keys):
35
+ for key in keys:
36
+ if key and isinstance(key, str) and key[0] == COMPOSITEKEY_NS:
37
+ raise Exception('first character of the key %s contains a null character which is not allowed' % key)
38
+
39
+
40
+ def generate_logging_prefix(channel_id, tx_id):
41
+ return '[%s-%s]' % (channel_id, tx_id)
src/version.py ADDED
@@ -0,0 +1,3 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+
3
+ VERSION = "0.9.0"