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 +10 -0
- pste-0.2.0/PKG-INFO +76 -0
- pste-0.2.0/README.md +60 -0
- pste-0.2.0/pyproject.toml +29 -0
- pste-0.2.0/src/pste/__init__.py +0 -0
- pste-0.2.0/src/pste/cli.py +179 -0
- pste-0.2.0/tests/__init__.py +0 -0
- pste-0.2.0/tests/test_cli.py +328 -0
pste-0.2.0/.gitignore
ADDED
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
|