start-vibing-stacks 2.16.0 → 2.18.0

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.
@@ -1,111 +1,199 @@
1
1
  ---
2
2
  name: scripting-automation
3
- version: 1.0.0
3
+ version: 2.0.0
4
+ description: "Local Python scripts and automation tooling for Python 3.13/3.14 with uv as the default package + project manager (uv init / uv add / uv run / uv tool — Astral, acquired by OpenAI Mar 2026, ~75M monthly downloads vs Poetry 66M, 10–100× faster than pip). Covers project layout for ETL/CLI/cron jobs, Pydantic Settings (.env), reusable httpx + tenacity client, WordPress REST integration, MariaDB/Postgres direct access (no ORM), structured CLI with argparse + match/case + rich, structured logging, and uv single-file scripts via PEP 723 inline metadata. Use for WordPress, ad-platform automation, ETL, scrapers, batch jobs."
4
5
  ---
5
6
 
6
- # Local Scripts & Automation — Python 3.12+
7
+ # Local Scripts & Automation — Python 3.13/3.14 with uv
7
8
 
8
- **ALWAYS invoke when building local scripts, CLI tools, or automation tasks.**
9
+ **ALWAYS invoke when building local scripts, CLI tools, scrapers, ad-platform automation, ETL, or cron-style jobs.**
9
10
 
10
- ## When to Use
11
+ ## When to use
11
12
 
12
- - WordPress API automation (create/update posts, manage media)
13
- - Ad campaign management (Google Ads, Facebook Ads, TikTok Ads API)
14
- - Data pipelines (CSV/Excel processing, database sync)
15
- - Web scraping and data extraction
16
- - File system operations and batch processing
17
- - Scheduled tasks and cron-like automation
18
- - API integrations without a web framework
13
+ - WordPress REST automation (posts, media, taxonomies)
14
+ - Ad-platform automation (Google Ads, Meta, TikTok, LinkedIn APIs)
15
+ - ETL / CSV / Excel pipelines
16
+ - Web scraping & data extraction
17
+ - Database sync, batch processing
18
+ - Scheduled tasks (cron, GitHub Actions cron, Vercel cron, systemd timers)
19
+ - "API integrations without a web framework"
19
20
 
20
- ## Project Structure
21
+ ## Toolchain (2026)
22
+
23
+ | Tool | Why |
24
+ |---|---|
25
+ | **`uv`** (Astral) | Project + package + Python-version manager. 10–100× faster than pip; surpassed Poetry in monthly downloads in early 2026. Astral was acquired by OpenAI in March 2026 with public commitment to keep `uv` open source. |
26
+ | `ruff` | Lint + format in one Rust binary |
27
+ | `pyright` | Type checking (correctness); `ty` (Astral) when speed matters |
28
+ | `httpx` + `tenacity` | HTTP with retries |
29
+ | `pydantic-settings` | Typed env config |
30
+ | `rich` | Terminal output, tables, progress bars, tracebacks |
31
+ | `tomllib` (3.11+ stdlib) | TOML reading — drop `tomli`/`toml` from deps |
32
+
33
+ ## Project Layout
21
34
 
22
35
  ```
23
36
  project/
24
- ├── pyproject.toml # Dependencies + project metadata
25
- ├── .env # API keys, credentials (NEVER commit)
26
- ├── .env.example # Template without real values
37
+ ├── pyproject.toml # Project + tool config (uv, ruff, pyright, pytest)
38
+ ├── uv.lock # Universal lockfile commit this
39
+ ├── .env # Secrets NEVER commit
40
+ ├── .env.example # Safe template
41
+ ├── README.md
42
+ ├── main.py # CLI entry
27
43
  ├── scripts/
28
44
  │ ├── __init__.py
29
- │ ├── wordpress.py # WordPress automation
30
- │ ├── ads_manager.py # Ad campaigns
31
- │ └── data_sync.py # Database sync
45
+ │ ├── wordpress.py # WordPress automation
46
+ │ ├── ads_manager.py # Ad campaigns
47
+ │ └── data_sync.py # DB sync
32
48
  ├── lib/
33
49
  │ ├── __init__.py
34
- │ ├── http_client.py # Reusable httpx client
35
- │ ├── config.py # Pydantic Settings
36
- │ ├── logger.py # Structured logging
37
- │ └── retry.py # Retry with backoff
38
- ├── data/ # Input/output data files
39
- ├── logs/ # Log files
40
- ├── tests/
41
- └── test_scripts.py
42
- └── main.py # Entry point / CLI
50
+ │ ├── http_client.py # Reusable httpx client
51
+ │ ├── config.py # Pydantic Settings
52
+ │ ├── logger.py # rich/structlog
53
+ │ └── retry.py # Retry helpers
54
+ ├── data/ # I/O files
55
+ ├── logs/
56
+ └── tests/
57
+ └── test_scripts.py
58
+ ```
59
+
60
+ ## uv quickstart
61
+
62
+ ```bash
63
+ # Install once (mac/linux)
64
+ curl -LsSf https://astral.sh/uv/install.sh | sh
65
+
66
+ # Bootstrap a project
67
+ uv init my-script && cd my-script
68
+ uv python pin 3.13 # writes .python-version
69
+ uv add httpx pydantic-settings tenacity rich
70
+ uv add --dev pytest ruff pyright
71
+
72
+ # Run anything inside the project venv
73
+ uv run python main.py
74
+ uv run pytest
75
+
76
+ # Install a global CLI tool, isolated
77
+ uv tool install ruff # works like pipx, just faster
78
+ ```
79
+
80
+ `uv` reads/writes `pyproject.toml` and produces a single universal `uv.lock` that resolves consistently across macOS/Linux/Windows. Commit the lockfile.
81
+
82
+ ## `pyproject.toml` (uv-native)
83
+
84
+ ```toml
85
+ [project]
86
+ name = "my-script"
87
+ version = "0.1.0"
88
+ requires-python = ">=3.13"
89
+ dependencies = [
90
+ "httpx>=0.27",
91
+ "pydantic-settings>=2.5",
92
+ "tenacity>=9.0",
93
+ "rich>=13.7",
94
+ ]
95
+
96
+ [dependency-groups]
97
+ dev = ["pytest>=9.0", "ruff>=0.5", "pyright>=1.1"]
98
+ ads = ["google-ads>=24.0", "facebook-business>=19.0"]
99
+
100
+ [tool.ruff]
101
+ line-length = 100
102
+ target-version = "py313"
103
+ [tool.ruff.lint]
104
+ select = ["E", "F", "W", "I", "B", "UP", "N", "S", "RUF"]
105
+ ```
106
+
107
+ ## Single-file scripts — PEP 723 inline metadata
108
+
109
+ For one-off scripts you want runnable without `pip install`:
110
+
111
+ ```python
112
+ #!/usr/bin/env -S uv run --script
113
+ # /// script
114
+ # requires-python = ">=3.13"
115
+ # dependencies = ["httpx>=0.27", "rich>=13.7"]
116
+ # ///
117
+
118
+ import httpx
119
+ from rich import print
120
+
121
+ r = httpx.get("https://api.example.com/status", timeout=5)
122
+ print(r.json())
43
123
  ```
44
124
 
125
+ `uv run script.py` reads the inline metadata, builds an isolated venv, runs the script. Killer for cron jobs, GitHub Actions one-liners, throwaway helpers.
126
+
45
127
  ## Configuration (Pydantic Settings)
46
128
 
47
129
  ```python
48
- from pydantic_settings import BaseSettings
130
+ from pydantic import Field
131
+ from pydantic_settings import BaseSettings, SettingsConfigDict
49
132
 
50
133
  class Settings(BaseSettings):
51
- WP_URL: str
52
- WP_USER: str
53
- WP_APP_PASSWORD: str
134
+ model_config = SettingsConfigDict(env_file=".env", extra="forbid")
135
+
136
+ WP_URL: str
137
+ WP_USER: str
138
+ WP_APP_PASSWORD: str = Field(min_length=10)
54
139
 
55
140
  GOOGLE_ADS_DEVELOPER_TOKEN: str = ""
56
- FACEBOOK_ACCESS_TOKEN: str = ""
57
- TIKTOK_ACCESS_TOKEN: str = ""
141
+ FACEBOOK_ACCESS_TOKEN: str = ""
142
+ TIKTOK_ACCESS_TOKEN: str = ""
58
143
 
59
- DB_HOST: str = "localhost"
60
- DB_PORT: int = 3306
61
- DB_NAME: str
62
- DB_USER: str
144
+ DB_HOST: str = "localhost"
145
+ DB_PORT: int = 3306
146
+ DB_NAME: str
147
+ DB_USER: str
63
148
  DB_PASSWORD: str
64
149
 
65
- LOG_LEVEL: str = "INFO"
66
- DRY_RUN: bool = False
150
+ LOG_LEVEL: str = "INFO"
151
+ DRY_RUN: bool = False
67
152
 
68
- model_config = {"env_file": ".env", "env_file_encoding": "utf-8"}
69
-
70
- settings = Settings()
153
+ settings = Settings() # validated at import
71
154
  ```
72
155
 
73
- ## HTTP Client (reusable)
156
+ ## HTTP Client (reusable, retried)
74
157
 
75
158
  ```python
76
159
  import httpx
77
- from tenacity import retry, stop_after_attempt, wait_exponential
160
+ from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type
78
161
 
79
162
  class ApiClient:
80
163
  def __init__(self, base_url: str, auth: tuple[str, str] | None = None):
81
164
  self.client = httpx.Client(
82
165
  base_url=base_url,
83
166
  auth=auth,
84
- timeout=30.0,
167
+ timeout=httpx.Timeout(connect=5, read=30, write=30, pool=5),
85
168
  headers={"User-Agent": "AutomationScript/1.0"},
86
169
  )
87
170
 
88
- @retry(stop=stop_after_attempt(3), wait=wait_exponential(min=1, max=10))
89
- def get(self, path: str, **kwargs) -> dict:
90
- r = self.client.get(path, **kwargs)
171
+ @retry(
172
+ stop=stop_after_attempt(3),
173
+ wait=wait_exponential(min=1, max=10),
174
+ retry=retry_if_exception_type((httpx.TransportError, httpx.HTTPStatusError)),
175
+ reraise=True,
176
+ )
177
+ def get(self, path: str, **kw) -> dict:
178
+ r = self.client.get(path, **kw)
91
179
  r.raise_for_status()
92
180
  return r.json()
93
181
 
94
- @retry(stop=stop_after_attempt(3), wait=wait_exponential(min=1, max=10))
95
- def post(self, path: str, **kwargs) -> dict:
96
- r = self.client.post(path, **kwargs)
182
+ @retry(stop=stop_after_attempt(3), wait=wait_exponential(min=1, max=10), reraise=True)
183
+ def post(self, path: str, **kw) -> dict:
184
+ r = self.client.post(path, **kw)
97
185
  r.raise_for_status()
98
186
  return r.json()
99
187
 
100
- def close(self):
188
+ def close(self) -> None:
101
189
  self.client.close()
102
190
  ```
103
191
 
104
- ## WordPress REST API Pattern
192
+ ## WordPress REST API pattern
105
193
 
106
194
  ```python
107
195
  from lib.http_client import ApiClient
108
- from lib.config import settings
196
+ from lib.config import settings
109
197
 
110
198
  wp = ApiClient(
111
199
  base_url=f"{settings.WP_URL}/wp-json/wp/v2",
@@ -113,26 +201,13 @@ wp = ApiClient(
113
201
  )
114
202
 
115
203
  def create_post(title: str, content: str, status: str = "draft") -> dict:
116
- return wp.post("/posts", json={
117
- "title": title,
118
- "content": content,
119
- "status": status,
120
- })
204
+ return wp.post("/posts", json={"title": title, "content": content, "status": status})
121
205
 
122
206
  def update_post(post_id: int, **fields) -> dict:
123
207
  return wp.post(f"/posts/{post_id}", json=fields)
124
-
125
- def bulk_update_posts(posts: list[dict]) -> list[dict]:
126
- results = []
127
- for post in posts:
128
- pid = post.pop("id")
129
- result = update_post(pid, **post)
130
- results.append(result)
131
- logger.info(f"Updated post {pid}: {result['title']['rendered']}")
132
- return results
133
208
  ```
134
209
 
135
- ## Database Access (direct, no ORM)
210
+ ## Database access direct, no ORM
136
211
 
137
212
  ```python
138
213
  import mariadb
@@ -155,47 +230,48 @@ def get_connection():
155
230
 
156
231
  def fetch_all(query: str, params: tuple = ()) -> list[dict]:
157
232
  with get_connection() as conn:
158
- cursor = conn.cursor(dictionary=True)
159
- cursor.execute(query, params)
160
- return cursor.fetchall()
233
+ cur = conn.cursor(dictionary=True)
234
+ cur.execute(query, params) # parameterised — never f-string
235
+ return cur.fetchall()
161
236
 
162
237
  def execute(query: str, params: tuple = ()) -> int:
163
238
  with get_connection() as conn:
164
- cursor = conn.cursor()
165
- cursor.execute(query, params)
239
+ cur = conn.cursor()
240
+ cur.execute(query, params)
166
241
  conn.commit()
167
- return cursor.rowcount
242
+ return cur.rowcount
168
243
  ```
169
244
 
170
- ## CLI Entry Point
245
+ For PostgreSQL prefer `psycopg[binary]` (psycopg 3) over psycopg2.
246
+
247
+ ## CLI Entry — argparse + match/case
171
248
 
172
249
  ```python
173
250
  import argparse
174
251
  import logging
252
+ from rich.logging import RichHandler
253
+
175
254
  from lib.config import settings
176
255
 
177
256
  logging.basicConfig(
178
257
  level=getattr(logging, settings.LOG_LEVEL),
179
- format="%(asctime)s [%(levelname)s] %(message)s",
180
- handlers=[
181
- logging.StreamHandler(),
182
- logging.FileHandler("logs/script.log"),
183
- ],
258
+ format="%(message)s",
259
+ handlers=[RichHandler(rich_tracebacks=True)],
184
260
  )
185
261
  logger = logging.getLogger(__name__)
186
262
 
187
- def main():
263
+ def main() -> int:
188
264
  parser = argparse.ArgumentParser(description="Automation Scripts")
189
- sub = parser.add_subparsers(dest="command")
265
+ sub = parser.add_subparsers(dest="command", required=True)
190
266
 
191
- sub.add_parser("wp-sync", help="Sync WordPress posts")
267
+ sub.add_parser("wp-sync", help="Sync WordPress posts")
192
268
  sub.add_parser("ads-report", help="Generate ads performance report")
193
269
  sub.add_parser("db-migrate", help="Run data migration")
194
270
 
195
271
  args = parser.parse_args()
196
272
 
197
273
  if settings.DRY_RUN:
198
- logger.warning("DRY RUN mode — no changes will be saved")
274
+ logger.warning("DRY RUN — no changes will be persisted")
199
275
 
200
276
  match args.command:
201
277
  case "wp-sync":
@@ -209,52 +285,62 @@ def main():
209
285
  run_migration()
210
286
  case _:
211
287
  parser.print_help()
288
+ return 2
289
+ return 0
212
290
 
213
291
  if __name__ == "__main__":
214
- main()
292
+ raise SystemExit(main())
215
293
  ```
216
294
 
217
- ## Essential Libraries
218
-
219
- ```toml
220
- # pyproject.toml
221
- [project]
222
- dependencies = [
223
- "httpx>=0.27",
224
- "pydantic-settings>=2.0",
225
- "tenacity>=8.0",
226
- "python-dotenv>=1.0",
227
- "mariadb>=1.1",
228
- "rich>=13.0",
229
- ]
230
-
231
- [project.optional-dependencies]
232
- dev = ["pytest>=8.0", "mypy>=1.8", "ruff>=0.3"]
233
- ads = ["google-ads>=24.0", "facebook-business>=19.0"]
234
- ```
235
-
236
- ## Logging (structured)
295
+ ## Logging (structured, prod-ready)
237
296
 
238
297
  ```python
239
- from rich.console import Console
240
- from rich.logging import RichHandler
241
298
  import logging
299
+ from rich.logging import RichHandler
242
300
 
243
- console = Console()
244
301
  logging.basicConfig(
245
302
  level="INFO",
246
- format="%(message)s",
247
- handlers=[RichHandler(console=console, rich_tracebacks=True)],
303
+ format="%(asctime)s %(levelname)s %(name)s %(message)s",
304
+ handlers=[
305
+ RichHandler(rich_tracebacks=True, show_time=False),
306
+ logging.FileHandler("logs/script.log"),
307
+ ],
248
308
  )
309
+
310
+ # For real prod observability use structlog + JSON formatter — see _shared/observability
249
311
  ```
250
312
 
313
+ ## Reading TOML/JSON configs
314
+
315
+ ```python
316
+ # 3.11+ — stdlib, no extra dep
317
+ import tomllib
318
+ with open("pyproject.toml", "rb") as f:
319
+ data = tomllib.load(f)
320
+ ```
321
+
322
+ Drop `tomli` / `toml` from `dependencies` for Python 3.11+ projects.
323
+
251
324
  ## FORBIDDEN
252
325
 
253
- 1. **Hardcoded credentials** use `.env` + Pydantic Settings
254
- 2. **No error handling on API calls** — always try/except + retry
255
- 3. **No logging** every script must log actions and errors
256
- 4. **`requests` library** use `httpx` (modern, sync+async)
257
- 5. **Print statements for output** use `logging` or `rich`
258
- 6. **No `--dry-run` flag** destructive scripts must support dry run
259
- 7. **SQL without parameterization** always use `?` placeholders
260
- 8. **No `.env.example`** always provide template for credentials
326
+ | Anti-pattern | Reason |
327
+ |---|---|
328
+ | Hardcoded credentials | Use `.env` + `BaseSettings(extra="forbid")` |
329
+ | No retry on flaky API | Use `tenacity` with exponential backoff + jitter |
330
+ | `print()` for output | Use `logging` + `rich` (machine-readable + human-readable) |
331
+ | `requests` library | Use `httpx` (sync + async, http/2, modern API) |
332
+ | `pip install` in new projects | Use `uv add` (Astral's `uv` is the 2026 default) |
333
+ | Missing `uv.lock` in repo | Builds become non-reproducible |
334
+ | f-string interpolation in SQL | SQL injection — always parameterise |
335
+ | No `--dry-run` on destructive scripts | Always provide a safe preview mode |
336
+ | No `.env.example` | Onboarding nightmare |
337
+ | Per-tool config files (`.flake8`, `.isort.cfg`, `pyproject` for black + isort + ruff) | Consolidate under `[tool.ruff]` + `[tool.pyright]` in `pyproject.toml` |
338
+ | `tomli` / `toml` deps for 3.11+ | Use stdlib `tomllib` |
339
+
340
+ ## See Also
341
+
342
+ - `python-patterns` — version policy, framework selection
343
+ - `async-patterns` — when to switch from `httpx.Client` to `AsyncClient`
344
+ - `pydantic-validation` — `BaseSettings(extra="forbid")` pattern
345
+ - `_shared/skills/secrets-management` — `.env` discipline + 3-layer gitleaks
346
+ - `_shared/skills/observability` — structlog + OpenTelemetry for prod scripts