adauto 0.2.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.
adauto-0.2.0/PKG-INFO ADDED
@@ -0,0 +1,18 @@
1
+ Metadata-Version: 2.4
2
+ Name: adauto
3
+ Version: 0.2.0
4
+ Summary: Developer marketing automation — pulse scanning, ethics filter, multi-platform posting
5
+ License: Proprietary
6
+ Requires-Python: >=3.11
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: click>=8.1
9
+ Requires-Dist: rich>=13.7
10
+ Requires-Dist: httpx>=0.27
11
+ Requires-Dist: tenacity>=8.2
12
+ Requires-Dist: praw>=7.7
13
+ Requires-Dist: tweepy>=4.14
14
+ Requires-Dist: requests>=2.31
15
+ Provides-Extra: dev
16
+ Requires-Dist: pytest>=8.0; extra == "dev"
17
+ Requires-Dist: ruff>=0.4; extra == "dev"
18
+ Requires-Dist: mypy>=1.9; extra == "dev"
@@ -0,0 +1,2 @@
1
+ """adauto — multi-platform developer marketing automation."""
2
+ __version__ = "0.2.0"
@@ -0,0 +1,3 @@
1
+ """python -m adauto → run CLI"""
2
+ from .cli import main
3
+ main()
@@ -0,0 +1,214 @@
1
+ """
2
+ Analytics & adaptive learning — track engagement, score post styles, feed learnings back.
3
+
4
+ Paradigm: deterministic signal loop.
5
+ 1. After posting, URLs are stored in DB.
6
+ 2. check_engagement() polls platform APIs for upvotes/comments.
7
+ 3. score_styles() computes a performance score per (platform, post_type, subreddit).
8
+ 4. best_examples() returns the top-performing posts as few-shot examples for generator.
9
+
10
+ No ML, no cloud. Pure SQLite + platform APIs.
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import json
15
+ import time
16
+ from datetime import datetime, timezone
17
+ from typing import Optional
18
+
19
+ from .db import get_conn
20
+
21
+
22
+ # ── Engagement polling ────────────────────────────────────────────────────────
23
+
24
+
25
+ def check_reddit_engagement(post_id: int, url: str) -> dict:
26
+ """
27
+ Poll Reddit API for upvotes/comments on a posted URL.
28
+ URL format: https://reddit.com/r/.../comments/XXXXX/...
29
+ Returns {"upvotes": N, "comments": N} or empty dict on failure.
30
+ """
31
+ try:
32
+ import requests
33
+ # Reddit JSON API: append .json to the post URL
34
+ json_url = url.rstrip("/") + ".json"
35
+ resp = requests.get(
36
+ json_url,
37
+ headers={"User-Agent": "adauto/0.1"},
38
+ timeout=10,
39
+ )
40
+ if resp.status_code != 200:
41
+ return {}
42
+ data = resp.json()
43
+ listing = data[0]["data"]["children"][0]["data"]
44
+ return {
45
+ "upvotes": listing.get("ups", 0),
46
+ "comments": listing.get("num_comments", 0),
47
+ }
48
+ except Exception:
49
+ return {}
50
+
51
+
52
+ def check_engagement_all(max_posts: int = 50) -> int:
53
+ """
54
+ Poll engagement for all recently posted URLs.
55
+ Returns number of posts updated.
56
+ """
57
+ with get_conn() as conn:
58
+ rows = conn.execute(
59
+ """SELECT id, platform, url FROM posts
60
+ WHERE status='posted' AND url IS NOT NULL
61
+ ORDER BY posted_at DESC LIMIT ?""",
62
+ (max_posts,),
63
+ ).fetchall()
64
+
65
+ updated = 0
66
+ for row in rows:
67
+ post_id, platform, url = row["id"], row["platform"], row["url"]
68
+ metrics = {}
69
+ if platform == "reddit":
70
+ metrics = check_reddit_engagement(post_id, url)
71
+ # future: devto, twitter
72
+
73
+ if metrics:
74
+ _record_metrics(post_id, metrics)
75
+ _update_post_metrics(post_id, metrics)
76
+ updated += 1
77
+ time.sleep(0.5) # be polite
78
+
79
+ return updated
80
+
81
+
82
+ def _record_metrics(post_id: int, metrics: dict) -> None:
83
+ with get_conn() as conn:
84
+ conn.execute(
85
+ """INSERT INTO metrics (post_id, upvotes, comments, clicks)
86
+ VALUES (?, ?, ?, ?)""",
87
+ (post_id,
88
+ metrics.get("upvotes", 0),
89
+ metrics.get("comments", 0),
90
+ metrics.get("clicks", 0)),
91
+ )
92
+
93
+
94
+ def _update_post_metrics(post_id: int, metrics: dict) -> None:
95
+ with get_conn() as conn:
96
+ conn.execute(
97
+ "UPDATE posts SET upvotes=?, comments=? WHERE id=?",
98
+ (metrics.get("upvotes", 0), metrics.get("comments", 0), post_id),
99
+ )
100
+
101
+
102
+ # ── Scoring ───────────────────────────────────────────────────────────────────
103
+
104
+
105
+ def score_styles(campaign_name: str = None) -> list[dict]:
106
+ """
107
+ Compute performance scores per (platform, post_type).
108
+ Score = weighted sum of upvotes + 3×comments (comments signal deeper engagement).
109
+
110
+ Returns list sorted by score descending.
111
+ """
112
+ with get_conn() as conn:
113
+ q = """
114
+ SELECT platform, post_type,
115
+ COUNT(*) as n,
116
+ AVG(upvotes) as avg_up,
117
+ AVG(comments) as avg_cm,
118
+ SUM(upvotes + 3*comments) as total_score
119
+ FROM posts
120
+ WHERE status='posted'
121
+ """
122
+ args = []
123
+ if campaign_name:
124
+ q += " AND campaign_name=?"
125
+ args.append(campaign_name)
126
+ q += " GROUP BY platform, post_type ORDER BY total_score DESC"
127
+ rows = conn.execute(q, args).fetchall()
128
+
129
+ return [
130
+ {
131
+ "platform": r["platform"],
132
+ "post_type": r["post_type"],
133
+ "n_posts": r["n"],
134
+ "avg_upvotes": round(r["avg_up"] or 0, 1),
135
+ "avg_comments": round(r["avg_cm"] or 0, 1),
136
+ "total_score": r["total_score"] or 0,
137
+ }
138
+ for r in rows
139
+ ]
140
+
141
+
142
+ def best_post_type(campaign_name: str, platform: str,
143
+ fallback: str = "showcase") -> str:
144
+ """Return the best-performing post_type for a campaign+platform combo."""
145
+ scores = score_styles(campaign_name)
146
+ plat_scores = [s for s in scores if s["platform"] == platform and s["n_posts"] >= 2]
147
+ if plat_scores:
148
+ return plat_scores[0]["post_type"]
149
+ return fallback
150
+
151
+
152
+ # ── Few-shot examples for adaptive generation ─────────────────────────────────
153
+
154
+
155
+ def best_examples(campaign_name: str, platform: str,
156
+ post_type: str = None, limit: int = 3) -> list[dict]:
157
+ """
158
+ Return top-performing posts as few-shot examples for the generator.
159
+ Sorted by (upvotes + 3*comments) descending.
160
+ """
161
+ with get_conn() as conn:
162
+ q = """
163
+ SELECT title, body, upvotes, comments, post_type
164
+ FROM posts
165
+ WHERE status='posted'
166
+ AND campaign_name=?
167
+ AND platform=?
168
+ AND (upvotes + comments) > 0
169
+ """
170
+ args = [campaign_name, platform]
171
+ if post_type:
172
+ q += " AND post_type=?"
173
+ args.append(post_type)
174
+ q += " ORDER BY (upvotes + 3*comments) DESC LIMIT ?"
175
+ args.append(limit)
176
+ rows = conn.execute(q, args).fetchall()
177
+
178
+ return [
179
+ {
180
+ "post_type": r["post_type"],
181
+ "title": r["title"],
182
+ "body": r["body"][:500] if r["body"] else "",
183
+ "upvotes": r["upvotes"],
184
+ "comments": r["comments"],
185
+ }
186
+ for r in rows
187
+ ]
188
+
189
+
190
+ def build_learning_context(campaign_name: str, platform: str,
191
+ post_type: str = None) -> str:
192
+ """
193
+ Build a 'what works' context block for the generator prompt.
194
+ Returns empty string if no data yet.
195
+ """
196
+ examples = best_examples(campaign_name, platform, post_type)
197
+ if not examples:
198
+ return ""
199
+
200
+ lines = ["WHAT HAS WORKED WELL (real posts, ranked by engagement):"]
201
+ for i, ex in enumerate(examples, 1):
202
+ lines.append(
203
+ f"\nExample {i} [{ex['post_type']}] "
204
+ f"({ex['upvotes']} upvotes, {ex['comments']} comments):"
205
+ )
206
+ if ex["title"]:
207
+ lines.append(f" Title: {ex['title']}")
208
+ lines.append(f" Body (excerpt): {ex['body'][:300]}")
209
+
210
+ lines.append(
211
+ "\nLearn from these: use similar tone, depth, and hook style "
212
+ "but write completely new content."
213
+ )
214
+ return "\n".join(lines)