
Fabric Api
- 70 installs
- 3 repo stars
- Updated June 29, 2026
- tristanmanchester/agent-skills
Helps with ai & agent building tasks during AI-assisted development.
About
fabric-api is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- fabric-api
- AI & Agent Building
- AI-coding skill
Fabric Api by the numbers
- 70 all-time installs (skills.sh)
- Ranked #5,726 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/tristanmanchester/agent-skills --skill fabric-apiAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 70 |
|---|---|
| repo stars | ★ 3 |
| Last updated | June 29, 2026 |
| Repository | tristanmanchester/agent-skills ↗ |
What it does
Helps with ai & agent building tasks during AI-assisted development.
Files
Fabric API (HTTP via Node/Python)
Use this skill when you need to read or write content in a user's Fabric workspace using the Fabric HTTP API (https://api.fabric.so).
This version avoids bash-only wrapper scripts. It ships cross-platform helpers:
- Node:
{baseDir}/scripts/fabric.mjs(recommended) - Python:
{baseDir}/scripts/fabric.py
Critical gotchas (read first)
- There is no
POST /v2/notesendpoint in the bundled OpenAPI spec. To create a “note”, use `POST /v2/notepads`. - Most create endpoints require `parentId`:
- A folder UUID or one of:
@alias::inbox,@alias::bin - Notepad creation requires:
parentId- and either
text(markdown string) orydoc(advanced/structured) tagsmust be an array of objects, each item either:{ "name": "tag name" }or{ "id": "<uuid>" }- Never strings, never nested arrays.
- Field name gotcha: the API schema uses
name(nottitle). If the user says “title”, map it tonamein requests.
When the user doesn’t specify a destination folder, default to:
parentId: "@alias::inbox"
Setup (OpenClaw / Clawdbot)
This skill expects the Fabric API key in:
FABRIC_API_KEY
OpenClaw config example (~/.openclaw/openclaw.json):
{
skills: {
entries: {
"fabric-api": {
enabled: true,
apiKey: "YOUR_FABRIC_API_KEY"
}
}
}
}Notes:
apiKeyis a convenience for skills that declareprimaryEnv; it injectsFABRIC_API_KEYfor the duration of an agent run.- Don’t paste the API key into prompts, client-side code, or logs.
HTTP basics
- Base URL:
https://api.fabric.so(override withFABRIC_BASEif needed) - Auth header:
X-Api-Key: $FABRIC_API_KEY - JSON header (for JSON bodies):
Content-Type: application/json
Convenience scripts (cross-platform)
Node helper (recommended)
node {baseDir}/scripts/fabric.mjs GET /v2/user/me
node {baseDir}/scripts/fabric.mjs POST /v2/notepads --json '{"name":"Test note","text":"Hello","parentId":"@alias::inbox"}'Python helper
python3 {baseDir}/scripts/fabric.py GET /v2/user/me
python3 {baseDir}/scripts/fabric.py POST /v2/notepads --json '{"name":"Test note","text":"Hello","parentId":"@alias::inbox"}'Notes:
- Both helpers print the response body on success.
- On HTTP errors (4xx/5xx), they print
HTTP <code> <reason>to stderr and still print the response body, then exit non‑zero (similar tocurl --fail-with-body). - If you pass an absolute URL (
https://...), the helpers do not attachX-Api-Keyunless you explicitly pass--with-key.
Core workflows
1) Create a notepad (note)
Endpoint: POST /v2/notepads
Rules:
- Map user “title” →
name - Use
textfor markdown content - Always include
parentId - If you’re debugging 400s, start minimal (required fields only), then add
name, thentags.
Minimal create:
node {baseDir}/scripts/fabric.mjs POST /v2/notepads --json '{"parentId":"@alias::inbox","text":"Hello"}'Create with a name:
node {baseDir}/scripts/fabric.mjs POST /v2/notepads --json '{"name":"Calendar Test Note","text":"Created via OpenClaw","parentId":"@alias::inbox"}'Create with tags (correct shape):
node {baseDir}/scripts/fabric.mjs POST /v2/notepads --json '{"name":"Ideas","text":"# Ideas\\n\\n- First\\n- Second\\n","parentId":"@alias::inbox","tags":[{"name":"ideas"},{"name":"draft"}]}'If you keep seeing tag validation errors, temporarily omit tags and create the notepad first.
2) Create a folder
Endpoint: POST /v2/folders
node {baseDir}/scripts/fabric.mjs POST /v2/folders --json '{"name":"My new folder","parentId":"@alias::inbox","description":null}'3) Create a bookmark
Endpoint: POST /v2/bookmarks
node {baseDir}/scripts/fabric.mjs POST /v2/bookmarks --json '{"url":"https://example.com","parentId":"@alias::inbox","name":"Example","tags":[{"name":"reading"}]}'4) Browse resources (list children of a folder)
Endpoint: POST /v2/resources/filter
Important:
- This endpoint’s
parentIdexpects a UUID (not an alias). - If you only have an alias, resolve it by listing resource roots and picking the inbox/bin folder ID.
node {baseDir}/scripts/fabric.mjs POST /v2/resources/filter --json '{"parentId":"PARENT_UUID_HERE","limit":50,"order":{"property":"modifiedAt","direction":"DESC"}}'5) Search
Endpoint: POST /v2/search
Use search when the user gives a fuzzy description (“the note about…”).
node {baseDir}/scripts/fabric.mjs POST /v2/search --json '{"queries":[{"mode":"text","text":"meeting notes","filters":{"kinds":["notepad"]}}],"pagination":{"page":1,"pageSize":20},"sort":{"field":"modifiedAt","order":"desc"}}'Error handling + retries (practical guidance)
- 400 Bad Request: schema validation. Re-check required fields, and that
tagsis[{name}|{id}]not nested. - 401/403: auth/subscription/permission. Stop and report the error details; don’t brute-force.
- 404: wrong endpoint, wrong ID, or no access.
- 429: rate limiting. Back off (sleep + jitter) and retry reads. Avoid blind retries on create (you may create duplicates).
- 5xx: transient; retry with backoff.
Reference files
- OpenAPI spec (source of truth):
{baseDir}/fabric-api.yaml - Extra schema notes:
{baseDir}/references/REFERENCE.md - Debug playbook:
{baseDir}/references/TROUBLESHOOTING.md
Fabric API skill reference
This file is extra detail for the fabric-api skill. Read it when you need the exact schema expectations or you’re debugging validation errors.
Endpoint map (from fabric-api.yaml)
Creation:
- Notepads (“notes”):
POST /v2/notepads - Folders:
POST /v2/folders - Bookmarks:
POST /v2/bookmarks - Files:
POST /v2/files(requires/v2/uploadfirst) - Tags:
POST /v2/tags
Read / browse:
- Who am I:
GET /v2/user/me - Resource roots:
GET /v2/resource-roots - Get resource by ID:
GET /v2/resources/{resourceId} - List resources:
POST /v2/resources/filter - Search:
POST /v2/search - List tags:
GET /v2/tags
Dangerous / destructive:
- Delete:
POST /v2/resource/delete - Recover:
POST /v2/resource/recover
“Notes” vs “notepads”
The Fabric HTTP API models a note-like item as a resource of kind `notepad` and exposes a dedicated create endpoint:
- Create:
POST /v2/notepads
If you previously tried POST /v2/notes and got a 404, that matches the OpenAPI spec: there is no /v2/notes path defined.
parentId: valid values and when aliases work
For create endpoints (/v2/notepads, /v2/folders, /v2/bookmarks, /v2/files), parentId is:
- a UUID (folder resource id), or
- one of the RootAlias enum values:
@alias::inbox@alias::bin
This alias resolution happens “during request handling” (server-side). It’s a convenience for creates.
For filtering/listing (POST /v2/resources/filter), parentId is defined as a UUID pattern and does not accept aliases. Use a UUID there.
How to resolve Inbox/Bin IDs
Call:
GET /v2/resource-roots
Find the root with type: SYSTEM and subtype: inbox or subtype: bin, then use folder.id as the UUID.
Tags: exact schema (and the common failure modes)
Many create endpoints share the same tags schema:
"tags": [
{ "name": "ideas" },
{ "id": "550e8400-e29b-41d4-a716-446655440000" }
]Key rules:
tagsis an array.- Each item must be an object.
- Each object must be either:
{ "name": "<string up to 255>" }(create or reuse tag by name), or{ "id": "<uuid>" }(attach an existing tag by id)- Don’t send:
["tag1","tag2"](strings are invalid)[["tag1"],["tag2"]](nested arrays are invalid){ "tags": { ... } }(wrong shape){ "name": ["ideas"] }(wrong type)
If you’re unsure, omit tags for the initial create request. Then:
1) List tags (GET /v2/tags?name=...) or create tag (POST /v2/tags) 2) Use tag IDs or names in a follow-up create/attach flow (Fabric currently supports tags on create; attaching after create may require an endpoint not in this spec).
Notepad creation schema in plain English
POST /v2/notepads body is roughly:
- Required:
parentId- and either:
text(markdown string) ORydoc(structured)- Optional:
name(nullable, 1-255 chars)tags(array, shape above)comment(object withcontent)
Minimal valid example:
{
"parentId": "@alias::inbox",
"text": "Hello"
}Recommended example:
{
"name": "My note title",
"parentId": "@alias::inbox",
"text": "# Heading\n\nSome text\n"
}name vs title
The schema uses name. If you have a “title” value (from a UI or user phrasing), map it to name in the payload.
File upload (3-step flow)
Creating a file is a two-endpoint flow:
1) GET /v2/upload?filename=...&size=... → returns presigned url and required headers 2) PUT the file bytes to the presigned URL (no API key) 3) POST /v2/files with:
attachment.path: the path part of the presigned URL (strip host + query string)attachment.filename: original filenameparentId: uuid or aliasmimeType
See the examples in SKILL.md and consult fabric-api.yaml for the exact FileCreation schema.
Keep the OpenAPI close
When debugging, open {baseDir}/fabric-api.yaml and search for the exact endpoint:
paths: /v2/notepadspaths: /v2/resources/filtercomponents: schemas: RootAlias,FileCreation, etc.
That spec is the source of truth for field names and required properties.
Troubleshooting Fabric API requests
This is a quick playbook for debugging the most common failures when using the Fabric HTTP API via this skill.
Always keep error bodies visible
When debugging schema validation (400s), the response body is usually the fastest way to fix the payload.
- If you use the Node/Python helpers shipped with this skill:
- They print
HTTP <code> <reason>to stderr on errors and still print the response body, then exit non‑zero. - Node:
node {baseDir}/scripts/fabric.mjs ... - Python:
python3 {baseDir}/scripts/fabric.py ...
- If you use raw
curl, prefer: --fail-with-body(shows 4xx/5xx bodies; newer curl)-sS(silent, but still prints errors)
400 Bad Request
Almost always schema validation.
Checklist:
- Are you using the correct endpoint?
- Create “note” →
POST /v2/notepads(not/v2/notes) - Did you include all required fields?
- Notepad create:
parentIdand (textorydoc) - Bookmark create:
urlandparentId - Folder create:
parentId(andnameif you want one) - Is
parentIdvalid? - UUID or
@alias::inbox/@alias::binfor create endpoints - Are
tagsshaped correctly? tags: [{"name":"x"},{"id":"<uuid>"}]
Common tag mistakes
tags: ["a","b"]→ invalid (strings)tags: [["a"],["b"]]→ invalid (nested arrays)tags: [{ "name": ["a"] }]→ invalid (name must be string)
401 Unauthorized / 403 Forbidden
- 401 usually means the API key is missing/invalid.
- 403 usually means:
- the key lacks access to the resource (workspace permissions), or
- your plan/subscription hit a limit.
Action:
- Stop and surface the
detail/ error message to the user. - Do not brute-force retries.
404 Not Found
Most commonly:
- Wrong path (
/v2/notesvs/v2/notepads) - Wrong
resourceId - Wrong
parentId(UUID doesn’t exist / you don’t have access) - Trying to use an alias where only UUID is accepted (e.g.
POST /v2/resources/filter)
429 Too Many Requests
Treat as rate limiting.
Recommended behaviour:
- Back off with jitter (sleep a bit, try again).
- Retry reads safely.
- Avoid automatic retries on creates unless you’ve built idempotency/dedupe checks (otherwise you can create duplicates).
5xx Server errors
Likely transient:
- Retry with exponential backoff.
- If repeatable, capture the full response body and report.
Debugging “parentId alias” issues
If @alias::inbox works for creates but you need the actual UUID (e.g. to list children):
1) GET /v2/resource-roots 2) Find the SYSTEM inbox root 3) Use its folder.id as the UUID for POST /v2/resources/filter
Capture a minimal repro payload
When you hit validation errors:
1) Remove optional fields until it works (start with just required fields) 2) Add fields back one by one:
- first
name - then
tags - then
comment
This makes it obvious which field is breaking schema validation.
#!/usr/bin/env node
/**
* Fabric API helper (Node.js)
*
* Cross-platform wrapper for calling the Fabric HTTP API without relying on bash.
*
* Usage:
* node scripts/fabric.mjs GET /v2/user/me
* node scripts/fabric.mjs POST /v2/notepads --json '{"name":"Hello","text":"World","parentId":"@alias::inbox"}'
* node scripts/fabric.mjs POST /v2/notepads --file payload.json
* cat payload.json | node scripts/fabric.mjs POST /v2/notepads
*
* Env:
* FABRIC_API_KEY (required for API paths like /v2/..., unless --no-key)
* FABRIC_BASE (optional, default: https://api.fabric.so)
*/
import fs from 'node:fs';
import { readFile } from 'node:fs/promises';
import process from 'node:process';
function printUsage(exitCode = 2) {
const msg = `Usage:
node scripts/fabric.mjs <METHOD> <PATH_OR_URL> [options]
Examples:
node scripts/fabric.mjs GET /v2/user/me
node scripts/fabric.mjs POST /v2/notepads --json '{"name":"Test","text":"Hello","parentId":"@alias::inbox"}'
node scripts/fabric.mjs POST /v2/notepads --file payload.json
cat payload.json | node scripts/fabric.mjs POST /v2/notepads
Options:
--base <url> Base URL for API paths (default: $FABRIC_BASE or https://api.fabric.so)
--json <string> Send request body as a literal string (typically JSON)
--file <path> Send request body from a file (text by default)
--raw With --file, send file bytes (octet-stream). Useful for presigned PUT uploads.
--header "K: V" Add a header (repeatable)
--no-key Do not attach X-Api-Key (useful for presigned URLs)
--with-key Force attaching X-Api-Key even for absolute URLs
--pretty Pretty-print JSON responses
--no-pretty Do not pretty-print JSON responses
-h, --help Show this help
Notes:
* If <PATH_OR_URL> is an absolute URL (https://...), the script will NOT send X-Api-Key unless you pass --with-key.
* For relative paths (e.g. /v2/notepads), the script requires FABRIC_API_KEY unless --no-key.
`;
process.stderr.write(msg);
process.exit(exitCode);
}
function isAbsoluteUrl(s) {
return /^https?:\/\//i.test(s);
}
function joinBase(base, path) {
const b = base.replace(/\/+$/, '');
const p = path.startsWith('/') ? path : `/${path}`;
return `${b}${p}`;
}
function parseHeaderLine(line) {
const idx = line.indexOf(':');
if (idx === -1) {
throw new Error(`Invalid --header value (missing ':'): ${line}`);
}
const key = line.slice(0, idx).trim();
const value = line.slice(idx + 1).trim();
if (!key) throw new Error(`Invalid --header key: ${line}`);
return [key, value];
}
async function readStdinAsBuffer() {
const chunks = [];
for await (const chunk of process.stdin) {
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
}
return Buffer.concat(chunks);
}
async function main() {
const argv = process.argv.slice(2);
if (argv.length === 0 || argv.includes('-h') || argv.includes('--help')) {
printUsage(0);
}
const method = (argv[0] || '').toUpperCase();
const target = argv[1];
if (!method || !target) {
printUsage(2);
}
let base = process.env.FABRIC_BASE || 'https://api.fabric.so';
let jsonBody = null;
let filePath = null;
let raw = false;
let noKey = false;
let withKey = false;
// Default pretty-print when stdout is a TTY
let pretty = process.stdout.isTTY;
const extraHeaders = [];
for (let i = 2; i < argv.length; i += 1) {
const a = argv[i];
if (a === '--base') {
base = argv[++i];
if (!base) throw new Error('Missing value for --base');
continue;
}
if (a === '--json') {
jsonBody = argv[++i];
if (jsonBody == null) throw new Error('Missing value for --json');
continue;
}
if (a === '--file') {
filePath = argv[++i];
if (!filePath) throw new Error('Missing value for --file');
continue;
}
if (a === '--raw') {
raw = true;
continue;
}
if (a === '--header') {
const h = argv[++i];
if (!h) throw new Error('Missing value for --header');
extraHeaders.push(parseHeaderLine(h));
continue;
}
if (a === '--no-key') {
noKey = true;
continue;
}
if (a === '--with-key') {
withKey = true;
continue;
}
if (a === '--pretty') {
pretty = true;
continue;
}
if (a === '--no-pretty') {
pretty = false;
continue;
}
// Unknown option
throw new Error(`Unknown option: ${a}`);
}
const abs = isAbsoluteUrl(target);
const url = abs ? target : joinBase(base, target);
// Default: don't leak API keys to presigned URLs
const shouldSendKey = !noKey && (!abs || withKey);
const apiKey = process.env.FABRIC_API_KEY;
if (shouldSendKey && (!apiKey || apiKey.trim() === '')) {
process.stderr.write('ERROR: FABRIC_API_KEY is not set.\n');
process.stderr.write('Set it in the environment or via OpenClaw skills config (skills.entries.fabric-api.apiKey).\n');
process.exit(2);
}
let body = undefined;
let bodyIsStream = false;
let inferredContentType = null;
if (jsonBody != null) {
body = jsonBody;
inferredContentType = 'application/json';
} else if (filePath != null) {
if (raw) {
body = fs.createReadStream(filePath);
bodyIsStream = true;
inferredContentType = 'application/octet-stream';
} else {
body = await readFile(filePath, 'utf8');
inferredContentType = 'application/json';
}
} else if (!process.stdin.isTTY) {
const buf = await readStdinAsBuffer();
if (buf.length > 0) {
// Assume JSON-by-default when piping into an API call.
body = buf.toString('utf8');
inferredContentType = 'application/json';
}
}
const headers = new Headers();
// User-provided headers first (so our defaults can be overridden if desired).
for (const [k, v] of extraHeaders) {
headers.set(k, v);
}
if (!headers.has('accept')) {
headers.set('Accept', 'application/json');
}
if (shouldSendKey) {
headers.set('X-Api-Key', apiKey);
}
if (body !== undefined && !headers.has('content-type') && inferredContentType) {
headers.set('Content-Type', inferredContentType);
}
const fetchOpts = {
method,
headers,
body,
};
// Node fetch requires this when streaming request bodies.
if (bodyIsStream) {
fetchOpts.duplex = 'half';
}
let res;
try {
res = await fetch(url, fetchOpts);
} catch (err) {
process.stderr.write(`ERROR: request failed: ${err?.message || String(err)}\n`);
process.exit(1);
}
const text = await res.text();
const contentType = res.headers.get('content-type') || '';
const isJson = /(^|\s|;)application\/(json|[^;]+\+json)(;|\s|$)/i.test(contentType);
if (!res.ok) {
process.stderr.write(`HTTP ${res.status} ${res.statusText}\n`);
if (text) process.stdout.write(text);
process.exit(1);
}
if (!text) return;
if (pretty && isJson) {
try {
const obj = JSON.parse(text);
process.stdout.write(JSON.stringify(obj, null, 2));
if (process.stdout.isTTY) process.stdout.write('\n');
return;
} catch {
// Fall back to raw text
}
}
process.stdout.write(text);
if (process.stdout.isTTY) process.stdout.write('\n');
}
main().catch((err) => {
process.stderr.write(`ERROR: ${err?.stack || err?.message || String(err)}\n`);
process.exit(1);
});
#!/usr/bin/env python3
"""Fabric API helper (Python)
Cross-platform wrapper for calling the Fabric HTTP API without relying on bash.
Usage:
python3 scripts/fabric.py GET /v2/user/me
python3 scripts/fabric.py POST /v2/notepads --json '{"name":"Test","text":"Hello","parentId":"@alias::inbox"}'
python3 scripts/fabric.py POST /v2/notepads --file payload.json
type payload.json | python scripts/fabric.py POST /v2/notepads # Windows cmd.exe
Env:
FABRIC_API_KEY (required for API paths like /v2/..., unless --no-key)
FABRIC_BASE (optional, default: https://api.fabric.so)
Notes:
* If the target is an absolute URL (https://...), the script will NOT send X-Api-Key unless --with-key.
* This script uses only Python's standard library (urllib).
"""
from __future__ import annotations
import argparse
import json
import os
import re
import sys
import urllib.error
import urllib.request
from pathlib import Path
from typing import Dict, List, Tuple
def is_absolute_url(s: str) -> bool:
return re.match(r"^https?://", s, re.IGNORECASE) is not None
def join_base(base: str, path: str) -> str:
b = base.rstrip("/")
p = path if path.startswith("/") else f"/{path}"
return f"{b}{p}"
def parse_header_line(line: str) -> Tuple[str, str]:
if ":" not in line:
raise ValueError(f"Invalid --header value (missing ':'): {line}")
key, value = line.split(":", 1)
key = key.strip()
value = value.strip()
if not key:
raise ValueError(f"Invalid --header key: {line}")
return key, value
def has_header(headers: Dict[str, str], name: str) -> bool:
name_l = name.lower()
return any(k.lower() == name_l for k in headers.keys())
def main() -> int:
parser = argparse.ArgumentParser(add_help=True)
parser.add_argument("method", help="HTTP method, e.g. GET/POST/PUT/PATCH/DELETE")
parser.add_argument("target", help="API path (e.g. /v2/notepads) or absolute URL (https://...)")
parser.add_argument("--base", default=os.environ.get("FABRIC_BASE", "https://api.fabric.so"))
parser.add_argument("--json", dest="json_body", default=None, help="Request body as literal string (typically JSON)")
parser.add_argument("--file", dest="file_path", default=None, help="Request body from file")
parser.add_argument("--raw", action="store_true", help="With --file, send raw bytes (octet-stream)")
parser.add_argument("--header", action="append", default=[], help="Header line 'K: V' (repeatable)")
parser.add_argument("--no-key", action="store_true", help="Do not attach X-Api-Key (useful for presigned URLs)")
parser.add_argument("--with-key", action="store_true", help="Force attaching X-Api-Key even for absolute URLs")
parser.add_argument("--pretty", action="store_true", help="Pretty-print JSON responses")
parser.add_argument("--no-pretty", action="store_true", help="Do not pretty-print JSON responses")
args = parser.parse_args()
method = (args.method or "").upper().strip()
if not method:
sys.stderr.write("ERROR: missing method\n")
return 2
abs_url = is_absolute_url(args.target)
url = args.target if abs_url else join_base(args.base, args.target)
# Default: don't leak API keys to presigned URLs
should_send_key = (not args.no_key) and ((not abs_url) or args.with_key)
api_key = os.environ.get("FABRIC_API_KEY", "").strip()
if should_send_key and not api_key:
sys.stderr.write("ERROR: FABRIC_API_KEY is not set.\n")
sys.stderr.write("Set it in the environment or via OpenClaw skills config (skills.entries.fabric-api.apiKey).\n")
return 2
headers: Dict[str, str] = {}
for h in args.header:
k, v = parse_header_line(h)
headers[k] = v
if not has_header(headers, "Accept"):
headers["Accept"] = "application/json"
if should_send_key:
headers["X-Api-Key"] = api_key
body: bytes | None = None
inferred_ct: str | None = None
if args.json_body is not None:
body = args.json_body.encode("utf-8")
inferred_ct = "application/json"
elif args.file_path is not None:
p = Path(args.file_path)
if args.raw:
body = p.read_bytes()
inferred_ct = "application/octet-stream"
else:
body = p.read_text(encoding="utf-8").encode("utf-8")
inferred_ct = "application/json"
elif not sys.stdin.isatty():
data = sys.stdin.buffer.read()
if data:
body = data
inferred_ct = "application/json"
if body is not None and (not has_header(headers, "Content-Type")) and inferred_ct:
headers["Content-Type"] = inferred_ct
req = urllib.request.Request(url=url, data=body, method=method, headers=headers)
try:
with urllib.request.urlopen(req) as resp:
resp_body = resp.read() or b""
ct = resp.headers.get("Content-Type", "") or ""
is_json = "json" in ct.lower()
pretty = args.pretty or (sys.stdout.isatty() and (not args.no_pretty))
if resp_body and pretty and is_json:
try:
obj = json.loads(resp_body.decode("utf-8"))
sys.stdout.write(json.dumps(obj, indent=2, ensure_ascii=False))
sys.stdout.write("\n")
return 0
except Exception:
pass
if resp_body:
sys.stdout.buffer.write(resp_body)
if sys.stdout.isatty():
sys.stdout.write("\n")
return 0
except urllib.error.HTTPError as e:
err_body = e.read() or b""
sys.stderr.write(f"HTTP {e.code} {e.reason}\n")
if err_body:
sys.stdout.buffer.write(err_body)
if sys.stdout.isatty():
sys.stdout.write("\n")
return 1
except Exception as e:
sys.stderr.write(f"ERROR: request failed: {e}\n")
return 1
if __name__ == "__main__":
raise SystemExit(main())