Building a Client¶
In this tutorial, you'll build a Python client to browse gopherspace programmatically. You'll learn how to:
- Connect to gopher servers asynchronously
- Parse and display directory listings
- Fetch text and binary content
- Handle errors gracefully
Difficulty: Intermediate Time: ~20 minutes Prerequisites: Mototli installed, basic Python async/await knowledge
Step 1: Create your project¶
Create a new directory for your client:
Create a client.py file:
import asyncio
from mototli.client import GopherClient
async def main():
print("Gopher Client Ready!")
if __name__ == "__main__":
asyncio.run(main())
Run it to verify your setup:
Step 2: Connect and fetch a directory¶
The GopherClient uses a context manager pattern for automatic connection handling:
import asyncio
from mototli.client import GopherClient
async def main():
# Create a client with a 30-second timeout
async with GopherClient(timeout=30.0) as client:
# Fetch the root directory
response = await client.get("gopher.floodgap.com", "/")
print(f"Fetched {len(response.items)} items")
if __name__ == "__main__":
asyncio.run(main())
Run it:
You should see something like:
Step 3: Display directory items¶
Each item in a directory listing has properties you can access:
import asyncio
from mototli.client import GopherClient
async def main():
async with GopherClient(timeout=30.0) as client:
response = await client.get("gopher.floodgap.com", "/")
for item in response.items:
# Skip informational items for cleaner output
if item.item_type.is_informational:
continue
# Show item type and display text
type_char = item.item_type.value
print(f"[{type_char}] {item.display_text}")
print(f" Selector: {item.selector}")
print(f" Host: {item.host}:{item.port}")
print()
if __name__ == "__main__":
asyncio.run(main())
Output:
[1] Welcome to Floodgap Systems
Selector: /gopher
Host: gopher.floodgap.com:70
[0] The Overbite Project
Selector: /overbite
Host: gopher.floodgap.com:70
...
Step 4: Fetch text content¶
Use get_text() for text files (item type 0):
import asyncio
from mototli.client import GopherClient
async def fetch_text_file():
async with GopherClient() as client:
# Fetch a specific text file
response = await client.get_text(
host="gopher.floodgap.com",
selector="/gopher/wbgopher"
)
# Content is bytes, decode to string
text = response.content.decode("utf-8", errors="replace")
print(text)
if __name__ == "__main__":
asyncio.run(fetch_text_file())
Step 5: Fetch binary content¶
Use get_binary() for binary files (images, archives, etc.):
import asyncio
from pathlib import Path
from mototli.client import GopherClient
async def download_file():
async with GopherClient() as client:
# Fetch a binary file
response = await client.get_binary(
host="gopher.floodgap.com",
selector="/gopher/welcome.gif"
)
# Save to disk
output_path = Path("welcome.gif")
output_path.write_bytes(response.content)
print(f"Downloaded {len(response.content)} bytes to {output_path}")
if __name__ == "__main__":
asyncio.run(download_file())
Step 6: Build an interactive browser¶
Let's combine everything into a simple interactive browser:
import asyncio
from mototli.client import GopherClient
from mototli.protocol import ItemType
class GopherBrowser:
def __init__(self):
self.client = GopherClient(timeout=30.0)
self.history: list[tuple[str, str, int]] = []
async def browse(self, host: str, selector: str = "/", port: int = 70):
"""Fetch and display a directory."""
async with self.client:
response = await self.client.get(host, selector, port=port)
self.history.append((host, selector, port))
# Collect non-info items for numbered selection
items = []
for item in response.items:
if item.item_type.is_informational:
print(f" {item.display_text}")
else:
items.append(item)
print(f"[{len(items):2}] [{item.item_type.value}] {item.display_text}")
return items
async def view_text(self, host: str, selector: str, port: int = 70):
"""Fetch and display a text file."""
async with self.client:
response = await self.client.get_text(host, selector, port=port)
print("\n" + "=" * 60)
print(response.content.decode("utf-8", errors="replace"))
print("=" * 60 + "\n")
async def main():
browser = GopherBrowser()
# Start at floodgap
host = "gopher.floodgap.com"
selector = "/"
port = 70
while True:
print(f"\n=== {host}{selector} ===\n")
items = await browser.browse(host, selector, port)
# Get user input
print("\nEnter number to follow link, 'q' to quit, 'b' to go back:")
choice = input("> ").strip().lower()
if choice == "q":
break
elif choice == "b":
if len(browser.history) > 1:
browser.history.pop() # Remove current
host, selector, port = browser.history.pop() # Get previous
continue
try:
idx = int(choice) - 1
if 0 <= idx < len(items):
item = items[idx]
if item.item_type == ItemType.DIRECTORY:
host = item.host
selector = item.selector
port = item.port
elif item.item_type == ItemType.TEXT:
await browser.view_text(item.host, item.selector, item.port)
else:
print(f"Unsupported type: {item.item_type}")
except ValueError:
print("Invalid input")
if __name__ == "__main__":
asyncio.run(main())
Run your browser:
Step 7: Add error handling¶
Real-world clients need robust error handling:
import asyncio
from mototli.client import GopherClient
async def safe_fetch(host: str, selector: str = "/"):
"""Fetch with proper error handling."""
try:
async with GopherClient(timeout=10.0) as client:
response = await client.get(host, selector)
return response
except TimeoutError:
print(f"Error: Connection to {host} timed out")
return None
except ConnectionRefusedError:
print(f"Error: {host} refused the connection")
return None
except OSError as e:
print(f"Error: Network error - {e}")
return None
async def main():
# Try a working server
response = await safe_fetch("gopher.floodgap.com")
if response:
print(f"Success! Got {len(response.items)} items")
# Try a non-existent server
response = await safe_fetch("nonexistent.example.com")
if __name__ == "__main__":
asyncio.run(main())
Summary¶
You've learned how to:
- Create a
GopherClientwith timeout configuration - Fetch directory listings with
get() - Access item properties (type, selector, host, port)
- Fetch text content with
get_text() - Download binary files with
get_binary() - Build an interactive browser
- Handle network errors gracefully
Common Issues¶
"Connection timed out"¶
The server may be slow or unreachable. Increase the timeout:
Encoding errors¶
Gopher predates Unicode. Use error handling when decoding:
"Connection refused"¶
The server isn't running or the port is wrong. Check:
Next Steps¶
- Understanding Gopher+ - Learn Gopher+ extensions
- Fetch Resources - Advanced fetching patterns
- Client API - Full API reference