pushizo 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pushizo/__init__.py ADDED
@@ -0,0 +1,63 @@
1
+ """Pushizo Python sunucu SDK'sı.
2
+
3
+ .. code-block:: python
4
+
5
+ from pushizo import Pushizo
6
+
7
+ hs = Pushizo(api_key=os.environ["HS_API_KEY"], app_id="app_…")
8
+ hs.notifications.create({"target": {"include_aliases": {"external_id": ["u1"]}}, "contents": {"en": "Shipped"}})
9
+ """
10
+
11
+ from ._client import DEFAULT_BASE_URL, Pushizo
12
+ from ._errors import (
13
+ AuthenticationError,
14
+ ConflictError,
15
+ ErrorCode,
16
+ PushizoError,
17
+ InvalidRequestError,
18
+ NetworkError,
19
+ NotFoundError,
20
+ PermissionDeniedError,
21
+ RateLimitError,
22
+ ServerError,
23
+ WebhookSignatureError,
24
+ )
25
+ from ._generated import API_VERSION
26
+ from ._identity import identity_token
27
+ from ._types import FileInput, RequestOptions
28
+ from ._version import __version__
29
+ from ._webhooks import (
30
+ WEBHOOK_DEFAULT_TOLERANCE_SECONDS,
31
+ WEBHOOK_EVENT_ID_HEADER,
32
+ WEBHOOK_SIGNATURE_HEADER,
33
+ WebhookVerifyResult,
34
+ construct_webhook_events,
35
+ verify_webhook_signature,
36
+ )
37
+
38
+ __all__ = [
39
+ "identity_token",
40
+ "API_VERSION",
41
+ "DEFAULT_BASE_URL",
42
+ "WEBHOOK_DEFAULT_TOLERANCE_SECONDS",
43
+ "WEBHOOK_EVENT_ID_HEADER",
44
+ "WEBHOOK_SIGNATURE_HEADER",
45
+ "AuthenticationError",
46
+ "ConflictError",
47
+ "ErrorCode",
48
+ "FileInput",
49
+ "Pushizo",
50
+ "PushizoError",
51
+ "InvalidRequestError",
52
+ "NetworkError",
53
+ "NotFoundError",
54
+ "PermissionDeniedError",
55
+ "RateLimitError",
56
+ "RequestOptions",
57
+ "ServerError",
58
+ "WebhookSignatureError",
59
+ "WebhookVerifyResult",
60
+ "__version__",
61
+ "construct_webhook_events",
62
+ "verify_webhook_signature",
63
+ ]
pushizo/_client.py ADDED
@@ -0,0 +1,367 @@
1
+ """Pushizo HTTP istemcisi: kimlik doğrulama, yeniden deneme, idempotency ve sayfalama."""
2
+
3
+ import hashlib
4
+ import hmac
5
+ import json
6
+ import mimetypes
7
+ import os
8
+ import time
9
+ import uuid
10
+ from email.utils import parsedate_to_datetime
11
+ from typing import Any, Callable, Dict, Iterator, List, Mapping, Optional, Tuple
12
+ from urllib.parse import quote
13
+
14
+ import httpx
15
+
16
+ from ._errors import PushizoError, NetworkError, error_class_for_status
17
+ from ._generated.resources import GeneratedResources
18
+ from ._identity import identity_token
19
+ from ._types import FileInput, RequestOptions
20
+ from ._version import __version__
21
+
22
+ DEFAULT_BASE_URL = "https://api.pushizo.com"
23
+ DEFAULT_TIMEOUT = 30.0
24
+ DEFAULT_MAX_RETRIES = 3
25
+ _BACKOFF_SECONDS = (0.3, 1.0, 3.0)
26
+
27
+
28
+ class Pushizo(GeneratedResources):
29
+ """Pushizo sunucu SDK istemcisi.
30
+
31
+ .. code-block:: python
32
+
33
+ from pushizo import Pushizo
34
+
35
+ hs = Pushizo(api_key=os.environ["HS_API_KEY"], app_id="app_…")
36
+ hs.notifications.create(
37
+ {"target": {"include_aliases": {"external_id": [user_id]}}, "contents": {"en": "Shipped"}},
38
+ idempotency_key=f"order-{order_id}",
39
+ )
40
+
41
+ Kaynaklar (``notifications``, ``users``, ``segments``, ``templates``, ``campaigns`` …; tam liste
42
+ ``pushizo._generated.resources.RESOURCE_NAMES``) ``api/openapi.yaml``'dan üretilir.
43
+ """
44
+
45
+ def __init__(
46
+ self,
47
+ api_key: str,
48
+ *,
49
+ app_id: Optional[str] = None,
50
+ base_url: str = DEFAULT_BASE_URL,
51
+ timeout: float = DEFAULT_TIMEOUT,
52
+ max_retries: int = DEFAULT_MAX_RETRIES,
53
+ auto_idempotency_key: bool = True,
54
+ user_agent: Optional[str] = None,
55
+ http_client: Optional[httpx.Client] = None,
56
+ sleep: Callable[[float], None] = time.sleep,
57
+ ) -> None:
58
+ """
59
+ :param api_key: ``hs_live_…`` API anahtarı (Bearer).
60
+ :param app_id: Çağrılarda ``app_id`` verilmezse kullanılacak varsayılan app.
61
+ :param base_url: API adresi; yerel geliştirmede ``http://localhost:4000``.
62
+ :param timeout: İstek zaman aşımı (saniye).
63
+ :param max_retries: 429/5xx ve ağ hatalarında en fazla yeniden deneme.
64
+ :param auto_idempotency_key: ``Idempotency-Key`` destekleyen çağrılarda anahtar verilmezse UUID üret.
65
+ :param http_client: Özel ``httpx.Client`` (proxy, TLS, test transport'u). Verilirse kapatılması çağırana aittir.
66
+ :param sleep: Yeniden deneme beklemesi (testlerde değiştirilebilir).
67
+ """
68
+ if not api_key:
69
+ raise ValueError("Pushizo: api_key gerekli.")
70
+ self.api_key = api_key
71
+ self.app_id = app_id
72
+ self.base_url = base_url.rstrip("/")
73
+ self.timeout = timeout
74
+ self.max_retries = max_retries
75
+ self.auto_idempotency_key = auto_idempotency_key
76
+ self.user_agent = user_agent or f"pushizo-python/{__version__}"
77
+ self._owns_http = http_client is None
78
+ self._http = http_client if http_client is not None else httpx.Client()
79
+ self._sleep = sleep
80
+ self._init_resources()
81
+
82
+ # -- yaşam döngüsü -------------------------------------------------------------------------
83
+
84
+ def close(self) -> None:
85
+ """İstemcinin kendi oluşturduğu bağlantı havuzunu kapatır."""
86
+ if self._owns_http:
87
+ self._http.close()
88
+
89
+ def __enter__(self) -> "Pushizo":
90
+ return self
91
+
92
+ def __exit__(self, *_exc: Any) -> None:
93
+ self.close()
94
+
95
+ # -- yardımcılar ---------------------------------------------------------------------------
96
+
97
+ @staticmethod
98
+ def identity_hash(sdk_secret: str, external_id: str) -> str:
99
+ """SDK identity verification için ``identity_hash = HMAC-SHA256(sdk_secret, external_id)`` (hex)."""
100
+ return hmac.new(sdk_secret.encode("utf-8"), external_id.encode("utf-8"), hashlib.sha256).hexdigest()
101
+
102
+ @staticmethod
103
+ def identity_token(private_key_pem: str, kid: str, app_id: str, external_id: str, expires_in: int = 3600) -> str:
104
+ """SDK JWT identity verification belirteci (ES256); bkz. :func:`pushizo.identity_token`."""
105
+ return identity_token(private_key_pem, kid, app_id, external_id, expires_in)
106
+
107
+ def request(
108
+ self,
109
+ method: str,
110
+ path: str,
111
+ *,
112
+ query: Optional[Mapping[str, Any]] = None,
113
+ body: Any = None,
114
+ request_options: Optional[RequestOptions] = None,
115
+ ) -> Any:
116
+ """Düşük seviyeli istek (SDK'da henüz metodu olmayan uçlar veya ``format=csv`` gibi JSON dışı yanıtlar için).
117
+
118
+ JSON yanıtlar ayrıştırılır, diğer içerik tipleri metin olarak döner.
119
+ """
120
+ return self._send(method, path, list(_query_items(query or {})), body, None, None, None, request_options)
121
+
122
+ # -- üretilmiş kaynakların kullandığı iç API -----------------------------------------------
123
+
124
+ def _resolve_app_id(self, app_id: Optional[str]) -> str:
125
+ resolved = app_id or self.app_id
126
+ if not resolved:
127
+ raise ValueError("Pushizo: app_id verilmedi (istemci seçeneği veya çağrı parametresi).")
128
+ return resolved
129
+
130
+ def _call(
131
+ self,
132
+ method: str,
133
+ path: str,
134
+ *,
135
+ path_params: Mapping[str, str],
136
+ query: Optional[Mapping[str, Any]] = None,
137
+ body: Any = None,
138
+ form: Optional[Mapping[str, Any]] = None,
139
+ files: Optional[Mapping[str, Optional[FileInput]]] = None,
140
+ app_id: Optional[str] = None,
141
+ query_app_id: bool = False,
142
+ body_app_id: bool = False,
143
+ idempotent: bool = False,
144
+ idempotency_key: Optional[str] = None,
145
+ request_options: Optional[RequestOptions] = None,
146
+ ) -> Any:
147
+ url_path = path
148
+ if "{appId}" in url_path:
149
+ url_path = url_path.replace("{appId}", quote(self._resolve_app_id(app_id), safe=""))
150
+ for name, value in path_params.items():
151
+ if value is None or value == "":
152
+ raise ValueError(f"Pushizo: '{name}' yol parametresi boş olamaz.")
153
+ url_path = url_path.replace("{" + name + "}", quote(str(value), safe=""))
154
+
155
+ items: List[Tuple[str, str]] = []
156
+ if query_app_id:
157
+ items.append(("app_id", self._resolve_app_id(app_id)))
158
+ items.extend(_query_items(query or {}))
159
+
160
+ if body_app_id and isinstance(body, Mapping) and not body.get("app_id"):
161
+ body = {**body, "app_id": self._resolve_app_id(app_id)}
162
+
163
+ key = idempotency_key or (request_options or {}).get("idempotency_key")
164
+ if not key and isinstance(body, Mapping) and isinstance(body.get("idempotency_key"), str):
165
+ key = body["idempotency_key"]
166
+ if not key and idempotent and self.auto_idempotency_key:
167
+ key = str(uuid.uuid4())
168
+
169
+ return self._send(method, url_path, items, body, form, files, key, request_options)
170
+
171
+ def _paginate(self, fetch: Callable[[Optional[str]], Any], cursor: Optional[str]) -> Iterator[Any]:
172
+ """``data`` / ``next_cursor`` / ``has_more`` zarfını sayfa sayfa dolaşır."""
173
+
174
+ def generator() -> Iterator[Any]:
175
+ next_cursor = cursor
176
+ while True:
177
+ page = fetch(next_cursor) or {}
178
+ for item in page.get("data") or []:
179
+ yield item
180
+ next_cursor = page.get("next_cursor")
181
+ if not page.get("has_more") or not next_cursor:
182
+ return
183
+
184
+ return generator()
185
+
186
+ # -- HTTP ----------------------------------------------------------------------------------
187
+
188
+ def _send(
189
+ self,
190
+ method: str,
191
+ path: str,
192
+ query: List[Tuple[str, str]],
193
+ body: Any,
194
+ form: Optional[Mapping[str, Any]],
195
+ files: Optional[Mapping[str, Optional[FileInput]]],
196
+ idempotency_key: Optional[str],
197
+ request_options: Optional[RequestOptions],
198
+ ) -> Any:
199
+ opts: RequestOptions = request_options or {}
200
+ headers: Dict[str, str] = {
201
+ "Authorization": f"Bearer {self.api_key}",
202
+ "Accept": "application/json",
203
+ "User-Agent": self.user_agent,
204
+ }
205
+ content: Optional[bytes] = None
206
+ data: Optional[Dict[str, str]] = None
207
+ upload: Optional[Dict[str, Tuple[str, bytes, str]]] = None
208
+ if files is not None:
209
+ data = {k: _form_value(v) for k, v in (form or {}).items() if v is not None}
210
+ upload = {k: _normalize_file(v) for k, v in files.items() if v is not None}
211
+ elif body is not None:
212
+ headers["Content-Type"] = "application/json"
213
+ content = json.dumps(body, ensure_ascii=False, separators=(",", ":")).encode("utf-8")
214
+ if idempotency_key:
215
+ headers["Idempotency-Key"] = idempotency_key
216
+ headers.update(opts.get("headers") or {})
217
+
218
+ url = f"{self.base_url}{path}"
219
+ timeout = opts.get("timeout", self.timeout)
220
+ max_retries = opts.get("max_retries", self.max_retries)
221
+ # Gövdedeki idempotency_key, API tarafında Idempotency-Key başlığıyla eşdeğerdir.
222
+ has_key = any(k.lower() == "idempotency-key" and v for k, v in headers.items()) or (
223
+ isinstance(body, Mapping) and isinstance(body.get("idempotency_key"), str) and bool(body["idempotency_key"])
224
+ )
225
+ attempt = 0
226
+ while True:
227
+ response: Optional[httpx.Response] = None
228
+ not_sent = False
229
+ try:
230
+ response = self._http.request(
231
+ method,
232
+ url,
233
+ params=query or None,
234
+ headers=headers,
235
+ content=content,
236
+ data=data,
237
+ files=upload,
238
+ timeout=timeout,
239
+ )
240
+ except httpx.TransportError as exc:
241
+ error = NetworkError(0, "NETWORK", str(exc) or exc.__class__.__name__)
242
+ not_sent = isinstance(exc, _NOT_SENT_ERRORS)
243
+ else:
244
+ if 200 <= response.status_code < 300:
245
+ return _parse_body(response)
246
+ error = _error_from_response(response)
247
+ if attempt >= max_retries or not _should_retry(method, error, has_key, not_sent):
248
+ raise error
249
+ self._sleep(_retry_delay(response, attempt))
250
+ attempt += 1
251
+
252
+
253
+ #: Sunucuda tekrarlanması ek yan etki doğurmayan HTTP metotları; 5xx ve ağ hatalarında serbestçe yeniden denenir.
254
+ _IDEMPOTENT_METHODS = frozenset({"GET", "HEAD", "OPTIONS", "PUT", "DELETE"})
255
+
256
+ #: İstek sunucuya hiç ulaşmadan oluşan aktarım hataları: bağlantı reddi / DNS çözümlenemedi (``ConnectError``),
257
+ #: bağlantı kurma ve bağlantı havuzu zaman aşımı.
258
+ _NOT_SENT_ERRORS = (httpx.ConnectError, httpx.ConnectTimeout, httpx.PoolTimeout)
259
+
260
+
261
+ def _should_retry(method: str, error: PushizoError, has_idempotency_key: bool, not_sent: bool) -> bool:
262
+ """Yeniden deneme politikası.
263
+
264
+ - 429: her metotta yeniden denenir (istek işlenmeden reddedilmiştir; ``Retry-After``'a uyulur).
265
+ - 5xx / ağ hatası: GET, HEAD, OPTIONS, PUT, DELETE her zaman; POST ve PATCH yalnızca ``Idempotency-Key``
266
+ taşıyorsa (otomatik anahtar dahil) veya istek sunucuya hiç gönderilmeden başarısız olduysa (bağlantı reddi,
267
+ DNS hatası, bağlantı zaman aşımı). Aksi hâlde sunucu isteği işlemiş olabilir; tekrar, çift kayıt üretebilir.
268
+ """
269
+ if error.status == 429:
270
+ return True
271
+ if not error.retryable:
272
+ return False
273
+ return method.upper() in _IDEMPOTENT_METHODS or has_idempotency_key or not_sent
274
+
275
+
276
+ def _query_items(query: Mapping[str, Any]) -> Iterator[Tuple[str, str]]:
277
+ for key, value in query.items():
278
+ if value is None:
279
+ continue
280
+ if isinstance(value, (list, tuple)):
281
+ for v in value:
282
+ if v is not None:
283
+ yield key, _scalar(v)
284
+ else:
285
+ yield key, _scalar(value)
286
+
287
+
288
+ def _scalar(value: Any) -> str:
289
+ if isinstance(value, bool):
290
+ return "true" if value else "false"
291
+ if isinstance(value, (str, int, float)):
292
+ return str(value)
293
+ return json.dumps(value, ensure_ascii=False, separators=(",", ":"))
294
+
295
+
296
+ def _form_value(value: Any) -> str:
297
+ return _scalar(value)
298
+
299
+
300
+ def _normalize_file(value: FileInput) -> Tuple[str, bytes, str]:
301
+ """Dosya girdisini (ad, bayt, içerik tipi) üçlüsüne çevirir; akış bir kez okunur ki yeniden denemeler aynı içeriği göndersin."""
302
+ filename: Optional[str] = None
303
+ content_type: Optional[str] = None
304
+ raw: Any = value
305
+ if isinstance(value, tuple):
306
+ filename = value[0]
307
+ raw = value[1]
308
+ if len(value) > 2:
309
+ content_type = value[2] # type: ignore[misc]
310
+ if not isinstance(raw, (bytes, bytearray)):
311
+ if filename is None:
312
+ name = getattr(raw, "name", None)
313
+ if isinstance(name, str):
314
+ filename = os.path.basename(name)
315
+ raw = raw.read()
316
+ if filename is None:
317
+ filename = "upload"
318
+ if content_type is None:
319
+ content_type = mimetypes.guess_type(filename)[0] or "application/octet-stream"
320
+ return filename, bytes(raw), content_type
321
+
322
+
323
+ def _parse_body(response: httpx.Response) -> Any:
324
+ if not response.content:
325
+ return None
326
+ content_type = response.headers.get("content-type", "")
327
+ if "json" in content_type or not content_type:
328
+ return response.json()
329
+ return response.text
330
+
331
+
332
+ def _error_from_response(response: httpx.Response) -> PushizoError:
333
+ envelope: Dict[str, Any] = {}
334
+ try:
335
+ parsed = response.json()
336
+ if isinstance(parsed, dict) and isinstance(parsed.get("error"), dict):
337
+ envelope = parsed["error"]
338
+ except ValueError:
339
+ envelope = {}
340
+ details = envelope.get("details")
341
+ return error_class_for_status(response.status_code)(
342
+ response.status_code,
343
+ str(envelope.get("code") or f"HTTP_{response.status_code}"),
344
+ str(envelope.get("message") or f"HTTP {response.status_code}"),
345
+ envelope.get("request_id") or response.headers.get("x-request-id"),
346
+ details if isinstance(details, dict) else None,
347
+ )
348
+
349
+
350
+ def _retry_delay(response: Optional[httpx.Response], attempt: int) -> float:
351
+ """``Retry-After`` (saniye veya HTTP tarihi) varsa ona uyar; yoksa 0.3 / 1 / 3 sn geri çekilme."""
352
+ if response is not None:
353
+ header = response.headers.get("retry-after")
354
+ if header:
355
+ header = header.strip()
356
+ try:
357
+ seconds = float(header)
358
+ if seconds > 0:
359
+ return seconds
360
+ except ValueError:
361
+ try:
362
+ delta = parsedate_to_datetime(header).timestamp() - time.time()
363
+ if delta > 0:
364
+ return delta
365
+ except (TypeError, ValueError):
366
+ pass
367
+ return _BACKOFF_SECONDS[min(attempt, len(_BACKOFF_SECONDS) - 1)]
pushizo/_errors.py ADDED
@@ -0,0 +1,130 @@
1
+ """Pushizo istisnaları ve hata kodları."""
2
+
3
+ from typing import Any, Dict, Optional, Type
4
+
5
+
6
+ class ErrorCode:
7
+ """API hata zarfındaki ``error.code`` değerleri (``HS_`` önekli katalog)."""
8
+
9
+ VALIDATION_ERROR = "HS_VALIDATION_ERROR"
10
+ BAD_REQUEST = "HS_BAD_REQUEST"
11
+ UNAUTHENTICATED = "HS_UNAUTHENTICATED"
12
+ FORBIDDEN = "HS_FORBIDDEN"
13
+ TENANT_MISMATCH = "HS_TENANT_MISMATCH"
14
+ NOT_FOUND = "HS_NOT_FOUND"
15
+ CONFLICT = "HS_CONFLICT"
16
+ IDEMPOTENCY_CONFLICT = "HS_IDEMPOTENCY_CONFLICT"
17
+ IDEMPOTENCY_IN_PROGRESS = "HS_IDEMPOTENCY_IN_PROGRESS"
18
+ NO_RECIPIENTS = "HS_NO_RECIPIENTS"
19
+ CHANNEL_NOT_CONFIGURED = "HS_CHANNEL_NOT_CONFIGURED"
20
+ CONTENT_INVALID = "HS_CONTENT_INVALID"
21
+ SUBSCRIPTION_INVALID = "HS_SUBSCRIPTION_INVALID"
22
+ UNPROCESSABLE = "HS_UNPROCESSABLE"
23
+ RATE_LIMITED = "HS_RATE_LIMITED"
24
+ QUOTA_EXCEEDED = "HS_QUOTA_EXCEEDED"
25
+ INTERNAL = "HS_INTERNAL"
26
+ NOT_IMPLEMENTED = "HS_NOT_IMPLEMENTED"
27
+ PROVIDER_UNAVAILABLE = "HS_PROVIDER_UNAVAILABLE"
28
+ SERVICE_UNAVAILABLE = "HS_SERVICE_UNAVAILABLE"
29
+ #: İstemci tarafı: bağlantı kurulamadı / zaman aşımı (``status`` 0).
30
+ NETWORK = "NETWORK"
31
+
32
+
33
+ class PushizoError(Exception):
34
+ """API hata zarfından (``{"error": {"code", "message", "request_id", "details"}}``) türetilen istisna.
35
+
36
+ Ağ hatalarında ``status`` 0 ve ``code`` ``"NETWORK"`` olur. HTTP durumuna göre alt sınıflar
37
+ (:class:`NotFoundError`, :class:`RateLimitError` …) fırlatılır; hepsi bu sınıftan türer.
38
+ """
39
+
40
+ def __init__(
41
+ self,
42
+ status: int,
43
+ code: str,
44
+ message: str,
45
+ request_id: Optional[str] = None,
46
+ details: Optional[Dict[str, Any]] = None,
47
+ ) -> None:
48
+ super().__init__(message)
49
+ self.status = status
50
+ self.code = code
51
+ self.message = message
52
+ self.request_id = request_id
53
+ self.details = details
54
+
55
+ @property
56
+ def retryable(self) -> bool:
57
+ """429, 5xx veya ağ hatası: yeniden denenebilir."""
58
+ return self.status == 0 or self.status == 429 or self.status >= 500
59
+
60
+ def __str__(self) -> str:
61
+ suffix = f" (request_id={self.request_id})" if self.request_id else ""
62
+ return f"[{self.status} {self.code}] {self.message}{suffix}"
63
+
64
+ def __repr__(self) -> str:
65
+ return (
66
+ f"{type(self).__name__}(status={self.status!r}, code={self.code!r}, "
67
+ f"message={self.message!r}, request_id={self.request_id!r})"
68
+ )
69
+
70
+
71
+ class NetworkError(PushizoError):
72
+ """Bağlantı kurulamadı veya zaman aşımı (``status`` 0)."""
73
+
74
+
75
+ class InvalidRequestError(PushizoError):
76
+ """400 / 422: istek doğrulanamadı veya işlenemedi (``details`` alan hatalarını içerir)."""
77
+
78
+
79
+ class AuthenticationError(PushizoError):
80
+ """401: API anahtarı eksik veya geçersiz."""
81
+
82
+
83
+ class PermissionDeniedError(PushizoError):
84
+ """403: anahtarın kapsamı yetersiz, IP izin listesi dışı veya kaynak başka kiracıya ait."""
85
+
86
+
87
+ class NotFoundError(PushizoError):
88
+ """404: kaynak bulunamadı."""
89
+
90
+
91
+ class ConflictError(PushizoError):
92
+ """409: kaynak çakışması veya idempotency anahtarı çakışması."""
93
+
94
+
95
+ class RateLimitError(PushizoError):
96
+ """429: hız sınırı veya plan kotası aşıldı."""
97
+
98
+
99
+ class ServerError(PushizoError):
100
+ """5xx: sunucu veya sağlayıcı geçici olarak kullanılamıyor."""
101
+
102
+
103
+ def error_class_for_status(status: int) -> Type[PushizoError]:
104
+ """HTTP durumuna karşılık gelen istisna sınıfı."""
105
+ if status == 0:
106
+ return NetworkError
107
+ if status in (400, 422):
108
+ return InvalidRequestError
109
+ if status == 401:
110
+ return AuthenticationError
111
+ if status == 403:
112
+ return PermissionDeniedError
113
+ if status == 404:
114
+ return NotFoundError
115
+ if status == 409:
116
+ return ConflictError
117
+ if status == 429:
118
+ return RateLimitError
119
+ if status >= 500:
120
+ return ServerError
121
+ return PushizoError
122
+
123
+
124
+ class WebhookSignatureError(Exception):
125
+ """Webhook imzası geçersiz. ``reason``: ``missing_header``, ``malformed_header``,
126
+ ``timestamp_out_of_tolerance``, ``signature_mismatch`` veya ``malformed_body``."""
127
+
128
+ def __init__(self, reason: str) -> None:
129
+ super().__init__(f"Pushizo webhook imzası geçersiz: {reason}")
130
+ self.reason = reason
@@ -0,0 +1,4 @@
1
+ # Bu dosya api/openapi.yaml'dan sdk/scripts/generate-server-sdks.mjs ile üretilmiştir; elle düzenlemeyin.
2
+ """OpenAPI'den üretilen modeller ve kaynaklar."""
3
+
4
+ API_VERSION = "1.0.0"