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.
Files changed (40) hide show
  1. rewloy-0.1.0/.gitignore +16 -0
  2. rewloy-0.1.0/CHANGELOG.md +43 -0
  3. rewloy-0.1.0/LICENSE +21 -0
  4. rewloy-0.1.0/PKG-INFO +581 -0
  5. rewloy-0.1.0/README.md +547 -0
  6. rewloy-0.1.0/SECURITY.md +13 -0
  7. rewloy-0.1.0/docs/DECISIONS.md +285 -0
  8. rewloy-0.1.0/openapi/openapi.json +92531 -0
  9. rewloy-0.1.0/pyproject.toml +60 -0
  10. rewloy-0.1.0/scripts/generate.py +94 -0
  11. rewloy-0.1.0/scripts/generator.py +894 -0
  12. rewloy-0.1.0/src/rewloy/__init__.py +38 -0
  13. rewloy-0.1.0/src/rewloy/_version.py +3 -0
  14. rewloy-0.1.0/src/rewloy/client.py +648 -0
  15. rewloy-0.1.0/src/rewloy/common.py +116 -0
  16. rewloy-0.1.0/src/rewloy/errors.py +115 -0
  17. rewloy-0.1.0/src/rewloy/generated/__init__.py +4 -0
  18. rewloy-0.1.0/src/rewloy/generated/methods.py +7461 -0
  19. rewloy-0.1.0/src/rewloy/generated/operations.py +385 -0
  20. rewloy-0.1.0/src/rewloy/generated/types.py +10296 -0
  21. rewloy-0.1.0/src/rewloy/httpx_transport.py +126 -0
  22. rewloy-0.1.0/src/rewloy/py.typed +0 -0
  23. rewloy-0.1.0/src/rewloy/sse.py +322 -0
  24. rewloy-0.1.0/src/rewloy/transport.py +215 -0
  25. rewloy-0.1.0/src/rewloy/types.py +9 -0
  26. rewloy-0.1.0/src/rewloy/webhooks.py +192 -0
  27. rewloy-0.1.0/tests/__init__.py +0 -0
  28. rewloy-0.1.0/tests/conftest.py +32 -0
  29. rewloy-0.1.0/tests/helpers.py +220 -0
  30. rewloy-0.1.0/tests/test_client.py +325 -0
  31. rewloy-0.1.0/tests/test_compat.py +69 -0
  32. rewloy-0.1.0/tests/test_deprecation.py +101 -0
  33. rewloy-0.1.0/tests/test_errors.py +115 -0
  34. rewloy-0.1.0/tests/test_generate.py +248 -0
  35. rewloy-0.1.0/tests/test_methods.py +113 -0
  36. rewloy-0.1.0/tests/test_paginate.py +90 -0
  37. rewloy-0.1.0/tests/test_retry.py +262 -0
  38. rewloy-0.1.0/tests/test_sse.py +306 -0
  39. rewloy-0.1.0/tests/test_transport.py +177 -0
  40. rewloy-0.1.0/tests/test_webhooks.py +157 -0
@@ -0,0 +1,16 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.py[cod]
4
+ dist/
5
+ build/
6
+ *.egg-info/
7
+ .mypy_cache/
8
+ .pytest_cache/
9
+ .coverage
10
+ htmlcov/
11
+ .DS_Store
12
+ .env
13
+ .env.*
14
+ .idea/
15
+ .vscode/
16
+ *.log
@@ -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.