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,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)
|