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.
- makememe-0.1.0.dist-info/METADATA +154 -0
- makememe-0.1.0.dist-info/RECORD +8 -0
- makememe-0.1.0.dist-info/WHEEL +4 -0
- makememe-0.1.0.dist-info/entry_points.txt +2 -0
- makememe-0.1.0.dist-info/licenses/LICENSE +21 -0
- memecli/__init__.py +6 -0
- memecli/__main__.py +4 -0
- memecli/cli.py +180 -0
|
@@ -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,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
memecli/__main__.py
ADDED
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()
|