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.
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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
+ sw-serverless>=10.2.0
@@ -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()