"""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()