pythons cripts
This commit is contained in:
@@ -18,6 +18,12 @@ All requests/responses are JSON. Send `Accept: application/json` (Laravel
|
||||
defaults to JSON error responses for API routes regardless, but set it to
|
||||
be safe on validation-error bodies).
|
||||
|
||||
**If you're an agent running on the machine this app is deployed to**, you
|
||||
don't need to build HTTP requests by hand — use the CLI wrappers in
|
||||
`python_scripts/` (`sites.py`, `accounts.py`, `posts.py`; see
|
||||
`python_scripts/README.md`). They default to `http://localhost:8001` and
|
||||
print the same JSON shapes documented below.
|
||||
|
||||
## Mental model
|
||||
|
||||
```
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
# Muninn API scripts
|
||||
|
||||
Thin Python CLI wrappers around this app's JSON API (see `../AI_API.md` for
|
||||
the full API reference). Meant to be called directly by an AI agent on the
|
||||
machine where this app is reachable — no setup beyond Python 3's standard
|
||||
library (no third-party dependencies).
|
||||
|
||||
Default base URL: `http://localhost:8001`. Override with the
|
||||
`MUNINN_API_URL` environment variable if the app runs elsewhere or on a
|
||||
different port.
|
||||
|
||||
Every command prints the API's JSON response to stdout and exits `0` on
|
||||
success. On an HTTP error or if the server can't be reached, it prints a
|
||||
JSON error object to stderr and exits `1`.
|
||||
|
||||
## Sites (read-only)
|
||||
|
||||
```
|
||||
python3 sites.py list [--name NAME]
|
||||
python3 sites.py get SITE_ID
|
||||
```
|
||||
|
||||
## Social Accounts (read-only)
|
||||
|
||||
```
|
||||
python3 accounts.py list [--network NETWORK] [--type TYPE]
|
||||
python3 accounts.py get ACCOUNT_ID
|
||||
```
|
||||
|
||||
## Posts (full CRUD)
|
||||
|
||||
```
|
||||
python3 posts.py list [--status STATUS] [--site-id ID] [--social-account-id ID] [--due]
|
||||
python3 posts.py get POST_ID
|
||||
python3 posts.py create --site-id ID --type TYPE --body BODY [--social-account-id ID] [--title TITLE] [--status STATUS] [--scheduled-at DATETIME]
|
||||
python3 posts.py update POST_ID [--site-id ID] [--social-account-id ID] [--type TYPE] [--title TITLE] [--body BODY] [--status STATUS] [--scheduled-at DATETIME] [--published-url URL]
|
||||
python3 posts.py delete POST_ID
|
||||
```
|
||||
|
||||
`--due` finds posts that are `scheduled` and past their `scheduled_at` —
|
||||
the query to run to discover what needs publishing right now.
|
||||
|
||||
Publishing itself is out of scope for these scripts (they only wrap the
|
||||
HTTP API). See `../AI_API.md` for how to actually publish a post
|
||||
(`php artisan posts:publish` for WordPress, the `sidecars/reddit-post`
|
||||
sidecar for Reddit, or your own method for anything else — then report
|
||||
back with `posts.py update POST_ID --status ...`).
|
||||
Binary file not shown.
Executable
+62
@@ -0,0 +1,62 @@
|
||||
"""Shared HTTP helper for the Muninn API scripts.
|
||||
|
||||
Not meant to be run directly - imported by sites.py, accounts.py, posts.py.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
|
||||
DEFAULT_BASE_URL = "http://localhost:8001"
|
||||
|
||||
|
||||
def base_url() -> str:
|
||||
return os.environ.get("MUNINN_API_URL", DEFAULT_BASE_URL).rstrip("/")
|
||||
|
||||
|
||||
def request(method: str, path: str, params: dict | None = None, body: dict | None = None) -> None:
|
||||
"""Call the API and print the JSON response to stdout.
|
||||
|
||||
Exits 0 on a 2xx response. On an HTTP error or network failure, prints
|
||||
a JSON error object to stderr and exits 1.
|
||||
"""
|
||||
url = f"{base_url()}{path}"
|
||||
|
||||
if params:
|
||||
query = "&".join(
|
||||
f"{key}={urllib.parse.quote(str(value))}"
|
||||
for key, value in params.items()
|
||||
if value is not None
|
||||
)
|
||||
if query:
|
||||
url = f"{url}?{query}"
|
||||
|
||||
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||
headers = {"Accept": "application/json"}
|
||||
if data is not None:
|
||||
headers["Content-Type"] = "application/json"
|
||||
|
||||
req = urllib.request.Request(url, data=data, method=method, headers=headers)
|
||||
|
||||
try:
|
||||
with urllib.request.urlopen(req) as response:
|
||||
payload = response.read().decode("utf-8")
|
||||
except urllib.error.HTTPError as error:
|
||||
body_text = error.read().decode("utf-8")
|
||||
try:
|
||||
parsed = json.loads(body_text)
|
||||
except json.JSONDecodeError:
|
||||
parsed = {"error": body_text}
|
||||
print(json.dumps(parsed, indent=2), file=sys.stderr)
|
||||
sys.exit(1)
|
||||
except urllib.error.URLError as error:
|
||||
print(json.dumps({"error": f"could not reach {url}: {error.reason}"}, indent=2), file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
if payload:
|
||||
parsed = json.loads(payload)
|
||||
print(json.dumps(parsed, indent=2))
|
||||
sys.exit(0)
|
||||
Executable
+40
@@ -0,0 +1,40 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Read Social Accounts from the Muninn API.
|
||||
|
||||
Usage:
|
||||
python3 accounts.py list [--network NETWORK] [--type TYPE]
|
||||
python3 accounts.py get ACCOUNT_ID
|
||||
|
||||
Base URL defaults to http://localhost:8001, override with the
|
||||
MUNINN_API_URL environment variable.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
|
||||
import _client
|
||||
|
||||
NETWORKS = ["twitter", "instagram", "facebook", "reddit", "tiktok", "linkedin", "youtube", "other"]
|
||||
TYPES = ["post", "comment", "like"]
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="Social Accounts API client")
|
||||
subparsers = parser.add_subparsers(dest="command", required=True)
|
||||
|
||||
list_parser = subparsers.add_parser("list", help="List all social accounts")
|
||||
list_parser.add_argument("--network", choices=NETWORKS, help="Exact match filter on network")
|
||||
list_parser.add_argument("--type", choices=TYPES, help="Exact match filter on type")
|
||||
|
||||
get_parser = subparsers.add_parser("get", help="Get a single social account")
|
||||
get_parser.add_argument("account_id", type=int)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.command == "list":
|
||||
_client.request("GET", "/api/accounts", params={"network": args.network, "type": args.type})
|
||||
elif args.command == "get":
|
||||
_client.request("GET", f"/api/accounts/{args.account_id}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+113
@@ -0,0 +1,113 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Read and write Posts on the Muninn API.
|
||||
|
||||
Usage:
|
||||
python3 posts.py list [--status STATUS] [--site-id ID] [--social-account-id ID] [--due]
|
||||
python3 posts.py get POST_ID
|
||||
python3 posts.py create --site-id ID --type TYPE --body BODY
|
||||
[--social-account-id ID] [--title TITLE]
|
||||
[--status STATUS] [--scheduled-at DATETIME]
|
||||
python3 posts.py update POST_ID [--site-id ID] [--social-account-id ID]
|
||||
[--type TYPE] [--title TITLE] [--body BODY]
|
||||
[--status STATUS] [--scheduled-at DATETIME]
|
||||
[--published-url URL]
|
||||
python3 posts.py delete POST_ID
|
||||
|
||||
Base URL defaults to http://localhost:8001, override with the
|
||||
MUNINN_API_URL environment variable.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
|
||||
import _client
|
||||
|
||||
TYPES = ["twitter", "instagram", "facebook", "reddit", "tiktok", "linkedin", "youtube", "blog", "other"]
|
||||
STATUSES = ["draft", "scheduled", "published", "failed"]
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="Posts API client")
|
||||
subparsers = parser.add_subparsers(dest="command", required=True)
|
||||
|
||||
list_parser = subparsers.add_parser("list", help="List posts")
|
||||
list_parser.add_argument("--status", choices=STATUSES)
|
||||
list_parser.add_argument("--site-id", type=int)
|
||||
list_parser.add_argument("--social-account-id", type=int)
|
||||
list_parser.add_argument(
|
||||
"--due", action="store_true", help="Only posts that are scheduled and due now"
|
||||
)
|
||||
|
||||
get_parser = subparsers.add_parser("get", help="Get a single post")
|
||||
get_parser.add_argument("post_id", type=int)
|
||||
|
||||
create_parser = subparsers.add_parser("create", help="Create a post")
|
||||
create_parser.add_argument("--site-id", type=int, required=True)
|
||||
create_parser.add_argument("--type", choices=TYPES, required=True)
|
||||
create_parser.add_argument("--body", required=True)
|
||||
create_parser.add_argument("--social-account-id", type=int)
|
||||
create_parser.add_argument("--title")
|
||||
create_parser.add_argument("--status", choices=STATUSES)
|
||||
create_parser.add_argument("--scheduled-at", help="ISO 8601 datetime, e.g. 2026-07-20T09:00:00")
|
||||
|
||||
update_parser = subparsers.add_parser("update", help="Update a post")
|
||||
update_parser.add_argument("post_id", type=int)
|
||||
update_parser.add_argument("--site-id", type=int)
|
||||
update_parser.add_argument("--social-account-id", type=int)
|
||||
update_parser.add_argument("--type", choices=TYPES)
|
||||
update_parser.add_argument("--title")
|
||||
update_parser.add_argument("--body")
|
||||
update_parser.add_argument("--status", choices=STATUSES)
|
||||
update_parser.add_argument("--scheduled-at", help="ISO 8601 datetime")
|
||||
update_parser.add_argument("--published-url")
|
||||
|
||||
delete_parser = subparsers.add_parser("delete", help="Delete a post")
|
||||
delete_parser.add_argument("post_id", type=int)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.command == "list":
|
||||
params = {
|
||||
"status": args.status,
|
||||
"site_id": args.site_id,
|
||||
"social_account_id": args.social_account_id,
|
||||
}
|
||||
if args.due:
|
||||
params["due"] = 1
|
||||
_client.request("GET", "/api/posts", params=params)
|
||||
|
||||
elif args.command == "get":
|
||||
_client.request("GET", f"/api/posts/{args.post_id}")
|
||||
|
||||
elif args.command == "create":
|
||||
body = {
|
||||
"site_id": args.site_id,
|
||||
"social_account_id": args.social_account_id,
|
||||
"type": args.type,
|
||||
"title": args.title,
|
||||
"body": args.body,
|
||||
"status": args.status,
|
||||
"scheduled_at": args.scheduled_at,
|
||||
}
|
||||
body = {key: value for key, value in body.items() if value is not None}
|
||||
_client.request("POST", "/api/posts", body=body)
|
||||
|
||||
elif args.command == "update":
|
||||
body = {
|
||||
"site_id": args.site_id,
|
||||
"social_account_id": args.social_account_id,
|
||||
"type": args.type,
|
||||
"title": args.title,
|
||||
"body": args.body,
|
||||
"status": args.status,
|
||||
"scheduled_at": args.scheduled_at,
|
||||
"published_url": args.published_url,
|
||||
}
|
||||
body = {key: value for key, value in body.items() if value is not None}
|
||||
_client.request("PATCH", f"/api/posts/{args.post_id}", body=body)
|
||||
|
||||
elif args.command == "delete":
|
||||
_client.request("DELETE", f"/api/posts/{args.post_id}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+36
@@ -0,0 +1,36 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Read Sites from the Muninn API.
|
||||
|
||||
Usage:
|
||||
python3 sites.py list [--name NAME]
|
||||
python3 sites.py get SITE_ID
|
||||
|
||||
Base URL defaults to http://localhost:8001, override with the
|
||||
MUNINN_API_URL environment variable.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
|
||||
import _client
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="Sites API client")
|
||||
subparsers = parser.add_subparsers(dest="command", required=True)
|
||||
|
||||
list_parser = subparsers.add_parser("list", help="List all sites")
|
||||
list_parser.add_argument("--name", help="Partial match filter on name")
|
||||
|
||||
get_parser = subparsers.add_parser("get", help="Get a single site")
|
||||
get_parser.add_argument("site_id", type=int)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.command == "list":
|
||||
_client.request("GET", "/api/sites", params={"name": args.name})
|
||||
elif args.command == "get":
|
||||
_client.request("GET", f"/api/sites/{args.site_id}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user