simplyworks-bitween 10.0.121__tar.gz
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.
- simplyworks_bitween-10.0.121/PKG-INFO +50 -0
- simplyworks_bitween-10.0.121/README.md +39 -0
- simplyworks_bitween-10.0.121/pyproject.toml +19 -0
- simplyworks_bitween-10.0.121/setup.cfg +4 -0
- simplyworks_bitween-10.0.121/src/simplyworks_bitween/__init__.py +236 -0
- simplyworks_bitween-10.0.121/src/simplyworks_bitween.egg-info/PKG-INFO +50 -0
- simplyworks_bitween-10.0.121/src/simplyworks_bitween.egg-info/SOURCES.txt +9 -0
- simplyworks_bitween-10.0.121/src/simplyworks_bitween.egg-info/dependency_links.txt +1 -0
- simplyworks_bitween-10.0.121/src/simplyworks_bitween.egg-info/requires.txt +1 -0
- simplyworks_bitween-10.0.121/src/simplyworks_bitween.egg-info/top_level.txt +1 -0
- simplyworks_bitween-10.0.121/tests/test_contract.py +90 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: simplyworks-bitween
|
|
3
|
+
Version: 10.0.121
|
|
4
|
+
Summary: The Bitween adapter contract for Python: handlers, mappers, validators and receivers.
|
|
5
|
+
Author: Simplify9
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Repository, https://github.com/simplify9/Bitween-api
|
|
8
|
+
Requires-Python: >=3.12
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
Requires-Dist: sw-serverless>=10.2.0
|
|
11
|
+
|
|
12
|
+
# simplyworks-bitween
|
|
13
|
+
|
|
14
|
+
The Bitween adapter contract for Python: the kinds of adapter Bitween runs and what it passes them.
|
|
15
|
+
It is the Python form of `SW.Bitween.Adapters/Contract/bitween-adapter-contract.v1.json`, as
|
|
16
|
+
`SimplyWorks.Bitween.Adapters` is its .NET one.
|
|
17
|
+
|
|
18
|
+
| Kind | Implement | Bitween calls |
|
|
19
|
+
|---|---|---|
|
|
20
|
+
| `Handler` | `handle(file) -> ExchangeFile` | `Handle` |
|
|
21
|
+
| `Mapper` | `map(file) -> ExchangeFile` | `Handle` |
|
|
22
|
+
| `Validator` | `validate(file) -> ValidationResult` | `Validate` |
|
|
23
|
+
| `Receiver` | `list_files()`, `get_file(id)`, `delete_file(id)`; optionally `initialize()`, `finalize()` | `Initialize`, `ListFiles`, `GetFile`, `DeleteFile`, `Finalize` |
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
import sw_serverless as sw
|
|
27
|
+
from simplyworks_bitween import ExchangeFile, Handler
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class Orders(Handler):
|
|
31
|
+
def __init__(self):
|
|
32
|
+
sw.expect("Url", description="Where orders go")
|
|
33
|
+
|
|
34
|
+
def handle(self, file: ExchangeFile) -> ExchangeFile:
|
|
35
|
+
# A partner's rejection is returned with bad_data=True, not raised.
|
|
36
|
+
return ExchangeFile(data=file.data, filename=file.filename)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
if __name__ == "__main__":
|
|
40
|
+
sw.run(Orders)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`ExchangeFile` has `data`, `filename`, `bad_data` and `content_type`, and is sent with the contract's
|
|
44
|
+
property names (`Data`, `Filename`, `BadData`, `ContentType`, `Hash`). A kind missing a method it
|
|
45
|
+
needs fails when the adapter starts.
|
|
46
|
+
|
|
47
|
+
`bitween adapter build` vendors this package, with the SDK, into every Python adapter it builds,
|
|
48
|
+
taken from this folder through SW.Bitween.Adapters.Tooling, so nothing needs installing from PyPI.
|
|
49
|
+
|
|
50
|
+
Tests: `PYTHONPATH=src:<SW-Serverless>/sdk/python/src python -m unittest discover -s tests`.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# simplyworks-bitween
|
|
2
|
+
|
|
3
|
+
The Bitween adapter contract for Python: the kinds of adapter Bitween runs and what it passes them.
|
|
4
|
+
It is the Python form of `SW.Bitween.Adapters/Contract/bitween-adapter-contract.v1.json`, as
|
|
5
|
+
`SimplyWorks.Bitween.Adapters` is its .NET one.
|
|
6
|
+
|
|
7
|
+
| Kind | Implement | Bitween calls |
|
|
8
|
+
|---|---|---|
|
|
9
|
+
| `Handler` | `handle(file) -> ExchangeFile` | `Handle` |
|
|
10
|
+
| `Mapper` | `map(file) -> ExchangeFile` | `Handle` |
|
|
11
|
+
| `Validator` | `validate(file) -> ValidationResult` | `Validate` |
|
|
12
|
+
| `Receiver` | `list_files()`, `get_file(id)`, `delete_file(id)`; optionally `initialize()`, `finalize()` | `Initialize`, `ListFiles`, `GetFile`, `DeleteFile`, `Finalize` |
|
|
13
|
+
|
|
14
|
+
```python
|
|
15
|
+
import sw_serverless as sw
|
|
16
|
+
from simplyworks_bitween import ExchangeFile, Handler
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Orders(Handler):
|
|
20
|
+
def __init__(self):
|
|
21
|
+
sw.expect("Url", description="Where orders go")
|
|
22
|
+
|
|
23
|
+
def handle(self, file: ExchangeFile) -> ExchangeFile:
|
|
24
|
+
# A partner's rejection is returned with bad_data=True, not raised.
|
|
25
|
+
return ExchangeFile(data=file.data, filename=file.filename)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
if __name__ == "__main__":
|
|
29
|
+
sw.run(Orders)
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
`ExchangeFile` has `data`, `filename`, `bad_data` and `content_type`, and is sent with the contract's
|
|
33
|
+
property names (`Data`, `Filename`, `BadData`, `ContentType`, `Hash`). A kind missing a method it
|
|
34
|
+
needs fails when the adapter starts.
|
|
35
|
+
|
|
36
|
+
`bitween adapter build` vendors this package, with the SDK, into every Python adapter it builds,
|
|
37
|
+
taken from this folder through SW.Bitween.Adapters.Tooling, so nothing needs installing from PyPI.
|
|
38
|
+
|
|
39
|
+
Tests: `PYTHONPATH=src:<SW-Serverless>/sdk/python/src python -m unittest discover -s tests`.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=69"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "simplyworks-bitween"
|
|
7
|
+
version = "10.0.121"
|
|
8
|
+
description = "The Bitween adapter contract for Python: handlers, mappers, validators and receivers."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.12"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [{ name = "Simplify9" }]
|
|
13
|
+
dependencies = ["sw-serverless>=10.2.0"]
|
|
14
|
+
|
|
15
|
+
[project.urls]
|
|
16
|
+
Repository = "https://github.com/simplify9/Bitween-api"
|
|
17
|
+
|
|
18
|
+
[tool.setuptools.packages.find]
|
|
19
|
+
where = ["src"]
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
"""The Bitween adapter contract for Python: the kinds of adapter Bitween runs, and what it passes.
|
|
2
|
+
|
|
3
|
+
Subclass a kind and implement its methods; the wire names, the encoding and the kind and contract
|
|
4
|
+
declarations are taken care of. The contract itself is ``bitween-adapter-contract.v1.json`` in
|
|
5
|
+
SW.Bitween.Adapters; this is its Python form, as ``SimplyWorks.Bitween.Adapters`` is its .NET one.
|
|
6
|
+
|
|
7
|
+
import sw_serverless as sw
|
|
8
|
+
from sw_serverless._runner import _call
|
|
9
|
+
from simplyworks_bitween import ExchangeFile, Handler
|
|
10
|
+
|
|
11
|
+
class Orders(Handler):
|
|
12
|
+
def __init__(self):
|
|
13
|
+
sw.expect("Url", description="Where orders go")
|
|
14
|
+
|
|
15
|
+
def handle(self, file: ExchangeFile) -> ExchangeFile:
|
|
16
|
+
...
|
|
17
|
+
|
|
18
|
+
if __name__ == "__main__":
|
|
19
|
+
sw.run(Orders)
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import hashlib
|
|
23
|
+
from dataclasses import dataclass, field
|
|
24
|
+
|
|
25
|
+
import sw_serverless as sw
|
|
26
|
+
from sw_serverless._runner import _call
|
|
27
|
+
|
|
28
|
+
CONTRACT = "bitween"
|
|
29
|
+
CONTRACT_VERSION = 1
|
|
30
|
+
|
|
31
|
+
__version__ = "10.0.121"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class ExchangeFile:
|
|
36
|
+
"""A file as Bitween passes it to and from adapters.
|
|
37
|
+
|
|
38
|
+
``data`` is the content: text, or base64 for binary content. ``bad_data`` marks a bad response,
|
|
39
|
+
a delivery the partner rejected - returned, not raised.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
data: str = ""
|
|
43
|
+
filename: str | None = None
|
|
44
|
+
bad_data: bool = False
|
|
45
|
+
content_type: str | None = None
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def hash(self):
|
|
49
|
+
"""SHA-1 of ``data`` as lower-case hex, as .NET adapters write it."""
|
|
50
|
+
return hashlib.sha1((self.data or "").encode("utf-8")).hexdigest()
|
|
51
|
+
|
|
52
|
+
def to_wire(self):
|
|
53
|
+
return {"Filename": self.filename, "Data": self.data or "", "Hash": self.hash,
|
|
54
|
+
"BadData": self.bad_data, "ContentType": self.content_type}
|
|
55
|
+
|
|
56
|
+
@classmethod
|
|
57
|
+
def from_wire(cls, value):
|
|
58
|
+
if not isinstance(value, dict):
|
|
59
|
+
raise ValueError("an ExchangeFile is a JSON object")
|
|
60
|
+
# Hash is recomputed, never trusted; unknown properties are ignored.
|
|
61
|
+
return cls(data=value.get("Data") or "", filename=value.get("Filename"),
|
|
62
|
+
bad_data=bool(value.get("BadData", False)), content_type=value.get("ContentType"))
|
|
63
|
+
|
|
64
|
+
@staticmethod
|
|
65
|
+
def __sw_schema__():
|
|
66
|
+
return {
|
|
67
|
+
"title": "ExchangeFile", "type": "object", "required": ["Data"], "additionalProperties": True,
|
|
68
|
+
"properties": {
|
|
69
|
+
"Filename": {"type": ["string", "null"]}, "Data": {"type": "string"},
|
|
70
|
+
"Hash": {"type": ["string", "null"]}, "BadData": {"type": "boolean", "default": False},
|
|
71
|
+
"ContentType": {"type": ["string", "null"]},
|
|
72
|
+
},
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass
|
|
77
|
+
class ValidationResult:
|
|
78
|
+
"""What a validator found: each failure as a key - often the field it concerns - and a message.
|
|
79
|
+
No failures means valid."""
|
|
80
|
+
|
|
81
|
+
validations: list[tuple[str, str]] = field(default_factory=list)
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def success(self):
|
|
85
|
+
return not self.validations
|
|
86
|
+
|
|
87
|
+
def add(self, key, message):
|
|
88
|
+
self.validations.append((key, message))
|
|
89
|
+
return self
|
|
90
|
+
|
|
91
|
+
def to_wire(self):
|
|
92
|
+
return {"Success": self.success, "Validations": [{"Key": k, "Value": v} for k, v in self.validations]}
|
|
93
|
+
|
|
94
|
+
@classmethod
|
|
95
|
+
def from_wire(cls, value):
|
|
96
|
+
return cls([(v.get("Key", ""), v.get("Value", "")) for v in (value or {}).get("Validations") or []])
|
|
97
|
+
|
|
98
|
+
@staticmethod
|
|
99
|
+
def __sw_schema__():
|
|
100
|
+
return {
|
|
101
|
+
"title": "ValidationResult", "type": "object", "required": ["Validations"], "additionalProperties": True,
|
|
102
|
+
"properties": {
|
|
103
|
+
"Success": {"type": "boolean"},
|
|
104
|
+
"Validations": {"type": "array", "items": {
|
|
105
|
+
"type": "object", "required": ["Key", "Value"],
|
|
106
|
+
"properties": {"Key": {"type": "string"}, "Value": {"type": "string"}}}},
|
|
107
|
+
},
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _as_file(value):
|
|
112
|
+
if isinstance(value, ExchangeFile):
|
|
113
|
+
return value
|
|
114
|
+
if isinstance(value, str):
|
|
115
|
+
return ExchangeFile(data=value)
|
|
116
|
+
raise TypeError(f"expected an ExchangeFile, got {type(value).__name__}")
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class _Kind:
|
|
120
|
+
__sw_contracts__ = {CONTRACT: CONTRACT_VERSION}
|
|
121
|
+
_required = ()
|
|
122
|
+
|
|
123
|
+
def __sw_check__(self):
|
|
124
|
+
# A declared kind without its methods fails when the adapter starts, not on first use.
|
|
125
|
+
missing = [m for m in self._required if getattr(getattr(type(self), m), "__sw_abstract__", False)]
|
|
126
|
+
if missing:
|
|
127
|
+
raise TypeError(f"{type(self).__name__} is a Bitween {self.__sw_kinds__[0]} "
|
|
128
|
+
f"but does not implement {', '.join(missing)}")
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _abstract(fn):
|
|
132
|
+
fn.__sw_abstract__ = True
|
|
133
|
+
return fn
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class Handler(_Kind):
|
|
137
|
+
"""Delivers a message and returns the partner's response. A rejected delivery is returned with
|
|
138
|
+
``bad_data=True``, not raised."""
|
|
139
|
+
|
|
140
|
+
__sw_kinds__ = ["handler"]
|
|
141
|
+
_required = ("handle",)
|
|
142
|
+
|
|
143
|
+
@_abstract
|
|
144
|
+
def handle(self, file: ExchangeFile) -> ExchangeFile:
|
|
145
|
+
raise NotImplementedError
|
|
146
|
+
|
|
147
|
+
@sw.command("Handle", description="Delivers a message and returns the partner's response.")
|
|
148
|
+
async def _sw_handle(self, file: ExchangeFile) -> ExchangeFile:
|
|
149
|
+
return _as_file(await _call(self.handle, file))
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class Mapper(_Kind):
|
|
153
|
+
"""Transforms a message into the shape the next step expects."""
|
|
154
|
+
|
|
155
|
+
__sw_kinds__ = ["mapper"]
|
|
156
|
+
_required = ("map",)
|
|
157
|
+
|
|
158
|
+
@_abstract
|
|
159
|
+
def map(self, file: ExchangeFile) -> ExchangeFile:
|
|
160
|
+
raise NotImplementedError
|
|
161
|
+
|
|
162
|
+
@sw.command("Handle", description="Transforms a message into the shape the next step expects.")
|
|
163
|
+
async def _sw_handle(self, file: ExchangeFile) -> ExchangeFile:
|
|
164
|
+
return _as_file(await _call(self.map, file))
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class Validator(_Kind):
|
|
168
|
+
"""Checks a message before it is accepted."""
|
|
169
|
+
|
|
170
|
+
__sw_kinds__ = ["validator"]
|
|
171
|
+
_required = ("validate",)
|
|
172
|
+
|
|
173
|
+
@_abstract
|
|
174
|
+
def validate(self, file: ExchangeFile) -> ValidationResult:
|
|
175
|
+
raise NotImplementedError
|
|
176
|
+
|
|
177
|
+
@sw.command("Validate", description="Checks a message before it is accepted.")
|
|
178
|
+
async def _sw_validate(self, file: ExchangeFile) -> ValidationResult:
|
|
179
|
+
result = await _call(self.validate, file)
|
|
180
|
+
if result is None:
|
|
181
|
+
return ValidationResult()
|
|
182
|
+
if isinstance(result, ValidationResult):
|
|
183
|
+
return result
|
|
184
|
+
# A list of (key, message) pairs, or a dict of key -> message, reads naturally too.
|
|
185
|
+
return ValidationResult(list(result.items()) if isinstance(result, dict) else [tuple(r) for r in result])
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
class Receiver(_Kind):
|
|
189
|
+
"""Fetches files from a source on a schedule. One session per run: ``initialize``, ``list_files``,
|
|
190
|
+
then for each file ``get_file`` and - once it is safely taken in - ``delete_file``, and finally
|
|
191
|
+
``finalize``, which is also called after a failure."""
|
|
192
|
+
|
|
193
|
+
__sw_kinds__ = ["receiver"]
|
|
194
|
+
_required = ("list_files", "get_file", "delete_file")
|
|
195
|
+
|
|
196
|
+
def initialize(self):
|
|
197
|
+
pass
|
|
198
|
+
|
|
199
|
+
@_abstract
|
|
200
|
+
def list_files(self) -> list[str]:
|
|
201
|
+
raise NotImplementedError
|
|
202
|
+
|
|
203
|
+
@_abstract
|
|
204
|
+
def get_file(self, file_id: str) -> ExchangeFile:
|
|
205
|
+
raise NotImplementedError
|
|
206
|
+
|
|
207
|
+
@_abstract
|
|
208
|
+
def delete_file(self, file_id: str) -> None:
|
|
209
|
+
raise NotImplementedError
|
|
210
|
+
|
|
211
|
+
def finalize(self):
|
|
212
|
+
pass
|
|
213
|
+
|
|
214
|
+
@sw.command("Initialize", description="Starts a run.")
|
|
215
|
+
async def _sw_initialize(self) -> None:
|
|
216
|
+
await _call(self.initialize)
|
|
217
|
+
|
|
218
|
+
@sw.command("ListFiles", description="The ids of the files waiting.")
|
|
219
|
+
async def _sw_list_files(self) -> list[str]:
|
|
220
|
+
return [str(f) for f in (await _call(self.list_files) or [])]
|
|
221
|
+
|
|
222
|
+
@sw.command("GetFile", description="One file, by an id ListFiles gave.")
|
|
223
|
+
async def _sw_get_file(self, file_id: str) -> ExchangeFile:
|
|
224
|
+
return _as_file(await _call(self.get_file, file_id))
|
|
225
|
+
|
|
226
|
+
@sw.command("DeleteFile", description="Removes a file from the source once it is safely taken in.")
|
|
227
|
+
async def _sw_delete_file(self, file_id: str) -> None:
|
|
228
|
+
await _call(self.delete_file, file_id)
|
|
229
|
+
|
|
230
|
+
@sw.command("Finalize", description="Ends a run, after a failure too.")
|
|
231
|
+
async def _sw_finalize(self) -> None:
|
|
232
|
+
await _call(self.finalize)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
__all__ = ["CONTRACT", "CONTRACT_VERSION", "ExchangeFile", "Handler", "Mapper", "Receiver", "ValidationResult",
|
|
236
|
+
"Validator"]
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: simplyworks-bitween
|
|
3
|
+
Version: 10.0.121
|
|
4
|
+
Summary: The Bitween adapter contract for Python: handlers, mappers, validators and receivers.
|
|
5
|
+
Author: Simplify9
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Repository, https://github.com/simplify9/Bitween-api
|
|
8
|
+
Requires-Python: >=3.12
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
Requires-Dist: sw-serverless>=10.2.0
|
|
11
|
+
|
|
12
|
+
# simplyworks-bitween
|
|
13
|
+
|
|
14
|
+
The Bitween adapter contract for Python: the kinds of adapter Bitween runs and what it passes them.
|
|
15
|
+
It is the Python form of `SW.Bitween.Adapters/Contract/bitween-adapter-contract.v1.json`, as
|
|
16
|
+
`SimplyWorks.Bitween.Adapters` is its .NET one.
|
|
17
|
+
|
|
18
|
+
| Kind | Implement | Bitween calls |
|
|
19
|
+
|---|---|---|
|
|
20
|
+
| `Handler` | `handle(file) -> ExchangeFile` | `Handle` |
|
|
21
|
+
| `Mapper` | `map(file) -> ExchangeFile` | `Handle` |
|
|
22
|
+
| `Validator` | `validate(file) -> ValidationResult` | `Validate` |
|
|
23
|
+
| `Receiver` | `list_files()`, `get_file(id)`, `delete_file(id)`; optionally `initialize()`, `finalize()` | `Initialize`, `ListFiles`, `GetFile`, `DeleteFile`, `Finalize` |
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
import sw_serverless as sw
|
|
27
|
+
from simplyworks_bitween import ExchangeFile, Handler
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class Orders(Handler):
|
|
31
|
+
def __init__(self):
|
|
32
|
+
sw.expect("Url", description="Where orders go")
|
|
33
|
+
|
|
34
|
+
def handle(self, file: ExchangeFile) -> ExchangeFile:
|
|
35
|
+
# A partner's rejection is returned with bad_data=True, not raised.
|
|
36
|
+
return ExchangeFile(data=file.data, filename=file.filename)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
if __name__ == "__main__":
|
|
40
|
+
sw.run(Orders)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`ExchangeFile` has `data`, `filename`, `bad_data` and `content_type`, and is sent with the contract's
|
|
44
|
+
property names (`Data`, `Filename`, `BadData`, `ContentType`, `Hash`). A kind missing a method it
|
|
45
|
+
needs fails when the adapter starts.
|
|
46
|
+
|
|
47
|
+
`bitween adapter build` vendors this package, with the SDK, into every Python adapter it builds,
|
|
48
|
+
taken from this folder through SW.Bitween.Adapters.Tooling, so nothing needs installing from PyPI.
|
|
49
|
+
|
|
50
|
+
Tests: `PYTHONPATH=src:<SW-Serverless>/sdk/python/src python -m unittest discover -s tests`.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
src/simplyworks_bitween/__init__.py
|
|
4
|
+
src/simplyworks_bitween.egg-info/PKG-INFO
|
|
5
|
+
src/simplyworks_bitween.egg-info/SOURCES.txt
|
|
6
|
+
src/simplyworks_bitween.egg-info/dependency_links.txt
|
|
7
|
+
src/simplyworks_bitween.egg-info/requires.txt
|
|
8
|
+
src/simplyworks_bitween.egg-info/top_level.txt
|
|
9
|
+
tests/test_contract.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
sw-serverless>=10.2.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
simplyworks_bitween
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import unittest
|
|
4
|
+
|
|
5
|
+
import sw_serverless as sw
|
|
6
|
+
from simplyworks_bitween import ExchangeFile, Handler, Mapper, Receiver, ValidationResult, Validator
|
|
7
|
+
|
|
8
|
+
CONTRACT = os.path.join(os.path.dirname(__file__), "..", "..", "..", "SW.Bitween.Adapters", "Contract")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def contract():
|
|
12
|
+
with open(os.path.join(CONTRACT, "bitween-adapter-contract.v1.json"), encoding="utf-8") as f:
|
|
13
|
+
return json.load(f)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Echo(Handler):
|
|
17
|
+
def handle(self, file):
|
|
18
|
+
return file
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Upper(Mapper):
|
|
22
|
+
def map(self, file):
|
|
23
|
+
return ExchangeFile(data=file.data.upper())
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Check(Validator):
|
|
27
|
+
def validate(self, file):
|
|
28
|
+
return {"Data": "empty"} if not file.data else None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Inbox(Receiver):
|
|
32
|
+
def list_files(self):
|
|
33
|
+
return []
|
|
34
|
+
|
|
35
|
+
def get_file(self, file_id):
|
|
36
|
+
return ExchangeFile(data="")
|
|
37
|
+
|
|
38
|
+
def delete_file(self, file_id):
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class ContractTests(unittest.TestCase):
|
|
43
|
+
def test_each_kind_exposes_exactly_the_contract_s_methods(self):
|
|
44
|
+
kinds = contract()["kinds"]
|
|
45
|
+
for cls in (Echo, Upper, Check, Inbox):
|
|
46
|
+
described = sw.describe(cls)
|
|
47
|
+
kind = described["kinds"][0]
|
|
48
|
+
self.assertEqual({"bitween": 1}, described["contracts"])
|
|
49
|
+
expected = [m["name"] for m in kinds[kind]["methods"]]
|
|
50
|
+
self.assertEqual(sorted(expected), sorted(c["name"] for c in described["commands"]), kind)
|
|
51
|
+
for method in kinds[kind]["methods"]:
|
|
52
|
+
command = next(c for c in described["commands"] if c["name"] == method["name"])
|
|
53
|
+
self.assertEqual(method["output"] is not None, command["returnsValue"], method["name"])
|
|
54
|
+
self.assertEqual(method["input"] is None, command["inputSchema"] is None, method["name"])
|
|
55
|
+
|
|
56
|
+
def test_exchange_files_carry_the_contract_s_property_names(self):
|
|
57
|
+
schema_file = os.path.join(CONTRACT, "exchange-file.schema.json")
|
|
58
|
+
with open(schema_file, encoding="utf-8") as f:
|
|
59
|
+
names = set(json.load(f)["properties"])
|
|
60
|
+
wire = ExchangeFile(data="{}", filename="a.json", bad_data=True, content_type="application/json").to_wire()
|
|
61
|
+
self.assertEqual(names, set(wire))
|
|
62
|
+
self.assertEqual("bf21a9e8fbc5a3846fb05b4fa0859e0917b2202f", wire["Hash"]) # SHA-1 of "{}"
|
|
63
|
+
|
|
64
|
+
def test_the_contract_s_examples_read_and_write_back_the_same(self):
|
|
65
|
+
for kind in contract()["kinds"].values():
|
|
66
|
+
for method in kind["methods"]:
|
|
67
|
+
for example in method.get("examples", []):
|
|
68
|
+
file = ExchangeFile.from_wire(example)
|
|
69
|
+
back = file.to_wire()
|
|
70
|
+
for key, value in example.items():
|
|
71
|
+
self.assertEqual(value, back[key])
|
|
72
|
+
|
|
73
|
+
def test_a_validation_result_says_success_from_its_failures(self):
|
|
74
|
+
self.assertEqual({"Success": True, "Validations": []}, ValidationResult().to_wire())
|
|
75
|
+
failed = ValidationResult().add("orderId", "missing").to_wire()
|
|
76
|
+
self.assertEqual({"Success": False, "Validations": [{"Key": "orderId", "Value": "missing"}]}, failed)
|
|
77
|
+
|
|
78
|
+
def test_a_kind_without_its_methods_fails_when_it_starts(self):
|
|
79
|
+
class Lazy(Receiver):
|
|
80
|
+
def list_files(self):
|
|
81
|
+
return []
|
|
82
|
+
with self.assertRaises(TypeError) as raised:
|
|
83
|
+
Lazy().__sw_check__()
|
|
84
|
+
self.assertIn("get_file", str(raised.exception))
|
|
85
|
+
self.assertIn("delete_file", str(raised.exception))
|
|
86
|
+
Echo().__sw_check__()
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
if __name__ == "__main__":
|
|
90
|
+
unittest.main()
|