rewloy 0.1.0__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.
- rewloy-0.1.0/.gitignore +16 -0
- rewloy-0.1.0/CHANGELOG.md +43 -0
- rewloy-0.1.0/LICENSE +21 -0
- rewloy-0.1.0/PKG-INFO +581 -0
- rewloy-0.1.0/README.md +547 -0
- rewloy-0.1.0/SECURITY.md +13 -0
- rewloy-0.1.0/docs/DECISIONS.md +285 -0
- rewloy-0.1.0/openapi/openapi.json +92531 -0
- rewloy-0.1.0/pyproject.toml +60 -0
- rewloy-0.1.0/scripts/generate.py +94 -0
- rewloy-0.1.0/scripts/generator.py +894 -0
- rewloy-0.1.0/src/rewloy/__init__.py +38 -0
- rewloy-0.1.0/src/rewloy/_version.py +3 -0
- rewloy-0.1.0/src/rewloy/client.py +648 -0
- rewloy-0.1.0/src/rewloy/common.py +116 -0
- rewloy-0.1.0/src/rewloy/errors.py +115 -0
- rewloy-0.1.0/src/rewloy/generated/__init__.py +4 -0
- rewloy-0.1.0/src/rewloy/generated/methods.py +7461 -0
- rewloy-0.1.0/src/rewloy/generated/operations.py +385 -0
- rewloy-0.1.0/src/rewloy/generated/types.py +10296 -0
- rewloy-0.1.0/src/rewloy/httpx_transport.py +126 -0
- rewloy-0.1.0/src/rewloy/py.typed +0 -0
- rewloy-0.1.0/src/rewloy/sse.py +322 -0
- rewloy-0.1.0/src/rewloy/transport.py +215 -0
- rewloy-0.1.0/src/rewloy/types.py +9 -0
- rewloy-0.1.0/src/rewloy/webhooks.py +192 -0
- rewloy-0.1.0/tests/__init__.py +0 -0
- rewloy-0.1.0/tests/conftest.py +32 -0
- rewloy-0.1.0/tests/helpers.py +220 -0
- rewloy-0.1.0/tests/test_client.py +325 -0
- rewloy-0.1.0/tests/test_compat.py +69 -0
- rewloy-0.1.0/tests/test_deprecation.py +101 -0
- rewloy-0.1.0/tests/test_errors.py +115 -0
- rewloy-0.1.0/tests/test_generate.py +248 -0
- rewloy-0.1.0/tests/test_methods.py +113 -0
- rewloy-0.1.0/tests/test_paginate.py +90 -0
- rewloy-0.1.0/tests/test_retry.py +262 -0
- rewloy-0.1.0/tests/test_sse.py +306 -0
- rewloy-0.1.0/tests/test_transport.py +177 -0
- rewloy-0.1.0/tests/test_webhooks.py +157 -0
rewloy-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Değişiklik günlüğü / Changelog
|
|
2
|
+
|
|
3
|
+
Bu kütüphanenin sürümleri. API'nin kendi değişiklikleri:
|
|
4
|
+
https://rewloy.com/gelistiriciler/degisiklikler
|
|
5
|
+
|
|
6
|
+
This library's releases. The API's own changes are listed at the link above.
|
|
7
|
+
|
|
8
|
+
## 0.1.0 (2026-10-04)
|
|
9
|
+
|
|
10
|
+
İlk önizleme. Rewloy API 1.0.0'a göre üretildi (4 Ekim 2026): 195 yol,
|
|
11
|
+
237 işlem.
|
|
12
|
+
|
|
13
|
+
First preview, generated from Rewloy API 1.0.0 as of 4 Oct 2026 (195 paths,
|
|
14
|
+
237 operations):
|
|
15
|
+
|
|
16
|
+
- **Client.** `Rewloy(api_key=…)`, `Rewloy(staff_session=…, merchant=…)` or
|
|
17
|
+
`Rewloy(holder_session=…)`, with `base_url`, `timeout`, `max_retries`,
|
|
18
|
+
`transport`, `user_agent` and `sleep`. Python 3.9 and later, no runtime
|
|
19
|
+
dependencies, thread-safe, synchronous.
|
|
20
|
+
- **Methods.** One method per operation, named by its operationId in
|
|
21
|
+
snake_case, typed with `TypedDict`s from the OpenAPI document
|
|
22
|
+
(`rewloy.types`); `METHOD_NAMES` and `OPERATION_IDS` map the names.
|
|
23
|
+
`request()` returns the whole answer (`status`, `request_id`, `mode`,
|
|
24
|
+
`replayed`). `mypy --strict` passes.
|
|
25
|
+
- **Retries** on network errors, timeouts, 429, 502–504 and 520–524, with
|
|
26
|
+
exponential backoff, jitter and `Retry-After`. Only safe requests are
|
|
27
|
+
retried; the timeout covers a whole attempt.
|
|
28
|
+
- **`Idempotency-Key`** for till actions, campaigns and card issue: generated
|
|
29
|
+
when omitted, reused across retries.
|
|
30
|
+
- **Pagination** with `paginate()`, typed per list.
|
|
31
|
+
- **Server-sent events** with `stream()`, `live_feed()` and
|
|
32
|
+
`holder_card_events()`, with reconnection and `Last-Event-ID`; closed by
|
|
33
|
+
`break`, `with` or `close()`.
|
|
34
|
+
- **Webhooks:** `verify_webhook()` and `sign_webhook()`.
|
|
35
|
+
- **Errors:** `RewloyError`, `RateLimitError`, `RewloyConnectionError` and
|
|
36
|
+
`RewloyTimeoutError`.
|
|
37
|
+
- **Deprecations:** one `DeprecationWarning` per deprecated operation,
|
|
38
|
+
attributed to the caller's line.
|
|
39
|
+
- **Transports:** the default one on `urllib` (no redirects, no dependency),
|
|
40
|
+
an optional one on `httpx` (`pip install "rewloy[httpx]"`), and any object
|
|
41
|
+
with `send`, `open_stream` and `close`.
|
|
42
|
+
- **Regeneration:** `python scripts/generate.py`, plus a daily workflow that
|
|
43
|
+
opens a pull request when the live document changes.
|
rewloy-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Rewloy
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
rewloy-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,581 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: rewloy
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: The official Python library for the Rewloy API: digital loyalty cards, typed from the OpenAPI document.
|
|
5
|
+
Project-URL: Homepage, https://rewloy.com/gelistiriciler
|
|
6
|
+
Project-URL: Documentation, https://rewloy.com/gelistiriciler/api
|
|
7
|
+
Project-URL: Repository, https://github.com/Rewloy/rewloy-python
|
|
8
|
+
Project-URL: Changelog, https://github.com/Rewloy/rewloy-python/blob/main/CHANGELOG.md
|
|
9
|
+
Author: Rewloy
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: api,loyalty,openapi,rewloy,sdk,wallet
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
24
|
+
Classifier: Typing :: Typed
|
|
25
|
+
Requires-Python: >=3.9
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: build>=1.2; extra == 'dev'
|
|
28
|
+
Requires-Dist: httpx>=0.27; extra == 'dev'
|
|
29
|
+
Requires-Dist: mypy>=1.11; extra == 'dev'
|
|
30
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
31
|
+
Provides-Extra: httpx
|
|
32
|
+
Requires-Dist: httpx>=0.27; extra == 'httpx'
|
|
33
|
+
Description-Content-Type: text/markdown
|
|
34
|
+
|
|
35
|
+
# Rewloy Python
|
|
36
|
+
|
|
37
|
+
**Rewloy API'nin resmî Python kütüphanesi.**
|
|
38
|
+
|
|
39
|
+
> **Durum: önizleme (0.x), PyPI'da yayımlandı. API kararlı; kütüphane arayüzü 1.0'a kadar değişebilir.**
|
|
40
|
+
|
|
41
|
+
[Rewloy](https://rewloy.com), işletmelerin dijital sadakat kartlarını
|
|
42
|
+
müşterinin telefonuna koyar. Kart türleri damga, puan, VIP, cashback, hediye
|
|
43
|
+
kartı, kupon ve indirimdir:
|
|
44
|
+
- iPhone'da Apple Cüzdan;
|
|
45
|
+
- Android'de Rewloy Cüzdan ve Google Cüzdan;
|
|
46
|
+
- her yerde web kartı.
|
|
47
|
+
|
|
48
|
+
Kasada QR okutulur; bakiye, ödül ve kampanyalar kartın kendisinde güncellenir.
|
|
49
|
+
Panelde yapılabilen her şey [Rewloy API v1](https://rewloy.com/gelistiriciler)
|
|
50
|
+
ile de yapılabilir; bu kütüphane onu Python'dan kullanır:
|
|
51
|
+
|
|
52
|
+
- **Tam tipli.** API'nin her işlemi, `operationId` adının snake_case hâliyle
|
|
53
|
+
bir metottur (`passAction` → `pass_action`). İstek gövdeleri, sorgular ve
|
|
54
|
+
yanıtlar OpenAPI belgesinden
|
|
55
|
+
([`openapi.json`](https://app.rewloy.com/v1/openapi.json)) üretilen
|
|
56
|
+
`TypedDict` tipleriyle gelir; `mypy --strict` geçer. CI belgeyi her gün okur
|
|
57
|
+
ve değişince yeniden üretir.
|
|
58
|
+
- **Bağımlılıksız.** Python 3.9 ve üstü; yalnız standart kütüphane (`urllib`,
|
|
59
|
+
`json`, `hmac`). Bağlantı havuzu ve HTTP/2 isteyen için `httpx` taşıması
|
|
60
|
+
isteğe bağlıdır.
|
|
61
|
+
- **Güvenli tekrar.** Geçici hatalarda ölçülü yeniden deneme; kasa işleminde
|
|
62
|
+
ve kampanyada `Idempotency-Key`.
|
|
63
|
+
- **Ötesi:** sayfalama, canlı akış (SSE), webhook imzası doğrulama,
|
|
64
|
+
kullanımdan kalkma uyarıları.
|
|
65
|
+
|
|
66
|
+
## Kurulum
|
|
67
|
+
|
|
68
|
+
Python 3.9 ya da üstü gerekir:
|
|
69
|
+
|
|
70
|
+
```sh
|
|
71
|
+
pip install rewloy
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
`httpx` taşıması için: `pip install "rewloy[httpx]"`.
|
|
75
|
+
|
|
76
|
+
## Başlarken
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
import os
|
|
80
|
+
from rewloy import Rewloy
|
|
81
|
+
|
|
82
|
+
rewloy = Rewloy(api_key=os.environ["REWLOY_API_KEY"])
|
|
83
|
+
|
|
84
|
+
kart = rewloy.get_pass("ABCD-EFGH-JKLM")
|
|
85
|
+
print(kart["type"], kart["balance"], kart["rewardReady"])
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Her işlem, adı `operationId`'nin snake_case hâli olan bir metottur
|
|
89
|
+
([API referansı](https://rewloy.com/gelistiriciler/api); `OPERATION_IDS` ve
|
|
90
|
+
`METHOD_NAMES` ikisini eşler). Argümanlar:
|
|
91
|
+
- adresteki parametreler konumsaldır: `get_pass(seri)`;
|
|
92
|
+
- `query`: sorgu parametreleri (sözlük);
|
|
93
|
+
- `body`: JSON gövde (sözlük);
|
|
94
|
+
- `merchant`: `Rewloy-Merchant` başlığı;
|
|
95
|
+
- `idempotency_key`: `Idempotency-Key` başlığı (kasa işlemi ve kampanya);
|
|
96
|
+
- `timeout` (saniye) ve `max_retries`.
|
|
97
|
+
|
|
98
|
+
Sorgu ve gövde sözlükleri API'deki adlarıyla yazılır (`programId`,
|
|
99
|
+
`kvkkConsent`); anahtarlar çevrilmez. Metot yanıttaki `data`yı döndürür:
|
|
100
|
+
- bir `TypedDict` ya da liste; yanıtı olduğu gibi alırsınız, API'nin sonradan
|
|
101
|
+
eklediği bir alan hemen sözlüğünüzdedir;
|
|
102
|
+
- sayfalı listelerde `Page` (`sayfa.data`, `sayfa.meta`);
|
|
103
|
+
- gövdesiz yanıtta (`204`) `None`;
|
|
104
|
+
- dosyada (QR, harita, CSV, `.pkpass`) `bytes`.
|
|
105
|
+
|
|
106
|
+
Tipler `rewloy.types` altındadır: `IssuePassBody`, `GetPassData`,
|
|
107
|
+
`ListCustomersItem`, `ErrorCode`… Çalışma anında yüklenmemeleri için
|
|
108
|
+
(`import rewloy` onları yüklemez; yaklaşık 80 ms tutar) yalnız açıklamada
|
|
109
|
+
kullanıyorsanız `TYPE_CHECKING` altında içe aktarın:
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
from typing import TYPE_CHECKING
|
|
113
|
+
|
|
114
|
+
if TYPE_CHECKING:
|
|
115
|
+
from rewloy.types import IssuePassBody
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Kimlik
|
|
119
|
+
|
|
120
|
+
| İstemci | Ne için |
|
|
121
|
+
|---|---|
|
|
122
|
+
| `Rewloy(api_key="rwk_…")` | API anahtarı: kasa, e-ticaret, kendi sisteminiz |
|
|
123
|
+
| `Rewloy(staff_session="rws_…", merchant=…)` | ekip oturumu: bir kişinin işletme uygulaması |
|
|
124
|
+
| `Rewloy(holder_session="rwh_…")` | kart sahibi oturumu: Rewloy Cüzdan gibi müşteri uygulamaları |
|
|
125
|
+
| `Rewloy()` | kimlik istemeyen uç noktalar: giriş, katılım, kod |
|
|
126
|
+
|
|
127
|
+
`merchant`, ekip oturumu birden fazla işletmede koltuk taşıyorsa hangi işletme
|
|
128
|
+
için çalıştığını söyler (`Rewloy-Merchant`). Her çağrıda `merchant=` ile
|
|
129
|
+
değiştirilebilir. Oturumlar kimliksiz bir istemciyle açılır:
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
oturum = Rewloy().login(body={"email": eposta, "password": parola})
|
|
133
|
+
ekip = Rewloy(staff_session=oturum["token"], merchant=isletme_id)
|
|
134
|
+
if oturum["mfaRequired"]:
|
|
135
|
+
ekip.prove_mfa(body={"code": "123456"})
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Bir işlem istemcinin kimlik türünü kabul etmiyor ama kimliksiz de çalışıyorsa
|
|
139
|
+
(örneğin `login`), istemci onu kimliksiz çağırır. API, işlemin kabul etmediği
|
|
140
|
+
bir kimliği reddeder (`CREDENTIAL_NOT_ALLOWED`). Kimlik `repr`de görünmez.
|
|
141
|
+
|
|
142
|
+
Diğer seçenekler:
|
|
143
|
+
- `base_url` (varsayılan `https://app.rewloy.com`);
|
|
144
|
+
- `timeout` (saniye; varsayılan 60, `0` sınırsız);
|
|
145
|
+
- `max_retries` (2);
|
|
146
|
+
- `transport`: HTTP katmanı (bkz. [Taşıma](#taşıma-ve-test-etmek));
|
|
147
|
+
- `user_agent`: gönderilen `User-Agent`a eklenir, örneğin `"KasaPOS/4.2"`;
|
|
148
|
+
- `sleep`: yeniden denemeler arasındaki beklemeyi değiştirir (testler için).
|
|
149
|
+
|
|
150
|
+
İstemci iş parçacıkları arasında paylaşılabilir. `with Rewloy(...) as rewloy:`
|
|
151
|
+
ya da `rewloy.close()` taşımanın tuttuklarını bırakır.
|
|
152
|
+
|
|
153
|
+
## Kart vermek ve kasada işlem
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
sonuc = rewloy.issue_pass(
|
|
157
|
+
body={"programId": program_id, "email": "ayse@ornek.com", "firstName": "Ayşe", "kvkkConsent": True},
|
|
158
|
+
)
|
|
159
|
+
seri, kart_adresi = sonuc["serial"], sonuc["cardUrl"]
|
|
160
|
+
|
|
161
|
+
islem = rewloy.pass_action(
|
|
162
|
+
seri,
|
|
163
|
+
body={"action": "earn-stamps", "locationId": sube_id, "count": 1},
|
|
164
|
+
idempotency_key=f"fis-{fis_no}",
|
|
165
|
+
)
|
|
166
|
+
if islem.get("duplicate"):
|
|
167
|
+
print("Bu fiş zaten işlenmiş")
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
`pass_action`, `send_campaign` ve `issue_pass` bir `Idempotency-Key` alır.
|
|
171
|
+
Verilmezse kütüphane bir UUID üretir ve aynı çağrının her denemesinde
|
|
172
|
+
aynısını gönderir. Kasada fiş numarası gibi kendi anahtarınızı vermek
|
|
173
|
+
daha iyidir: uygulama çöküp yeniden başlasa bile aynı fiş ikinci kez işlenmez,
|
|
174
|
+
aynı anahtarla tekrar ilk sonucu `duplicate: true` ile döndürür.
|
|
175
|
+
|
|
176
|
+
## Sayfalama
|
|
177
|
+
|
|
178
|
+
```python
|
|
179
|
+
for musteri in rewloy.paginate("listCustomers", query={"consent": "yes", "limit": 200}):
|
|
180
|
+
print(musteri["displayName"], musteri["identifiers"])
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
`paginate` sayfalı her listeyi (`page`/`limit` ve `meta`) öğe öğe dolaşır ve
|
|
184
|
+
son sayfada durur; tembeldir, bıraktığınız yerde istek de durur. Adreste
|
|
185
|
+
parametresi olan listelere `path={"id": …}` verilir. Tek bir sayfa için
|
|
186
|
+
metodun kendisi yeter: `sayfa = rewloy.list_customers(query={"page": 2})`
|
|
187
|
+
(`sayfa.data`, `sayfa.meta`).
|
|
188
|
+
|
|
189
|
+
## Canlı akış
|
|
190
|
+
|
|
191
|
+
```python
|
|
192
|
+
with rewloy.live_feed() as akis:
|
|
193
|
+
for olay in akis:
|
|
194
|
+
if olay.event == "event":
|
|
195
|
+
ev = olay.json()
|
|
196
|
+
print(ev["kind"], ev["location"], ev["program"], ev["delta"], ev["unit"], ev["name"])
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
`live_feed` (işletmenin tezgâh akışı) ve `holder_card_events` (kart sahibinin
|
|
200
|
+
kartındaki değişiklik) sunucu olayları (`text/event-stream`) yayınlar.
|
|
201
|
+
`rewloy.stream("liveFeed", …)` aynı işi görür. Her olay `event`, `data` ve
|
|
202
|
+
`id` taşır; `json()` `data`yı ayrıştırır.
|
|
203
|
+
|
|
204
|
+
- **Yeniden bağlanma.** Bağlantı koparsa akış kendiliğinden yeniden bağlanır:
|
|
205
|
+
sunucunun `retry:` süresi kadar bekler, bir olay `id` taşıdıysa
|
|
206
|
+
`Last-Event-ID` gönderir. `reconnect=False` bunu kapatır.
|
|
207
|
+
- **Sessiz bağlantı.** API 25 saniyede bir `: hb` gönderir; 60 saniye hiç veri
|
|
208
|
+
gelmezse bağlantı kopmuş sayılır (`idle_timeout=`).
|
|
209
|
+
- **Durdurmak:** döngüden `break` (bağlantı hemen kapanır), `with` bloğundan
|
|
210
|
+
çıkmak ya da başka bir iş parçacığından `akis.close()`.
|
|
211
|
+
- **Bitiren hatalar.** Yeniden bağlanmanın düzeltemeyeceği bir hata (`401`,
|
|
212
|
+
`403`, `404`) akışı `RewloyError` ile bitirir.
|
|
213
|
+
|
|
214
|
+
## Webhook doğrulama
|
|
215
|
+
|
|
216
|
+
Rewloy her teslimi imzalar:
|
|
217
|
+
|
|
218
|
+
```
|
|
219
|
+
Rewloy-Signature: t=<unix saniye>,v1=<hex HMAC-SHA256(sır, "<t>.<ham gövde>")>
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
`verify_webhook` imzayı **ham gövdeyle** ve webhook oluşturulurken bir kez
|
|
223
|
+
gösterilen sırla (`whsec_…`) doğrular:
|
|
224
|
+
- karşılaştırmayı `hmac.compare_digest` ile sabit sürede yapar;
|
|
225
|
+
- `t` şimdiden 300 saniyeden (`tolerance=`) uzaksa reddeder;
|
|
226
|
+
- gövdeyi ayrıştırılmış olarak (`dict`) döndürür.
|
|
227
|
+
|
|
228
|
+
Tutmazsa `WebhookSignatureError` atar: 400 ile yanıtlayın ve hiçbir işlem
|
|
229
|
+
yapmayın. Gövde mutlaka ham olmalıdır (`str` ya da `bytes`). JSON olarak
|
|
230
|
+
ayrıştırılıp yeniden yazılan bir gövde imzayı tutturmaz; ayrıştırılmış bir
|
|
231
|
+
`dict` verirseniz `TypeError` alırsınız.
|
|
232
|
+
|
|
233
|
+
Flask:
|
|
234
|
+
|
|
235
|
+
```python
|
|
236
|
+
import os
|
|
237
|
+
from flask import Flask, request
|
|
238
|
+
from rewloy import WebhookSignatureError, verify_webhook
|
|
239
|
+
|
|
240
|
+
app = Flask(__name__)
|
|
241
|
+
|
|
242
|
+
@app.post("/rewloy/webhook")
|
|
243
|
+
def rewloy_webhook():
|
|
244
|
+
try:
|
|
245
|
+
olay = verify_webhook(
|
|
246
|
+
request.get_data(),
|
|
247
|
+
request.headers.get("Rewloy-Signature"),
|
|
248
|
+
os.environ["REWLOY_WEBHOOK_SECRET"],
|
|
249
|
+
)
|
|
250
|
+
except WebhookSignatureError:
|
|
251
|
+
return "", 400
|
|
252
|
+
# Rewloy-Delivery bir teslimin her denemesinde aynıdır: işlediyseniz atlayın.
|
|
253
|
+
if daha_once_islendi(request.headers.get("Rewloy-Delivery")):
|
|
254
|
+
return "", 200
|
|
255
|
+
if olay["type"] == "pass.activity":
|
|
256
|
+
print(olay["data"]["card"], olay["data"]["kind"], olay["data"]["delta"])
|
|
257
|
+
return "", 200
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
FastAPI (Django'da ham gövde `request.body`dir):
|
|
261
|
+
|
|
262
|
+
```python
|
|
263
|
+
from fastapi import FastAPI, HTTPException, Request
|
|
264
|
+
|
|
265
|
+
app = FastAPI()
|
|
266
|
+
|
|
267
|
+
@app.post("/rewloy/webhook")
|
|
268
|
+
async def rewloy_webhook(request: Request) -> dict[str, bool]:
|
|
269
|
+
try:
|
|
270
|
+
olay = verify_webhook(await request.body(), request.headers.get("rewloy-signature"), SIR)
|
|
271
|
+
except WebhookSignatureError:
|
|
272
|
+
raise HTTPException(status_code=400)
|
|
273
|
+
...
|
|
274
|
+
return {"ok": True}
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
Başlıklar:
|
|
278
|
+
- `Rewloy-Event`: olay türü (`pass.issued`, `pass.activity`, `pass.voided`,
|
|
279
|
+
`webhook.test`); gövdedeki `type` ile aynı.
|
|
280
|
+
- `Rewloy-Delivery`: teslimin kimliği. Teslim "en az bir kez"dir: çift gelen
|
|
281
|
+
teslimi bununla ayıklayın.
|
|
282
|
+
|
|
283
|
+
Gövde kişinin iletişim bilgisini taşımaz; kişiyi `customer_id` ile API'den
|
|
284
|
+
okuyun. 2xx dışı bir yanıt yaklaşık 45 saat boyunca 8 kez yeniden denenir ve
|
|
285
|
+
her deneme yeni bir `t` ile imzalanır. Sonuç `PassEvent` ya da
|
|
286
|
+
`WebhookTestEvent` tipindedir: `olay["type"]` ile `mypy` türü daraltır, bilinmeyen
|
|
287
|
+
yeni bir tür için bir `else` dalı bırakın. Kendi işleyicinizi test etmek için
|
|
288
|
+
`sign_webhook(govde, sir)` aynı başlığı üretir.
|
|
289
|
+
|
|
290
|
+
## Hatalar ve yeniden deneme
|
|
291
|
+
|
|
292
|
+
```python
|
|
293
|
+
from rewloy import RateLimitError, RewloyError
|
|
294
|
+
|
|
295
|
+
try:
|
|
296
|
+
rewloy.pass_action(
|
|
297
|
+
seri,
|
|
298
|
+
body={"action": "spend", "locationId": sube_id, "amountMinor": 5000},
|
|
299
|
+
idempotency_key=f"fis-{fis_no}",
|
|
300
|
+
)
|
|
301
|
+
except RateLimitError as err:
|
|
302
|
+
print(f"{err.retry_after} saniye sonra yeniden deneyin")
|
|
303
|
+
except RewloyError as err:
|
|
304
|
+
if err.code == "INSUFFICIENT_BALANCE":
|
|
305
|
+
print(err.detail)
|
|
306
|
+
else:
|
|
307
|
+
raise
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
`RewloyError` şunları taşır:
|
|
311
|
+
- `status`: HTTP durumu;
|
|
312
|
+
- `code`: API'nin sabit kodu ([hata kodları](https://rewloy.com/gelistiriciler/hatalar));
|
|
313
|
+
kodunuz buna göre davranmalı (`rewloy.types.ErrorCode` bugünkü kodları sayar);
|
|
314
|
+
- `title`: kodun katalogdaki başlığı;
|
|
315
|
+
- `detail`: API'nin açıklaması (Türkçe, değişebilir);
|
|
316
|
+
- `details`: varsa ayrıntı; doğrulama hatasında `[{"field", "rule", "message"}]`;
|
|
317
|
+
- `request_id`: `x-request-id`; destek talebinde bunu verin;
|
|
318
|
+
- `body`, `headers`, `docs` ve `operation`.
|
|
319
|
+
|
|
320
|
+
Alt sınıflar:
|
|
321
|
+
- `RateLimitError`: `429`; `retry_after` saniye;
|
|
322
|
+
- `RewloyConnectionError`: yanıt gelmedi (`status` 0, `code`
|
|
323
|
+
`CONNECTION_ERROR`);
|
|
324
|
+
- `RewloyTimeoutError`: zaman aşımı (`TIMEOUT`).
|
|
325
|
+
|
|
326
|
+
Rewloy'un olmayan bir hata gövdesi (örneğin bir vekil sunucunun 502 sayfası)
|
|
327
|
+
`HTTP_502` gibi bir kodla gelir. Yanlış kullanım (yanlış önekli bir anahtar,
|
|
328
|
+
eksik adres parametresi) Python'un `ValueError`ı ya da `TypeError`ıdır.
|
|
329
|
+
|
|
330
|
+
**Yeniden deneme.** Şunlar en çok `max_retries` kez (varsayılan 2) yeniden
|
|
331
|
+
denenir: bağlantı hatası, zaman aşımı, `429`, `502`, `503`, `504` ve
|
|
332
|
+
Cloudflare'in `520`–`524` hataları.
|
|
333
|
+
- **Bekleme:** üstel ve rastgele (0,5 sn, 1 sn, 2 sn… en çok 8 sn); yanıt
|
|
334
|
+
`Retry-After` taşıyorsa o kadar. `Retry-After` 60 saniyeden uzunsa
|
|
335
|
+
beklenmez, hata size gelir.
|
|
336
|
+
- **Yalnız tekrarı güvenli istekler:** `GET`, `PUT`, `DELETE` ve
|
|
337
|
+
`Idempotency-Key` taşıyan `POST`. İlk istek hâlâ işlenirken gelen
|
|
338
|
+
`409 IDEMPOTENCY_IN_PROGRESS` de beklenip yeniden denenir. Diğer `POST` ve
|
|
339
|
+
`PATCH` istekleri hiç tekrar edilmez.
|
|
340
|
+
- **Süre:** her deneme `timeout` (varsayılan 60 sn) içinde bitmelidir; süre
|
|
341
|
+
gövdenin tamamını kapsar.
|
|
342
|
+
|
|
343
|
+
## Kullanımdan kalkma
|
|
344
|
+
|
|
345
|
+
Kalkacak bir uç nokta en az 180 gün önceden duyurulur. O süre boyunca her
|
|
346
|
+
yanıtı `Deprecation`, `Sunset` ve `Link` başlıklarını taşır.
|
|
347
|
+
|
|
348
|
+
- **Uyarı.** Kütüphane her işlem için bir kez `warnings.warn` ile bir
|
|
349
|
+
`DeprecationWarning` yayar. Uyarı işlemi, `Sunset` tarihini ve değişiklik
|
|
350
|
+
günlüğündeki kaydı söyler; satır olarak sizin çağrınızı gösterir, bu yüzden
|
|
351
|
+
Python'un varsayılan süzgeci onu `__main__` kodunda gösterir.
|
|
352
|
+
- **Tipler.** O metodun belge dizgisi (docstring) kaldırılacağını söyler;
|
|
353
|
+
kalkacak yanıt alanları da metodun belgesinde ve tiplerde işaretlidir.
|
|
354
|
+
- **Yönetmek.** `python -W default` her yerde gösterir; `-W ignore::DeprecationWarning`
|
|
355
|
+
ya da `warnings.filterwarnings` susturur. `-W error` ile uyarı istisna
|
|
356
|
+
olurdu ama sunucu işi yapmış olurdu ve yanıt kaybolurdu: bu yüzden çağrı
|
|
357
|
+
döner ve uyarı `rewloy` kaydedicisine (`logging`) yazılır.
|
|
358
|
+
|
|
359
|
+
## Yanıtın tamamı ve test modu
|
|
360
|
+
|
|
361
|
+
```python
|
|
362
|
+
yanit = rewloy.request(
|
|
363
|
+
"sendCampaign",
|
|
364
|
+
body={"body": "Bu hafta kahveler 2 damga!"},
|
|
365
|
+
idempotency_key="kampanya-2026-10-03",
|
|
366
|
+
)
|
|
367
|
+
yanit.status # 201
|
|
368
|
+
yanit.replayed # True: aynı anahtarın ilk yanıtı yeniden döndü (Idempotent-Replayed)
|
|
369
|
+
yanit.request_id # x-request-id
|
|
370
|
+
yanit.mode # Rewloy-Mode
|
|
371
|
+
yanit.data # kampanya
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
`request(işlem, …)` her işlemi çağırır (`operationId` ya da metot adıyla) ve
|
|
375
|
+
yanıtın tamamını döndürür: `data`, sayfalı listede `meta`, `status`,
|
|
376
|
+
`headers`, `request_id`, `mode` ve `replayed`. Adres parametreleri
|
|
377
|
+
`path={"serial": …}` ile verilir. `data` burada tipli değildir; `typing.cast`
|
|
378
|
+
ya da metodun kendisi.
|
|
379
|
+
|
|
380
|
+
`mode`, yanıtın `Rewloy-Mode` başlığıdır. Platformda test modu hazırlanıyor:
|
|
381
|
+
gerçek mesaj göndermeyen, gerçek kart vermeyen test anahtarları. Geldiğinde
|
|
382
|
+
test yanıtları bunu bu başlıkla söyleyecek. Başlık yoksa `None`. Canlı akışta
|
|
383
|
+
aynı bilgi `akis.mode`dadır.
|
|
384
|
+
|
|
385
|
+
İşlem tablosu da dışa açıktır: `OPERATIONS["passAction"]` →
|
|
386
|
+
`OperationMeta(id, method_name, http_method, path, auth, merchant, idempotency, body, response, paged, stream, deprecated)`.
|
|
387
|
+
|
|
388
|
+
## Taşıma ve test etmek
|
|
389
|
+
|
|
390
|
+
Varsayılan taşıma `urllib`dir: bağımlılık yok, yönlendirme izlenmez (API
|
|
391
|
+
yönlendirmez; izlemek anahtarı başka yere taşıyabilir), yalnız `http` ve
|
|
392
|
+
`https`, ortam değişkenlerindeki vekil (`HTTPS_PROXY`) kullanılır. Her istek
|
|
393
|
+
yeni bir bağlantı açar. Bağlantı havuzu, HTTP/2 ya da kendi vekil ve sertifika
|
|
394
|
+
ayarlarınız için:
|
|
395
|
+
|
|
396
|
+
```python
|
|
397
|
+
import httpx
|
|
398
|
+
from rewloy import Rewloy
|
|
399
|
+
from rewloy.httpx_transport import HttpxTransport # pip install "rewloy[httpx]"
|
|
400
|
+
|
|
401
|
+
rewloy = Rewloy(api_key=anahtar, transport=HttpxTransport(httpx.Client(http2=True)))
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
`transport=` aynı zamanda sahte bir API'dir: `send(request)`, `open_stream(request)`
|
|
405
|
+
ve `close()` olan her nesne olur. Kendi kodunuzu ağsız test etmek için:
|
|
406
|
+
|
|
407
|
+
```python
|
|
408
|
+
from rewloy import Headers, HttpRequest, HttpResponse, Rewloy
|
|
409
|
+
|
|
410
|
+
class SahteTasima:
|
|
411
|
+
def send(self, request: HttpRequest) -> HttpResponse:
|
|
412
|
+
return HttpResponse(200, "OK", Headers([("Content-Type", "application/json")]),
|
|
413
|
+
b'{"data": {"serial": "ABCD-EFGH-JKLM", "balance": 3}}')
|
|
414
|
+
def open_stream(self, request: HttpRequest): raise NotImplementedError
|
|
415
|
+
def close(self) -> None: pass
|
|
416
|
+
|
|
417
|
+
rewloy = Rewloy(api_key="rwk_test", transport=SahteTasima())
|
|
418
|
+
assert rewloy.get_pass("ABCD-EFGH-JKLM")["balance"] == 3
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
### asyncio
|
|
422
|
+
|
|
423
|
+
İstemci eşzamanlıdır (decision 18: [docs/DECISIONS.md](docs/DECISIONS.md)). Bir
|
|
424
|
+
`asyncio` uygulamasında iş parçacığına verin; istemci iş parçacığı güvenlidir:
|
|
425
|
+
|
|
426
|
+
```python
|
|
427
|
+
kart = await asyncio.to_thread(rewloy.get_pass, "ABCD-EFGH-JKLM")
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
## Geliştirme
|
|
431
|
+
|
|
432
|
+
```sh
|
|
433
|
+
python3 -m venv .venv && . .venv/bin/activate
|
|
434
|
+
pip install -e ".[dev]"
|
|
435
|
+
python scripts/generate.py # canlı belgeden: openapi/openapi.json ve src/rewloy/generated/
|
|
436
|
+
python scripts/generate.py --file openapi/openapi.json # kayıtlı belgeden
|
|
437
|
+
mypy && pytest
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
- `src/rewloy/generated/` elle düzenlenmez; üreteç `scripts/generator.py`'dir.
|
|
441
|
+
- Testler ağa çıkmaz: yerel bir sahte API (`http.server`) ile çalışır.
|
|
442
|
+
- CI her gün canlı belgeyi okur ve bir değişiklik varsa bir pull request açar.
|
|
443
|
+
- Kararlar: [docs/DECISIONS.md](docs/DECISIONS.md).
|
|
444
|
+
|
|
445
|
+
## Belgeler
|
|
446
|
+
|
|
447
|
+
| | |
|
|
448
|
+
|---|---|
|
|
449
|
+
| Başlarken | https://rewloy.com/gelistiriciler |
|
|
450
|
+
| API referansı | https://rewloy.com/gelistiriciler/api |
|
|
451
|
+
| OpenAPI 3.1 | https://app.rewloy.com/v1/openapi.json |
|
|
452
|
+
| Hata kodları | https://rewloy.com/gelistiriciler/hatalar |
|
|
453
|
+
| API'nin değişiklik günlüğü | https://rewloy.com/gelistiriciler/degisiklikler |
|
|
454
|
+
| Bu kütüphanenin değişiklikleri | [CHANGELOG.md](CHANGELOG.md) |
|
|
455
|
+
|
|
456
|
+
**Sürümler:**
|
|
457
|
+
- Kütüphane anlamsal sürümleme ([SemVer](https://semver.org)) kullanır. 1.0'a
|
|
458
|
+
kadar arayüzü değişebilir.
|
|
459
|
+
- API'ye alan eklemek geriye uyumludur; kütüphanenin tipleri her gün
|
|
460
|
+
güncellenir.
|
|
461
|
+
- Kalkacak bir uç nokta en az 180 gün önce duyurulur ve bu süre boyunca
|
|
462
|
+
`Deprecation` ve `Sunset` başlıklarını taşır.
|
|
463
|
+
|
|
464
|
+
## Güvenlik
|
|
465
|
+
|
|
466
|
+
Bir güvenlik açığı bulursanız [SECURITY.md](SECURITY.md) dosyasındaki yoldan
|
|
467
|
+
özel olarak bildirin. Lütfen herkese açık issue açmayın.
|
|
468
|
+
|
|
469
|
+
## Lisans
|
|
470
|
+
|
|
471
|
+
[MIT](LICENSE)
|
|
472
|
+
|
|
473
|
+
---
|
|
474
|
+
|
|
475
|
+
## English
|
|
476
|
+
|
|
477
|
+
**The official Python library for the Rewloy API.**
|
|
478
|
+
|
|
479
|
+
> **Status: preview (0.x), published on PyPI. The API is stable; the
|
|
480
|
+
> library's interface may change until 1.0.**
|
|
481
|
+
|
|
482
|
+
The documentation of the API itself is in Turkish (links above). In short:
|
|
483
|
+
|
|
484
|
+
- Every operation of the API is a method named by its operationId in
|
|
485
|
+
snake_case (`passAction` is `pass_action`), typed with `TypedDict`s from the
|
|
486
|
+
OpenAPI document, which CI reads daily and regenerates from. `mypy --strict`
|
|
487
|
+
passes.
|
|
488
|
+
- No dependencies: Python 3.9 or later and the standard library (`urllib`,
|
|
489
|
+
`json`, `hmac`). An optional `httpx` transport adds pooling and HTTP/2.
|
|
490
|
+
- Safe retries, `Idempotency-Key` handling, pagination, server-sent events,
|
|
491
|
+
webhook signature verification and deprecation warnings.
|
|
492
|
+
|
|
493
|
+
### Install
|
|
494
|
+
|
|
495
|
+
Python 3.9 or later:
|
|
496
|
+
|
|
497
|
+
```sh
|
|
498
|
+
pip install rewloy
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
### Use
|
|
502
|
+
|
|
503
|
+
```python
|
|
504
|
+
import os
|
|
505
|
+
from rewloy import Rewloy
|
|
506
|
+
|
|
507
|
+
rewloy = Rewloy(api_key=os.environ["REWLOY_API_KEY"]) # or staff_session=…, merchant=… or holder_session=…
|
|
508
|
+
|
|
509
|
+
created = rewloy.issue_pass(body={"programId": program_id, "email": email, "kvkkConsent": True})
|
|
510
|
+
result = rewloy.pass_action(
|
|
511
|
+
created["serial"],
|
|
512
|
+
body={"action": "earn-stamps", "locationId": location_id},
|
|
513
|
+
idempotency_key=f"receipt-{receipt_no}", # generated when omitted, reused across retries
|
|
514
|
+
)
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
- **Arguments.** Path parameters are positional. `query`, `body`, `merchant`,
|
|
518
|
+
`idempotency_key`, `timeout` (seconds) and `max_retries` are keywords. The
|
|
519
|
+
dicts use the API's own key names.
|
|
520
|
+
- **Results.** A method returns the answer's `data`: a `TypedDict` or list, a
|
|
521
|
+
`Page` (`.data`, `.meta`) for paged lists, `None` for 204, `bytes` for files.
|
|
522
|
+
Types are in `rewloy.types` (`from rewloy.types import IssuePassBody`).
|
|
523
|
+
- **The whole answer.** `rewloy.request("sendCampaign", body=…)` returns
|
|
524
|
+
`status`, `headers`, `request_id`, `mode` (the `Rewloy-Mode` header, for the
|
|
525
|
+
coming test mode) and `replayed` (`Idempotent-Replayed`) with `data`.
|
|
526
|
+
- **Pagination.** `rewloy.paginate("listCustomers", query=…)` iterates the
|
|
527
|
+
items of every page, lazily.
|
|
528
|
+
- **Streams.** `with rewloy.live_feed() as stream: for event in stream: …`
|
|
529
|
+
iterates server-sent events (`event`, `data`, `id`, `json()`). It reconnects
|
|
530
|
+
with `Last-Event-ID` unless `reconnect=False`; `close()` works from another
|
|
531
|
+
thread.
|
|
532
|
+
- **Threads and asyncio.** The client is thread-safe. It is synchronous:
|
|
533
|
+
in async code use `await asyncio.to_thread(rewloy.get_pass, serial)`.
|
|
534
|
+
- **Testing.** `transport=` takes anything with `send()`, `open_stream()` and
|
|
535
|
+
`close()`; the README above has a fake.
|
|
536
|
+
|
|
537
|
+
### Webhooks
|
|
538
|
+
|
|
539
|
+
Verify the **raw** body (`request.get_data()` in Flask, `await request.body()`
|
|
540
|
+
in FastAPI, `request.body` in Django) with the secret shown when the webhook
|
|
541
|
+
was created:
|
|
542
|
+
|
|
543
|
+
```python
|
|
544
|
+
event = verify_webhook(raw_body, headers.get("Rewloy-Signature"), secret)
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
- **Check.** `Rewloy-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256(secret,
|
|
548
|
+
"<t>.<raw body>")>` is compared with `hmac.compare_digest`, and `t` must be
|
|
549
|
+
within 300 seconds.
|
|
550
|
+
- **Refusal.** On failure it raises `WebhookSignatureError`: answer 400.
|
|
551
|
+
- **Headers.** `Rewloy-Event` is the event type. `Rewloy-Delivery` is the
|
|
552
|
+
same on every retry of a delivery: deduplicate on it. Delivery is at least
|
|
553
|
+
once.
|
|
554
|
+
|
|
555
|
+
### Errors, retries, deprecations
|
|
556
|
+
|
|
557
|
+
- **Errors.** Failures raise `RewloyError` with `status`, `code` (the API's
|
|
558
|
+
stable code), `title`, `detail`, `details`, `request_id` and `body`.
|
|
559
|
+
Subclasses: `RateLimitError` (`retry_after`), `RewloyConnectionError` and
|
|
560
|
+
`RewloyTimeoutError`. Misuse raises `ValueError` or `TypeError`.
|
|
561
|
+
- **What is retried.** Network errors, timeouts, 429, 502–504 and
|
|
562
|
+
Cloudflare's 520–524, up to `max_retries` (default 2), with exponential
|
|
563
|
+
backoff and jitter, honouring `Retry-After`. The timeout covers a whole
|
|
564
|
+
attempt, body included.
|
|
565
|
+
- **Only when safe.** Only GET, PUT, DELETE, and POST with an
|
|
566
|
+
`Idempotency-Key`, are retried.
|
|
567
|
+
- **Deprecations.** A deprecated operation's answers carry `Deprecation`,
|
|
568
|
+
`Sunset` and `Link`. The client issues one `DeprecationWarning` per
|
|
569
|
+
operation, attributed to your calling line. Under `-W error` the call still
|
|
570
|
+
returns and the notice goes to the `rewloy` logger.
|
|
571
|
+
|
|
572
|
+
### Security and licence
|
|
573
|
+
|
|
574
|
+
Report vulnerabilities privately, as [SECURITY.md](SECURITY.md) says.
|
|
575
|
+
[MIT](LICENSE) licensed.
|
|
576
|
+
|
|
577
|
+
## Yeni sürüm yayımlamak / Releasing
|
|
578
|
+
|
|
579
|
+
`src/rewloy/_version.py`'deki sürümü ve CHANGELOG'u güncelleyin, commit'leyin, `v<sürüm>` etiketini gönderin. `release.yml` PyPI'a güvenilir yayıncı (trusted publishing) yoluyla, jetonsuz yayımlar.
|
|
580
|
+
|
|
581
|
+
Bump the version in `src/rewloy/_version.py` and the changelog, commit, and push a `v<version>` tag. `release.yml` publishes to PyPI through trusted publishing, with no token.
|