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.
- fabric_chaincode_python-2.5.2.dist-info/METADATA +101 -0
- fabric_chaincode_python-2.5.2.dist-info/RECORD +40 -0
- fabric_chaincode_python-2.5.2.dist-info/WHEEL +5 -0
- fabric_chaincode_python-2.5.2.dist-info/licenses/LICENSE +201 -0
- fabric_chaincode_python-2.5.2.dist-info/licenses/NOTICE +18 -0
- fabric_chaincode_python-2.5.2.dist-info/top_level.txt +1 -0
- src/__init__.py +5 -0
- src/fabric_contract_api/__init__.py +51 -0
- src/fabric_contract_api/contractapi/__init__.py +29 -0
- src/fabric_contract_api/contractapi/contract.py +141 -0
- src/fabric_contract_api/contractapi/contract_chaincode.py +552 -0
- src/fabric_contract_api/contractapi/system_contract.py +45 -0
- src/fabric_contract_api/contractapi/transaction_context.py +160 -0
- src/fabric_contract_api/contractapi/utils/__init__.py +4 -0
- src/fabric_contract_api/contractapi/utils/undefined_interface.py +33 -0
- src/fabric_contract_api/internal/__init__.py +16 -0
- src/fabric_contract_api/internal/contract_function.py +453 -0
- src/fabric_contract_api/internal/transaction_handler.py +131 -0
- src/fabric_contract_api/internal/types.py +180 -0
- src/fabric_contract_api/internal/types_handler.py +224 -0
- src/fabric_contract_api/internal/utils.py +59 -0
- src/fabric_contract_api/metadata/__init__.py +32 -0
- src/fabric_contract_api/metadata/metadata.py +456 -0
- src/fabric_contract_api/metadata/schema.py +238 -0
- src/fabric_contract_api/serializer/__init__.py +5 -0
- src/fabric_contract_api/serializer/json_transaction_serializer.py +363 -0
- src/fabric_contract_api/serializer/transaction_serializer.py +45 -0
- src/fabric_shim/__init__.py +2 -0
- src/fabric_shim/chaincode.py +8 -0
- src/fabric_shim/handler.py +336 -0
- src/fabric_shim/interfaces.py +134 -0
- src/fabric_shim/iterators.py +107 -0
- src/fabric_shim/logger.py +1 -0
- src/fabric_shim/logging.py +63 -0
- src/fabric_shim/msg_queue_handler.py +91 -0
- src/fabric_shim/response.py +49 -0
- src/fabric_shim/server.py +155 -0
- src/fabric_shim/stub.py +342 -0
- src/fabric_shim/utils.py +41 -0
- src/version.py +3 -0
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Contract chaincode metadata dataclasses and helpers.
|
|
3
|
+
|
|
4
|
+
This is the Python equivalent of Go's ``metadata/metadata.go``. It defines
|
|
5
|
+
dataclasses for the various metadata pieces (info, contract, transaction,
|
|
6
|
+
parameter, return, components, contract-chaincode) and provides:
|
|
7
|
+
|
|
8
|
+
* :func:`read_metadata_file` — read a user-supplied ``metadata.json`` file
|
|
9
|
+
from one of the well-known metadata folders.
|
|
10
|
+
* :func:`validate_against_schema` — validate the produced metadata against
|
|
11
|
+
the JSON schema shipped in :mod:`fabric_contract_api.metadata.schema`.
|
|
12
|
+
* :meth:`ContractChaincodeMetadata.append` — merge two metadata trees.
|
|
13
|
+
* :meth:`ContractChaincodeMetadata.compile_schemas` — pre-compile parameter
|
|
14
|
+
and return schemas into ``CompiledSchema`` validators so that they can be
|
|
15
|
+
used by the JSON serializer at call time.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import json
|
|
21
|
+
import os
|
|
22
|
+
from dataclasses import dataclass, field
|
|
23
|
+
from typing import Any, Dict, List, Optional
|
|
24
|
+
|
|
25
|
+
try: # jsonschema is optional — the JSON serializer degrades gracefully.
|
|
26
|
+
import jsonschema # type: ignore
|
|
27
|
+
_HAS_JSONSCHEMA = True
|
|
28
|
+
except Exception: # pragma: no cover - optional dep
|
|
29
|
+
_HAS_JSONSCHEMA = False
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
METADATA_FOLDER = "META-INF"
|
|
33
|
+
METADATA_FOLDER_SECONDARY = "contract-metadata"
|
|
34
|
+
METADATA_FILE = "metadata.json"
|
|
35
|
+
|
|
36
|
+
_SCHEMA_JSON_PATH = os.path.join(os.path.dirname(__file__), "schema", "schema.json")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def get_json_schema() -> bytes:
|
|
40
|
+
"""Return the raw bytes of the JSON schema shipped with this package."""
|
|
41
|
+
with open(_SCHEMA_JSON_PATH, "rb") as fh:
|
|
42
|
+
return fh.read()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# ---------------------------------------------------------------------------
|
|
46
|
+
# Dataclasses
|
|
47
|
+
# ---------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass
|
|
51
|
+
class ParameterMetadata:
|
|
52
|
+
"""Metadata describing a single parameter of a transaction function."""
|
|
53
|
+
|
|
54
|
+
description: str = ""
|
|
55
|
+
name: str = ""
|
|
56
|
+
schema: Optional[Dict[str, Any]] = None
|
|
57
|
+
compiled_schema: Any = None # an opaque, serializer-specific validator
|
|
58
|
+
|
|
59
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
60
|
+
d: Dict[str, Any] = {"name": self.name}
|
|
61
|
+
if self.description:
|
|
62
|
+
d["description"] = self.description
|
|
63
|
+
if self.schema is not None:
|
|
64
|
+
d["schema"] = self.schema
|
|
65
|
+
return d
|
|
66
|
+
|
|
67
|
+
@classmethod
|
|
68
|
+
def from_dict(cls, d: Dict[str, Any]) -> "ParameterMetadata":
|
|
69
|
+
return cls(
|
|
70
|
+
description=d.get("description", "") or "",
|
|
71
|
+
name=d.get("name", "") or "",
|
|
72
|
+
schema=d.get("schema"),
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass
|
|
77
|
+
class ReturnMetadata:
|
|
78
|
+
"""Metadata describing the return value of a transaction function."""
|
|
79
|
+
|
|
80
|
+
schema: Optional[Dict[str, Any]] = None
|
|
81
|
+
compiled_schema: Any = None
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@dataclass
|
|
85
|
+
class TransactionMetadata:
|
|
86
|
+
"""Metadata describing a single transaction function."""
|
|
87
|
+
|
|
88
|
+
name: str = ""
|
|
89
|
+
tag: List[str] = field(default_factory=list)
|
|
90
|
+
parameters: List[ParameterMetadata] = field(default_factory=list)
|
|
91
|
+
returns: ReturnMetadata = field(default_factory=ReturnMetadata)
|
|
92
|
+
|
|
93
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
94
|
+
d: Dict[str, Any] = {"name": self.name}
|
|
95
|
+
if self.tag:
|
|
96
|
+
d["tag"] = list(self.tag)
|
|
97
|
+
if self.parameters:
|
|
98
|
+
d["parameters"] = [p.to_dict() for p in self.parameters]
|
|
99
|
+
if self.returns.schema is not None:
|
|
100
|
+
d["returns"] = self.returns.schema
|
|
101
|
+
return d
|
|
102
|
+
|
|
103
|
+
@classmethod
|
|
104
|
+
def from_dict(cls, d: Dict[str, Any]) -> "TransactionMetadata":
|
|
105
|
+
params = [ParameterMetadata.from_dict(p) for p in (d.get("parameters") or [])]
|
|
106
|
+
returns = ReturnMetadata(schema=d.get("returns"))
|
|
107
|
+
return cls(
|
|
108
|
+
name=d.get("name", "") or "",
|
|
109
|
+
tag=list(d.get("tag") or []),
|
|
110
|
+
parameters=params,
|
|
111
|
+
returns=returns,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@dataclass
|
|
116
|
+
class ContactMetadata:
|
|
117
|
+
name: str = ""
|
|
118
|
+
url: str = ""
|
|
119
|
+
email: str = ""
|
|
120
|
+
|
|
121
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
122
|
+
d: Dict[str, Any] = {}
|
|
123
|
+
if self.name:
|
|
124
|
+
d["name"] = self.name
|
|
125
|
+
if self.url:
|
|
126
|
+
d["url"] = self.url
|
|
127
|
+
if self.email:
|
|
128
|
+
d["email"] = self.email
|
|
129
|
+
return d
|
|
130
|
+
|
|
131
|
+
@classmethod
|
|
132
|
+
def from_dict(cls, d: Dict[str, Any]) -> "ContactMetadata":
|
|
133
|
+
return cls(
|
|
134
|
+
name=d.get("name", "") or "",
|
|
135
|
+
url=d.get("url", "") or "",
|
|
136
|
+
email=d.get("email", "") or "",
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@dataclass
|
|
141
|
+
class LicenseMetadata:
|
|
142
|
+
name: str = ""
|
|
143
|
+
url: str = ""
|
|
144
|
+
|
|
145
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
146
|
+
d: Dict[str, Any] = {}
|
|
147
|
+
if self.name:
|
|
148
|
+
d["name"] = self.name
|
|
149
|
+
if self.url:
|
|
150
|
+
d["url"] = self.url
|
|
151
|
+
return d
|
|
152
|
+
|
|
153
|
+
@classmethod
|
|
154
|
+
def from_dict(cls, d: Dict[str, Any]) -> "LicenseMetadata":
|
|
155
|
+
return cls(
|
|
156
|
+
name=d.get("name", "") or "",
|
|
157
|
+
url=d.get("url", "") or "",
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@dataclass
|
|
162
|
+
class InfoMetadata:
|
|
163
|
+
"""Top-level info block (title, version, contact, license, ...)."""
|
|
164
|
+
|
|
165
|
+
description: str = ""
|
|
166
|
+
title: str = ""
|
|
167
|
+
contact: Optional[ContactMetadata] = None
|
|
168
|
+
license: Optional[LicenseMetadata] = None
|
|
169
|
+
version: str = ""
|
|
170
|
+
terms_of_service: str = ""
|
|
171
|
+
|
|
172
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
173
|
+
d: Dict[str, Any] = {}
|
|
174
|
+
if self.description:
|
|
175
|
+
d["description"] = self.description
|
|
176
|
+
if self.title:
|
|
177
|
+
d["title"] = self.title
|
|
178
|
+
if self.contact:
|
|
179
|
+
d["contact"] = self.contact.to_dict()
|
|
180
|
+
if self.license:
|
|
181
|
+
d["license"] = self.license.to_dict()
|
|
182
|
+
if self.version:
|
|
183
|
+
d["version"] = self.version
|
|
184
|
+
if self.terms_of_service:
|
|
185
|
+
d["termsOfService"] = self.terms_of_service
|
|
186
|
+
return d
|
|
187
|
+
|
|
188
|
+
@classmethod
|
|
189
|
+
def from_dict(cls, d: Dict[str, Any]) -> "InfoMetadata":
|
|
190
|
+
return cls(
|
|
191
|
+
description=d.get("description", "") or "",
|
|
192
|
+
title=d.get("title", "") or "",
|
|
193
|
+
contact=ContactMetadata.from_dict(d["contact"]) if d.get("contact") else None,
|
|
194
|
+
license=LicenseMetadata.from_dict(d["license"]) if d.get("license") else None,
|
|
195
|
+
version=d.get("version", "") or "",
|
|
196
|
+
terms_of_service=d.get("termsOfService", "") or "",
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
@dataclass
|
|
201
|
+
class ContractMetadata:
|
|
202
|
+
"""Metadata describing a single contract."""
|
|
203
|
+
|
|
204
|
+
info: Optional[InfoMetadata] = None
|
|
205
|
+
name: str = ""
|
|
206
|
+
transactions: List[TransactionMetadata] = field(default_factory=list)
|
|
207
|
+
default: bool = False
|
|
208
|
+
|
|
209
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
210
|
+
d: Dict[str, Any] = {
|
|
211
|
+
"name": self.name,
|
|
212
|
+
"transactions": [t.to_dict() for t in self.transactions],
|
|
213
|
+
"default": self.default,
|
|
214
|
+
}
|
|
215
|
+
if self.info:
|
|
216
|
+
d["info"] = self.info.to_dict()
|
|
217
|
+
return d
|
|
218
|
+
|
|
219
|
+
@classmethod
|
|
220
|
+
def from_dict(cls, d: Dict[str, Any]) -> "ContractMetadata":
|
|
221
|
+
return cls(
|
|
222
|
+
info=InfoMetadata.from_dict(d["info"]) if d.get("info") else None,
|
|
223
|
+
name=d.get("name", "") or "",
|
|
224
|
+
transactions=[
|
|
225
|
+
TransactionMetadata.from_dict(t) for t in (d.get("transactions") or [])
|
|
226
|
+
],
|
|
227
|
+
default=bool(d.get("default", False)),
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
@dataclass
|
|
232
|
+
class ObjectMetadata:
|
|
233
|
+
"""Description of a single component (struct) used by the contract API."""
|
|
234
|
+
|
|
235
|
+
id: str = ""
|
|
236
|
+
properties: Dict[str, Dict[str, Any]] = field(default_factory=dict)
|
|
237
|
+
required: List[str] = field(default_factory=list)
|
|
238
|
+
additional_properties: bool = False
|
|
239
|
+
|
|
240
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
241
|
+
return {
|
|
242
|
+
"$id": self.id,
|
|
243
|
+
"properties": dict(self.properties),
|
|
244
|
+
"required": list(self.required),
|
|
245
|
+
"additionalProperties": self.additional_properties,
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
@classmethod
|
|
249
|
+
def from_dict(cls, d: Dict[str, Any]) -> "ObjectMetadata":
|
|
250
|
+
return cls(
|
|
251
|
+
id=d.get("$id", "") or "",
|
|
252
|
+
properties=dict(d.get("properties") or {}),
|
|
253
|
+
required=list(d.get("required") or []),
|
|
254
|
+
additional_properties=bool(d.get("additionalProperties", False)),
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
@dataclass
|
|
259
|
+
class ComponentMetadata:
|
|
260
|
+
"""Stores map of schemas of all components."""
|
|
261
|
+
|
|
262
|
+
schemas: Dict[str, Dict[str, Any]] = field(default_factory=dict)
|
|
263
|
+
|
|
264
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
265
|
+
if not self.schemas:
|
|
266
|
+
return {}
|
|
267
|
+
return {"schemas": dict(self.schemas)}
|
|
268
|
+
|
|
269
|
+
@classmethod
|
|
270
|
+
def from_dict(cls, d: Dict[str, Any]) -> "ComponentMetadata":
|
|
271
|
+
return cls(schemas=dict(d.get("schemas") or {}))
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
@dataclass
|
|
275
|
+
class ContractChaincodeMetadata:
|
|
276
|
+
"""Top-level metadata describing a chaincode built from contracts."""
|
|
277
|
+
|
|
278
|
+
info: Optional[InfoMetadata] = None
|
|
279
|
+
contracts: Dict[str, ContractMetadata] = field(default_factory=dict)
|
|
280
|
+
components: ComponentMetadata = field(default_factory=ComponentMetadata)
|
|
281
|
+
|
|
282
|
+
# -- (de)serialization ---------------------------------------------------
|
|
283
|
+
|
|
284
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
285
|
+
d: Dict[str, Any] = {"contracts": {}}
|
|
286
|
+
if self.info:
|
|
287
|
+
d["info"] = self.info.to_dict()
|
|
288
|
+
d["contracts"] = {k: v.to_dict() for k, v in self.contracts.items()}
|
|
289
|
+
components_dict = self.components.to_dict()
|
|
290
|
+
if components_dict:
|
|
291
|
+
d["components"] = components_dict
|
|
292
|
+
return d
|
|
293
|
+
|
|
294
|
+
def to_json(self) -> str:
|
|
295
|
+
return json.dumps(self.to_dict(), sort_keys=True, indent=2)
|
|
296
|
+
|
|
297
|
+
@classmethod
|
|
298
|
+
def from_dict(cls, d: Dict[str, Any]) -> "ContractChaincodeMetadata":
|
|
299
|
+
return cls(
|
|
300
|
+
info=InfoMetadata.from_dict(d["info"]) if d.get("info") else None,
|
|
301
|
+
contracts={
|
|
302
|
+
k: ContractMetadata.from_dict(v)
|
|
303
|
+
for k, v in (d.get("contracts") or {}).items()
|
|
304
|
+
},
|
|
305
|
+
components=ComponentMetadata.from_dict(d.get("components") or {}),
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
# -- merging -------------------------------------------------------------
|
|
309
|
+
|
|
310
|
+
def append(self, source: "ContractChaincodeMetadata") -> None:
|
|
311
|
+
"""Merge *source* into this metadata.
|
|
312
|
+
|
|
313
|
+
Source values only override target values that are not yet set.
|
|
314
|
+
Mirrors ``ContractChaincodeMetadata.Append`` from the Go code.
|
|
315
|
+
"""
|
|
316
|
+
if self.info is None:
|
|
317
|
+
self.info = source.info
|
|
318
|
+
|
|
319
|
+
if not self.contracts:
|
|
320
|
+
for key, value in source.contracts.items():
|
|
321
|
+
self.contracts[key] = value
|
|
322
|
+
|
|
323
|
+
if not self.components.schemas:
|
|
324
|
+
self.components = source.components
|
|
325
|
+
|
|
326
|
+
# -- schema compilation --------------------------------------------------
|
|
327
|
+
|
|
328
|
+
def compile_schemas(self) -> Optional[Exception]:
|
|
329
|
+
"""Pre-compile every parameter and return schema.
|
|
330
|
+
|
|
331
|
+
The compiled schema is stored on the metadata object so the JSON
|
|
332
|
+
serializer can re-use it without re-parsing on every call.
|
|
333
|
+
"""
|
|
334
|
+
for contract_name, contract in self.contracts.items():
|
|
335
|
+
for tx in contract.transactions:
|
|
336
|
+
for i, param in enumerate(tx.parameters):
|
|
337
|
+
compiled, err = _compile_schema(param.name, param.schema, self.components)
|
|
338
|
+
if err is not None:
|
|
339
|
+
return err
|
|
340
|
+
param.compiled_schema = compiled
|
|
341
|
+
tx.parameters[i] = param
|
|
342
|
+
|
|
343
|
+
if tx.returns.schema is not None:
|
|
344
|
+
compiled, err = _compile_schema("return", tx.returns.schema, self.components)
|
|
345
|
+
if err is not None:
|
|
346
|
+
return err
|
|
347
|
+
tx.returns.compiled_schema = compiled
|
|
348
|
+
return None
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
# ---------------------------------------------------------------------------
|
|
352
|
+
# Schema helpers
|
|
353
|
+
# ---------------------------------------------------------------------------
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def _compile_schema(prop_name: str, schema: Optional[Dict[str, Any]],
|
|
357
|
+
components: ComponentMetadata):
|
|
358
|
+
"""Combine a per-parameter schema with the components map.
|
|
359
|
+
|
|
360
|
+
Returns ``(compiled, None)`` on success or ``(None, Exception)`` on
|
|
361
|
+
failure. The compiled value is opaque to the metadata layer — it is
|
|
362
|
+
passed straight through to the serializer, which interprets it.
|
|
363
|
+
"""
|
|
364
|
+
if schema is None:
|
|
365
|
+
return None, None
|
|
366
|
+
|
|
367
|
+
# The JSON serializer uses ``jsonschema`` validators when available.
|
|
368
|
+
# The combined schema wraps the parameter schema under a "properties"
|
|
369
|
+
# key so the validator can check ``{"paramName": value}``.
|
|
370
|
+
combined = {
|
|
371
|
+
"components": components.to_dict() or {},
|
|
372
|
+
"properties": {prop_name: schema},
|
|
373
|
+
}
|
|
374
|
+
if not _HAS_JSONSCHEMA:
|
|
375
|
+
# No validator available — return the raw combined dict so the
|
|
376
|
+
# serializer can still attempt a manual check.
|
|
377
|
+
return {"raw": combined, "schema": schema}, None
|
|
378
|
+
|
|
379
|
+
try:
|
|
380
|
+
# We construct a validator whose root schema is the combined dict
|
|
381
|
+
# so that $ref references to #/components/schemas/... resolve.
|
|
382
|
+
validator_cls = jsonschema.validators.validator_for(combined)
|
|
383
|
+
validator_cls.check_schema(combined)
|
|
384
|
+
return validator_cls(combined), None
|
|
385
|
+
except Exception as exc:
|
|
386
|
+
return None, exc
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def read_metadata_file() -> "ContractChaincodeMetadata":
|
|
390
|
+
"""Read the metadata file from one of the known folders.
|
|
391
|
+
|
|
392
|
+
Raises :class:`FileNotFoundError` when the file cannot be found.
|
|
393
|
+
Mirrors Go's ``ReadMetadataFile``.
|
|
394
|
+
"""
|
|
395
|
+
cwd = os.getcwd()
|
|
396
|
+
primary_path = os.path.join(cwd, METADATA_FOLDER, METADATA_FILE)
|
|
397
|
+
secondary_path = os.path.join(cwd, METADATA_FOLDER_SECONDARY, METADATA_FILE)
|
|
398
|
+
|
|
399
|
+
for path in (primary_path, secondary_path):
|
|
400
|
+
if os.path.isfile(path):
|
|
401
|
+
with open(path, "rb") as fh:
|
|
402
|
+
data = fh.read()
|
|
403
|
+
return ContractChaincodeMetadata.from_dict(json.loads(data))
|
|
404
|
+
|
|
405
|
+
raise FileNotFoundError(
|
|
406
|
+
f"metadata file not found at {primary_path} or {secondary_path}"
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def validate_against_schema(metadata: ContractChaincodeMetadata) -> Optional[Exception]:
|
|
411
|
+
"""Validate *metadata* against the JSON schema shipped with this package.
|
|
412
|
+
|
|
413
|
+
Returns ``None`` if the metadata is valid, otherwise an ``Exception``
|
|
414
|
+
describing the failure.
|
|
415
|
+
"""
|
|
416
|
+
if not _HAS_JSONSCHEMA:
|
|
417
|
+
# Without jsonschema we cannot validate. Be permissive: the metadata
|
|
418
|
+
# produced by reflection is always valid; only user-supplied metadata
|
|
419
|
+
# files would slip through here.
|
|
420
|
+
return None
|
|
421
|
+
|
|
422
|
+
raw = json.loads(metadata.to_json())
|
|
423
|
+
schema = json.loads(get_json_schema())
|
|
424
|
+
try:
|
|
425
|
+
validator_cls = jsonschema.validators.validator_for(schema)
|
|
426
|
+
validator_cls.check_schema(schema)
|
|
427
|
+
validator = validator_cls(schema)
|
|
428
|
+
errors = list(validator.iter_errors(raw))
|
|
429
|
+
except Exception as exc:
|
|
430
|
+
return exc
|
|
431
|
+
|
|
432
|
+
if errors:
|
|
433
|
+
from ..internal.utils import validate_errors_to_string
|
|
434
|
+
msgs = [f"{err.message} at {list(err.absolute_path)}" for err in errors]
|
|
435
|
+
return ValueError(
|
|
436
|
+
"cannot use metadata. Metadata did not match schema:\n"
|
|
437
|
+
+ validate_errors_to_string(msgs)
|
|
438
|
+
)
|
|
439
|
+
return None
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
__all__ = [
|
|
443
|
+
"ComponentMetadata",
|
|
444
|
+
"ContactMetadata",
|
|
445
|
+
"ContractChaincodeMetadata",
|
|
446
|
+
"ContractMetadata",
|
|
447
|
+
"InfoMetadata",
|
|
448
|
+
"LicenseMetadata",
|
|
449
|
+
"ObjectMetadata",
|
|
450
|
+
"ParameterMetadata",
|
|
451
|
+
"ReturnMetadata",
|
|
452
|
+
"TransactionMetadata",
|
|
453
|
+
"get_json_schema",
|
|
454
|
+
"read_metadata_file",
|
|
455
|
+
"validate_against_schema",
|
|
456
|
+
]
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""JSON-schema utilities for contract metadata.
|
|
3
|
+
|
|
4
|
+
This is the Python equivalent of Go's ``metadata/schema.go``. It walks a
|
|
5
|
+
Python type annotation (or ``typing`` alias) and produces a JSON-schema
|
|
6
|
+
fragment suitable for embedding into the chaincode metadata.
|
|
7
|
+
|
|
8
|
+
For complex types (dataclasses, plain classes, lists, dicts) the schema is
|
|
9
|
+
either inlined (for ``list``/``dict`` of basic types) or stored as a
|
|
10
|
+
component reference (``{"$ref": "#/components/schemas/ClassName"}``) so that
|
|
11
|
+
the schema can be reused across transactions and avoid infinite recursion
|
|
12
|
+
for self-referential structs.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import dataclasses
|
|
18
|
+
import datetime as _dt
|
|
19
|
+
import typing as _t
|
|
20
|
+
from typing import Any, Dict, List, Optional, Tuple, Type
|
|
21
|
+
|
|
22
|
+
from .metadata import ComponentMetadata
|
|
23
|
+
from ..internal.types import BasicTypes, is_bytes
|
|
24
|
+
from ..internal.types_handler import _strip_optional
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def get_schema(field_type: Any, components: ComponentMetadata) -> Dict[str, Any]:
|
|
28
|
+
"""Return the JSON-schema fragment for *field_type*.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
field_type: a Python type or ``typing`` alias.
|
|
32
|
+
components: the mutable component-metadata registry. Structs are
|
|
33
|
+
added to this registry and the returned schema is a ``$ref``
|
|
34
|
+
pointing to it.
|
|
35
|
+
"""
|
|
36
|
+
return _get_schema(field_type, components, nested=False)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _get_schema(field_type: Any, components: ComponentMetadata,
|
|
40
|
+
nested: bool) -> Dict[str, Any]:
|
|
41
|
+
field_type = _strip_optional(field_type)
|
|
42
|
+
|
|
43
|
+
# bytes / bytearray -> {"type": "string", "format": "byte"}
|
|
44
|
+
if is_bytes(field_type):
|
|
45
|
+
return {"type": "string", "format": "byte"}
|
|
46
|
+
|
|
47
|
+
# typing.Any / object -> empty schema (matches anything)
|
|
48
|
+
if field_type is _t.Any or field_type is object:
|
|
49
|
+
return {}
|
|
50
|
+
|
|
51
|
+
# datetime / date -> date-time format
|
|
52
|
+
if field_type is _dt.datetime or field_type is _dt.date:
|
|
53
|
+
return {"type": "string", "format": "date-time"}
|
|
54
|
+
|
|
55
|
+
# Basic scalar types
|
|
56
|
+
if field_type in BasicTypes:
|
|
57
|
+
return BasicTypes[field_type].get_schema()
|
|
58
|
+
|
|
59
|
+
origin = _t.get_origin(field_type)
|
|
60
|
+
args = _t.get_args(field_type)
|
|
61
|
+
|
|
62
|
+
if origin is list or origin is _t.List:
|
|
63
|
+
return _build_list_schema(args, components, nested)
|
|
64
|
+
if origin is tuple or origin is _t.Tuple:
|
|
65
|
+
return _build_list_schema(args, components, nested)
|
|
66
|
+
if origin is dict or origin is _t.Dict:
|
|
67
|
+
return _build_dict_schema(args, components, nested)
|
|
68
|
+
|
|
69
|
+
if isinstance(field_type, type) and (
|
|
70
|
+
dataclasses.is_dataclass(field_type) or hasattr(field_type, "__dict__")):
|
|
71
|
+
return _build_struct_schema(field_type, components, nested)
|
|
72
|
+
|
|
73
|
+
raise ValueError(f"{field_type!r} was not a valid type")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _build_list_schema(args: Tuple[Any, ...], components: ComponentMetadata,
|
|
77
|
+
nested: bool) -> Dict[str, Any]:
|
|
78
|
+
if not args:
|
|
79
|
+
return {"type": "array", "items": {}}
|
|
80
|
+
item_schema = _get_schema(args[0], components, nested)
|
|
81
|
+
return {"type": "array", "items": item_schema}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _build_dict_schema(args: Tuple[Any, ...], components: ComponentMetadata,
|
|
85
|
+
nested: bool) -> Dict[str, Any]:
|
|
86
|
+
if not args or len(args) != 2:
|
|
87
|
+
return {"type": "object"}
|
|
88
|
+
value_schema = _get_schema(args[1], components, nested)
|
|
89
|
+
return {
|
|
90
|
+
"type": "object",
|
|
91
|
+
"additionalProperties": value_schema,
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _build_struct_schema(obj_type: Type[Any], components: ComponentMetadata,
|
|
96
|
+
nested: bool) -> Dict[str, Any]:
|
|
97
|
+
"""Build (or reference) the schema for a struct type."""
|
|
98
|
+
err = _add_component_if_not_exists(obj_type, components)
|
|
99
|
+
if err is not None:
|
|
100
|
+
raise err
|
|
101
|
+
ref_path = "" if nested else "#/components/schemas/"
|
|
102
|
+
return {"$ref": f"{ref_path}{obj_type.__name__}"}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _add_component_if_not_exists(obj_type: Type[Any],
|
|
106
|
+
components: ComponentMetadata) -> Optional[Exception]:
|
|
107
|
+
"""Register *obj_type* in *components* if not already present.
|
|
108
|
+
|
|
109
|
+
Returns ``None`` on success, or an ``Exception`` describing the failure.
|
|
110
|
+
"""
|
|
111
|
+
name = obj_type.__name__
|
|
112
|
+
if name in components.schemas:
|
|
113
|
+
return None
|
|
114
|
+
|
|
115
|
+
schema = {
|
|
116
|
+
"$id": name,
|
|
117
|
+
"required": [],
|
|
118
|
+
"properties": {},
|
|
119
|
+
"additionalProperties": False,
|
|
120
|
+
}
|
|
121
|
+
components.schemas[name] = schema # reserve slot for cyclic refs
|
|
122
|
+
|
|
123
|
+
try:
|
|
124
|
+
if dataclasses.is_dataclass(obj_type):
|
|
125
|
+
# Resolve PEP 563 string annotations to actual types so that
|
|
126
|
+
# ``from __future__ import annotations`` works correctly.
|
|
127
|
+
try:
|
|
128
|
+
resolved = _t.get_type_hints(obj_type)
|
|
129
|
+
except Exception:
|
|
130
|
+
resolved = {}
|
|
131
|
+
for field in dataclasses.fields(obj_type):
|
|
132
|
+
field_type = resolved.get(field.name, field.type)
|
|
133
|
+
err = _get_field_dataclass(field, field_type, schema, components)
|
|
134
|
+
if err:
|
|
135
|
+
del components.schemas[name]
|
|
136
|
+
return err
|
|
137
|
+
else:
|
|
138
|
+
# Plain class — use __init__ type hints as field descriptors.
|
|
139
|
+
try:
|
|
140
|
+
hints = _t.get_type_hints(obj_type.__init__)
|
|
141
|
+
except Exception:
|
|
142
|
+
hints = {}
|
|
143
|
+
for fname, ftype in hints.items():
|
|
144
|
+
if fname in ("self", "return"):
|
|
145
|
+
continue
|
|
146
|
+
err = _get_field_plain(fname, ftype, schema, components)
|
|
147
|
+
if err:
|
|
148
|
+
del components.schemas[name]
|
|
149
|
+
return err
|
|
150
|
+
except Exception as exc: # pragma: no cover - defensive
|
|
151
|
+
del components.schemas[name]
|
|
152
|
+
return exc
|
|
153
|
+
|
|
154
|
+
components.schemas[name] = schema
|
|
155
|
+
return None
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _resolve_field_name(field_name: str, metadata_tag: str, json_tag: str) -> Tuple[str, bool, bool]:
|
|
159
|
+
"""Resolve the public field name, required flag and "skip" flag.
|
|
160
|
+
|
|
161
|
+
Mirrors Go's ``getField`` parsing logic for struct tags:
|
|
162
|
+
* If the ``metadata`` tag is ``-`` the field is skipped.
|
|
163
|
+
* If the ``metadata`` tag is ``name[,optional]`` the field is renamed and
|
|
164
|
+
may be marked optional.
|
|
165
|
+
* If the ``metadata`` tag is empty the ``json`` tag (without ``,omitempty``
|
|
166
|
+
suffix) is used, falling back to the Python attribute name.
|
|
167
|
+
"""
|
|
168
|
+
if metadata_tag == "-":
|
|
169
|
+
return "", False, True
|
|
170
|
+
|
|
171
|
+
name = metadata_tag
|
|
172
|
+
required = True
|
|
173
|
+
if "," in name:
|
|
174
|
+
parts = name.split(",")
|
|
175
|
+
name = parts[0]
|
|
176
|
+
for extra in parts[1:]:
|
|
177
|
+
if extra.strip() == "optional":
|
|
178
|
+
required = False
|
|
179
|
+
|
|
180
|
+
if name == "":
|
|
181
|
+
# Fall back to the json tag, if any.
|
|
182
|
+
if json_tag:
|
|
183
|
+
if "," in json_tag:
|
|
184
|
+
name = json_tag.split(",")[0]
|
|
185
|
+
else:
|
|
186
|
+
name = json_tag
|
|
187
|
+
|
|
188
|
+
if name == "" or name == "-":
|
|
189
|
+
name = field_name
|
|
190
|
+
|
|
191
|
+
return name, required, False
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _get_field_dataclass(field: Any, field_type: Any, schema: Dict[str, Any],
|
|
195
|
+
components: ComponentMetadata) -> Optional[Exception]:
|
|
196
|
+
"""Populate *schema* from a single dataclass field."""
|
|
197
|
+
metadata_tag = ""
|
|
198
|
+
if isinstance(field.metadata, dict):
|
|
199
|
+
metadata_tag = field.metadata.get("metadata", "")
|
|
200
|
+
json_tag = ""
|
|
201
|
+
# dataclass fields don't have native json tags — fall back to the
|
|
202
|
+
# field.name. The user can still pass `metadata={"metadata": "myName"}`
|
|
203
|
+
# to rename it.
|
|
204
|
+
name, required, skip = _resolve_field_name(field.name, metadata_tag, json_tag)
|
|
205
|
+
if skip:
|
|
206
|
+
return None
|
|
207
|
+
|
|
208
|
+
try:
|
|
209
|
+
prop_schema = _get_schema(field_type, components, nested=True)
|
|
210
|
+
except Exception as exc:
|
|
211
|
+
return exc
|
|
212
|
+
|
|
213
|
+
if required:
|
|
214
|
+
schema["required"].append(name)
|
|
215
|
+
schema["properties"][name] = prop_schema
|
|
216
|
+
return None
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _get_field_plain(field_name: str, field_type: Any,
|
|
220
|
+
schema: Dict[str, Any],
|
|
221
|
+
components: ComponentMetadata) -> Optional[Exception]:
|
|
222
|
+
"""Populate *schema* from a single plain-class ``__init__`` parameter."""
|
|
223
|
+
# No native "metadata"/"json" tags for plain classes; use the raw name.
|
|
224
|
+
name = field_name
|
|
225
|
+
required = True
|
|
226
|
+
|
|
227
|
+
try:
|
|
228
|
+
prop_schema = _get_schema(field_type, components, nested=True)
|
|
229
|
+
except Exception as exc:
|
|
230
|
+
return exc
|
|
231
|
+
|
|
232
|
+
if required:
|
|
233
|
+
schema["required"].append(name)
|
|
234
|
+
schema["properties"][name] = prop_schema
|
|
235
|
+
return None
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
__all__ = ["get_schema"]
|