pste 0.2.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.
- pste/__init__.py +0 -0
- pste/cli.py +179 -0
- pste-0.2.0.dist-info/METADATA +76 -0
- pste-0.2.0.dist-info/RECORD +6 -0
- pste-0.2.0.dist-info/WHEEL +4 -0
- pste-0.2.0.dist-info/entry_points.txt +2 -0
pste/__init__.py
ADDED
|
File without changes
|
pste/cli.py
ADDED
|
@@ -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())
|
|
@@ -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`
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
pste/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
pste/cli.py,sha256=hKj1vFmXqH-KW_S1RGeQ7z7qQ9SCPdo6RNv4Z9gfDwg,5786
|
|
3
|
+
pste-0.2.0.dist-info/METADATA,sha256=8J3HvnGUAhZMMFkzVU5rPkT9ZTvri6yuqPPqcD2BJ9s,2349
|
|
4
|
+
pste-0.2.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
5
|
+
pste-0.2.0.dist-info/entry_points.txt,sha256=4sw_Ig-h57j_Db07kdV_29L3K3xPV2stpq53HcVdVTU,39
|
|
6
|
+
pste-0.2.0.dist-info/RECORD,,
|