wasat 0.0.1__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.
wasat-0.0.1/PKG-INFO ADDED
@@ -0,0 +1,146 @@
1
+ Metadata-Version: 2.4
2
+ Name: wasat
3
+ Version: 0.0.1
4
+ Summary: A async client library for the Gemini protocol
5
+ Keywords: API,async,client,library,Gemini
6
+ Author: Dave Pearson
7
+ Author-email: Dave Pearson <davep@davep.org>
8
+ License-Expression: MIT
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Programming Language :: Python :: 3 :: Only
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Programming Language :: Python :: 3.14
16
+ Classifier: Topic :: Software Development :: Libraries
17
+ Classifier: Typing :: Typed
18
+ Requires-Python: >=3.12
19
+ Project-URL: Homepage, https://wasat.davep.dev/
20
+ Project-URL: Repository, https://github.com/davep/wasat
21
+ Project-URL: Documentation, https://wasat.davep.dev/
22
+ Project-URL: Source, https://github.com/davep/wasat
23
+ Project-URL: Issues, https://github.com/davep/wasat/issues
24
+ Project-URL: Discussions, https://github.com/davep/wasat/discussions
25
+ Description-Content-Type: text/markdown
26
+
27
+ # Wasat: Async Gemini Protocol Client Library
28
+
29
+ Wasat is an object-oriented, fully type-hinted asynchronous client library for the [Gemini Protocol](gemini://geminiprotocol.net), built with zero external dependencies.
30
+
31
+ ## Features
32
+
33
+ - **Async All the Way**: Built on top of Python's standard `asyncio` loop with streaming/chunking support.
34
+ - **Type Safe**: Fully typed API utilizing Python 3.14 standards (strictly avoiding `Any`).
35
+ - **TOFU (Trust-On-First-Use) Support**: Secure by default with a built-in file-based TOFU store and custom interactive trust confirmation hooks.
36
+ - **Auto Redirect Handling**: Automatically handles temporary and permanent redirects (with protection against loops and infinite redirect limits), caching permanent redirects locally.
37
+ - **Client Authentication**: Native support for client TLS certificates.
38
+ - **Zero-Dependency**: Runs purely on Python's standard library.
39
+ - **CLI Utility**: Includes a `wasat` command-line interface out of the box.
40
+
41
+ ---
42
+
43
+ ## Installation
44
+
45
+ You can install `wasat` directly in your project virtual environment using `uv` or `pip`:
46
+
47
+ ```bash
48
+ uv pip install -e .
49
+ # or
50
+ pip install -e .
51
+ ```
52
+
53
+ ---
54
+
55
+ ## Quick Start
56
+
57
+ ### 1. Make a Simple Request
58
+ Use `Client` with standard async context managers to query a Gemini capsule and decode the response:
59
+
60
+ ```python
61
+ import asyncio
62
+ from wasat import Client, WasatError
63
+
64
+ async def main():
65
+ # 'tofu' mode is ideal for standard Gemini capsules (self-signed certs)
66
+ client = Client(verify_mode="tofu")
67
+
68
+ try:
69
+ # Perform the request (automatically resolves host, port, TLS, and redirects)
70
+ async with await client.request("gemini://geminiprotocol.net/") as response:
71
+ print(f"Status: {response.status.value} ({response.status.name})")
72
+ print(f"MIME type: {response.mime_type}")
73
+
74
+ # Fetch the decoded body text
75
+ body = await response.text()
76
+ print(body)
77
+
78
+ except WasatError as e:
79
+ print(f"Request failed: {e}")
80
+
81
+ if __name__ == "__main__":
82
+ asyncio.run(main())
83
+ ```
84
+
85
+ ### 2. Streaming Chunk Responses
86
+ For large files or media streams, read the response body in chunks to prevent exhausting memory:
87
+
88
+ ```python
89
+ async with await client.request("gemini://example.com/large-file.txt") as response:
90
+ if response.status.is_success:
91
+ async for chunk in response.iter_chunks(chunk_size=1024):
92
+ # Process each chunk as it arrives
93
+ sys.stdout.buffer.write(chunk)
94
+ ```
95
+
96
+ ### 3. Client Certificate Authentication
97
+ If a server requires client auth (status code `60`), supply your certificate files to the client configuration:
98
+
99
+ ```python
100
+ client = Client(
101
+ verify_mode="tofu",
102
+ client_cert="/path/to/client.crt",
103
+ client_key="/path/to/client.key" # Optional if key is embedded in cert
104
+ )
105
+ ```
106
+
107
+ ### 4. Interactive TOFU Confirmation
108
+ Implement a custom asynchronous callback to prompt the user before trusting new self-signed certificates:
109
+
110
+ ```python
111
+ import sys
112
+
113
+ async def confirm_cert(host: str, port: int, fingerprint: str) -> bool:
114
+ print(f"New certificate encountered for {host}:{port}")
115
+ print(f"Fingerprint: sha256:{fingerprint}")
116
+ response = input("Do you trust this certificate? [y/N]: ").strip().lower()
117
+ return response == "y"
118
+
119
+ client = Client(
120
+ verify_mode="tofu",
121
+ on_new_certificate=confirm_cert
122
+ )
123
+ ```
124
+
125
+ ---
126
+
127
+ ## Command Line Interface (CLI)
128
+
129
+ Wasat comes with a command-line interface to fetch Gemini capsules from your shell:
130
+
131
+ ```bash
132
+ # Basic fetch using the entrypoint script (uses TOFU)
133
+ uv run wasat gemini://geminiprotocol.net/
134
+
135
+ # Alternatively, execute the package directly using python -m
136
+ uv run python -m wasat gemini://geminiprotocol.net/
137
+
138
+ # Fetch a local or custom port capsule
139
+ uv run wasat gemini://localhost:1965/index.gmi
140
+ ```
141
+
142
+ ## License
143
+
144
+ MIT
145
+
146
+ [//]: # (README.md ends here)
wasat-0.0.1/README.md ADDED
@@ -0,0 +1,120 @@
1
+ # Wasat: Async Gemini Protocol Client Library
2
+
3
+ Wasat is an object-oriented, fully type-hinted asynchronous client library for the [Gemini Protocol](gemini://geminiprotocol.net), built with zero external dependencies.
4
+
5
+ ## Features
6
+
7
+ - **Async All the Way**: Built on top of Python's standard `asyncio` loop with streaming/chunking support.
8
+ - **Type Safe**: Fully typed API utilizing Python 3.14 standards (strictly avoiding `Any`).
9
+ - **TOFU (Trust-On-First-Use) Support**: Secure by default with a built-in file-based TOFU store and custom interactive trust confirmation hooks.
10
+ - **Auto Redirect Handling**: Automatically handles temporary and permanent redirects (with protection against loops and infinite redirect limits), caching permanent redirects locally.
11
+ - **Client Authentication**: Native support for client TLS certificates.
12
+ - **Zero-Dependency**: Runs purely on Python's standard library.
13
+ - **CLI Utility**: Includes a `wasat` command-line interface out of the box.
14
+
15
+ ---
16
+
17
+ ## Installation
18
+
19
+ You can install `wasat` directly in your project virtual environment using `uv` or `pip`:
20
+
21
+ ```bash
22
+ uv pip install -e .
23
+ # or
24
+ pip install -e .
25
+ ```
26
+
27
+ ---
28
+
29
+ ## Quick Start
30
+
31
+ ### 1. Make a Simple Request
32
+ Use `Client` with standard async context managers to query a Gemini capsule and decode the response:
33
+
34
+ ```python
35
+ import asyncio
36
+ from wasat import Client, WasatError
37
+
38
+ async def main():
39
+ # 'tofu' mode is ideal for standard Gemini capsules (self-signed certs)
40
+ client = Client(verify_mode="tofu")
41
+
42
+ try:
43
+ # Perform the request (automatically resolves host, port, TLS, and redirects)
44
+ async with await client.request("gemini://geminiprotocol.net/") as response:
45
+ print(f"Status: {response.status.value} ({response.status.name})")
46
+ print(f"MIME type: {response.mime_type}")
47
+
48
+ # Fetch the decoded body text
49
+ body = await response.text()
50
+ print(body)
51
+
52
+ except WasatError as e:
53
+ print(f"Request failed: {e}")
54
+
55
+ if __name__ == "__main__":
56
+ asyncio.run(main())
57
+ ```
58
+
59
+ ### 2. Streaming Chunk Responses
60
+ For large files or media streams, read the response body in chunks to prevent exhausting memory:
61
+
62
+ ```python
63
+ async with await client.request("gemini://example.com/large-file.txt") as response:
64
+ if response.status.is_success:
65
+ async for chunk in response.iter_chunks(chunk_size=1024):
66
+ # Process each chunk as it arrives
67
+ sys.stdout.buffer.write(chunk)
68
+ ```
69
+
70
+ ### 3. Client Certificate Authentication
71
+ If a server requires client auth (status code `60`), supply your certificate files to the client configuration:
72
+
73
+ ```python
74
+ client = Client(
75
+ verify_mode="tofu",
76
+ client_cert="/path/to/client.crt",
77
+ client_key="/path/to/client.key" # Optional if key is embedded in cert
78
+ )
79
+ ```
80
+
81
+ ### 4. Interactive TOFU Confirmation
82
+ Implement a custom asynchronous callback to prompt the user before trusting new self-signed certificates:
83
+
84
+ ```python
85
+ import sys
86
+
87
+ async def confirm_cert(host: str, port: int, fingerprint: str) -> bool:
88
+ print(f"New certificate encountered for {host}:{port}")
89
+ print(f"Fingerprint: sha256:{fingerprint}")
90
+ response = input("Do you trust this certificate? [y/N]: ").strip().lower()
91
+ return response == "y"
92
+
93
+ client = Client(
94
+ verify_mode="tofu",
95
+ on_new_certificate=confirm_cert
96
+ )
97
+ ```
98
+
99
+ ---
100
+
101
+ ## Command Line Interface (CLI)
102
+
103
+ Wasat comes with a command-line interface to fetch Gemini capsules from your shell:
104
+
105
+ ```bash
106
+ # Basic fetch using the entrypoint script (uses TOFU)
107
+ uv run wasat gemini://geminiprotocol.net/
108
+
109
+ # Alternatively, execute the package directly using python -m
110
+ uv run python -m wasat gemini://geminiprotocol.net/
111
+
112
+ # Fetch a local or custom port capsule
113
+ uv run wasat gemini://localhost:1965/index.gmi
114
+ ```
115
+
116
+ ## License
117
+
118
+ MIT
119
+
120
+ [//]: # (README.md ends here)
@@ -0,0 +1,108 @@
1
+ [project]
2
+ name = "wasat"
3
+ version = "0.0.1"
4
+ description = "A async client library for the Gemini protocol"
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "Dave Pearson", email = "davep@davep.org" }
8
+ ]
9
+ requires-python = ">=3.12"
10
+ dependencies = []
11
+ license = "MIT"
12
+ keywords = [
13
+ "API",
14
+ "async",
15
+ "client",
16
+ "library",
17
+ "Gemini",
18
+ ]
19
+ classifiers = [
20
+ "Development Status :: 3 - Alpha",
21
+ "Operating System :: OS Independent",
22
+ "Programming Language :: Python :: 3 :: Only",
23
+ "Programming Language :: Python :: 3",
24
+ "Programming Language :: Python :: 3.12",
25
+ "Programming Language :: Python :: 3.13",
26
+ "Programming Language :: Python :: 3.14",
27
+ "Topic :: Software Development :: Libraries",
28
+ "Typing :: Typed",
29
+ ]
30
+
31
+ [project.urls]
32
+ Homepage = "https://wasat.davep.dev/"
33
+ Repository = "https://github.com/davep/wasat"
34
+ Documentation = "https://wasat.davep.dev/"
35
+ Source = "https://github.com/davep/wasat"
36
+ Issues = "https://github.com/davep/wasat/issues"
37
+ Discussions = "https://github.com/davep/wasat/discussions"
38
+
39
+ [project.scripts]
40
+ wasat = "wasat.__main__:main"
41
+
42
+ [build-system]
43
+ requires = ["uv_build>=0.11.21,<0.12.0"]
44
+ build-backend = "uv_build"
45
+
46
+ [[tool.uv.index]]
47
+ name = "testpypi"
48
+ url = "https://test.pypi.org/simple/"
49
+ publish-url = "https://test.pypi.org/legacy/"
50
+ explicit = true
51
+
52
+ [dependency-groups]
53
+ dev = [
54
+ "codespell>=2.4.2",
55
+ "mypy>=2.1.0",
56
+ "pre-commit>=4.6.0",
57
+ "ruff>=0.15.17",
58
+ ]
59
+ docs = [
60
+ "mkdocs>=1.6.1,<2",
61
+ "mkdocs-material>=9.7.6",
62
+ "mkdocstrings[python]>=1.0.4",
63
+ ]
64
+ test = [
65
+ "pytest>=9.1.0",
66
+ "pytest-cov>=7.1.0",
67
+ ]
68
+
69
+ [tool.pyright]
70
+ venvPath="."
71
+ venv=".venv"
72
+ exclude=[".venv"]
73
+
74
+ [tool.ruff.lint]
75
+ select = [
76
+ # pycodestyle
77
+ "E",
78
+ # Pyflakes
79
+ "F",
80
+ # pyupgrade
81
+ "UP",
82
+ # flake8-bugbear
83
+ "B",
84
+ # flake8-simplify
85
+ "SIM",
86
+ # isort
87
+ "I",
88
+ ]
89
+
90
+ [tool.ruff.lint.pycodestyle]
91
+ max-line-length = 120
92
+
93
+ [tool.coverage.run]
94
+ omit = [
95
+ "tests/*"
96
+ ]
97
+
98
+ [tool.coverage.report]
99
+ exclude_lines = [
100
+ "pragma: no cover",
101
+ "def __repr__",
102
+ "raise AssertionError",
103
+ "raise NotImplementedError",
104
+ "if __name__ == .__main__.:",
105
+ "if TYPE_CHECKING:",
106
+ "class .*\\bProtocol\\):",
107
+ "@(abc\\.)?abstractmethod",
108
+ ]
@@ -0,0 +1,51 @@
1
+ """Wasat: An asynchronous, type-hinted client library for the Gemini Protocol."""
2
+
3
+ ##############################################################################
4
+ # Python imports.
5
+ from importlib.metadata import version
6
+
7
+ ######################################################################
8
+ # Main library information.
9
+ __author__ = "Dave Pearson"
10
+ __copyright__ = "Copyright 2026, Dave Pearson"
11
+ __credits__ = ["Dave Pearson"]
12
+ __maintainer__ = "Dave Pearson"
13
+ __email__ = "davep@davep.org"
14
+ __version__: str = version("wasat")
15
+ __licence__ = "MIT"
16
+
17
+ ##############################################################################
18
+ # Local imports.
19
+ from .client import Client
20
+ from .exceptions import (
21
+ ConnectionError,
22
+ ProtocolError,
23
+ RedirectError,
24
+ SecurityError,
25
+ URIError,
26
+ WasatError,
27
+ )
28
+ from .response import Response
29
+ from .status import StatusCode
30
+ from .trust import FileTrustStore, TrustStore
31
+ from .uri import GEMINI_DEFAULT_PORT, GeminiURI
32
+
33
+ ##############################################################################
34
+ # Exports.
35
+ __all__ = [
36
+ "Client",
37
+ "Response",
38
+ "StatusCode",
39
+ "GeminiURI",
40
+ "GEMINI_DEFAULT_PORT",
41
+ "TrustStore",
42
+ "FileTrustStore",
43
+ "WasatError",
44
+ "URIError",
45
+ "ProtocolError",
46
+ "ConnectionError",
47
+ "SecurityError",
48
+ "RedirectError",
49
+ ]
50
+
51
+ ### __init__.py ends here
@@ -0,0 +1,78 @@
1
+ """Entry point for executing the wasat package directly."""
2
+
3
+ ##############################################################################
4
+ # Python imports.
5
+ from argparse import ArgumentParser, Namespace
6
+ from asyncio import run
7
+ from sys import exit, stderr
8
+
9
+ ##############################################################################
10
+ # Local imports.
11
+ from . import Client, WasatError, __version__
12
+
13
+
14
+ ##############################################################################
15
+ def get_args() -> Namespace:
16
+ """Parse command-line arguments.
17
+
18
+ Returns:
19
+ Namespace: Parsed command-line arguments.
20
+ """
21
+ parser = ArgumentParser(
22
+ prog="wasat",
23
+ description="An asynchronous client library and CLI for the Gemini protocol.",
24
+ )
25
+ parser.add_argument(
26
+ "url",
27
+ help="The Gemini URL to request.",
28
+ )
29
+ parser.add_argument(
30
+ "--version",
31
+ action="version",
32
+ version=f"%(prog)s {__version__}",
33
+ )
34
+ parser.add_argument(
35
+ "-v",
36
+ "--verbose",
37
+ action="store_true",
38
+ help="Enable verbose output.",
39
+ )
40
+
41
+ return parser.parse_args()
42
+
43
+
44
+ ##############################################################################
45
+ async def run_cli() -> None:
46
+ """Run the Wasat CLI asynchronously."""
47
+ args = get_args()
48
+
49
+ try:
50
+ async with await Client(verify_mode="tofu").request(args.url) as response:
51
+ if args.verbose or not response.status.is_success:
52
+ print("--- Gemini Response ---")
53
+ print(f"Status: {response.status.value} ({response.status.name})")
54
+ print(f"Meta: {response.meta}")
55
+ print("-----------------------")
56
+ if response.status.is_success:
57
+ print(await response.text())
58
+ else:
59
+ exit(1)
60
+ except WasatError as e:
61
+ print(f"Error: {e}", file=stderr)
62
+ exit(1)
63
+
64
+
65
+ ##############################################################################
66
+ def main() -> None:
67
+ """CLI entry point."""
68
+ try:
69
+ run(run_cli())
70
+ except KeyboardInterrupt:
71
+ exit(130)
72
+
73
+
74
+ ##############################################################################
75
+ if __name__ == "__main__":
76
+ main()
77
+
78
+ ### __main__.py ends here