-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathpublish_devto_localstack.py
More file actions
131 lines (107 loc) · 4.91 KB
/
publish_devto_localstack.py
File metadata and controls
131 lines (107 loc) · 4.91 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
"""Publish the LocalStack alternative Dev.to article for kaz/EVA.
Usage: python3 publish_devto_localstack.py [--draft]
"""
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-localstack-alternative.md'
DEVTO_API = 'https://dev.to/api/articles'
DEVTO_ARTICLE_ID = 3394243 # draft created 2026-03-24
CHAT_CHANNEL = '#kaz'
CHAT_AGENT = 'kaz'
TITLE = "LocalStack Now Requires an Account — Here's How to Test AWS in Python Without One in 2026"
TAGS = ['python', 'testing', 'aws', 'devtools']
FIRST_COMMENT = """\
Happy to answer questions on any of the approaches. A few things worth knowing:
- **April 6 deadline**: the `LS_ACKNOWLEDGE_ACCOUNT_REQUIREMENT=1` bypass still works until April 6 — if you're using it, set a calendar reminder now so you don't hit a surprise break.
- **Moto coverage**: Moto covers S3, DynamoDB, SQS, SNS, Lambda, Secrets Manager, and a lot more. The [service coverage docs](https://docs.getmoto.org/en/latest/docs/services/index.html) are worth checking if you're on something less common.
- **testcontainers-python users**: there's no official `LOCALSTACK_AUTH_TOKEN` support in the library as of March 2026. The `with_env` workaround still works — I'll keep the article updated if an official patch lands.
What service are you testing? The right migration path varies a lot depending on S3-only vs multi-service.\
"""
PYCODERS_DESCRIPTION = (
"LocalStack now requires an account for all usage, including CI. This post walks through "
"the drop-in alternatives for Python AWS testing: Moto for unit/integration tests, "
"testcontainers workarounds, and a conftest.py template that replaces LocalStack for the "
"most common services (S3, DynamoDB, SQS). Also covers the April 6 bypass window and "
"the free token path if you want to stay on LocalStack."
)
PYTHON_WEEKLY_SUBJECT = "Submission: LocalStack Now Requires an Account — Here's How to Test AWS in Python Without One"
PYTHON_WEEKLY_BODY = """\
Hi,
Submitting for consideration in Python Weekly:
**Title:** LocalStack Now Requires an Account — Here's How to Test AWS in Python Without One in 2026
**URL:** {url}
**Description:** LocalStack now requires an account for all usage, including CI. This post covers the drop-in alternatives: Moto for unit/integration tests, testcontainers workarounds, and a conftest.py template covering S3/DynamoDB/SQS. Also covers the April 6 bypass window and the free token path for teams that want to stay on LocalStack.
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):
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() in ('## Publishing Notes', '## Positioning 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)
url = f"{DEVTO_API}/{DEVTO_ARTICLE_ID}"
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_chat('#general', f"📝 LocalStack alternative article live: {article_url}")
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_localstack FAILED: {e}"
print(msg)
post_chat(CHAT_CHANNEL, msg)
sys.exit(1)