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 +18 -0
- adauto-0.2.0/adauto/__init__.py +2 -0
- adauto-0.2.0/adauto/__main__.py +3 -0
- adauto-0.2.0/adauto/analytics.py +214 -0
- adauto-0.2.0/adauto/cli.py +916 -0
- adauto-0.2.0/adauto/config.py +90 -0
- adauto-0.2.0/adauto/crash.py +245 -0
- adauto-0.2.0/adauto/db.py +221 -0
- adauto-0.2.0/adauto/discover.py +85 -0
- adauto-0.2.0/adauto/ethics.py +383 -0
- adauto-0.2.0/adauto/generator.py +337 -0
- adauto-0.2.0/adauto/license.py +174 -0
- adauto-0.2.0/adauto/licensing_core.py +51 -0
- adauto-0.2.0/adauto/platforms/__init__.py +6 -0
- adauto-0.2.0/adauto/platforms/devto.py +99 -0
- adauto-0.2.0/adauto/platforms/hackernews.py +92 -0
- adauto-0.2.0/adauto/platforms/reddit.py +139 -0
- adauto-0.2.0/adauto/platforms/twitter.py +136 -0
- adauto-0.2.0/adauto/pulse.py +359 -0
- adauto-0.2.0/adauto/scheduler.py +46 -0
- adauto-0.2.0/adauto/server.py +568 -0
- adauto-0.2.0/adauto/service.py +211 -0
- adauto-0.2.0/adauto/strategy.py +247 -0
- adauto-0.2.0/adauto.egg-info/PKG-INFO +18 -0
- adauto-0.2.0/adauto.egg-info/SOURCES.txt +29 -0
- adauto-0.2.0/adauto.egg-info/dependency_links.txt +1 -0
- adauto-0.2.0/adauto.egg-info/entry_points.txt +2 -0
- adauto-0.2.0/adauto.egg-info/requires.txt +12 -0
- adauto-0.2.0/adauto.egg-info/top_level.txt +1 -0
- adauto-0.2.0/pyproject.toml +45 -0
- adauto-0.2.0/setup.cfg +4 -0
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,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)
|