-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathpublish_devto_article.py
More file actions
146 lines (116 loc) · 5.28 KB
/
publish_devto_article.py
File metadata and controls
146 lines (116 loc) · 5.28 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
"""Publish the Automation Cookbook Dev.to article for kaz/EVA.
Strips the metadata header and publishing notes, posts to Dev.to API
as @peytongreen_dev. If DEVTO_ARTICLE_ID env var is set, updates that
existing draft to published=true instead of creating a new article.
Usage: python3 publish_devto_article.py [--draft]
--draft: create as draft (unpublished) for review; omit to publish live
"""
import json
import sys
import os
import subprocess
sys.path.insert(0, '/Users/services/drebin/tools')
sys.path.insert(0, '/Users/services/kaz')
from devto_post_publish import post_publish_actions, post_chat
ARTICLE_FILE = '/Users/services/EVA/devto-article-automation-cookbook.md'
DEVTO_API = 'https://dev.to/api/articles'
DEVTO_ARTICLE_ID = 3393523 # draft created 2026-03-24; update to publish
CHAT_CHANNEL = '#kaz'
CHAT_AGENT = 'kaz'
TITLE = 'Python Automation Cookbook, Part 1: The 25 scripts I reach for every week'
TAGS = ['python', 'automation', 'productivity', 'discuss']
FIRST_COMMENT = """\
Happy to share the full versions of any of these. A few patterns that didn't make it into the article:
- **The retry decorator** in the article is simplified — the production version I use adds jitter (random 0-500ms between retries) to avoid thundering herd on shared APIs. `time.sleep(delay + random.uniform(0, 0.5))`
- **File watcher on macOS**: `watchdog` is reliable but `FSEventsObserver` specifically (not `PollingObserver`) — polling at the default interval will eat CPU on large directories.
- **The debounce pattern**: I originally tried using `threading.Timer` but it caused issues under test. The `asyncio` version in the article is cleaner and works in both sync and async contexts with a small wrapper.
What automation tasks are you most commonly scripting? Curious if there are patterns worth covering in Part 2.\
"""
PYCODERS_DESCRIPTION = (
"25 reusable Python automation scripts covering retry logic, file watching, debouncing, "
"scheduled tasks, config management, and more. Practical patterns with actual code, "
"not toy examples. Part 1 of a two-part series."
)
PYTHON_WEEKLY_SUBJECT = "Submission: Python Automation Cookbook, Part 1 — The 25 Scripts I Reach For Every Week"
PYTHON_WEEKLY_BODY = """\
Hi,
Submitting for consideration in Python Weekly:
**Title:** Python Automation Cookbook, Part 1: The 25 scripts I reach for every week
**URL:** {url}
**Description:** 25 reusable Python automation scripts covering retry logic, file watching, debouncing, scheduled tasks, config management, and more. Practical patterns with actual working code. Part 1 of a two-part series.
Thanks
"""
def get_api_key():
raw = subprocess.check_output(
['node', '/Users/services/drebin/tools/account-helper.js', 'get', 'dev.to', '--json'],
timeout=10
).decode()
return json.loads(raw)['api_key']
def extract_body(filepath):
"""Strip metadata header (lines before '## Article') and publishing notes footer."""
with open(filepath) as f:
lines = f.readlines()
start = None
for i, line in enumerate(lines):
if line.strip() == '## Article':
start = i + 1
break
if start is None:
raise ValueError("Could not find '## Article' marker in file")
end = len(lines)
for i in range(start, len(lines)):
if lines[i].strip() == '## Publishing Notes':
end = i
break
return ''.join(lines[start:end]).strip()
def publish(draft=False):
import httpx
api_key = get_api_key()
body = extract_body(ARTICLE_FILE)
# Check current published state before updating — never revert a live article to draft
url = f"{DEVTO_API}/{DEVTO_ARTICLE_ID}"
check = httpx.get(url, headers={'api-key': api_key}, timeout=15)
check.raise_for_status()
currently_published = check.json().get('published', False)
if draft and currently_published:
print(f"WARNING: Article {DEVTO_ARTICLE_ID} is already published — --draft flag ignored to prevent revert.")
draft = False # keep it published
payload = {
'article': {
'title': TITLE,
'body_markdown': body,
'published': not draft,
'tags': TAGS,
}
}
resp = httpx.put(
url,
headers={'api-key': api_key, 'Content-Type': 'application/json'},
json=payload,
timeout=30,
)
resp.raise_for_status()
result = resp.json()
article_url = result.get('url', '?')
article_id = result.get('id', DEVTO_ARTICLE_ID)
status = 'DRAFT' if draft else 'PUBLISHED'
msg = f"Dev.to article {status}: {article_url} | '{TITLE}' | @peytongreen_dev"
print(msg)
post_chat(CHAT_CHANNEL, f"📝 {msg}")
if not draft:
post_publish_actions(
article_id, article_url, FIRST_COMMENT,
PYCODERS_DESCRIPTION, PYTHON_WEEKLY_SUBJECT, PYTHON_WEEKLY_BODY,
article_title=TITLE,
)
return result
if __name__ == '__main__':
draft = '--draft' in sys.argv
try:
result = publish(draft=draft)
print(f"ID: {result.get('id')} URL: {result.get('url')}")
except Exception as e:
msg = f"⚠️ publish_devto_article FAILED: {e}"
print(msg)
post_chat(CHAT_CHANNEL, msg)
sys.exit(1)