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,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"]
@@ -0,0 +1,5 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Serializers for :mod:`fabric_contract_api`."""
3
+
4
+ from .transaction_serializer import TransactionSerializer # noqa: F401
5
+ from .json_transaction_serializer import JSONSerializer # noqa: F401