makememe 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,154 @@
1
+ Metadata-Version: 2.4
2
+ Name: makememe
3
+ Version: 0.1.0
4
+ Summary: A tiny zero-dependency CLI for generating memes via the free memegen.link API. Agent-friendly.
5
+ Project-URL: Homepage, https://pypi.org/project/makememe/
6
+ Project-URL: memegen.link API, https://api.memegen.link
7
+ Author: Dhruv Mehra
8
+ License: MIT
9
+ License-File: LICENSE
10
+ Keywords: agent,claude,cli,codex,meme,memegen
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Topic :: Multimedia :: Graphics
16
+ Classifier: Topic :: Utilities
17
+ Requires-Python: >=3.8
18
+ Description-Content-Type: text/markdown
19
+
20
+ # meme
21
+
22
+ A tiny, zero-dependency CLI for generating memes via the free
23
+ [memegen.link](https://memegen.link) API. No API key, no signup, stdlib-only.
24
+
25
+ Built to be **agent-friendly**: predictable stdout, a `--json` mode, and a
26
+ bundled Claude Code skill so coding agents (Claude Code, Codex, etc.) can drive
27
+ it directly.
28
+
29
+ ```bash
30
+ meme drake "not reading docs" "reading docs" -o out.png
31
+ ```
32
+
33
+ ## Install
34
+
35
+ Requires Python 3.8+.
36
+
37
+ ```bash
38
+ pip install makememe
39
+ # or, as an isolated tool:
40
+ uv tool install makememe # or: pipx install makememe
41
+ ```
42
+
43
+ Run once without installing:
44
+
45
+ ```bash
46
+ uvx --from makememe meme drake "a" "b"
47
+ ```
48
+
49
+ ## Usage
50
+
51
+ ```bash
52
+ meme <template> "top line" "bottom line" [-o out.png]
53
+ ```
54
+
55
+ | Flag | Meaning |
56
+ |------|---------|
57
+ | `-o, --out` | output file (default `meme.png`) |
58
+ | `--bg URL` | use a custom background image instead of a template |
59
+ | `--ext` | `png` (default), `jpg`, `webp`, or `gif` |
60
+ | `--style` / `--font` | template style variant / font override |
61
+ | `--print-url` | print the image URL, don't download |
62
+ | `--json` | machine-readable output (for scripts/agents) |
63
+ | `--list` | list available template ids |
64
+
65
+ ### Examples
66
+
67
+ ```bash
68
+ meme drake "manual deploys" "ci/cd"
69
+ meme same "after I sold" "if I held" "same picture"
70
+ meme --bg https://example.com/pic.png "_" "DODGED"
71
+ meme regret "SOLD @ 620" "NOW 780 (+26%)" --print-url
72
+ meme --list
73
+ meme --list --json
74
+ ```
75
+
76
+ ### Text that starts with `-`
77
+
78
+ If a caption line begins with `-` (e.g. `"-26%"`), put `--` before your lines so
79
+ it isn't parsed as a flag:
80
+
81
+ ```bash
82
+ meme regret --json -- "-26%" "WHY"
83
+ ```
84
+
85
+ (Put flags like `--json`/`-o` *before* the `--`.)
86
+
87
+ ## For agents (Claude Code / Codex / scripts)
88
+
89
+ The tool is designed to be parsed:
90
+
91
+ - **Plain mode** prints *only the output path* to stdout (status goes to stderr),
92
+ so `path=$(meme drake "a" "b")` just works.
93
+ - **`--json` mode** prints a single JSON object:
94
+
95
+ ```json
96
+ { "path": "meme.png", "bytes": 12345, "url": "https://api.memegen.link/..." }
97
+ ```
98
+
99
+ `--list --json` returns the template catalog as JSON; `--print-url --json`
100
+ returns `{"url": "..."}`; failures return `{"error": "...", "url": "..."}`
101
+ with a non-zero exit code.
102
+
103
+ Typical agent flow:
104
+
105
+ ```bash
106
+ meme --list --json # discover template ids
107
+ meme drake "old way" "new way" --json # generate, capture the path
108
+ ```
109
+
110
+ ### Claude Code skill
111
+
112
+ A ready-made skill lives in [`skill/meme/`](skill/meme/SKILL.md). Install it so
113
+ Claude Code auto-discovers the tool:
114
+
115
+ ```bash
116
+ # user-level (all projects)
117
+ mkdir -p ~/.claude/skills
118
+ cp -r skill/meme ~/.claude/skills/meme
119
+
120
+ # or project-level
121
+ mkdir -p .claude/skills
122
+ cp -r skill/meme .claude/skills/meme
123
+ ```
124
+
125
+ Then just ask Claude Code things like *"make a drake meme about writing tests"*
126
+ and it will call `meme` for you.
127
+
128
+ ## Robustness
129
+
130
+ The CLI is built to fail gracefully, never with a raw traceback:
131
+
132
+ - Network errors, dead hosts, bad template ids (404), oversized text (414), and
133
+ non-image backgrounds (415) all exit non-zero with a one-line message — and
134
+ **no partial/garbage file is written**.
135
+ - `Ctrl-C` exits cleanly (code 130); piping into `head` etc. won't spew a
136
+ `BrokenPipeError`.
137
+ - Arbitrary text — emoji, CJK, `% # & / ? " \`, tabs, control chars, 10k-char
138
+ lines — is escaped safely.
139
+
140
+ Run the test suite (stdlib only, no network needed):
141
+
142
+ ```bash
143
+ python -m unittest discover -s tests
144
+ ```
145
+
146
+ ## How it works
147
+
148
+ It builds a memegen.link URL from your template + text (handling all the fiddly
149
+ path-segment escaping — spaces, `_`, `-`, `?`, `/`, `%`, etc.), downloads the
150
+ image, and saves it. That's the whole trick.
151
+
152
+ ## License
153
+
154
+ MIT
@@ -0,0 +1,8 @@
1
+ memecli/__init__.py,sha256=EE9SwxUJO-M12n6gndkv9FRmwGpUVLp6in5PnNabgKs,248
2
+ memecli/__main__.py,sha256=MSmt_5Xg84uHqzTN38JwgseJK8rsJn_11A8WD99VtEo,61
3
+ memecli/cli.py,sha256=5SXEU2hMt3Q-uvEFHZRW_XmV6mutQ_jeChfWD81T3nU,6359
4
+ makememe-0.1.0.dist-info/METADATA,sha256=7xhT3cJmr5rdEeqAQxkG-t1jO-Gkjm-_XNeYK29DcMY,4303
5
+ makememe-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
6
+ makememe-0.1.0.dist-info/entry_points.txt,sha256=ljxdezWRFBcA7ejxPFh_gEnuxKRf9MISdSYdC94VXBA,42
7
+ makememe-0.1.0.dist-info/licenses/LICENSE,sha256=gguAGcIQbbkCVERLQSJFmckVv0LJD-mHUA5ygWMzofQ,1068
8
+ makememe-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ meme = memecli.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Dhruv Mehra
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
memecli/__init__.py ADDED
@@ -0,0 +1,6 @@
1
+ """meme - a tiny zero-dependency CLI over the free memegen.link API."""
2
+
3
+ from .cli import build_url, download, escape, get_templates, main
4
+
5
+ __version__ = "0.1.0"
6
+ __all__ = ["main", "build_url", "escape", "download", "get_templates", "__version__"]
memecli/__main__.py ADDED
@@ -0,0 +1,4 @@
1
+ from .cli import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
memecli/cli.py ADDED
@@ -0,0 +1,180 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ meme - a tiny CLI over the free memegen.link API.
4
+
5
+ Builds a meme image URL from a template + text lines, downloads it, saves an
6
+ image. No API key, no third-party dependencies (Python stdlib only). Handles URL
7
+ escaping so you never hit the bare-% problem.
8
+
9
+ Examples:
10
+ meme drake "not reading docs" "reading docs"
11
+ meme regret "SOLD IRCTC @ Rs620" "NOW Rs780 (+26%) WHY"
12
+ meme same "after I sold" "if I held" "same picture"
13
+ meme --bg https://example.com/pic.png "_" "DODGED"
14
+ meme drake "a" "b" --print-url # just print the URL, don't download
15
+ meme drake "a" "b" --json # machine-readable output for agents
16
+ meme --list # list available template ids
17
+ meme --list --json # template ids as JSON
18
+
19
+ Find templates: https://api.memegen.link/templates/ (or `meme --list`)
20
+ """
21
+
22
+ import argparse
23
+ import json
24
+ import os
25
+ import sys
26
+ import urllib.parse
27
+ import urllib.request
28
+
29
+ API = "https://api.memegen.link"
30
+
31
+
32
+ # memegen path-segment escaping. Order matters: escape the escape chars first.
33
+ # Confirmed from the API: space->_, _->__, -->--, ?->~q, newline->~n, "->''
34
+ # Others use memegen's documented tilde codes; verify with --print-url if unsure.
35
+ def escape(text):
36
+ if text == "":
37
+ return "_" # memegen renders an empty line as a single underscore
38
+ text = text.replace("_", "__").replace("-", "--")
39
+ text = text.replace(" ", "_").replace("\n", "~n")
40
+ text = text.replace("?", "~q").replace('"', "''").replace("/", "~s")
41
+ # let quote percent-encode the rest (%, #, etc) - memegen decodes %25 reliably.
42
+ # keep the memegen tokens we just produced intact.
43
+ return urllib.parse.quote(text, safe="_~'.!*()")
44
+
45
+
46
+ def build_url(template, lines, ext="png", bg=None, style=None, font=None):
47
+ parts = [escape(l) for l in lines] if lines else ["_"]
48
+ base = "custom" if bg else template
49
+ path = "/".join(parts)
50
+ url = f"{API}/images/{base}/{path}.{ext}"
51
+ q = {}
52
+ if bg:
53
+ q["background"] = bg
54
+ if style:
55
+ q["style"] = style
56
+ if font:
57
+ q["font"] = font
58
+ if q:
59
+ url += "?" + urllib.parse.urlencode(q)
60
+ return url
61
+
62
+
63
+ def get_templates(timeout=20):
64
+ req = urllib.request.Request(
65
+ f"{API}/templates/", headers={"User-Agent": "meme-cli"})
66
+ with urllib.request.urlopen(req, timeout=timeout) as r:
67
+ return json.load(r)
68
+
69
+
70
+ def list_templates(as_json=False):
71
+ data = get_templates()
72
+ if as_json:
73
+ slim = [
74
+ {"id": t["id"], "lines": t.get("lines"), "name": t["name"]}
75
+ for t in data
76
+ ]
77
+ print(json.dumps(slim, indent=2))
78
+ return
79
+ for t in data:
80
+ print(f"{t['id']:<18} {t.get('lines', '?')} lines {t['name']}")
81
+
82
+
83
+ def download(url, out, timeout=30):
84
+ req = urllib.request.Request(url, headers={"User-Agent": "meme-cli"})
85
+ with urllib.request.urlopen(req, timeout=timeout) as r:
86
+ data = r.read()
87
+ with open(out, "wb") as f:
88
+ f.write(data)
89
+ return len(data)
90
+
91
+
92
+ def build_parser():
93
+ ap = argparse.ArgumentParser(
94
+ prog="meme", description="Generate a meme via memegen.link.",
95
+ epilog="Tip: if a text line starts with '-' (e.g. \"-26%\"), put '--' "
96
+ "before your lines so it isn't read as a flag:\n"
97
+ " meme regret --json -- \"-26%\" \"WHY\"",
98
+ formatter_class=argparse.RawDescriptionHelpFormatter)
99
+ ap.add_argument("template", nargs="?",
100
+ help="template id (see --list), or any id when using --bg")
101
+ ap.add_argument("lines", nargs="*", help="text lines, in order")
102
+ ap.add_argument("-o", "--out", default="meme.png",
103
+ help="output file (default meme.png)")
104
+ ap.add_argument("--bg",
105
+ help="custom background image URL (uses the 'custom' template)")
106
+ ap.add_argument("--ext", default="png", choices=["png", "jpg", "webp", "gif"])
107
+ ap.add_argument("--style", help="template style variant, if any")
108
+ ap.add_argument("--font", help="font name (see /fonts/)")
109
+ ap.add_argument("--print-url", action="store_true",
110
+ help="print the URL and exit, no download")
111
+ ap.add_argument("--json", action="store_true",
112
+ help="emit machine-readable JSON (good for agents/scripts)")
113
+ ap.add_argument("--list", action="store_true",
114
+ help="list template ids and exit")
115
+ return ap
116
+
117
+
118
+ def _run(argv=None):
119
+ ap = build_parser()
120
+ args = ap.parse_args(argv)
121
+
122
+ if args.list:
123
+ try:
124
+ list_templates(as_json=args.json)
125
+ except Exception as e:
126
+ sys.exit(f"could not fetch templates: {e}")
127
+ return
128
+
129
+ if not args.template and not args.bg:
130
+ ap.error("a template id is required (or use --bg for a custom background)")
131
+
132
+ # with --bg there is no template positional, so fold it back into the lines
133
+ if args.bg:
134
+ lines = ([args.template] if args.template else []) + args.lines
135
+ template = "custom"
136
+ else:
137
+ lines = args.lines
138
+ template = args.template
139
+
140
+ url = build_url(template, lines, args.ext, args.bg, args.style, args.font)
141
+
142
+ if args.print_url:
143
+ if args.json:
144
+ print(json.dumps({"url": url}))
145
+ else:
146
+ print(url)
147
+ return
148
+
149
+ try:
150
+ n = download(url, args.out)
151
+ except Exception as e:
152
+ if args.json:
153
+ print(json.dumps({"error": str(e), "url": url}))
154
+ sys.exit(1)
155
+ sys.exit(f"download failed: {e}\nurl was: {url}")
156
+
157
+ if args.json:
158
+ print(json.dumps({"path": args.out, "bytes": n, "url": url}))
159
+ else:
160
+ print(args.out) # stdout: just the path, easy to capture
161
+ print(f"saved {n} bytes from {url}", file=sys.stderr)
162
+
163
+
164
+ def main(argv=None):
165
+ """Entry point. Wraps _run so the CLI never dies with a raw traceback."""
166
+ try:
167
+ return _run(argv)
168
+ except KeyboardInterrupt:
169
+ print("interrupted", file=sys.stderr)
170
+ sys.exit(130)
171
+ except BrokenPipeError:
172
+ # downstream closed the pipe (e.g. `meme --list | head`). Silence the
173
+ # traceback Python would otherwise emit when flushing stdout at exit.
174
+ devnull = os.open(os.devnull, os.O_WRONLY)
175
+ os.dup2(devnull, sys.stdout.fileno())
176
+ sys.exit(0)
177
+
178
+
179
+ if __name__ == "__main__":
180
+ main()