posthubify 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.
@@ -0,0 +1,100 @@
1
+ Metadata-Version: 2.4
2
+ Name: posthubify
3
+ Version: 0.1.0
4
+ Summary: PostHubify resmî Python SDK'sı — sosyal medya yayını, gelen kutusu, kişiler, reklam ve telekom (SMS/OTP) API'si.
5
+ Author: PostHubify
6
+ License: MIT
7
+ Project-URL: Homepage, https://posthubify.com
8
+ Project-URL: Documentation, https://docs.posthubify.com
9
+ Keywords: posthubify,social-media,api,sdk,ads,sms,otp
10
+ Requires-Python: >=3.8
11
+ Description-Content-Type: text/markdown
12
+ Provides-Extra: dev
13
+ Requires-Dist: pytest>=7.0; extra == "dev"
14
+
15
+ # PostHubify Python SDK
16
+
17
+ PostHubify `/v1` API'sinin resmî Python istemcisi — sosyal medya yayını, gelen kutusu,
18
+ kişiler, analitik, reklam (ads) ve telekom (SMS/OTP) için tek paket.
19
+
20
+ - **Sıfır runtime bağımlılığı** — yalnızca Python standart kütüphanesi (`urllib`).
21
+ - **Tam yüzey** — `/v1` spec'inin 190+ operasyonu birebir kapsanır (kontrat testiyle doğrulanır).
22
+ - Python 3.8+
23
+
24
+ ## Kurulum
25
+
26
+ ```bash
27
+ pip install posthubify
28
+ ```
29
+
30
+ ## Hızlı başlangıç
31
+
32
+ ```python
33
+ from posthubify import Posthubify, PosthubifyError
34
+
35
+ ph = Posthubify(api_key="sk_...", base_url="https://api.posthubify.com/v1")
36
+
37
+ # Bağlantı testi
38
+ print(ph.ping()) # {"pong": True, "version": "v1", ...}
39
+
40
+ # Gönderi yayınla
41
+ draft = ph.posts.create({
42
+ "content": "Merhaba dünya!",
43
+ "accountIds": ["acc_123"],
44
+ })
45
+
46
+ # Gelen kutusu analitiği
47
+ vol = ph.inbox_analytics.volume(from_date="2026-06-01", to_date="2026-06-14")
48
+ print(vol["summary"]) # {"received": ..., "sent": ..., ...}
49
+
50
+ # Reklam kampanyası
51
+ camps = ph.ads.campaigns("adacc_1", status="enabled")
52
+
53
+ # Hata yönetimi
54
+ try:
55
+ ph.posts.get("yok")
56
+ except PosthubifyError as e:
57
+ print(e.status, e.message, e.code) # 404 "..." "..."
58
+ ```
59
+
60
+ ## Resource grupları
61
+
62
+ İstemci, Node SDK'sıyla birebir aynı yüzeyi snake_case niteliklerle sunar:
63
+
64
+ `profiles` · `accounts` · `posts` · `media` · `tools` · `ads` · `insights` ·
65
+ `platform_analytics` · `inbox_analytics` · `inbox` · `comments` · `reviews` ·
66
+ `contacts` · `automations` · `broadcasts` · `sequences` · `webhooks` · `api_keys` ·
67
+ `queue` (`.schedules`) · `account_groups` · `engagement` (`.x`) · `users` ·
68
+ `invite_tokens` · `numbers` · `senders` · `sms` · `otp`
69
+
70
+ Üst seviye: `ph.openapi()`, `ph.ping()`, `ph.me()`, `ph.analytics()`,
71
+ `ph.analytics_posts()`, `ph.analytics_timeseries()`, `ph.logs()`, `ph.usage()`.
72
+
73
+ ## Medya yükleme
74
+
75
+ ```python
76
+ with open("logo.png", "rb") as f:
77
+ asset = ph.media.upload(f.read(), filename="logo.png")
78
+ print(asset["url"])
79
+ ```
80
+
81
+ ## Geliştirme
82
+
83
+ ```bash
84
+ cd packages/python-sdk
85
+ python3 -m unittest discover -s tests # kontrat + transport testleri
86
+ ```
87
+
88
+ Kontrat testi (`tests/test_contract.py`) her SDK metodunun ürettiği uç yolunun
89
+ OpenAPI spec'inde (`tests/openapi.json`) tanımlı olduğunu doğrular. Spec fixture'ı
90
+ yeni uç eklendiğinde güncellemek için repo kökünden:
91
+
92
+ ```bash
93
+ npx tsx -e "import {buildOpenApiSpec} from './apps/server/src/openapi.ts'; \
94
+ import {writeFileSync} from 'node:fs'; \
95
+ writeFileSync('packages/python-sdk/tests/openapi.json', JSON.stringify(buildOpenApiSpec(),null,2))"
96
+ ```
97
+
98
+ ## Lisans
99
+
100
+ MIT
@@ -0,0 +1,86 @@
1
+ # PostHubify Python SDK
2
+
3
+ PostHubify `/v1` API'sinin resmî Python istemcisi — sosyal medya yayını, gelen kutusu,
4
+ kişiler, analitik, reklam (ads) ve telekom (SMS/OTP) için tek paket.
5
+
6
+ - **Sıfır runtime bağımlılığı** — yalnızca Python standart kütüphanesi (`urllib`).
7
+ - **Tam yüzey** — `/v1` spec'inin 190+ operasyonu birebir kapsanır (kontrat testiyle doğrulanır).
8
+ - Python 3.8+
9
+
10
+ ## Kurulum
11
+
12
+ ```bash
13
+ pip install posthubify
14
+ ```
15
+
16
+ ## Hızlı başlangıç
17
+
18
+ ```python
19
+ from posthubify import Posthubify, PosthubifyError
20
+
21
+ ph = Posthubify(api_key="sk_...", base_url="https://api.posthubify.com/v1")
22
+
23
+ # Bağlantı testi
24
+ print(ph.ping()) # {"pong": True, "version": "v1", ...}
25
+
26
+ # Gönderi yayınla
27
+ draft = ph.posts.create({
28
+ "content": "Merhaba dünya!",
29
+ "accountIds": ["acc_123"],
30
+ })
31
+
32
+ # Gelen kutusu analitiği
33
+ vol = ph.inbox_analytics.volume(from_date="2026-06-01", to_date="2026-06-14")
34
+ print(vol["summary"]) # {"received": ..., "sent": ..., ...}
35
+
36
+ # Reklam kampanyası
37
+ camps = ph.ads.campaigns("adacc_1", status="enabled")
38
+
39
+ # Hata yönetimi
40
+ try:
41
+ ph.posts.get("yok")
42
+ except PosthubifyError as e:
43
+ print(e.status, e.message, e.code) # 404 "..." "..."
44
+ ```
45
+
46
+ ## Resource grupları
47
+
48
+ İstemci, Node SDK'sıyla birebir aynı yüzeyi snake_case niteliklerle sunar:
49
+
50
+ `profiles` · `accounts` · `posts` · `media` · `tools` · `ads` · `insights` ·
51
+ `platform_analytics` · `inbox_analytics` · `inbox` · `comments` · `reviews` ·
52
+ `contacts` · `automations` · `broadcasts` · `sequences` · `webhooks` · `api_keys` ·
53
+ `queue` (`.schedules`) · `account_groups` · `engagement` (`.x`) · `users` ·
54
+ `invite_tokens` · `numbers` · `senders` · `sms` · `otp`
55
+
56
+ Üst seviye: `ph.openapi()`, `ph.ping()`, `ph.me()`, `ph.analytics()`,
57
+ `ph.analytics_posts()`, `ph.analytics_timeseries()`, `ph.logs()`, `ph.usage()`.
58
+
59
+ ## Medya yükleme
60
+
61
+ ```python
62
+ with open("logo.png", "rb") as f:
63
+ asset = ph.media.upload(f.read(), filename="logo.png")
64
+ print(asset["url"])
65
+ ```
66
+
67
+ ## Geliştirme
68
+
69
+ ```bash
70
+ cd packages/python-sdk
71
+ python3 -m unittest discover -s tests # kontrat + transport testleri
72
+ ```
73
+
74
+ Kontrat testi (`tests/test_contract.py`) her SDK metodunun ürettiği uç yolunun
75
+ OpenAPI spec'inde (`tests/openapi.json`) tanımlı olduğunu doğrular. Spec fixture'ı
76
+ yeni uç eklendiğinde güncellemek için repo kökünden:
77
+
78
+ ```bash
79
+ npx tsx -e "import {buildOpenApiSpec} from './apps/server/src/openapi.ts'; \
80
+ import {writeFileSync} from 'node:fs'; \
81
+ writeFileSync('packages/python-sdk/tests/openapi.json', JSON.stringify(buildOpenApiSpec(),null,2))"
82
+ ```
83
+
84
+ ## Lisans
85
+
86
+ MIT
@@ -0,0 +1,13 @@
1
+ """PostHubify resmî Python SDK'sı.
2
+
3
+ from posthubify import Posthubify, PosthubifyError
4
+
5
+ ph = Posthubify(api_key="sk_...", base_url="https://api.posthubify.com/v1")
6
+ print(ph.ping())
7
+ """
8
+
9
+ from .client import Posthubify
10
+ from .errors import PosthubifyError
11
+
12
+ __version__ = "0.1.0"
13
+ __all__ = ["Posthubify", "PosthubifyError", "__version__"]
@@ -0,0 +1,154 @@
1
+ """HTTP taşıma katmanı — stdlib urllib tabanlı (sıfır runtime bağımlılık).
2
+
3
+ Node @posthubify/node transport deseninin Python aynası: Bearer auth, JSON gövde,
4
+ multipart yükleme, ``{ data }`` zarfı açma, 2xx-dışı → PosthubifyError.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import urllib.error
11
+ import urllib.parse
12
+ import urllib.request
13
+ import uuid
14
+ from typing import Any, Dict, Mapping, Optional, Tuple
15
+
16
+ from .errors import PosthubifyError
17
+
18
+ Query = Optional[Mapping[str, Any]]
19
+
20
+ _DEFAULT_BASE = "http://localhost:8787/v1"
21
+ _USER_AGENT = "posthubify-python/0.1.0"
22
+
23
+
24
+ def _qval(v: object) -> str:
25
+ # bool → JSON-küçük harf ('true'/'false'); Python str(True)='True' backend `=== 'true'` ile eşleşmez.
26
+ if isinstance(v, bool):
27
+ return "true" if v else "false"
28
+ return str(v)
29
+
30
+
31
+ def _encode_query(query: Query) -> str:
32
+ if not query:
33
+ return ""
34
+ pairs = [
35
+ (str(k), _qval(v))
36
+ for k, v in query.items()
37
+ if v is not None
38
+ ]
39
+ return urllib.parse.urlencode(pairs) if pairs else ""
40
+
41
+
42
+ class Transport:
43
+ """Düşük seviye istek motoru. ``req`` ham gövdeyi, ``data`` ``{data}`` zarfını açar."""
44
+
45
+ def __init__(self, api_key: str, base_url: str = _DEFAULT_BASE, timeout: float = 30.0) -> None:
46
+ if not api_key:
47
+ raise ValueError("api_key gerekli (sk_…)")
48
+ self._key = api_key
49
+ self._base = base_url.rstrip("/")
50
+ self._timeout = timeout
51
+
52
+ # -- genel istek --
53
+ def req(
54
+ self,
55
+ method: str,
56
+ path: str,
57
+ *,
58
+ query: Query = None,
59
+ body: Any = None,
60
+ files: Optional[Tuple[str, bytes, str]] = None,
61
+ idempotency_key: Optional[str] = None,
62
+ ) -> Any:
63
+ """İsteği gönderir; 2xx → ayrıştırılmış gövde, değilse PosthubifyError.
64
+
65
+ files: (alan_adı, bytes, dosya_adı) — çok parçalı yükleme (media.upload).
66
+ """
67
+ qs = _encode_query(query)
68
+ url = f"{self._base}{path}" + (f"?{qs}" if qs else "")
69
+ headers: Dict[str, str] = {
70
+ "Authorization": f"Bearer {self._key}",
71
+ "User-Agent": _USER_AGENT,
72
+ "Accept": "application/json",
73
+ }
74
+ if idempotency_key:
75
+ headers["Idempotency-Key"] = idempotency_key
76
+
77
+ data_bytes: Optional[bytes] = None
78
+ if files is not None:
79
+ field, content, filename = files
80
+ boundary = uuid.uuid4().hex
81
+ data_bytes, content_type = _build_multipart(field, content, filename, boundary)
82
+ headers["Content-Type"] = content_type
83
+ elif body is not None:
84
+ data_bytes = json.dumps(body).encode("utf-8")
85
+ headers["Content-Type"] = "application/json"
86
+
87
+ request = urllib.request.Request(url, data=data_bytes, headers=headers, method=method)
88
+ try:
89
+ with urllib.request.urlopen(request, timeout=self._timeout) as resp:
90
+ raw = resp.read()
91
+ return _parse(raw)
92
+ except urllib.error.HTTPError as exc: # 4xx/5xx
93
+ raw = exc.read()
94
+ parsed = _parse(raw)
95
+ message = None
96
+ code = None
97
+ if isinstance(parsed, dict):
98
+ message = parsed.get("error")
99
+ code = parsed.get("code")
100
+ raise PosthubifyError(
101
+ exc.code,
102
+ message or f"HTTP {exc.code}",
103
+ code,
104
+ parsed,
105
+ ) from None
106
+ except urllib.error.URLError as exc: # ağ/bağlantı
107
+ raise PosthubifyError(0, f"Ağ hatası: {exc.reason}") from None
108
+
109
+ def data(
110
+ self,
111
+ method: str,
112
+ path: str,
113
+ *,
114
+ query: Query = None,
115
+ body: Any = None,
116
+ files: Optional[Tuple[str, bytes, str]] = None,
117
+ idempotency_key: Optional[str] = None,
118
+ ) -> Any:
119
+ """``{ data: ... }`` zarfını açar."""
120
+ result = self.req(
121
+ method, path, query=query, body=body, files=files, idempotency_key=idempotency_key
122
+ )
123
+ if isinstance(result, dict) and "data" in result:
124
+ return result["data"]
125
+ return result
126
+
127
+
128
+ def _parse(raw: bytes) -> Any:
129
+ if not raw:
130
+ return None
131
+ try:
132
+ return json.loads(raw.decode("utf-8"))
133
+ except (ValueError, UnicodeDecodeError):
134
+ return None
135
+
136
+
137
+ _MIME_BY_EXT = {"png": "image/png", "jpg": "image/jpeg", "jpeg": "image/jpeg", "mp4": "video/mp4"}
138
+
139
+
140
+ def _mime_for(filename: str) -> str:
141
+ # Sistem-bağımsız sabit eşleme (diğer SDK'larla parite + backend image/*|video/* kontrolü garantisi).
142
+ ext = filename.rsplit(".", 1)[-1].lower() if "." in filename else ""
143
+ return _MIME_BY_EXT.get(ext, "application/octet-stream")
144
+
145
+
146
+ def _build_multipart(field: str, content: bytes, filename: str, boundary: str) -> Tuple[bytes, str]:
147
+ mime = _mime_for(filename)
148
+ pre = (
149
+ f"--{boundary}\r\n"
150
+ f'Content-Disposition: form-data; name="{field}"; filename="{filename}"\r\n'
151
+ f"Content-Type: {mime}\r\n\r\n"
152
+ ).encode("utf-8")
153
+ post = f"\r\n--{boundary}--\r\n".encode("utf-8")
154
+ return pre + content + post, f"multipart/form-data; boundary={boundary}"
@@ -0,0 +1,258 @@
1
+ """Posthubify istemcisi — Node @posthubify/node ``Posthubify`` sınıfının Python aynası.
2
+
3
+ Tüm resource grupları snake_case nitelik olarak bağlanır (Node camelCase → Python):
4
+ profiles, accounts, posts, media, tools, ads, insights, platform_analytics,
5
+ inbox_analytics, inbox, comments, reviews, contacts, automations, broadcasts,
6
+ sequences, webhooks, api_keys, queue, account_groups, engagement, users,
7
+ invite_tokens, numbers, senders, sms, otp.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from typing import Any, Dict, List, Optional
13
+
14
+ from ._http import Transport, _DEFAULT_BASE
15
+ from .resources.accounts import AccountsResource, ProfilesResource
16
+ from .resources.ads import AdsResource
17
+ from .resources.analytics import (
18
+ InboxAnalyticsResource,
19
+ InsightsResource,
20
+ PlatformAnalyticsResource,
21
+ )
22
+ from .resources.messaging import (
23
+ AutomationsResource,
24
+ CommentAutomationsResource,
25
+ WorkflowsResource,
26
+ BroadcastsResource,
27
+ CommentsResource,
28
+ ContactsResource,
29
+ InboxResource,
30
+ ReviewsResource,
31
+ SequencesResource,
32
+ CustomFieldsResource,
33
+ WhatsAppResource,
34
+ GmbResource,
35
+ DiscordResource,
36
+ DiscoveryResource,
37
+ )
38
+ from .resources.platform import (
39
+ AccountGroupsResource,
40
+ ApiKeysResource,
41
+ EngagementResource,
42
+ InviteTokensResource,
43
+ QueueResource,
44
+ UsersResource,
45
+ WebhooksResource,
46
+ )
47
+ from .resources.posts import MediaResource, PostsResource, ToolsResource
48
+ from .resources.telecom import (
49
+ NumbersResource,
50
+ OtpResource,
51
+ SendersResource,
52
+ SmsResource,
53
+ )
54
+
55
+
56
+ class Posthubify:
57
+ """PostHubify /v1 API istemcisi.
58
+
59
+ Örnek:
60
+ >>> from posthubify import Posthubify
61
+ >>> ph = Posthubify(api_key="sk_...", base_url="https://api.posthubify.com/v1")
62
+ >>> ph.ping()
63
+ {"pong": True, "version": "v1", ...}
64
+ >>> ph.posts.list(platform="x", limit=10)
65
+ """
66
+
67
+ def __init__(
68
+ self,
69
+ api_key: str,
70
+ *,
71
+ base_url: str = _DEFAULT_BASE,
72
+ timeout: float = 30.0,
73
+ transport: Optional[Transport] = None,
74
+ ) -> None:
75
+ # transport enjeksiyonu testler içindir (Node'daki opts.fetch karşılığı).
76
+ self._http = transport or Transport(api_key, base_url=base_url, timeout=timeout)
77
+
78
+ # --- resource grupları (Node attribute adlarının snake_case aynası) ---
79
+ self.profiles = ProfilesResource(self._http)
80
+ self.accounts = AccountsResource(self._http)
81
+ self.posts = PostsResource(self._http)
82
+ self.media = MediaResource(self._http)
83
+ self.tools = ToolsResource(self._http)
84
+ self.ads = AdsResource(self._http)
85
+ self.insights = InsightsResource(self._http)
86
+ self.platform_analytics = PlatformAnalyticsResource(self._http)
87
+ self.inbox_analytics = InboxAnalyticsResource(self._http)
88
+ self.inbox = InboxResource(self._http)
89
+ self.comments = CommentsResource(self._http)
90
+ self.reviews = ReviewsResource(self._http)
91
+ self.contacts = ContactsResource(self._http)
92
+ self.automations = AutomationsResource(self._http)
93
+ self.comment_automations = CommentAutomationsResource(self._http)
94
+ self.workflows = WorkflowsResource(self._http)
95
+ self.broadcasts = BroadcastsResource(self._http)
96
+ self.sequences = SequencesResource(self._http)
97
+ self.custom_fields = CustomFieldsResource(self._http)
98
+ self.whatsapp = WhatsAppResource(self._http)
99
+ self.gmb = GmbResource(self._http)
100
+ self.discord = DiscordResource(self._http)
101
+ self.discovery = DiscoveryResource(self._http)
102
+ self.webhooks = WebhooksResource(self._http)
103
+ self.api_keys = ApiKeysResource(self._http)
104
+ self.queue = QueueResource(self._http)
105
+ self.account_groups = AccountGroupsResource(self._http)
106
+ self.engagement = EngagementResource(self._http)
107
+ self.users = UsersResource(self._http)
108
+ self.invite_tokens = InviteTokensResource(self._http)
109
+ self.numbers = NumbersResource(self._http)
110
+ self.senders = SendersResource(self._http)
111
+ self.sms = SmsResource(self._http)
112
+ self.otp = OtpResource(self._http)
113
+
114
+ # --- üst seviye uçlar (Node sınıf metodları) ---
115
+ def openapi(self) -> Dict[str, Any]:
116
+ """OpenAPI 3.1 spec dokümanı (auth gerekmez ama anahtarla da çalışır)."""
117
+ return self._http.req("GET", "/openapi.json")
118
+
119
+ def ping(self) -> Dict[str, Any]:
120
+ """Entegrasyon sağlık testi: bağlantı + anahtar doğrulaması (geçersiz anahtar → 401)."""
121
+ return self._http.data("GET", "/ping")
122
+
123
+ def me(self) -> Dict[str, Any]:
124
+ """API anahtarı kimliği."""
125
+ return self._http.data("GET", "/me")
126
+
127
+ def generate(
128
+ self,
129
+ command: str,
130
+ platforms: List[str],
131
+ *,
132
+ variants: Optional[int] = None,
133
+ context: Optional[str] = None,
134
+ max_chars: Optional[int] = None,
135
+ ) -> Dict[str, Any]:
136
+ """AI içerik üretimi (F1) — komut → platform-başına taslak + varyant (çok-dil)."""
137
+ return self._http.data("POST", "/generate", body={
138
+ "command": command, "platforms": platforms, "variants": variants,
139
+ "context": context, "maxChars": max_chars,
140
+ })
141
+
142
+ def subtitles(
143
+ self,
144
+ text: str,
145
+ *,
146
+ format: str = "srt",
147
+ duration_sec: Optional[float] = None,
148
+ max_chars_per_line: Optional[int] = None,
149
+ max_lines_per_cue: Optional[int] = None,
150
+ chars_per_sec: Optional[float] = None,
151
+ gap_ms: Optional[int] = None,
152
+ ) -> Dict[str, Any]:
153
+ """Altyazı üretimi (F4) — metin → zamanlanmış SRT/VTT (saf dönüşüm, AI/ücret yok).
154
+
155
+ duration_sec verilirse küeler bu süreye orantılı dağıtılır (sesle/videoyla senkron; ≤86400).
156
+ chars_per_sec okuma hızıdır (kesirli olabilir, örn. 15.5)."""
157
+ return self._http.data("POST", "/subtitles", body={
158
+ "text": text, "format": format, "durationSec": duration_sec,
159
+ "maxCharsPerLine": max_chars_per_line, "maxLinesPerCue": max_lines_per_cue,
160
+ "charsPerSec": chars_per_sec, "gapMs": gap_ms,
161
+ })
162
+
163
+ def videos(
164
+ self,
165
+ image_url: str,
166
+ format: str,
167
+ *,
168
+ audio_url: Optional[str] = None,
169
+ subtitle_text: Optional[str] = None,
170
+ subtitle_rtl: Optional[bool] = None,
171
+ bumper_duration_sec: Optional[float] = None,
172
+ main_duration_sec: Optional[float] = None,
173
+ ) -> Dict[str, Any]:
174
+ """Promo video render (F4) — uzak görsel (+ops. ses/altyazı) → markalı video (ffmpeg) → R2 URL.
175
+
176
+ format: '9:16' | '1:1' | '16:9'. AĞIR işlem (yazma izni + düşük hız sınırı).
177
+ Medya SSRF-korumalı indirilir (https + içerik-tipi + ≤25MB)."""
178
+ return self._http.data("POST", "/videos", body={
179
+ "imageUrl": image_url, "format": format, "audioUrl": audio_url,
180
+ "subtitleText": subtitle_text, "subtitleRtl": subtitle_rtl,
181
+ "bumperDurationSec": bumper_duration_sec, "mainDurationSec": main_duration_sec,
182
+ })
183
+
184
+ def analytics(self) -> Dict[str, Any]:
185
+ """Bağlı hesap + gönderi analitiği (platform API'lerinden canlı)."""
186
+ return self._http.data("GET", "/analytics")
187
+
188
+ def analytics_posts(
189
+ self,
190
+ *,
191
+ platform: Optional[str] = None,
192
+ account_id: Optional[str] = None,
193
+ source: Optional[str] = None,
194
+ limit: Optional[int] = None,
195
+ cursor: Optional[str] = None,
196
+ ) -> Dict[str, Any]:
197
+ """Gönderi analitiği listesi — source='external': platformdan senkronlanan, PostHubify-dışı gönderiler (B5)."""
198
+ return self._http.req(
199
+ "GET",
200
+ "/analytics/posts",
201
+ query={
202
+ "platform": platform,
203
+ "accountId": account_id,
204
+ "source": source,
205
+ "limit": limit,
206
+ "cursor": cursor,
207
+ },
208
+ )
209
+
210
+ def analytics_timeseries(
211
+ self,
212
+ *,
213
+ days: Optional[int] = None,
214
+ platform: Optional[str] = None,
215
+ account_id: Optional[str] = None,
216
+ from_date: Optional[str] = None,
217
+ to_date: Optional[str] = None,
218
+ source: Optional[str] = None,
219
+ ) -> Dict[str, Any]:
220
+ """Depolanan günlük metrik zaman serisi (hesap/post/inbox toplamları)."""
221
+ return self._http.data(
222
+ "GET",
223
+ "/analytics/timeseries",
224
+ query={
225
+ "days": days,
226
+ "platform": platform,
227
+ "accountId": account_id,
228
+ "fromDate": from_date,
229
+ "toDate": to_date,
230
+ "source": source,
231
+ },
232
+ )
233
+
234
+ def logs(
235
+ self,
236
+ *,
237
+ limit: Optional[int] = None,
238
+ category: Optional[str] = None,
239
+ status: Optional[str] = None,
240
+ platform: Optional[str] = None,
241
+ days: Optional[int] = None,
242
+ ) -> Any:
243
+ """Etkinlik/denetim günlüğü kayıtları."""
244
+ return self._http.data(
245
+ "GET",
246
+ "/logs",
247
+ query={
248
+ "limit": limit,
249
+ "category": category,
250
+ "status": status,
251
+ "platform": platform,
252
+ "days": days,
253
+ },
254
+ )
255
+
256
+ def usage(self) -> Dict[str, Any]:
257
+ """Plan + cüzdan + 30 günlük kullanım sayaçları."""
258
+ return self._http.data("GET", "/usage")
@@ -0,0 +1,32 @@
1
+ """PostHubify API hata türü (Node @posthubify/node PosthubifyError aynası)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Optional
6
+
7
+
8
+ class PosthubifyError(Exception):
9
+ """Bir API isteği 2xx olmayan durum döndürdüğünde fırlatılır.
10
+
11
+ Attributes:
12
+ status: HTTP durum kodu (ör. 400, 401, 404, 429, 502).
13
+ message: Sunucunun ``error`` alanı ya da ``HTTP <status>``.
14
+ code: Sunucunun makine-okunur ``code`` alanı (varsa).
15
+ body: Ayrıştırılmış yanıt gövdesi (varsa).
16
+ """
17
+
18
+ def __init__(
19
+ self,
20
+ status: int,
21
+ message: str,
22
+ code: Optional[str] = None,
23
+ body: Any = None,
24
+ ) -> None:
25
+ super().__init__(message)
26
+ self.status = status
27
+ self.message = message
28
+ self.code = code
29
+ self.body = body
30
+
31
+ def __repr__(self) -> str: # pragma: no cover - sadece hata ayıklama
32
+ return f"PosthubifyError(status={self.status!r}, message={self.message!r}, code={self.code!r})"
@@ -0,0 +1 @@
1
+ """Resource sınıfları — Posthubify istemcisi bunları bağlar."""