python-blossom
High-level Python client for the Blossom protocol (blob storage via Nostr auth).
Implements core BUDs:
- BUD-01: Blob retrieval (GET/HEAD)
- BUD-02: Upload, List, Delete
- BUD-03: User Server List event generation (kind 10063)
- BUD-04: Mirror blob
- BUD-05: Media optimization (optional endpoints)
- BUD-06: Upload requirements (HEAD /upload)
Installation
Install from PyPI:
pip install python-blossom
Or install from GitHub:
pip install git+https://github.com/Jxck-S/python-blossom.git
Quick Start
from python_blossom import BlossomClient
nsec = 'nsec....' # Your Nostr private key (nsec)
servers = [
'https://blossom.band',
'https://nostr.download',
'https://blossom.primal.net'
]
client = BlossomClient(nsec=nsec, default_servers=servers)
# Optional: set a custom request timeout (default is 30 seconds)
# client = BlossomClient(nsec=nsec, default_servers=servers, timeout=10)
# Upload blob data
result = client.upload_blob(servers[0], data=b'blob data', mime_type='image/png')
sha256 = result['sha256']
print(f"Uploaded: {result['url']}")
# Get blob
blob = client.get_blob(servers[0], sha256, mime_type='image/png')
downloaded_data = blob.get_bytes()
print(f"Downloaded: {len(downloaded_data)} bytes")
# HEAD request to check blob metadata
meta = client.head_upload_requirements(servers[0], data=b'blob data', mime_type='image/png')
print(f"Server supports: {meta}")
# List user's blobs
blobs = client.list_blobs(servers[0], pubkey=client.pubkey_hex, use_auth=True)
print(f"Found {len(blobs)} blobs")
# Delete blob
client.delete_blob(servers[0], sha256, description='Cleanup')
# Mirror blob to another server
mirror_result = client.mirror_blob(servers[1], servers[0], sha256)
print(f"Mirrored to: {mirror_result['url']}")
# Generate User Server List event (kind 10063, BUD-03)
event = client.generate_server_list_event(servers=servers)
print(f"Event ID: {event['id']}")
# Publish server list event to relays
event_id = client.publish_server_list_event(relays=['wss://relay.damus.io'])
print(f"Published: {event_id}")
Async Client
AsyncBlossomClient has the same methods and return values as BlossomClient, just awaitable. Use it inside asyncio apps (bots, web servers) so uploads don't block the event loop, and to upload to many servers at once:
import asyncio
from python_blossom import AsyncBlossomClient
async def main():
async with AsyncBlossomClient(nsec=nsec, default_servers=servers) as client:
# Uploads to every server concurrently (file is read, hashed and signed once)
results = await client.upload_to_all(file_path='photo.jpg', max_concurrency=5)
blob = await client.get_blob(servers[0], results[servers[0]]['sha256'])
await client.publish_server_list_event(relays=['wss://relay.damus.io'])
asyncio.run(main())
Both clients keep a pooled HTTP connection. Use them as context managers (with / async with) or call client.close() / await client.aclose() when you're done. To customise the HTTP layer (proxies, retries, custom transports), pass your own httpx.Client / httpx.AsyncClient as http_client=.
Publishing Media to Nostr with Redundancy
Here's how to upload a photo to multiple Blossom servers and publish it to Nostr with automatic failover:
from python_blossom import BlossomClient
from pynostr.event import Event
from pynostr.key import PrivateKey
import json
nsec = 'nsec....' # Your Nostr private key
servers = [
'https://blossom.band',
'https://nostr.download',
'https://blossom.primal.net'
]
client = BlossomClient(nsec=nsec, default_servers=servers)
# Step 1: Publish your server list to Nostr (do this once or when you change servers)
# This advertises all your Blossom servers to the Nostr network
# Clients use this to find backup servers if the primary one is down
relays = ['wss://relay.damus.io', 'wss://relay.snort.social']
client.publish_server_list_event(relays=relays, servers=servers)
print(f"✓ Published server list to relays")
# Step 2: Upload photo to ALL servers for redundancy
with open('photo.jpg', 'rb') as f:
photo_data = f.read()
upload_results = client.upload_to_all(
data=photo_data,
mime_type='image/jpeg',
description='My vacation photo'
)
# Get the primary URL (first successful upload)
primary_url = None
for server, result in upload_results.items():
if 'url' in result and not 'error' in result:
primary_url = result['url']
sha256 = result['sha256']
print(f"✓ Uploaded to {server}: {primary_url}")
break
# Step 3: Create Nostr note with media tag pointing to primary URL
# If the primary server goes down, clients will use your server list event
# (kind 10063) to find the photo on your other servers
private_key = PrivateKey.from_nsec(nsec)
event = Event(
content="Check out this photo! 📸",
kind=1 # Text note
)
# Add image tag with primary URL
# Format: ["image", url, "m" MIME type, "alt" description, "x" sha256]
event.add_tag("image", primary_url, "m", "image/jpeg", "alt", "My vacation photo", "x", sha256)
event.sign(private_key.hex())
# Publish to relays using pynostr
from pynostr.relay_manager import RelayManager
relay_manager = RelayManager(timeout=5)
for relay in relays:
relay_manager.add_relay(relay)
relay_manager.publish_event(event)
relay_manager.run_sync()
relay_manager.close_all_relay_connections()
Timeout Configuration
All HTTP requests use a default timeout of 30 seconds (configurable per-client). Without a timeout, a slow or unresponsive Blossom server can cause the calling thread to hang indefinitely.
from python_blossom import BlossomClient, DEFAULT_TIMEOUT
# Use the default (30s)
client = BlossomClient(nsec=nsec, default_servers=servers)
# Set a shorter timeout for all requests made by this client
client = BlossomClient(nsec=nsec, default_servers=servers, timeout=10)
# Disable timeout entirely (not recommended)
client = BlossomClient(nsec=nsec, default_servers=servers, timeout=None)
# Inspect the default
print(DEFAULT_TIMEOUT) # 30.0
The timeout value applies to every HTTP call made by the client (sync and async). To set separate connect vs. read timeouts, pass a tuple, or an httpx.Timeout for full control:
client = BlossomClient(nsec=nsec, timeout=(5, 30)) # 5s connect, 30s read
On timeout, httpx.TimeoutException is raised. Network failures raise httpx.TransportError; HTTP error responses raise BlossomError subclasses (see below).
API Methods
Upload & Download
upload_blob(server, data=None, file_path=None, mime_type=None, description=None, use_auth=False)
- Upload blob data (raw bytes or file)
- Auto-detects MIME type if not specified
- Returns blob descriptor with
sha256,url,size,type
get_blob(server, sha256, mime_type=None, use_auth=False)
- Download blob by SHA256 hash
- Returns
Blobobject withcontent,sha256,mime_typeproperties - Call
blob.get_bytes()orblob.save(file_path)to access data
head_upload_requirements(server, data=None, file_path=None, mime_type=None, use_auth=False)
- Check server upload requirements before uploading
- Returns headers:
content_type,content_length,accept_ranges
Media (BUD-05)
media_upload(server, data=None, file_path=None, mime_type=None, description=None, use_auth=False)
- Upload blob with media optimization
- Returns blob descriptor
media_head(server, data=None, file_path=None, mime_type=None, use_auth=False)
- Check media upload requirements
- Returns headers
Blob Management
upload_blob(server, data=None, file_path=None, mime_type=None, description=None, use_auth=True)
- Upload blob data (raw bytes or file)
- Auto-detects MIME type if not specified
- Returns blob descriptor with
sha256,url,size,type
upload_to_all(data=None, file_path=None, mime_type=None, description=None, use_auth=True)
- Upload blob to all default servers (concurrently with
AsyncBlossomClient, one after another withBlossomClient) - Uses servers configured during client initialization
- Returns dict mapping server URLs to results/errors
list_blobs(server, pubkey=None, cursor=None, limit=None, use_auth=False)
- List blobs for a public key (accepts npub or hex format)
- Returns list of blob descriptors
- Requires
use_auth=Truefor private server listings
delete_blob(server, sha256, description=None)
- Delete blob from server
- Requires authorization
- Returns confirmation
mirror_blob(server, source_url, sha256, description=None)
- Mirror blob from one server to another
- Returns blob descriptor on destination server
Head Blob
head_blob(server, sha256, use_auth=False)
- Get blob metadata without downloading content
- Returns headers:
content_type,content_length,accept_ranges
Server Management (BUD-03)
Server list events (Nostr kind 10063) are like "advertising" - they publicly announce which Blossom servers a user uploads media to. When you publish a server list event to the Nostr network, anyone can look up your public key and discover which servers you use. NOSTR clients use this to know where to look for published media in a NOSTR note.
generate_server_list_event(servers=None)
- Generate a Nostr server list event (kind 10063, BUD-03)
- Contains tags with your Blossom server URLs
- Automatically signed with your private key
- Returns Nostr event dict ready for publishing
publish_server_list_event(relays, servers=None)
- Publish your server list event to Nostr relays
- Advertises your Blossom servers publicly by your public key
relays: List of relay URLs (wss://) where the event will be publishedservers: Optional list of server URLs (defaults to configured servers on the client)- Returns event ID of the published event
fetch_server_list(relays, pubkey=None, timeout=2.0)
- Look up which Blossom servers a user has advertised
- Query Nostr relays for a user's server list event
relays: List of relay URLs to querypubkey: Optional public key in any format (npub or hex) to look up (defaults to your own)- Returns list of server URLs that user has advertised
Authorization
Methods with use_auth=True parameter automatically generate NIP-98 authorization events:
- kind 24242
- Tags:
t=<verb>,expiration,x=<hash>(when applicable) - Base64 encoded in
Authorization: Nostr <base64>header - Auto-signed with your private key
Error Handling
Raises BlossomError or specific subclasses on errors:
InvalidAuthorizationEvent: 401 auth failureBlobNotFound: 404 blob not foundTooManyRequests: 429 rate limitBlossomError: Other HTTP errors
from python_blossom.errors import BlobNotFound, TooManyRequests
try:
blob = client.get_blob(server, sha256)
except BlobNotFound:
print("Blob not found")
except TooManyRequests:
print("Rate limited - try again later")
TODO / Unsupported Endpoints
The following Blossom endpoints are not yet implemented:
- BUD-09: PUT
/report- Report abuse/misinformation (planned for future release)
License
Unlicense / Public Domain (matches upstream Blossom spec repository).
Metadata
Release files for python-blossom 1.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| python_blossom-1.1.0.tar.gz | 26.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| python_blossom-1.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 46.6 kB
Release files / python_blossom-1.1.0.tar.gz
| Download URL | python_blossom-1.1.0.tar.gz |
|---|---|
| Size | 26.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
28a844be35aed9c9fdabfad8273ac4abbbe5f806fcf0e43f08e1ba639c285155
|
|
BLAKE2b-256 checksum How to use checksums |
7b5ba1efcc68191ab91d09159d477f664bdf95a187fc5f70790ba2d3ef63244e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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 Oct 14, 2026.
Transparency logRelease files / python_blossom-1.1.0-py3-none-any.whl
| Download URL | python_blossom-1.1.0-py3-none-any.whl |
|---|---|
| Size | 19.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
75b927ebdff0f8c6b460c9f714d50ef1725c31d8555d73c4f82ccaff4528e316
|
|
BLAKE2b-256 checksum How to use checksums |
5e99327b359fe8047ea6ef680286d36319c87508e8a3ead474f287e161ee092e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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 Oct 14, 2026.
Transparency log