makememe 0.1.2__tar.gz → 0.1.4__tar.gz
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.2 → makememe-0.1.4}/PKG-INFO +3 -3
- {makememe-0.1.2 → makememe-0.1.4}/README.md +2 -2
- {makememe-0.1.2 → makememe-0.1.4}/pyproject.toml +1 -1
- {makememe-0.1.2 → makememe-0.1.4}/src/memecli/SKILL.md +12 -6
- {makememe-0.1.2 → makememe-0.1.4}/src/memecli/cli.py +31 -7
- {makememe-0.1.2 → makememe-0.1.4}/tests/test_cli.py +35 -0
- {makememe-0.1.2 → makememe-0.1.4}/.github/workflows/publish.yml +0 -0
- {makememe-0.1.2 → makememe-0.1.4}/.github/workflows/tests.yml +0 -0
- {makememe-0.1.2 → makememe-0.1.4}/.gitignore +0 -0
- {makememe-0.1.2 → makememe-0.1.4}/LICENSE +0 -0
- {makememe-0.1.2 → makememe-0.1.4}/src/memecli/__init__.py +0 -0
- {makememe-0.1.2 → makememe-0.1.4}/src/memecli/__main__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: makememe
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: A tiny zero-dependency CLI for generating memes via the free memegen.link API. Agent-friendly.
|
|
5
5
|
Project-URL: Homepage, https://pypi.org/project/makememe/
|
|
6
6
|
Project-URL: memegen.link API, https://api.memegen.link
|
|
@@ -67,7 +67,7 @@ meme <template> "top line" "bottom line" [-o out.png]
|
|
|
67
67
|
|
|
68
68
|
| Flag | Meaning |
|
|
69
69
|
|------|---------|
|
|
70
|
-
| `-o, --out` | output file (default
|
|
70
|
+
| `-o, --out` | output file (default: a unique file in a temp folder, so it never writes into your current directory) |
|
|
71
71
|
| `--bg URL` | use a custom background image instead of a template |
|
|
72
72
|
| `--ext` | `png` (default), `jpg`, `webp`, or `gif` |
|
|
73
73
|
| `--style` / `--font` | template style variant / font override |
|
|
@@ -106,7 +106,7 @@ The tool is designed to be parsed:
|
|
|
106
106
|
- **`--json` mode** prints a single JSON object:
|
|
107
107
|
|
|
108
108
|
```json
|
|
109
|
-
{ "path": "meme.png", "bytes": 12345, "url": "https://api.memegen.link/..." }
|
|
109
|
+
{ "path": "/tmp/makememe/meme-ab12cd.png", "bytes": 12345, "url": "https://api.memegen.link/..." }
|
|
110
110
|
```
|
|
111
111
|
|
|
112
112
|
`--list --json` returns the template catalog as JSON; `--print-url --json`
|
|
@@ -48,7 +48,7 @@ meme <template> "top line" "bottom line" [-o out.png]
|
|
|
48
48
|
|
|
49
49
|
| Flag | Meaning |
|
|
50
50
|
|------|---------|
|
|
51
|
-
| `-o, --out` | output file (default
|
|
51
|
+
| `-o, --out` | output file (default: a unique file in a temp folder, so it never writes into your current directory) |
|
|
52
52
|
| `--bg URL` | use a custom background image instead of a template |
|
|
53
53
|
| `--ext` | `png` (default), `jpg`, `webp`, or `gif` |
|
|
54
54
|
| `--style` / `--font` | template style variant / font override |
|
|
@@ -87,7 +87,7 @@ The tool is designed to be parsed:
|
|
|
87
87
|
- **`--json` mode** prints a single JSON object:
|
|
88
88
|
|
|
89
89
|
```json
|
|
90
|
-
{ "path": "meme.png", "bytes": 12345, "url": "https://api.memegen.link/..." }
|
|
90
|
+
{ "path": "/tmp/makememe/meme-ab12cd.png", "bytes": 12345, "url": "https://api.memegen.link/..." }
|
|
91
91
|
```
|
|
92
92
|
|
|
93
93
|
`--list --json` returns the template catalog as JSON; `--print-url --json`
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "makememe"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.4"
|
|
8
8
|
description = "A tiny zero-dependency CLI for generating memes via the free memegen.link API. Agent-friendly."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.8"
|
|
@@ -26,9 +26,13 @@ uv tool install makememe
|
|
|
26
26
|
meme --list --json
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
-
Common ids: `drake`, `db` (distracted boyfriend), `
|
|
30
|
-
`gru` (gru's plan), `cmm` (change my mind),
|
|
31
|
-
`
|
|
29
|
+
Common ids: `drake`, `db` (distracted boyfriend), `ds` (daily struggle /
|
|
30
|
+
"two buttons"), `gru` (gru's plan), `cmm` (change my mind),
|
|
31
|
+
`fine` (this is fine), `success` (success kid), `rollsafe`,
|
|
32
|
+
`same` (same picture), `regret`.
|
|
33
|
+
|
|
34
|
+
**Always verify an id with `meme --list --json` before using it** — guessing
|
|
35
|
+
ids (e.g. `buttons`, `twobuttons`) leads to 404s. When unsure, list first.
|
|
32
36
|
|
|
33
37
|
2. **Generate.** Pass the template id then the caption lines in order. Use
|
|
34
38
|
`--json` so you can capture the output path reliably:
|
|
@@ -40,15 +44,17 @@ uv tool install makememe
|
|
|
40
44
|
Output:
|
|
41
45
|
|
|
42
46
|
```json
|
|
43
|
-
{ "path": "meme.png", "bytes": 12345, "url": "https://api.memegen.link/..." }
|
|
47
|
+
{ "path": "/tmp/makememe/meme-ab12cd.png", "bytes": 12345, "url": "https://api.memegen.link/..." }
|
|
44
48
|
```
|
|
45
49
|
|
|
46
50
|
3. **Tell the user the path** (and show the image if the surface supports it).
|
|
47
51
|
|
|
48
52
|
## Key flags
|
|
49
53
|
|
|
50
|
-
-
|
|
51
|
-
|
|
54
|
+
- By default the image is saved to a temp folder (`<tmp>/makememe/`) with a
|
|
55
|
+
unique name, so it never clutters the user's working directory. The path is
|
|
56
|
+
in the output — report it to the user. Use `-o path.png` only if the user
|
|
57
|
+
wants it saved somewhere specific.
|
|
52
58
|
- `--bg <image-url>` — use a custom background image instead of a template;
|
|
53
59
|
pass caption lines as usual.
|
|
54
60
|
- `--ext png|jpg|webp|gif` — output format.
|
|
@@ -23,12 +23,23 @@ import argparse
|
|
|
23
23
|
import json
|
|
24
24
|
import os
|
|
25
25
|
import sys
|
|
26
|
+
import tempfile
|
|
26
27
|
import urllib.parse
|
|
27
28
|
import urllib.request
|
|
28
29
|
|
|
29
30
|
API = "https://api.memegen.link"
|
|
30
31
|
|
|
31
32
|
|
|
33
|
+
def default_output(ext):
|
|
34
|
+
"""A unique output path in a grouped temp folder, so we never write into the
|
|
35
|
+
user's project/root dir and never clobber a previous meme."""
|
|
36
|
+
d = os.path.join(tempfile.gettempdir(), "makememe")
|
|
37
|
+
os.makedirs(d, exist_ok=True)
|
|
38
|
+
fd, path = tempfile.mkstemp(prefix="meme-", suffix=f".{ext}", dir=d)
|
|
39
|
+
os.close(fd)
|
|
40
|
+
return path
|
|
41
|
+
|
|
42
|
+
|
|
32
43
|
def get_version():
|
|
33
44
|
"""Read the installed package version; fall back gracefully from source."""
|
|
34
45
|
try:
|
|
@@ -134,8 +145,8 @@ def build_parser():
|
|
|
134
145
|
ap.add_argument("template", nargs="?",
|
|
135
146
|
help="template id (see --list), or any id when using --bg")
|
|
136
147
|
ap.add_argument("lines", nargs="*", help="text lines, in order")
|
|
137
|
-
ap.add_argument("-o", "--out", default=
|
|
138
|
-
help="output file (default
|
|
148
|
+
ap.add_argument("-o", "--out", default=None,
|
|
149
|
+
help="output file (default: a temp folder, not your current dir)")
|
|
139
150
|
ap.add_argument("--bg",
|
|
140
151
|
help="custom background image URL (uses the 'custom' template)")
|
|
141
152
|
ap.add_argument("--ext", default="png", choices=["png", "jpg", "webp", "gif"])
|
|
@@ -190,18 +201,31 @@ def _run(argv=None):
|
|
|
190
201
|
print(url)
|
|
191
202
|
return
|
|
192
203
|
|
|
204
|
+
out = args.out if args.out else default_output(args.ext)
|
|
205
|
+
|
|
193
206
|
try:
|
|
194
|
-
n = download(url,
|
|
207
|
+
n = download(url, out)
|
|
195
208
|
except Exception as e:
|
|
209
|
+
# a 404 almost always means a bad template id — point the user/agent at --list
|
|
210
|
+
hint = None
|
|
211
|
+
if getattr(e, "code", None) == 404 and not args.bg:
|
|
212
|
+
hint = (f"template '{template}' not found (404). "
|
|
213
|
+
"Run `meme --list` to find the correct id.")
|
|
196
214
|
if args.json:
|
|
197
|
-
|
|
215
|
+
payload = {"error": str(e), "url": url}
|
|
216
|
+
if hint:
|
|
217
|
+
payload["hint"] = hint
|
|
218
|
+
print(json.dumps(payload))
|
|
198
219
|
sys.exit(1)
|
|
199
|
-
|
|
220
|
+
msg = f"download failed: {e}\nurl was: {url}"
|
|
221
|
+
if hint:
|
|
222
|
+
msg += f"\nhint: {hint}"
|
|
223
|
+
sys.exit(msg)
|
|
200
224
|
|
|
201
225
|
if args.json:
|
|
202
|
-
print(json.dumps({"path":
|
|
226
|
+
print(json.dumps({"path": out, "bytes": n, "url": url}))
|
|
203
227
|
else:
|
|
204
|
-
print(
|
|
228
|
+
print(out) # stdout: just the path, easy to capture
|
|
205
229
|
print(f"saved {n} bytes from {url}", file=sys.stderr)
|
|
206
230
|
|
|
207
231
|
|
|
@@ -93,6 +93,19 @@ class TestCrashSafety(unittest.TestCase):
|
|
|
93
93
|
finally:
|
|
94
94
|
cli.download = orig
|
|
95
95
|
|
|
96
|
+
def test_404_adds_list_hint(self):
|
|
97
|
+
class FakeHTTPError(Exception):
|
|
98
|
+
code = 404
|
|
99
|
+
orig = cli.download
|
|
100
|
+
cli.download = lambda *a, **k: (_ for _ in ()).throw(FakeHTTPError("404"))
|
|
101
|
+
try:
|
|
102
|
+
buf = io.StringIO()
|
|
103
|
+
with redirect_stdout(buf), redirect_stderr(io.StringIO()):
|
|
104
|
+
self.assertEqual(self._exit_code(["buttons", "a", "b", "--json"]), 1)
|
|
105
|
+
self.assertIn("meme --list", buf.getvalue())
|
|
106
|
+
finally:
|
|
107
|
+
cli.download = orig
|
|
108
|
+
|
|
96
109
|
def test_download_failure_is_clean_exit_in_json(self):
|
|
97
110
|
orig = cli.download
|
|
98
111
|
cli.download = lambda *a, **k: (_ for _ in ()).throw(OSError("boom"))
|
|
@@ -103,6 +116,28 @@ class TestCrashSafety(unittest.TestCase):
|
|
|
103
116
|
cli.download = orig
|
|
104
117
|
|
|
105
118
|
|
|
119
|
+
class TestDefaultOutput(unittest.TestCase):
|
|
120
|
+
def test_default_is_in_tempdir_not_cwd(self):
|
|
121
|
+
import tempfile
|
|
122
|
+
p = cli.default_output("png")
|
|
123
|
+
self.assertTrue(p.startswith(tempfile.gettempdir()))
|
|
124
|
+
self.assertTrue(p.endswith(".png"))
|
|
125
|
+
# cleanup the empty file mkstemp created
|
|
126
|
+
try:
|
|
127
|
+
os.remove(p)
|
|
128
|
+
except OSError:
|
|
129
|
+
pass
|
|
130
|
+
|
|
131
|
+
def test_default_paths_are_unique(self):
|
|
132
|
+
a, b = cli.default_output("png"), cli.default_output("png")
|
|
133
|
+
self.assertNotEqual(a, b)
|
|
134
|
+
for p in (a, b):
|
|
135
|
+
try:
|
|
136
|
+
os.remove(p)
|
|
137
|
+
except OSError:
|
|
138
|
+
pass
|
|
139
|
+
|
|
140
|
+
|
|
106
141
|
class TestNewCommands(unittest.TestCase):
|
|
107
142
|
def test_version_flag_prints_and_exits(self):
|
|
108
143
|
buf = io.StringIO()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|