Skip to main content

UrsaProxy

A Bearblog-to-Gemini proxy built with Xitzin. It fetches content from a Bearblog RSS feed and HTML pages, converts them to Gemtext format, and serves them over the Gemini protocol.

Features

  • Proxies Bearblog content to Gemini protocol
  • Converts HTML to Gemtext via Markdown intermediate format
  • Generates Atom feeds with Gemini URLs
  • Configurable TTL caching for feed and post data
  • Support for static pages not in RSS feed

Installation

Requires Python 3.13+.

# Using uv
uv add ursaproxy

# Using pip
pip install ursaproxy

Configuration

UrsaProxy can be configured via a TOML file or environment variables.

TOML Configuration (Recommended)

Create an ursaproxy.toml file in your working directory:

bearblog_url = "https://example.bearblog.dev"
blog_name = "My Gemini Blog"
cert_file = "/path/to/cert.pem"
key_file = "/path/to/key.pem"

# Optional settings
gemini_host = "gemini.example.com"
cache_ttl_feed = 300
cache_ttl_post = 1800
host = "localhost"
port = 1965

[pages]
about = "About Me"
now = "What I am doing now"

Environment Variables

Environment variables override TOML settings:

Variable Required Default Description
BEARBLOG_URL Yes - The Bearblog URL to proxy
BLOG_NAME Yes - Display name for the blog
CERT_FILE No None Path to TLS certificate file
KEY_FILE No None Path to TLS private key file
PAGES No {} JSON dict of static pages
GEMINI_HOST No None Hostname for Gemini URLs in feed
CACHE_TTL_FEED No 300 Feed cache TTL in seconds
CACHE_TTL_POST No 1800 Post cache TTL in seconds
HOST No localhost Server bind address
PORT No 1965 Server port

Configuration Priority

Settings are loaded in this order (highest to lowest priority):

  1. Environment variables
  2. ursaproxy.toml file
  3. Default values

Usage

Standalone

ursaproxy

The server will start on gemini://localhost:1965/ by default.

As a Library

UrsaProxy can be embedded in other Xitzin applications for virtual hosting:

from ursaproxy import create_app, Settings

# Create settings programmatically
settings = Settings(
    bearblog_url="https://myblog.bearblog.dev",
    blog_name="My Blog",
)

# Create the app
app = create_app(settings)

# Mount in your main Xitzin app or run directly
app.run(certfile="cert.pem", keyfile="key.pem")

Or load settings from TOML/environment:

from ursaproxy import create_app, load_settings

app = create_app(load_settings())

Routes

Route Description
/ Landing page with recent posts and page links
/post/{slug} Individual blog post with date
/page/{slug} Static page (without date)
/about About page from feed metadata
/feed Atom feed with Gemini URLs

Development

For contributing, clone the repository and install with dev dependencies:

git clone https://github.com/alanbato/ursaproxy.git
cd ursaproxy
uv sync --group dev --group test

Commands

# Run linting
uv run ruff check .

# Run linting with auto-fix
uv run ruff check --fix .

# Format code
uv run ruff format .

# Type check
uv run ty check

# Run all pre-commit hooks
uv run pre-commit run --all-files

# Run tests
uv run pytest

# Run tests with verbose output
uv run pytest -v

Project Structure

src/ursaproxy/
├── __init__.py      # Xitzin app, routes, and entry point
├── config.py        # Pydantic settings for environment config
├── fetcher.py       # HTTP client for fetching Bearblog content
├── converter.py     # HTML -> Markdown -> Gemtext pipeline
├── cache.py         # Simple TTL cache implementation
└── templates/       # Jinja2 templates
    ├── index.gmi    # Landing page template
    ├── post.gmi     # Post/page template
    ├── about.gmi    # About page template
    └── feed.xml     # Atom feed template

Testing

The test suite uses pytest with fixtures for offline testing:

# Run all tests
uv run pytest

# Run specific test file
uv run pytest tests/test_converter.py

# Run with coverage (if installed)
uv run pytest --cov=ursaproxy

HTTP requests are mocked using respx, so tests run completely offline.

How It Works

  1. Feed Fetching: Fetches RSS feed from {BEARBLOG_URL}/feed/?type=rss
  2. HTML Fetching: Fetches individual pages from {BEARBLOG_URL}/{slug}/
  3. Conversion Pipeline:
    • Parse HTML with BeautifulSoup
    • Extract content from <main> element
    • Remove nav, footer, scripts, styles
    • Convert to Markdown with markdownify
    • Convert to Gemtext with md2gemini
  4. Caching: Feed and posts are cached with configurable TTLs
  5. Serving: Content served via Gemini protocol using Xitzin

License

MIT

Metadata

Release files for ursaproxy 0.4.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for ursaproxy 0.4.0
File Size Uploaded Provenance Actions
ursaproxy-0.4.0.tar.gz 8.2 kB With provenance Details for ursaproxy-0.4.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for ursaproxy 0.4.0
File Interpreter ABI Platform Actions
ursaproxy-0.4.0-py3-none-any.whl Python 3 none any Details for ursaproxy-0.4.0-py3-none-any.whl

Total release size: 19.5 kB

Release files / ursaproxy-0.4.0.tar.gz

Download URL ursaproxy-0.4.0.tar.gz
Size 8.2 kB
Tags Source
SHA-256 checksum
How to use checksums
b0adb2a45f49e05dfe3b6d8a99fba956babf6672bd4924f9961091f2644a89c7
BLAKE2b-256 checksum
How to use checksums
8093663ef9f299241323b90c3590586d659a014c6ddda510dd42d68de926c61c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.7

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Feb 5, 2026.

Transparency log

Release files / ursaproxy-0.4.0-py3-none-any.whl

Download URL ursaproxy-0.4.0-py3-none-any.whl
Size 11.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
46d1d98c8bff3f48db77d06c21de636cf98773c0d9c31c5e64f2e7eca14336e3
BLAKE2b-256 checksum
How to use checksums
8519b4267fd7cf4f7bd1432318ed1053d871c3c549b1ec3b558be01aee533ca5
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.7

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Feb 5, 2026.

Transparency log

Security for ursaproxy 0.4.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for ursaproxy-0.4.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Feb 5, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page