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 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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ pste = pste.cli:main