Skip to main content

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

  • Python 3.10+
  • pass installed and configured
  • pass-otp (optional, for OTP support)

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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pass_client-0.1.0.tar.gz (22.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pass_client-0.1.0-py3-none-any.whl (18.7 kB view details)

Uploaded Python 3

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

Hashes for pass_client-0.1.0.tar.gz
Algorithm Hash digest
SHA256 01f9b27d9dd0f510f2cd787a99605325f6ec0f7b90aeb5a728081b4ccd9ea1dc
MD5 b314468a902fab86f6d8d704d55b3ce6
BLAKE2b-256 f1f7676de12a69eb251f7ce6d7872c076e99a5a5b23484773dc1a12cf173223b

See more details on using hashes here.

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

Hashes for pass_client-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 60e5607ae85d5d4421499f2c27115fb7dbfe1e669769f820524c8bac3ad75a38
MD5 80698260e9ea11b6717c45d4a3b57840
BLAKE2b-256 a2fb64c83742b3975d0dab637ed686c6d34a084e26955813fb795d912a7eb79a

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page