"""iamb: one string in, one address out, and the address is the thing the string is. https://iamb.ai import iamb url = iamb.post("hello") # 'https://iamb.ai/7k3m9q2x8d4f' (a str that also carries .key and .edit) iamb.get(url) # 'hello' iamb.replace(url, "hello again") # the address stays; needs the edit key, which url carries iamb.append(log, "one more line") # a log, a leaderboard site = iamb.bundle({"index.html": "

hi

", "app.js": "console.log(1)"}) iamb.post("level 2", parent=game) # children: levels, replies, saves iamb.children(game) # newest first One file, no dependencies. Works as a command too: python iamb.py post notes.md | python iamb.py get """ import json, os, re, sys, urllib.error, urllib.parse, urllib.request BASE = os.environ.get("IAMB_URL", "https://iamb.ai").rstrip("/") __all__ = ["post", "get", "meta", "replace", "append", "children", "bundle", "delete", "Iamb", "IambError"] __version__ = "0.9.0" class IambError(Exception): """A refusal: .status (404, 413 ...), .code (a short word), and the sentence the server said.""" def __init__(self, status, code, message): super().__init__(message) self.status, self.code = status, code class Iamb(str): """An iamb's address. It is a str, so it prints and concatenates as one; it also knows its key and, where you made it, its edit key.""" key = edit = type = "" facts = {} def __new__(cls, facts): self = super().__new__(cls, facts["url"]) self.facts, self.key, self.edit, self.type = facts, facts["key"], facts.get("edit", ""), facts.get("type", "") return self def _key(ref): m = re.search(r"(?:^|/)([0-9a-hjkmnp-tv-z]{12})(?:[/.?#]|$)", str(ref)) if not m: raise ValueError("not an iamb address or key: %r" % (ref,)) return m.group(1) def _ask(method, path, body=None, edit="", query=None, raw=False, content_type=None): url = BASE + path + ("?" + urllib.parse.urlencode({k: v for k, v in (query or {}).items() if v not in (None, False, "")}) if query else "") data = body.encode() if isinstance(body, str) else body headers = {"Accept": "*/*" if raw else "application/json", "User-Agent": "iamb.py"} if edit: headers["Authorization"] = "Bearer " + edit if content_type: headers["Content-Type"] = content_type try: with urllib.request.urlopen(urllib.request.Request(url, data=data, method=method, headers=headers), timeout=30) as r: return (r.read(), r.headers.get("Content-Type", "")) if raw else json.loads(r.read() or b"{}") except urllib.error.HTTPError as e: text = e.read().decode("utf-8", "replace") try: j = json.loads(text) raise IambError(e.code, j.get("error", ""), j.get("message", text)) from None except ValueError: raise IambError(e.code, "", text.strip()) from None def post(content, *, type=None, title=None, parent=None, ttl=None, freeze=False, append_open=False): """Post a string (or bytes). Returns its address. type: say what it is where a guess could be wrong ('css', 'js', 'txt' ...).""" q = {"type": type, "title": title, "parent": _key(parent) if parent else None, "ttl": ttl, "freeze": 1 if freeze else None, "append": "open" if append_open else None} return Iamb(_ask("POST", "/", content, query=q)) def get(ref, file=None): """Read an iamb: a str where it is text, bytes where it is not. file: a named string of a bundle.""" raw, ctype = _ask("GET", "/" + _key(ref) + ("/" + file if file else ""), raw=True) return raw.decode("utf-8") if "charset" in ctype or ctype.startswith("text/") else raw def meta(ref): """What is known of an iamb: type, bytes, version, created, parent, files ...""" return _ask("GET", "/" + _key(ref) + ".meta") def replace(ref, content, edit=None, file=None): """Put a new string in its place. The address stays. edit: its edit key (taken from ref where you made it in this program).""" return Iamb(_ask("PUT", "/" + _key(ref) + ("/" + file if file else ""), content, edit=edit or getattr(ref, "edit", ""))) def append(ref, line, edit=None): """Add one line to the end. Without an edit key this works where the maker left the iamb open (append_open=True).""" return Iamb(_ask("POST", "/" + _key(ref) + "/append", line, edit=edit or getattr(ref, "edit", ""))) def children(ref, limit=50): """The iambs posted with this one as their parent, newest first: a list of facts (key, url, type, title ...).""" return _ask("GET", "/" + _key(ref) + "/children", query={"limit": limit})["children"] def bundle(files, entry=None, title=None, ttl=None): """Post several named strings as one address ({'index.html': '...', 'app.js': '...', 'img/ship.png': b'...'}). Relative links between them work.""" import base64 packed = {name: ({"base64": base64.b64encode(c).decode()} if isinstance(c, (bytes, bytearray)) else c) for name, c in files.items()} return Iamb(_ask("POST", "/bundle", json.dumps({"files": packed, "entry": entry, "title": title, "ttl": ttl}), content_type="application/json")) def delete(ref, edit=None): """Remove an iamb for good. Needs its edit key.""" _ask("DELETE", "/" + _key(ref), edit=edit or getattr(ref, "edit", ""), raw=True) def main(argv=None): """The command: iamb post [file|-], iamb get
, iamb bundle .""" a = sys.argv[1:] if argv is None else list(argv) try: if a[:1] == ["post"]: data = sys.stdin.buffer.read() if len(a) < 2 or a[1] == "-" else open(a[1], "rb").read() made = post(data) print(made) print("edit key (keep it to change this later):", made.edit, file=sys.stderr) elif a[:1] == ["get"] and len(a) > 1: out = get(a[1]) sys.stdout.buffer.write(out if isinstance(out, bytes) else out.encode()) elif a[:1] == ["bundle"] and len(a) > 1: root = a[1].rstrip("/") files = {os.path.relpath(os.path.join(d, f), root).replace(os.sep, "/"): open(os.path.join(d, f), "rb").read() for d, _, fs in os.walk(root) for f in fs} made = bundle(files) print(made) print("edit key:", made.edit, file=sys.stderr) else: print(__doc__) except IambError as e: sys.exit("iamb: %s" % e) if __name__ == "__main__": main()