posthubify 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.
posthubify/__init__.py ADDED
@@ -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__"]
posthubify/_http.py ADDED
@@ -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}"
posthubify/client.py ADDED
@@ -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")
posthubify/errors.py ADDED
@@ -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."""