pste 0.2.0__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.
pste-0.2.0/.gitignore ADDED
@@ -0,0 +1,10 @@
1
+ __pycache__/
2
+ *.pyc
3
+ .env
4
+ data/
5
+ dist/
6
+ .venv/
7
+ *.egg-info/
8
+ *.egg
9
+ .pytest_cache/
10
+ build/
pste-0.2.0/PKG-INFO ADDED
@@ -0,0 +1,76 @@
1
+ Metadata-Version: 2.4
2
+ Name: pste
3
+ Version: 0.2.0
4
+ Summary: Command-line client for pste, a self-hosted paste server inspired by sprunge
5
+ Project-URL: Homepage, https://github.com/crognlie/pste
6
+ Project-URL: Repository, https://github.com/crognlie/pste
7
+ License: MIT
8
+ Keywords: cli,paste,pastebin,sprunge
9
+ Requires-Python: >=3.11
10
+ Requires-Dist: pygments>=2.17
11
+ Requires-Dist: requests>=2.31
12
+ Provides-Extra: test
13
+ Requires-Dist: pytest>=8.0; extra == 'test'
14
+ Requires-Dist: responses>=0.25; extra == 'test'
15
+ Description-Content-Type: text/markdown
16
+
17
+ # pste
18
+
19
+ Command-line client for [pste-server](../server/), a self-hosted paste server inspired by [sprunge](http://sprunge.us).
20
+
21
+ ## Installation
22
+
23
+ ```bash
24
+ pip install pste
25
+ # or
26
+ pipx install pste
27
+ ```
28
+
29
+ ## Configuration
30
+
31
+ Set `PSTE_URL` to the bookmark URL printed by `pste-admin key add` on the server — the same URL you'd save in a browser to use the web form:
32
+
33
+ ```bash
34
+ export PSTE_URL="https://pste.example.com/?key=your-api-key"
35
+ ```
36
+
37
+ ## Usage
38
+
39
+ ```bash
40
+ # Create a paste from stdin
41
+ echo "hello world" | pste
42
+ pste < file.txt
43
+ cat file.txt | pste
44
+
45
+ # Create with options
46
+ echo "hello" | pste -s # single-view (deleted after first read)
47
+ echo "hello" | pste -e 7d # expires in 7 days
48
+ echo "hello" | pste -e 2W # expires in 2 weeks
49
+ pste -l python < script.py # explicit syntax highlighting language
50
+ pste -l < script.py # auto-detect language (Pygments, >0.5 confidence)
51
+
52
+ # Fetch a paste (by ID or full URL)
53
+ pste AB1234
54
+ pste https://pste.example.com/AB1234
55
+ pste https://pste.example.com/AB1234?python # ?lang is stripped; always fetches plain text
56
+ ```
57
+
58
+ ## Language flag behaviour
59
+
60
+ | Invocation | What is sent | URL returned |
61
+ |---|---|---|
62
+ | `pste` (no `-l`) | nothing | plain URL |
63
+ | `pste -l python` | `lang=python` | `https://host/AB1234?python` |
64
+ | `pste -l` (no value) | `auto_detect=1` | `https://host/AB1234?python` if detected, else plain URL |
65
+
66
+ When a `?<lang>` URL is returned, opening it in a browser shows Pygments-highlighted HTML with table line numbers (Ctrl-A selects only the code) and a Copy button. Fetching via `pste` always strips the `?lang` and returns plain text.
67
+
68
+ ## Expiry format
69
+
70
+ `-e`/`--expire` accepts an integer followed by a unit:
71
+ - `H` — hours
72
+ - `D` — days
73
+ - `W` — weeks
74
+ - `M` — minutes
75
+
76
+ Examples: `24H`, `7D`, `2W`, `30M`
pste-0.2.0/README.md ADDED
@@ -0,0 +1,60 @@
1
+ # pste
2
+
3
+ Command-line client for [pste-server](../server/), a self-hosted paste server inspired by [sprunge](http://sprunge.us).
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install pste
9
+ # or
10
+ pipx install pste
11
+ ```
12
+
13
+ ## Configuration
14
+
15
+ Set `PSTE_URL` to the bookmark URL printed by `pste-admin key add` on the server — the same URL you'd save in a browser to use the web form:
16
+
17
+ ```bash
18
+ export PSTE_URL="https://pste.example.com/?key=your-api-key"
19
+ ```
20
+
21
+ ## Usage
22
+
23
+ ```bash
24
+ # Create a paste from stdin
25
+ echo "hello world" | pste
26
+ pste < file.txt
27
+ cat file.txt | pste
28
+
29
+ # Create with options
30
+ echo "hello" | pste -s # single-view (deleted after first read)
31
+ echo "hello" | pste -e 7d # expires in 7 days
32
+ echo "hello" | pste -e 2W # expires in 2 weeks
33
+ pste -l python < script.py # explicit syntax highlighting language
34
+ pste -l < script.py # auto-detect language (Pygments, >0.5 confidence)
35
+
36
+ # Fetch a paste (by ID or full URL)
37
+ pste AB1234
38
+ pste https://pste.example.com/AB1234
39
+ pste https://pste.example.com/AB1234?python # ?lang is stripped; always fetches plain text
40
+ ```
41
+
42
+ ## Language flag behaviour
43
+
44
+ | Invocation | What is sent | URL returned |
45
+ |---|---|---|
46
+ | `pste` (no `-l`) | nothing | plain URL |
47
+ | `pste -l python` | `lang=python` | `https://host/AB1234?python` |
48
+ | `pste -l` (no value) | `auto_detect=1` | `https://host/AB1234?python` if detected, else plain URL |
49
+
50
+ When a `?<lang>` URL is returned, opening it in a browser shows Pygments-highlighted HTML with table line numbers (Ctrl-A selects only the code) and a Copy button. Fetching via `pste` always strips the `?lang` and returns plain text.
51
+
52
+ ## Expiry format
53
+
54
+ `-e`/`--expire` accepts an integer followed by a unit:
55
+ - `H` — hours
56
+ - `D` — days
57
+ - `W` — weeks
58
+ - `M` — minutes
59
+
60
+ Examples: `24H`, `7D`, `2W`, `30M`
@@ -0,0 +1,29 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "pste"
7
+ version = "0.2.0"
8
+ description = "Command-line client for pste, a self-hosted paste server inspired by sprunge"
9
+ readme = "README.md"
10
+ license = {text = "MIT"}
11
+ keywords = ["pastebin", "paste", "sprunge", "cli"]
12
+ requires-python = ">=3.11"
13
+ dependencies = [
14
+ "requests>=2.31",
15
+ "pygments>=2.17",
16
+ ]
17
+
18
+ [project.urls]
19
+ Homepage = "https://github.com/crognlie/pste"
20
+ Repository = "https://github.com/crognlie/pste"
21
+
22
+ [project.optional-dependencies]
23
+ test = ["pytest>=8.0", "responses>=0.25"]
24
+
25
+ [project.scripts]
26
+ pste = "pste.cli:main"
27
+
28
+ [tool.hatch.build.targets.wheel]
29
+ packages = ["src/pste"]
File without changes
@@ -0,0 +1,179 @@
1
+ import argparse
2
+ import os
3
+ import re
4
+ import sys
5
+ from datetime import datetime, timedelta, timezone
6
+ from urllib.parse import parse_qs, urlparse
7
+
8
+ import requests
9
+
10
+ TTL_RE = re.compile(r"^(\d+)([MmWwDdHh])$")
11
+ _UNIT_SECONDS = {
12
+ "H": 3600,
13
+ "D": 86400,
14
+ "W": 7 * 86400,
15
+ "M": 60,
16
+ }
17
+
18
+ _TIMEOUT = 10
19
+ _URL_ERROR = (
20
+ "error: PSTE_URL not set — set it to your pste bookmark URL, "
21
+ "e.g. https://pste.example.com/?key=your-key"
22
+ )
23
+
24
+
25
+ def _parse_pste_url():
26
+ """Return (server, api_key) from PSTE_URL, or (None, None) if unset."""
27
+ raw = os.environ.get("PSTE_URL", "").strip()
28
+ if not raw:
29
+ return None, None
30
+ parsed = urlparse(raw)
31
+ key = parse_qs(parsed.query).get("key", [None])[0]
32
+ server = f"{parsed.scheme}://{parsed.netloc}".rstrip("/")
33
+ return server, key
34
+
35
+
36
+ def _parse_ttl(value: str) -> str:
37
+ """Parse TTL string like '7d', '2W', '48H'; return ISO8601 UTC expires_at."""
38
+ m = TTL_RE.match(value)
39
+ if not m:
40
+ print(f"error: invalid expire format {value!r} (expected e.g. 7d, 2W, 48H, 3M)", file=sys.stderr)
41
+ sys.exit(1)
42
+ n, unit = int(m.group(1)), m.group(2).upper()
43
+ dt = datetime.now(timezone.utc) + timedelta(seconds=n * _UNIT_SECONDS[unit])
44
+ return dt.strftime("%Y-%m-%dT%H:%M:%SZ")
45
+
46
+
47
+ def _validate_lang(lang: str) -> str:
48
+ from pygments.lexers import get_lexer_by_name
49
+ from pygments.util import ClassNotFound
50
+ try:
51
+ get_lexer_by_name(lang)
52
+ except ClassNotFound:
53
+ print(f"error: unknown language/lexer {lang!r}", file=sys.stderr)
54
+ sys.exit(1)
55
+ return lang
56
+
57
+
58
+ def _fetch_paste(url: str) -> int:
59
+ try:
60
+ resp = requests.get(url, timeout=_TIMEOUT)
61
+ except requests.exceptions.ConnectTimeout:
62
+ print("error: http connection timed out", file=sys.stderr)
63
+ return 1
64
+ except requests.exceptions.ReadTimeout:
65
+ print("error: http request timed out", file=sys.stderr)
66
+ return 1
67
+
68
+ if resp.status_code == 404:
69
+ print(f"error: paste not found", file=sys.stderr)
70
+ return 1
71
+ if resp.status_code != 200:
72
+ print(f"error: server returned HTTP {resp.status_code}", file=sys.stderr)
73
+ return 1
74
+
75
+ print(resp.text.rstrip())
76
+ return 0
77
+
78
+
79
+ def _create_paste(content: str, args, server: str, api_key: str) -> int:
80
+ data = {"pste": content}
81
+ if args.single_view:
82
+ data["single_view"] = "1"
83
+ if args.expire:
84
+ data["expires_at"] = _parse_ttl(args.expire)
85
+ if args.lang is None:
86
+ pass # no -l flag: no lang, no auto-detection
87
+ elif args.lang == "":
88
+ data["auto_detect"] = "1" # -l with no value: request auto-detection
89
+ else:
90
+ data["lang"] = _validate_lang(args.lang) # -l python: explicit lang
91
+
92
+ try:
93
+ resp = requests.post(
94
+ server + "/",
95
+ data=data,
96
+ headers={"Authorization": f"Bearer {api_key}"},
97
+ timeout=_TIMEOUT,
98
+ )
99
+ except requests.exceptions.ConnectTimeout:
100
+ print("error: http connection timed out", file=sys.stderr)
101
+ return 1
102
+ except requests.exceptions.ReadTimeout:
103
+ print("error: http request timed out", file=sys.stderr)
104
+ return 1
105
+
106
+ if resp.status_code == 401:
107
+ print("error: authentication failed — check your API key", file=sys.stderr)
108
+ return 1
109
+ if resp.status_code == 403:
110
+ print("error: API key is disabled", file=sys.stderr)
111
+ return 1
112
+ if resp.status_code != 200:
113
+ print(f"error: server returned HTTP {resp.status_code}: {resp.text.strip()}", file=sys.stderr)
114
+ return 1
115
+
116
+ print(resp.text.strip())
117
+ return 0
118
+
119
+
120
+ def main() -> int:
121
+ parser = argparse.ArgumentParser(
122
+ description=(
123
+ "Upload text from stdin to a pste server. "
124
+ "If [id] is provided, the paste is fetched instead."
125
+ ),
126
+ epilog="environment variables:\n PSTE_URL server URL with API key, e.g. https://pste.example.com/?key=your-key",
127
+ formatter_class=argparse.RawTextHelpFormatter,
128
+ )
129
+ parser.add_argument("id", nargs="?", help="Fetch paste by ID or full URL")
130
+ parser.add_argument("-s", "--single-view", action="store_true", help="Single-view paste")
131
+ parser.add_argument("-e", "--expire", metavar="TTL", help="Expiry: e.g. 7D, 2W, 48H, 30M (M=minutes)")
132
+ parser.add_argument(
133
+ "-l", "--lang", "--language",
134
+ metavar="LEXER", nargs="?", const="", default=None,
135
+ help="Syntax highlighting language (omit value to auto-detect)",
136
+ )
137
+ args = parser.parse_args()
138
+
139
+ if args.id is not None:
140
+ # Fetch mode
141
+ if args.single_view or args.expire or args.lang is not None:
142
+ print("error: -s/-e/-l flags are not valid when fetching a paste", file=sys.stderr)
143
+ return 1
144
+
145
+ paste_id = args.id
146
+ if paste_id.startswith("http"):
147
+ # Strip any ?lang query string — bare GET always returns plain text
148
+ parsed = urlparse(paste_id)
149
+ clean_url = parsed._replace(query="").geturl()
150
+ return _fetch_paste(clean_url)
151
+
152
+ # Bare ID — need server from PSTE_URL
153
+ server, _ = _parse_pste_url()
154
+ if not server:
155
+ print(_URL_ERROR, file=sys.stderr)
156
+ return 1
157
+ return _fetch_paste(f"{server}/{paste_id}")
158
+
159
+ # Create mode — read from stdin
160
+ server, api_key = _parse_pste_url()
161
+ if not server or not api_key:
162
+ print(_URL_ERROR, file=sys.stderr)
163
+ return 1
164
+
165
+ try:
166
+ content = sys.stdin.read()
167
+ except UnicodeDecodeError as e:
168
+ print(f"error: {e}", file=sys.stderr)
169
+ return 2
170
+
171
+ if not content:
172
+ print("error: empty input", file=sys.stderr)
173
+ return 1
174
+
175
+ return _create_paste(content, args, server, api_key)
176
+
177
+
178
+ if __name__ == "__main__":
179
+ sys.exit(main())
File without changes
@@ -0,0 +1,328 @@
1
+ import io
2
+ import sys
3
+
4
+ import pytest
5
+ import responses as rsps_lib
6
+
7
+ from pste.cli import _parse_ttl, _validate_lang, main
8
+
9
+
10
+ SERVER = "http://pste.example.com"
11
+ PSTE_URL = f"{SERVER}/?key=testkey"
12
+
13
+
14
+ @pytest.fixture(autouse=True)
15
+ def _env(monkeypatch):
16
+ monkeypatch.setenv("PSTE_URL", PSTE_URL)
17
+
18
+
19
+ # ---------------------------------------------------------------------------
20
+ # TTL parsing
21
+ # ---------------------------------------------------------------------------
22
+
23
+ def test_parse_ttl_valid_formats():
24
+ for fmt in ("7d", "7D", "2W", "2w", "48H", "48h", "3M", "3m"):
25
+ result = _parse_ttl(fmt)
26
+ assert result.endswith("Z")
27
+ assert "T" in result
28
+
29
+
30
+ def test_parse_ttl_invalid_exits():
31
+ with pytest.raises(SystemExit):
32
+ _parse_ttl("badformat")
33
+
34
+
35
+ def test_parse_ttl_invalid_fraction_exits():
36
+ with pytest.raises(SystemExit):
37
+ _parse_ttl("1.5d")
38
+
39
+
40
+ # ---------------------------------------------------------------------------
41
+ # Lang validation
42
+ # ---------------------------------------------------------------------------
43
+
44
+ def test_validate_lang_known():
45
+ assert _validate_lang("python") == "python"
46
+
47
+
48
+ def test_validate_lang_unknown_exits():
49
+ with pytest.raises(SystemExit):
50
+ _validate_lang("notareallexer12345")
51
+
52
+
53
+ # ---------------------------------------------------------------------------
54
+ # Fetch mode
55
+ # ---------------------------------------------------------------------------
56
+
57
+ @rsps_lib.activate
58
+ def test_fetch_by_id_success(monkeypatch, capsys):
59
+ monkeypatch.setattr(sys, "argv", ["pste", "ABC123"])
60
+ rsps_lib.add(rsps_lib.GET, f"{SERVER}/ABC123", body="hello world\n", status=200)
61
+ rc = main()
62
+ assert rc == 0
63
+ assert "hello world" in capsys.readouterr().out
64
+
65
+
66
+ @rsps_lib.activate
67
+ def test_fetch_404(monkeypatch, capsys):
68
+ monkeypatch.setattr(sys, "argv", ["pste", "ZZZZZZ"])
69
+ rsps_lib.add(rsps_lib.GET, f"{SERVER}/ZZZZZZ", status=404)
70
+ rc = main()
71
+ assert rc == 1
72
+ assert "not found" in capsys.readouterr().err
73
+
74
+
75
+ @rsps_lib.activate
76
+ def test_fetch_server_error(monkeypatch, capsys):
77
+ monkeypatch.setattr(sys, "argv", ["pste", "ABC123"])
78
+ rsps_lib.add(rsps_lib.GET, f"{SERVER}/ABC123", status=500)
79
+ rc = main()
80
+ assert rc == 1
81
+ assert "500" in capsys.readouterr().err
82
+
83
+
84
+ def test_fetch_timeout(monkeypatch, capsys):
85
+ import requests.exceptions
86
+ monkeypatch.setattr(sys, "argv", ["pste", "ABC123"])
87
+ with rsps_lib.RequestsMock() as r:
88
+ r.add(rsps_lib.GET, f"{SERVER}/ABC123", body=requests.exceptions.ConnectTimeout())
89
+ rc = main()
90
+ assert rc == 1
91
+ assert "timed out" in capsys.readouterr().err
92
+
93
+
94
+ @rsps_lib.activate
95
+ def test_fetch_from_full_url_no_pste_url_needed(monkeypatch, capsys):
96
+ """Full URL can be fetched without PSTE_URL set."""
97
+ monkeypatch.delenv("PSTE_URL", raising=False)
98
+ monkeypatch.setattr(sys, "argv", ["pste", f"{SERVER}/ABC123"])
99
+ rsps_lib.add(rsps_lib.GET, f"{SERVER}/ABC123", body="content\n", status=200)
100
+ rc = main()
101
+ assert rc == 0
102
+ assert "content" in capsys.readouterr().out
103
+
104
+
105
+ @rsps_lib.activate
106
+ def test_fetch_full_url_with_lang_query_stripped(monkeypatch, capsys):
107
+ """Full URL ending in ?python is stripped before the GET request."""
108
+ monkeypatch.setattr(sys, "argv", ["pste", f"{SERVER}/ABC123?python"])
109
+ rsps_lib.add(rsps_lib.GET, f"{SERVER}/ABC123", body="plain content\n", status=200)
110
+ rc = main()
111
+ assert rc == 0
112
+ assert "plain content" in capsys.readouterr().out
113
+ # Confirm the request was made without the query string
114
+ assert rsps_lib.calls[0].request.url == f"{SERVER}/ABC123"
115
+
116
+
117
+ def test_fetch_by_id_no_pste_url(monkeypatch, capsys):
118
+ monkeypatch.delenv("PSTE_URL", raising=False)
119
+ monkeypatch.setattr(sys, "argv", ["pste", "ABC123"])
120
+ rc = main()
121
+ assert rc == 1
122
+ assert "PSTE_URL" in capsys.readouterr().err
123
+
124
+
125
+ def test_fetch_flags_rejected_in_fetch_mode(monkeypatch, capsys):
126
+ monkeypatch.setattr(sys, "argv", ["pste", "-s", "ABC123"])
127
+ rc = main()
128
+ assert rc == 1
129
+ assert "not valid" in capsys.readouterr().err
130
+
131
+
132
+ def test_fetch_bare_l_flag_rejected_in_fetch_mode(monkeypatch, capsys):
133
+ """-l with no value is still a flag and must be rejected in fetch mode."""
134
+ monkeypatch.setattr(sys, "argv", ["pste", "-l", "ABC123"])
135
+ # -l consumes ABC123 as the lang value, leaving no positional ID → create mode
136
+ # but stdin is empty → error
137
+ monkeypatch.setattr(sys, "stdin", io.StringIO(""))
138
+ rc = main()
139
+ assert rc == 1
140
+
141
+
142
+ # ---------------------------------------------------------------------------
143
+ # Create mode
144
+ # ---------------------------------------------------------------------------
145
+
146
+ @rsps_lib.activate
147
+ def test_create_success(monkeypatch, capsys):
148
+ monkeypatch.setattr(sys, "argv", ["pste"])
149
+ monkeypatch.setattr(sys, "stdin", io.StringIO("hello world"))
150
+ rsps_lib.add(rsps_lib.POST, f"{SERVER}/", body=f"{SERVER}/AB1234\n", status=200)
151
+ rc = main()
152
+ assert rc == 0
153
+ assert "AB1234" in capsys.readouterr().out
154
+
155
+
156
+ @rsps_lib.activate
157
+ def test_create_sends_auth_header(monkeypatch):
158
+ monkeypatch.setattr(sys, "argv", ["pste"])
159
+ monkeypatch.setattr(sys, "stdin", io.StringIO("hello"))
160
+ rsps_lib.add(rsps_lib.POST, f"{SERVER}/", body=f"{SERVER}/AB1234\n", status=200)
161
+ main()
162
+ assert rsps_lib.calls[0].request.headers["Authorization"] == "Bearer testkey"
163
+
164
+
165
+ def test_create_no_pste_url(monkeypatch, capsys):
166
+ monkeypatch.delenv("PSTE_URL", raising=False)
167
+ monkeypatch.setattr(sys, "argv", ["pste"])
168
+ monkeypatch.setattr(sys, "stdin", io.StringIO("hello"))
169
+ rc = main()
170
+ assert rc == 1
171
+ assert "PSTE_URL" in capsys.readouterr().err
172
+
173
+
174
+ @rsps_lib.activate
175
+ def test_create_auth_failure(monkeypatch, capsys):
176
+ monkeypatch.setattr(sys, "argv", ["pste"])
177
+ monkeypatch.setattr(sys, "stdin", io.StringIO("hello"))
178
+ rsps_lib.add(rsps_lib.POST, f"{SERVER}/", status=401)
179
+ rc = main()
180
+ assert rc == 1
181
+ assert "authentication" in capsys.readouterr().err
182
+
183
+
184
+ @rsps_lib.activate
185
+ def test_create_disabled_key(monkeypatch, capsys):
186
+ monkeypatch.setattr(sys, "argv", ["pste"])
187
+ monkeypatch.setattr(sys, "stdin", io.StringIO("hello"))
188
+ rsps_lib.add(rsps_lib.POST, f"{SERVER}/", status=403)
189
+ rc = main()
190
+ assert rc == 1
191
+ assert "disabled" in capsys.readouterr().err
192
+
193
+
194
+ @rsps_lib.activate
195
+ def test_create_server_error(monkeypatch, capsys):
196
+ monkeypatch.setattr(sys, "argv", ["pste"])
197
+ monkeypatch.setattr(sys, "stdin", io.StringIO("hello"))
198
+ rsps_lib.add(rsps_lib.POST, f"{SERVER}/", status=500, body="oops")
199
+ rc = main()
200
+ assert rc == 1
201
+ assert "500" in capsys.readouterr().err
202
+
203
+
204
+ def test_create_empty_stdin(monkeypatch, capsys):
205
+ monkeypatch.setattr(sys, "argv", ["pste"])
206
+ monkeypatch.setattr(sys, "stdin", io.StringIO(""))
207
+ rc = main()
208
+ assert rc == 1
209
+ assert "empty" in capsys.readouterr().err
210
+
211
+
212
+ @rsps_lib.activate
213
+ def test_create_single_view_flag(monkeypatch):
214
+ monkeypatch.setattr(sys, "argv", ["pste", "-s"])
215
+ monkeypatch.setattr(sys, "stdin", io.StringIO("secret"))
216
+ rsps_lib.add(rsps_lib.POST, f"{SERVER}/", body=f"{SERVER}/AB1234\n", status=200)
217
+ main()
218
+ assert "single_view=1" in rsps_lib.calls[0].request.body
219
+
220
+
221
+ @rsps_lib.activate
222
+ def test_create_explicit_lang_flag(monkeypatch, capsys):
223
+ """-l python sends lang=python and prints the ?python URL."""
224
+ monkeypatch.setattr(sys, "argv", ["pste", "-l", "python"])
225
+ monkeypatch.setattr(sys, "stdin", io.StringIO("print('hi')"))
226
+ rsps_lib.add(rsps_lib.POST, f"{SERVER}/", body=f"{SERVER}/AB1234?python\n", status=200)
227
+ rc = main()
228
+ assert rc == 0
229
+ body = rsps_lib.calls[0].request.body
230
+ assert "lang=python" in body
231
+ assert "auto_detect" not in body
232
+ assert "AB1234?python" in capsys.readouterr().out
233
+
234
+
235
+ @rsps_lib.activate
236
+ def test_create_lang_auto_detect_bare_flag(monkeypatch, capsys):
237
+ """-l with no value sends auto_detect=1 (not lang=); prints returned URL."""
238
+ monkeypatch.setattr(sys, "argv", ["pste", "-l"])
239
+ monkeypatch.setattr(sys, "stdin", io.StringIO("print('hi')"))
240
+ rsps_lib.add(rsps_lib.POST, f"{SERVER}/", body=f"{SERVER}/AB1234?python\n", status=200)
241
+ rc = main()
242
+ assert rc == 0
243
+ body = rsps_lib.calls[0].request.body
244
+ assert "auto_detect=1" in body
245
+ assert "lang=" not in body
246
+ assert "AB1234?python" in capsys.readouterr().out
247
+
248
+
249
+ @rsps_lib.activate
250
+ def test_create_no_lang_flag_sends_neither(monkeypatch):
251
+ """No -l flag: neither lang nor auto_detect is sent."""
252
+ monkeypatch.setattr(sys, "argv", ["pste"])
253
+ monkeypatch.setattr(sys, "stdin", io.StringIO("hello"))
254
+ rsps_lib.add(rsps_lib.POST, f"{SERVER}/", body=f"{SERVER}/AB1234\n", status=200)
255
+ main()
256
+ body = rsps_lib.calls[0].request.body
257
+ assert "lang=" not in body
258
+ assert "auto_detect" not in body
259
+
260
+
261
+ @rsps_lib.activate
262
+ def test_create_expire_flag(monkeypatch):
263
+ monkeypatch.setattr(sys, "argv", ["pste", "-e", "7d"])
264
+ monkeypatch.setattr(sys, "stdin", io.StringIO("hello"))
265
+ rsps_lib.add(rsps_lib.POST, f"{SERVER}/", body=f"{SERVER}/AB1234\n", status=200)
266
+ main()
267
+ assert "expires_at=" in rsps_lib.calls[0].request.body
268
+
269
+
270
+ def test_fetch_read_timeout(monkeypatch, capsys):
271
+ import responses as rsps_lib2
272
+ import requests
273
+ monkeypatch.setattr(sys, "argv", ["pste", "AB1234"])
274
+ with rsps_lib2.RequestsMock() as rsps:
275
+ rsps.add(rsps_lib2.GET, f"{SERVER}/AB1234", body=requests.exceptions.ReadTimeout())
276
+ rc = main()
277
+ assert rc == 1
278
+ assert "timed out" in capsys.readouterr().err
279
+
280
+
281
+ def test_fetch_connect_timeout(monkeypatch, capsys):
282
+ import responses as rsps_lib2
283
+ import requests
284
+ monkeypatch.setattr(sys, "argv", ["pste", "AB1234"])
285
+ with rsps_lib2.RequestsMock() as rsps:
286
+ rsps.add(rsps_lib2.GET, f"{SERVER}/AB1234", body=requests.exceptions.ConnectTimeout())
287
+ rc = main()
288
+ assert rc == 1
289
+ assert "timed out" in capsys.readouterr().err
290
+
291
+
292
+ def test_create_read_timeout(monkeypatch, capsys):
293
+ import responses as rsps_lib2
294
+ import requests
295
+ monkeypatch.setattr(sys, "argv", ["pste"])
296
+ monkeypatch.setattr(sys, "stdin", io.StringIO("hello"))
297
+ with rsps_lib2.RequestsMock() as rsps:
298
+ rsps.add(rsps_lib2.POST, f"{SERVER}/", body=requests.exceptions.ReadTimeout())
299
+ rc = main()
300
+ assert rc == 1
301
+ assert "timed out" in capsys.readouterr().err
302
+
303
+
304
+ def test_create_connect_timeout(monkeypatch, capsys):
305
+ import responses as rsps_lib2
306
+ import requests
307
+ monkeypatch.setattr(sys, "argv", ["pste"])
308
+ monkeypatch.setattr(sys, "stdin", io.StringIO("hello"))
309
+ with rsps_lib2.RequestsMock() as rsps:
310
+ rsps.add(rsps_lib2.POST, f"{SERVER}/", body=requests.exceptions.ConnectTimeout())
311
+ rc = main()
312
+ assert rc == 1
313
+ assert "timed out" in capsys.readouterr().err
314
+
315
+
316
+ def test_create_unicode_decode_error(monkeypatch, capsys):
317
+ """UnicodeDecodeError on stdin.read() returns exit code 2."""
318
+ import io
319
+
320
+ class _BadStdin:
321
+ def read(self):
322
+ raise UnicodeDecodeError("utf-8", b"", 0, 1, "invalid")
323
+
324
+ monkeypatch.setattr(sys, "argv", ["pste"])
325
+ monkeypatch.setattr(sys, "stdin", _BadStdin())
326
+ rc = main()
327
+ assert rc == 2
328
+ assert "error" in capsys.readouterr().err