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,131 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Transaction handler for before/after/unknown transactions.
3
+
4
+ This is the Python equivalent of Go's ``internal/transaction_handler.go``.
5
+ A :class:`TransactionHandler` is a thin specialisation of
6
+ :class:`ContractFunction` for the three "lifecycle" hooks every contract can
7
+ define:
8
+
9
+ * ``BeforeTransaction`` — called before the named function.
10
+ * ``AfterTransaction`` — called after the named function (and receives its
11
+ return value).
12
+ * ``UnknownTransaction`` — called when an unknown function name is requested.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import inspect
18
+ from typing import Any, Callable, Optional, Tuple
19
+
20
+ from .contract_function import ContractFunction
21
+
22
+
23
+ class TransactionHandlerType:
24
+ """Enum-like constants for the three handler types."""
25
+
26
+ BEFORE = 1
27
+ UNKNOWN = 2
28
+ AFTER = 3
29
+
30
+ @staticmethod
31
+ def to_string(value: int) -> str:
32
+ if value == TransactionHandlerType.BEFORE:
33
+ return "Before"
34
+ if value == TransactionHandlerType.AFTER:
35
+ return "After"
36
+ if value == TransactionHandlerType.UNKNOWN:
37
+ return "Unknown"
38
+ raise ValueError("invalid transaction handler type")
39
+
40
+
41
+ class TransactionHandler(ContractFunction):
42
+ """Specialised :class:`ContractFunction` for before/after/unknown hooks."""
43
+
44
+ def __init__(self, fn: Callable[..., Any], context_handler_type: Any,
45
+ handles_type: int) -> None:
46
+ super().__init__(
47
+ fn=fn,
48
+ call_type=0,
49
+ param_details=_empty_params(),
50
+ return_details=_empty_returns(),
51
+ )
52
+ # Re-parse to populate params/returns correctly.
53
+ param_details, return_details = self._parse(fn, context_handler_type)
54
+ self.params = param_details
55
+ self.returns = return_details
56
+ self.handles_type = handles_type
57
+
58
+ async def call(self, ctx: Any, data: Any, serializer) -> Tuple[str, Any, Optional[Exception]]: # type: ignore[override]
59
+ """Invoke the hook.
60
+
61
+ For ``TransactionHandlerTypeAfter`` *data* is the success value (or
62
+ ``None``) returned by the named transaction function.
63
+ """
64
+ values = []
65
+ if self.params.context is not None:
66
+ values.append(ctx)
67
+
68
+ # Only after-transactions may take the success value as an additional
69
+ # parameter.
70
+ if self.handles_type == TransactionHandlerType.AFTER and len(self.params.fields) == 1:
71
+ if data is None:
72
+ # Mirror Go's behaviour: when the named function returned no
73
+ # success value, pass an "undefined" sentinel.
74
+ from ..contractapi.utils.undefined_interface import UndefinedInterface
75
+ values.append(UndefinedInterface())
76
+ else:
77
+ values.append(data)
78
+
79
+ try:
80
+ if inspect.iscoroutinefunction(self.function):
81
+ some_resp = await self.function(*values)
82
+ else:
83
+ some_resp = self.function(*values)
84
+ except Exception as exc:
85
+ return "", None, exc
86
+
87
+ return self._handle_response(some_resp, None, None, serializer)
88
+
89
+
90
+ def _empty_params():
91
+ from .contract_function import _ContractFunctionParams
92
+ return _ContractFunctionParams()
93
+
94
+
95
+ def _empty_returns():
96
+ from .contract_function import _ContractFunctionReturns
97
+ return _ContractFunctionReturns()
98
+
99
+
100
+ def new_transaction_handler(fn: Callable[..., Any], context_handler_type: Any,
101
+ handles_type: int) -> TransactionHandler:
102
+ """Create a new :class:`TransactionHandler`.
103
+
104
+ Mirrors Go's ``NewTransactionHandler``. Raises ``ValueError`` if the
105
+ function does not satisfy the constraints of the requested handler type.
106
+ """
107
+ try:
108
+ th = TransactionHandler(fn, context_handler_type, handles_type)
109
+ except ValueError as exc:
110
+ htype_str = TransactionHandlerType.to_string(handles_type)
111
+ raise ValueError(f"error creating {htype_str}. {exc}") from exc
112
+
113
+ if handles_type != TransactionHandlerType.AFTER and len(th.params.fields) > 0:
114
+ htype_str = TransactionHandlerType.to_string(handles_type)
115
+ raise ValueError(
116
+ f"{htype_str} transactions may not take any params other than the transaction context"
117
+ )
118
+ if handles_type == TransactionHandlerType.AFTER and len(th.params.fields) > 1:
119
+ raise ValueError("after transactions must take at most one non-context param")
120
+ # In Python, "interface" matches any type, so we keep the Go constraint
121
+ # simple: the after-transaction may take at most one extra parameter,
122
+ # of any type.
123
+
124
+ return th
125
+
126
+
127
+ __all__ = [
128
+ "TransactionHandler",
129
+ "TransactionHandlerType",
130
+ "new_transaction_handler",
131
+ ]
@@ -0,0 +1,180 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Basic type converters used by the JSON transaction serializer.
3
+
4
+ This module is the Python equivalent of Go's
5
+ ``fabric-contract-api-go/internal/types`` package. Each basic Python type
6
+ has a small adapter that knows how to:
7
+
8
+ 1. Convert a ``str`` parameter coming from the Fabric transaction payload
9
+ into the underlying Python type (``int``, ``float``, ``bool``, ...).
10
+ 2. Produce a JSON-schema snippet (OpenAPI-style) describing the type so that
11
+ it can be embedded into the chaincode metadata.
12
+
13
+ The Go implementation ships separate converters for ``int8``/``int16``/
14
+ ``int32``/``int64``/``uint8``/.../``uint64``/``float32``/``float64``. In
15
+ Python the only relevant numeric types are :class:`int` and :class:`float`,
16
+ so we collapse the integer family onto ``int`` (unbounded) and the float
17
+ family onto ``float`` (IEEE-754 double precision).
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import datetime as _dt
23
+ from typing import Any, Callable, Dict, Optional, Tuple, Type
24
+
25
+ # ``inspect`` annotations store classes (and ``typing`` aliases). We use
26
+ # ``type`` as a plain alias throughout this module for readability.
27
+ TypeT = Type[Any]
28
+
29
+
30
+ class _BasicType:
31
+ """Adapter for a single basic Python type.
32
+
33
+ Subclasses set :attr:`python_type` and override :meth:`convert` and
34
+ :meth:`get_schema`.
35
+ """
36
+
37
+ python_type: TypeT = object
38
+
39
+ def convert(self, value: str) -> Any: # pragma: no cover - overridden
40
+ raise NotImplementedError
41
+
42
+ def get_schema(self) -> Dict[str, Any]:
43
+ raise NotImplementedError
44
+
45
+
46
+ class _StringType(_BasicType):
47
+ python_type = str
48
+
49
+ def convert(self, value: str) -> str:
50
+ return value
51
+
52
+ def get_schema(self) -> Dict[str, Any]:
53
+ return {"type": "string"}
54
+
55
+
56
+ class _BoolType(_BasicType):
57
+ python_type = bool
58
+
59
+ def convert(self, value: str) -> bool:
60
+ if value == "":
61
+ return False
62
+ # Python's bool() would return True for any non-empty string, but we
63
+ # want strict parsing here so that "false" really means False.
64
+ lowered = value.strip().lower()
65
+ if lowered in ("true", "t", "1", "yes", "y"):
66
+ return True
67
+ if lowered in ("false", "f", "0", "no", "n"):
68
+ return False
69
+ raise ValueError(f"cannot convert passed value {value!r} to bool")
70
+
71
+ def get_schema(self) -> Dict[str, Any]:
72
+ return {"type": "boolean"}
73
+
74
+
75
+ class _IntType(_BasicType):
76
+ python_type = int
77
+
78
+ def convert(self, value: str) -> int:
79
+ if value == "":
80
+ return 0
81
+ try:
82
+ return int(value, 10)
83
+ except ValueError as exc:
84
+ raise ValueError(
85
+ f"cannot convert passed value {value!r} to int"
86
+ ) from exc
87
+
88
+ def get_schema(self) -> Dict[str, Any]:
89
+ # JSON-schema "integer" with 64-bit range, matching the Go
90
+ # implementation's int64 property.
91
+ return {"type": "integer", "format": "int64"}
92
+
93
+
94
+ class _FloatType(_BasicType):
95
+ python_type = float
96
+
97
+ def convert(self, value: str) -> float:
98
+ if value == "":
99
+ return 0.0
100
+ try:
101
+ return float(value)
102
+ except ValueError as exc:
103
+ raise ValueError(
104
+ f"cannot convert passed value {value!r} to float"
105
+ ) from exc
106
+
107
+ def get_schema(self) -> Dict[str, Any]:
108
+ return {"type": "number", "format": "float"}
109
+
110
+
111
+ class _BytesType(_BasicType):
112
+ """Bytes/bytearray — handled as base64-free raw strings.
113
+
114
+ Fabric sends bytes as raw UTF-8 strings; we mirror that behaviour here.
115
+ The Go implementation uses ``[]byte(param)`` which is a byte-for-byte
116
+ cast of the UTF-8 bytes.
117
+ """
118
+
119
+ python_type = bytes
120
+
121
+ def convert(self, value: str) -> bytes:
122
+ return value.encode("utf-8")
123
+
124
+ def get_schema(self) -> Dict[str, Any]:
125
+ return {"type": "string", "format": "byte"}
126
+
127
+
128
+ class _InterfaceType(_BasicType):
129
+ """``typing.Any`` / untyped parameter — accept the raw string."""
130
+
131
+ python_type = object
132
+
133
+ def convert(self, value: str) -> str:
134
+ return value
135
+
136
+ def get_schema(self) -> Dict[str, Any]:
137
+ return {}
138
+
139
+
140
+ # Order matters: when several Python types share the same identity (e.g. bool
141
+ # is a subclass of int), we need to check the more specific one first.
142
+ BasicTypes: Dict[TypeT, _BasicType] = {
143
+ str: _StringType(),
144
+ bool: _BoolType(),
145
+ int: _IntType(),
146
+ float: _FloatType(),
147
+ bytes: _BytesType(),
148
+ bytearray: _BytesType(),
149
+ object: _InterfaceType(),
150
+ }
151
+
152
+ # Sentinel values mirroring Go's ``types.ErrorType`` and ``types.TimeType``.
153
+ ErrorType: TypeT = type("ErrorType", (), {"__doc__": "Sentinel for the Go-style error return type"})
154
+ TimeType: TypeT = _dt.datetime
155
+
156
+
157
+ def is_bytes(t: TypeT) -> bool:
158
+ """Return ``True`` if *t* is a bytes/bytearray type.
159
+
160
+ Mirrors ``types.IsBytes`` from the Go implementation.
161
+ """
162
+ return t in (bytes, bytearray)
163
+
164
+
165
+ def basic_type_for(t: TypeT) -> Optional[_BasicType]:
166
+ """Return the basic-type adapter for *t*, or ``None``.
167
+
168
+ Handles the ``bool`` is-a-subclass-of-``int`` gotcha by checking bool
169
+ before int.
170
+ """
171
+ return BasicTypes.get(t)
172
+
173
+
174
+ __all__ = [
175
+ "BasicTypes",
176
+ "ErrorType",
177
+ "TimeType",
178
+ "is_bytes",
179
+ "basic_type_for",
180
+ ]
@@ -0,0 +1,224 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Static type validation for contract functions.
3
+
4
+ This module is the Python equivalent of Go's
5
+ ``fabric-contract-api-go/internal/types_handler``. The goal is to walk
6
+ through every parameter and return annotation of a contract's transaction
7
+ function and verify that each one is a type the contract API can serialise.
8
+
9
+ Allowed parameter types are:
10
+
11
+ * the basic scalar types (``str``, ``bool``, ``int``, ``float``)
12
+ * ``bytes`` / ``bytearray`` (treated as raw byte strings)
13
+ * ``list[T]`` / ``typing.List[T]`` where ``T`` is itself an allowed type
14
+ * ``dict[str, V]`` / ``typing.Dict[str, V]`` where ``V`` is allowed
15
+ * arbitrary dataclasses or plain classes (treated as structured objects and
16
+ serialised as JSON)
17
+ * ``typing.Any`` / ``object`` — accepted as opaque strings
18
+ * :class:`datetime.datetime` — accepted as an RFC3339-formatted timestamp
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import dataclasses
24
+ import datetime as _dt
25
+ import typing as _t
26
+ from typing import Any, List, Optional, Sequence, Tuple, Type
27
+
28
+ from . import types as _types
29
+ from .utils import slice_as_comma_sentence
30
+
31
+
32
+ def _basic_types_as_slice() -> List[str]:
33
+ """Return a sorted list of the basic-type names."""
34
+ return sorted(t.__name__ for t in _types.BasicTypes)
35
+
36
+
37
+ def list_basic_types() -> str:
38
+ """Return a comma-and-and sentence of the supported basic types."""
39
+ return slice_as_comma_sentence(_basic_types_as_slice())
40
+
41
+
42
+ def _origin_is(tp: Any, origin: Any) -> bool:
43
+ """True if *tp*'s typing origin is *origin*.
44
+
45
+ Examples:
46
+ ``_origin_is(list[int], list)`` -> True
47
+ ``_origin_is(dict[str, int], dict)`` -> True
48
+ """
49
+ return _t.get_origin(tp) is origin
50
+
51
+
52
+ def _type_kind(tp: Any) -> str:
53
+ """Return a human-friendly kind name for *tp*."""
54
+ if _t.get_origin(tp) is not None:
55
+ return str(_t.get_origin(tp))
56
+ if isinstance(tp, type):
57
+ return tp.__name__
58
+ return str(tp)
59
+
60
+
61
+ def _strip_optional(tp: Any) -> Any:
62
+ """Strip a ``typing.Optional[X]`` / ``typing.Union[X, None]`` wrapper.
63
+
64
+ The contract API treats ``Optional[X]`` the same as ``X`` for the purposes
65
+ of validation (a missing argument coming through as ``""`` is still
66
+ converted to the zero-value of ``X``).
67
+ """
68
+ origin = _t.get_origin(tp)
69
+ if origin is _t.Union:
70
+ args = [a for a in _t.get_args(tp) if a is not type(None)]
71
+ if len(args) == 1:
72
+ return args[0]
73
+ return tp
74
+
75
+
76
+ def type_is_valid(tp: Any, additional_types: Optional[Sequence[Any]] = None,
77
+ allow_error: bool = False) -> Optional[str]:
78
+ """Validate that *tp* is a type the contract API can serialise.
79
+
80
+ Returns ``None`` when the type is valid, or an error message otherwise.
81
+
82
+ Args:
83
+ tp: The type to validate (a ``type`` instance, a ``typing`` alias,
84
+ or :data:`typing.Any`).
85
+ additional_types: A list of types that should additionally be allowed
86
+ (used for cyclic struct references).
87
+ allow_error: When ``True`` the sentinel :data:`types.ErrorType` is
88
+ also accepted as a valid type — this is used when validating
89
+ *return* types of contract functions.
90
+ """
91
+ additional_types = list(additional_types or [])
92
+
93
+ # Unwrap Optional[X] -> X
94
+ tp = _strip_optional(tp)
95
+
96
+ if tp is _t.Any or tp is object:
97
+ return None
98
+
99
+ # typing.List / typing.Dict / typing.Tuple etc.
100
+ origin = _t.get_origin(tp)
101
+ args = _t.get_args(tp)
102
+
103
+ if origin is list or origin is _t.List:
104
+ if not args:
105
+ return "list must have an element type, e.g. list[str]"
106
+ return type_is_valid(args[0], additional_types, False)
107
+
108
+ if origin is dict or origin is _t.Dict:
109
+ if not args or len(args) != 2:
110
+ return "map must have exactly two type arguments, e.g. dict[str, int]"
111
+ key_type, value_type = args
112
+ if key_type is not str:
113
+ return f"map key type {_type_kind(key_type)} is not valid. Expected string"
114
+ return type_is_valid(value_type, additional_types, False)
115
+
116
+ if origin is tuple or origin is _t.Tuple:
117
+ if not args:
118
+ return "tuples must have at least one element"
119
+ for arg in args:
120
+ err = type_is_valid(arg, additional_types, False)
121
+ if err:
122
+ return err
123
+ return None
124
+
125
+ # bytes / bytearray
126
+ if _types.is_bytes(tp):
127
+ return None
128
+
129
+ # datetime
130
+ if tp is _dt.datetime or tp is _dt.date:
131
+ return None
132
+
133
+ # Basic scalar
134
+ if tp in _types.BasicTypes:
135
+ return None
136
+
137
+ # Cyclic structs already in additional_types
138
+ if tp in additional_types:
139
+ return None
140
+
141
+ # Sentinel for the error return type
142
+ if tp is _types.ErrorType:
143
+ if allow_error:
144
+ return None
145
+ return f"type {tp!r} is not valid as a parameter"
146
+
147
+ # Struct: dataclass or plain class
148
+ if isinstance(tp, type):
149
+ if dataclasses.is_dataclass(tp) or hasattr(tp, "__dict__"):
150
+ new_additional = list(additional_types) + [tp]
151
+ return _struct_of_valid_type(tp, new_additional)
152
+
153
+ err_str = " error," if allow_error else ""
154
+ return (f"type {_type_kind(tp)} is not valid. Expected a struct or one of "
155
+ f"the basic types{err_str} {list_basic_types()} or an array/slice of these")
156
+
157
+
158
+ def _struct_of_valid_type(tp: Type[Any], additional_types: Sequence[Any]) -> Optional[str]:
159
+ """Validate each field of a struct (dataclass or plain class).
160
+
161
+ Plain classes have their ``__init__`` parameters inspected (so that
162
+ user-defined ``__init__`` methods can drive the schema); dataclasses are
163
+ inspected through their declared fields.
164
+ """
165
+ if dataclasses.is_dataclass(tp):
166
+ # Resolve PEP 563 string annotations to actual types.
167
+ try:
168
+ hints = _t.get_type_hints(tp)
169
+ except Exception:
170
+ hints = {}
171
+ fields = dataclasses.fields(tp)
172
+ for field in fields:
173
+ resolved_type = hints.get(field.name, field.type)
174
+ err = type_is_valid(resolved_type, additional_types, False)
175
+ if err:
176
+ return err
177
+ return None
178
+
179
+ # Plain class — inspect __init__ annotations if present.
180
+ try:
181
+ hints = _t.get_type_hints(tp.__init__)
182
+ except Exception:
183
+ hints = {}
184
+ for name, hint in hints.items():
185
+ if name == "self":
186
+ continue
187
+ if name == "return":
188
+ continue
189
+ err = type_is_valid(hint, additional_types, False)
190
+ if err:
191
+ return err
192
+ return None
193
+
194
+
195
+ def type_matches_interface(to_match: Any, iface: Any) -> Optional[str]:
196
+ """Check whether *to_match* structurally satisfies the *iface* protocol.
197
+
198
+ Mirrors ``typeMatchesInterface`` from the Go implementation. In Python we
199
+ rely on :class:`typing.Protocol` and :func:`typing.runtime_checkable` to
200
+ do this — but also fall back to method-name comparison for plain
201
+ :class:`abc.ABC` subclasses, which is how :class:`TransactionContextInterface`
202
+ is defined.
203
+ """
204
+ if iface is None:
205
+ return "type passed for interface is not an interface"
206
+ if not getattr(iface, "__abstractmethods__", None) and not _t.is_protocol(iface):
207
+ return "type passed for interface is not an interface"
208
+
209
+ expected_methods = getattr(iface, "__abstractmethods__", frozenset())
210
+ for method_name in expected_methods:
211
+ if not hasattr(to_match, method_name):
212
+ return f"missing function {method_name}"
213
+ return None
214
+
215
+
216
+ def _is_protocol(tp: Any) -> bool:
217
+ return _t.is_protocol(tp) if hasattr(_t, "is_protocol") else False
218
+
219
+
220
+ __all__ = [
221
+ "list_basic_types",
222
+ "type_is_valid",
223
+ "type_matches_interface",
224
+ ]
@@ -0,0 +1,59 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Internal helper utilities used throughout :mod:`fabric_contract_api`.
3
+
4
+ These helpers are the Python equivalent of the helpers in Go's
5
+ ``fabric-contract-api-go/internal/utils`` package.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Iterable, List, Sequence
11
+
12
+
13
+ def string_in_slice(needle: str, haystack: Sequence[str]) -> bool:
14
+ """Return ``True`` if *needle* is in *haystack*.
15
+
16
+ Mirrors ``utils.StringInSlice`` from the Go implementation.
17
+ """
18
+ return needle in haystack
19
+
20
+
21
+ def slice_as_comma_sentence(items: Sequence[str]) -> str:
22
+ """Format *items* as a comma-separated "and" sentence.
23
+
24
+ Mirrors ``utils.SliceAsCommaSentence`` from the Go implementation, e.g.::
25
+
26
+ ["a"] -> "a"
27
+ ["a", "b"] -> "a and b"
28
+ ["a", "b", "c"] -> "a, b and c"
29
+ """
30
+ items = list(items)
31
+ if not items:
32
+ return ""
33
+ if len(items) == 1:
34
+ return items[0]
35
+ if len(items) == 2:
36
+ return f"{items[0]} and {items[1]}"
37
+ head = ", ".join(items[:-1])
38
+ return f"{head} and {items[-1]}"
39
+
40
+
41
+ def validate_errors_to_string(errors: Iterable[str]) -> str:
42
+ """Convert a list of JSON-schema validation errors into a readable string.
43
+
44
+ Mirrors ``utils.ValidateErrorsToString`` from the Go implementation. The
45
+ errors are sorted lexicographically and numbered 1..N.
46
+ """
47
+ sorted_errors: List[str] = sorted(str(e) for e in errors)
48
+ if not sorted_errors:
49
+ return ""
50
+ return "\n".join(
51
+ f"{i + 1}. {err}" for i, err in enumerate(sorted_errors)
52
+ )
53
+
54
+
55
+ __all__ = [
56
+ "string_in_slice",
57
+ "slice_as_comma_sentence",
58
+ "validate_errors_to_string",
59
+ ]
@@ -0,0 +1,32 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Metadata package for :mod:`fabric_contract_api`.
3
+
4
+ Mirrors Go's ``metadata`` package: dataclasses describing the contract
5
+ chaincode metadata, JSON-schema helpers, and the embedded schema used to
6
+ validate user-supplied metadata files.
7
+ """
8
+
9
+ # Import the metadata dataclasses and helpers eagerly. ``schema.py`` is
10
+ # imported lazily (via ``from .schema import get_schema``) by callers that
11
+ # actually need it to avoid a circular import with the ``internal`` package.
12
+ from .metadata import ( # noqa: F401
13
+ ComponentMetadata,
14
+ ContactMetadata,
15
+ ContractChaincodeMetadata,
16
+ ContractMetadata,
17
+ InfoMetadata,
18
+ LicenseMetadata,
19
+ ObjectMetadata,
20
+ ParameterMetadata,
21
+ ReturnMetadata,
22
+ TransactionMetadata,
23
+ get_json_schema,
24
+ read_metadata_file,
25
+ validate_against_schema,
26
+ )
27
+
28
+
29
+ def get_schema(field_type, components): # pragma: no cover - thin proxy
30
+ """Lazy re-export of :func:`fabric_contract_api.metadata.schema.get_schema`."""
31
+ from .schema import get_schema as _impl
32
+ return _impl(field_type, components)