Typed Python API client for pass (Unix password store)
Project description
pass-client
A typed Python API client for pass (the standard Unix password manager).
Features
- Full type hints and dataclass models
- Sync and async clients
- Complete pass-otp extension support
- Custom exception hierarchy for error handling
- Concurrent operations (async)
Requirements
Installation
# From source
pip install -e .
# With dev dependencies
pip install -e ".[dev]"
Quick Start
from pass_client import PassClient
client = PassClient()
# Get a password
entry = client.get("websites/github")
print(entry.password)
print(entry.username) # Parsed from metadata
# Generate a new password
generated = client.generate("websites/newsite", length=20)
print(generated.password)
# Insert with metadata
client.insert("websites/example", "mypassword", metadata={
"username": "user@example.com",
"url": "https://example.com",
})
API Reference
PassClient
The synchronous client for interacting with pass.
from pass_client import PassClient
client = PassClient()
# Or with custom store path
client = PassClient(store_path=Path("/custom/store"))
Password Operations
# Get password entry (includes metadata parsing)
entry = client.get("path/to/entry")
print(entry.password) # First line
print(entry.username) # From "username:" or "user:" field
print(entry.url) # From "url:" or "site:" field
print(entry.metadata) # All parsed key-value pairs
# Get just the password
password = client.get_password("path/to/entry")
# Insert new entry
client.insert("path/to/new", "password123")
client.insert("path/to/new", "password123", metadata={
"username": "user@example.com",
"url": "https://example.com",
"notes": "My account",
})
client.insert("path/to/existing", "newpass", force=True) # Overwrite
# Generate password
generated = client.generate("path/to/entry", length=25)
generated = client.generate("path/to/entry", length=16, no_symbols=True)
generated = client.generate("path/to/entry", in_place=True) # Keep metadata
print(generated.password)
print(generated.strength) # "weak", "moderate", "strong", "very_strong"
# Remove entry
client.remove("path/to/entry", force=True)
client.remove("path/to/dir", recursive=True, force=True)
# Move/rename
client.move("old/path", "new/path")
# Copy
client.copy("source/path", "dest/path")
# Check existence
if client.exists("path/to/entry"):
print("Entry exists")
Search Operations
# List entries
entries = client.list() # Root level
entries = client.list("websites") # Subdirectory
for entry in entries:
print(entry.name, entry.path, entry.entry_type)
# Find by name pattern
results = client.find("github")
for result in results:
print(result.path)
# Search content (grep)
results = client.grep("api.*key")
for result in results:
print(result.path, result.context)
Clipboard
# Copy to clipboard (clears after timeout)
client.clip("path/to/entry")
client.clip("path/to/entry", timeout=30) # 30 seconds
client.clip("path/to/entry", line=2) # Copy second line
Git Operations
# Run git commands in password store
output = client.git("status")
output = client.git("log", "--oneline", "-5")
# Convenience methods
client.git_push()
client.git_pull()
# Get commit history
commits = client.git_log(limit=10)
for commit in commits:
print(commit.hash, commit.message, commit.author, commit.timestamp)
Store Info
info = client.info()
print(info.store_path) # Path to store
print(info.gpg_id) # GPG key ID
print(info.git_enabled) # Whether git is configured
print(info.entry_count) # Number of entries
AsyncPassClient
Async version with identical API plus concurrent operations.
import asyncio
from pass_client import AsyncPassClient
async def main():
client = AsyncPassClient()
# All methods are async
entry = await client.get("websites/github")
print(entry.password)
# Concurrent fetches
entries = await client.get_many([
"websites/github",
"websites/gitlab",
"email/personal",
])
for path, entry in entries.items():
print(f"{path}: {entry.username}")
# Concurrent inserts
results = await client.insert_many([
("sites/new1", "pass1", {"username": "user1"}),
("sites/new2", "pass2", {"username": "user2"}),
])
asyncio.run(main())
Context Manager
from pass_client import AsyncPassClientContext
async with AsyncPassClientContext() as client:
entry = await client.get("websites/github")
OTP Support (pass-otp)
Full support for the pass-otp extension.
from pass_client import PassClient, OTPUri
client = PassClient()
# Generate OTP code
code = client.otp_code("2fa/github")
print(code.code) # "123456"
print(code.formatted) # "123 456"
# Copy OTP to clipboard
client.otp_code_clip("2fa/github", timeout=10)
# Get OTP URI details
uri = client.otp_uri("2fa/github")
print(uri.issuer) # "GitHub"
print(uri.account) # "user@example.com"
print(uri.secret) # Base32 secret
print(uri.algorithm) # OTPAlgorithm.SHA1
print(uri.digits) # 6
print(uri.period) # 30 (TOTP)
# Get full OTP entry
entry = client.otp_get("2fa/github")
print(entry.path)
print(entry.issuer)
print(entry.is_totp) # True for TOTP
# Check if OTP is configured
if client.otp_has("websites/github"):
code = client.otp_code("websites/github")
# Insert OTP (new entry)
client.otp_insert("2fa/gitlab",
"otpauth://totp/GitLab:user@example.com?secret=JBSWY3DPEHPK3PXP&issuer=GitLab")
# Insert with just the secret
client.otp_insert("2fa/aws", "ABCDEFGHIJKLMNOP", from_secret=True)
# Append OTP to existing password entry
client.otp_append("websites/github",
"otpauth://totp/GitHub:user?secret=SECRETKEY&issuer=GitHub")
# Validate URI syntax
if client.otp_validate("otpauth://totp/Test:user?secret=ABC"):
print("Valid OTP URI")
# Display QR code in terminal (requires qrencode)
client.otp_uri_qrcode("2fa/github")
Async OTP with Concurrent Fetches
async def get_all_codes():
client = AsyncPassClient()
# Fetch multiple OTP codes concurrently
codes = await client.otp_code_many([
"2fa/github",
"2fa/gitlab",
"2fa/aws",
])
for path, code in codes.items():
print(f"{path}: {code.formatted}")
Parsing OTP URIs
from pass_client import OTPUri, OTPType, OTPAlgorithm
# Parse from string
uri = OTPUri.from_uri(
"otpauth://totp/GitHub:user@example.com?secret=JBSWY3DPEHPK3PXP&issuer=GitHub"
)
print(uri.otp_type) # OTPType.TOTP
print(uri.issuer) # "GitHub"
print(uri.account) # "user@example.com"
print(uri.secret) # "JBSWY3DPEHPK3PXP"
print(uri.algorithm) # OTPAlgorithm.SHA1
print(uri.digits) # 6
print(uri.period) # 30
# Convert back to URI string
uri_string = uri.to_uri()
Error Handling
All exceptions inherit from PassError:
from pass_client import (
PassClient,
PassError,
PassNotFoundError,
PassGPGError,
PassOTPNotFoundError,
PassOTPExtensionNotFoundError,
)
client = PassClient()
try:
entry = client.get("nonexistent/path")
except PassNotFoundError as e:
print(f"Entry not found: {e.path}")
except PassGPGError as e:
print(f"GPG decryption failed: {e.message}")
except PassError as e:
print(f"Pass error: {e.message}")
# OTP-specific errors
try:
code = client.otp_code("entry/without/otp")
except PassOTPNotFoundError as e:
print(f"No OTP configured for: {e.path}")
except PassOTPExtensionNotFoundError:
print("Install pass-otp: apt install pass-extension-otp")
Exception Hierarchy
PassError
├── PassNotFoundError
├── PassStoreNotInitializedError
├── PassGPGError
├── PassClipboardError
├── PassGitError
├── PassGenerateError
├── PassInsertError
└── PassOTPError
├── PassOTPNotFoundError
├── PassOTPInvalidURIError
└── PassOTPExtensionNotFoundError
Models
PasswordEntry
@dataclass
class PasswordEntry:
path: str
password: str
metadata: dict[str, str]
# Properties
username: Optional[str] # From "username:" or "user:"
url: Optional[str] # From "url:" or "site:"
notes: Optional[str] # From "notes:"
OTPCode
@dataclass
class OTPCode:
code: str
path: str
remaining_seconds: Optional[int]
# Properties
formatted: str # "123 456" or "1234 5678"
OTPUri
@dataclass
class OTPUri:
uri: str
otp_type: OTPType # TOTP or HOTP
issuer: Optional[str]
account: str
secret: str
algorithm: OTPAlgorithm # SHA1, SHA256, SHA512
digits: int # 6 or 8
period: int # TOTP interval (default 30)
counter: Optional[int] # HOTP counter
Development
# Install dev dependencies
pip install -e ".[dev]"
# Run tests
pytest
# Run tests with coverage
pytest --cov=pass_client
# Type checking
mypy pass_client
# Linting
ruff check pass_client
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pass_client-0.1.0.tar.gz.
File metadata
- Download URL: pass_client-0.1.0.tar.gz
- Upload date:
- Size: 22.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.10
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
01f9b27d9dd0f510f2cd787a99605325f6ec0f7b90aeb5a728081b4ccd9ea1dc
|
|
| MD5 |
b314468a902fab86f6d8d704d55b3ce6
|
|
| BLAKE2b-256 |
f1f7676de12a69eb251f7ce6d7872c076e99a5a5b23484773dc1a12cf173223b
|
File details
Details for the file pass_client-0.1.0-py3-none-any.whl.
File metadata
- Download URL: pass_client-0.1.0-py3-none-any.whl
- Upload date:
- Size: 18.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.10
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
60e5607ae85d5d4421499f2c27115fb7dbfe1e669769f820524c8bac3ad75a38
|
|
| MD5 |
80698260e9ea11b6717c45d4a3b57840
|
|
| BLAKE2b-256 |
a2fb64c83742b3975d0dab637ed686c6d34a084e26955813fb795d912a7eb79a
|