A drop-in replacement for ops.pebble.Client that uses the Pebble CLI
Project description
Shimmer - shiny Pebble client
A 100% compatible drop-in replacement for ops.pebble.Client that uses the Pebble CLI tool instead of socket communication.
Overview
Shimmer provides PebbleCliClient, a class that implements the same interface as ops.pebble.Client but communicates with Pebble via CLI commands instead of socket communication. This is useful for environments with restricted socket access (such as a Rock or Juju container).
Installation
Install from PyPI
uv pip install shimmer
Install development version
git clone https://github.com/tonyandrewmeyer/shimmer
cd shimmer
uv pip install -e .
Quick Start
from shimmer import PebbleCliClient as Client
# Create a client instance:
client = Client(
socket_path="/var/lib/pebble/default/.pebble.socket", # Optional: for env setup
pebble_binary="pebble", # Path to pebble binary
timeout=30.0, # Default command timeout
)
# Use exactly like ops.pebble.Client:
services = client.get_services()
client.start_services(["myservice"])
# Execute commands:
process = client.exec(["echo", "hello world"])
stdout, stderr = process.wait_output()
print(stdout) # "hello world\n"
# File operations:
client.push("/path/to/file", "content")
content = client.pull("/path/to/file").read()
# Layer management
layer = """
services:
myservice:
override: replace
command: python3 -m http.server 8080
startup: enabled
"""
client.add_layer("mylayer", layer)
client.replan_services()
Advanced Usage
Custom Binary Path
client = PebbleCliClient(pebble_binary="/usr/local/bin/pebble")
Environment Configuration
# If using custom Pebble directory:
client = PebbleCliClient(socket_path="/custom/path/.pebble.socket")
# This automatically sets:
# PEBBLE=/custom/path
# PEBBLE_SOCKET=/custom/path/.pebble.socket
Error Handling
from shimmer import APIError, ConnectionError, TimeoutError
try:
client.start_services(["nonexistent"])
except APIError as e:
print(f"API Error: {e.message} (code: {e.code})")
except ConnectionError:
print("Could not connect to Pebble")
except TimeoutError:
print("Operation timed out")
Process Execution
# Simple command:
process = client.exec(["ls", "-la"])
stdout, stderr = process.wait_output()
# With environment and options:
process = client.exec(
["python3", "script.py"],
environment={"PYTHONPATH": "/app"},
working_dir="/app",
timeout=60.0,
user="appuser",
)
# Streaming I/O:
process = client.exec(["cat"], stdin="Hello World\n")
stdout, stderr = process.wait_output()
Architecture
The PebbleCliClient works by:
- Command Translation - Converts API calls to CLI commands
- Output Parsing - Parses CLI output back to Python objects
- Error Mapping - Maps CLI errors to compatible exceptions
- Process Management - Handles subprocess execution and I/O
Limitations
While this client aims for 100% compatibility, there are some limitations due to CLI constraints:
- Performance - CLI calls have higher overhead than socket communication
- Concurrency - Each operation spawns a new process
- Streaming - Some streaming operations may be buffered
- Platform - Requires Pebble binary in PATH or specified location
Other minor limitations:
replan_services(),start_services(),stop_services(), andrestart_services()are only able to return the change ID if no timeout is set.notify()only supports custom notices.get_notices()cannot include thelast_occurred,last_data,repeat_after, orexpire_afterfieldsget_changes()cannot include thekind,tasks,err,ready_time, ordatafields, and guesses at thereadyfieldautostart_services()is an alias forreplan()(possibly we could fix this by getting the current state?)wait_change()is yet to be implementedack_warnings()is yet to be implementedget_warnings()is only implemented for the 'no warnings' caselist_file()looks up the user and group IDs locally, which is very likely to be wrongget_identities()is unable to get the user ID for local identities
Comparison with ops.pebble.Client
| Feature | ops.pebble.Client | PebbleCliClient |
|---|---|---|
| Communication | Unix socket | CLI commands |
| Performance | High | Moderate |
| Setup | Requires socket access | Requires binary |
| Compatibility | Native | 100% API compatible |
| Dependencies | ops library | ops library + CLI |
| Use Cases | Production charms | Testing, development, debugging |
Examples
Service Management
from shimmer import PebbleCliClient as Client
client = Client()
# Get all services:
services = client.get_services()
for service in services:
print(f"{service.name}: {service.current}")
# Start specific services:
change_id = client.start_services(["web", "db"])
print(f"Started services, change: {change_id}")
# Wait for change to complete:
change = client.wait_change(change_id)
print(f"Change {change.id} status: {change.status}")
File Management
# Create directory structure:
client.make_dir("/app/config", make_parents=True, permissions=0o755)
# Write configuration file:
config = """
server:
port: 8080
host: 0.0.0.0
"""
client.push("/app/config/server.yaml", config)
# Read file back:
content = client.pull("/app/config/server.yaml").read()
print(content)
# List directory contents:
files = client.list_files("/app/config")
for file in files:
print(f"{file.name} ({file.type})")
Layer Management
# Define service layer:
layer = {
"summary": "Web application layer",
"description": "Defines the web application service",
"services": {
"webapp": {
"override": "replace",
"summary": "Web application",
"command": "python3 -m uvicorn app:main --host 0.0.0.0 --port 8080",
"startup": "enabled",
"environment": {
"PYTHONPATH": "/app"
},
"user": "webapp",
"group": "webapp",
}
},
"checks": {
"webapp-health": {
"override": "replace",
"level": "alive",
"http": {"url": "http://localhost:8080/health"},
"period": "10s",
"timeout": "3s",
"threshold": 3,
}
}
}
# Add and apply layer:
client.add_layer("webapp", layer)
change_id = client.replan_services()
# Wait for services to start:
client.wait_change(change_id)
# Check service status:
services = client.get_services(["webapp"])
print(f"webapp status: {services[0].current}")
Command Execution
# Execute simple command:
process = client.exec(["whoami"])
stdout, stderr = process.wait_output()
print(f"Running as: {stdout.strip()}")
# Execute with service context:
process = client.exec(
["python3", "-c", "import os; print(os.getcwd())"],
service_context="webapp"
)
stdout, stderr = process.wait_output()
print(f"Service working directory: {stdout.strip()}")
# Execute interactive command:
process = client.exec(["python3", "-c", "print(input('Name: '))"])
process.stdin.write("Alice\n")
process.stdin.close()
stdout, stderr = process.wait_output()
print(f"Output: {stdout.strip()}")
Health Checks
# Get all checks:
checks = client.get_checks()
for check in checks:
print(f"{check.name}: {check.status} ({check.level})")
# Start specific checks:
started = client.start_checks(["webapp-health"])
print(f"Started checks: {started}")
# Monitor check status:
import time
for _ in range(5):
checks = client.get_checks(names=["webapp-health"])
if checks:
check = checks[0]
print(f"Check status: {check.status}, failures: {check.failures}")
time.sleep(2)
Notice Management
# Get recent notices:
notices = client.get_notices()
for notice in notices:
print(f"{notice.type}: {notice.key} (occurred: {notice.occurrences})")
# Create custom notice:
notice_id = client.notify(
type="custom",
key="myapp.com/deployment",
data={"version": "1.2.3", "environment": "production"}
)
print(f"Created notice: {notice_id}")
# Get specific notice:
notice = client.get_notice(notice_id)
print(f"Notice data: {notice.last_data}")
Troubleshooting
Common Issues
-
"Pebble binary not found"
# Specify full path to pebble client = PebbleCliClient(pebble_binary="/snap/bin/pebble")
-
"Permission denied"
# Ensure user has access to Pebble directory sudo chown -R $USER:$USER $PEBBLE
-
"Connection error"
# Check if Pebble is running import subprocess result = subprocess.run(["pebble", "version"], capture_output=True) print(result.stdout)
Debugging
Enable debug logging to see CLI commands:
import logging
logging.basicConfig(level=logging.DEBUG)
# Commands will be logged before execution:
client = PebbleCliClient()
services = client.get_services()
Contributing
Contributions are welcome! Please see CONTRIBUTING.md for guidelines.
Changelog
See CHANGELOG.md for version history and changes.
Related Projects
- ops - The operator framework
- pebble - The Pebble service manager
- juju - Juju
- charmcraft - Juju charm development tools
- rockcraft - Rock development tools
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 pebble_shimmer-1.0.0a4.tar.gz.
File metadata
- Download URL: pebble_shimmer-1.0.0a4.tar.gz
- Upload date:
- Size: 26.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.12.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
39bdf37faba90482518a9ad27c7af74d85378ce7ee35c1ab694fa720a4f04118
|
|
| MD5 |
0ebeb169e9f5c13bc1292e9165d74725
|
|
| BLAKE2b-256 |
b7b70e87f6b1da34485bd0b42731248710c37945a64146c7840b1cf41cd91538
|
Provenance
The following attestation bundles were made for pebble_shimmer-1.0.0a4.tar.gz:
Publisher:
publish.yaml on tonyandrewmeyer/shimmer
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pebble_shimmer-1.0.0a4.tar.gz -
Subject digest:
39bdf37faba90482518a9ad27c7af74d85378ce7ee35c1ab694fa720a4f04118 - Sigstore transparency entry: 311565280
- Sigstore integration time:
-
Permalink:
tonyandrewmeyer/shimmer@4fcfcdc880ef044f479ddbc58d9b4c893731f573 -
Branch / Tag:
refs/tags/v1.0.0a4 - Owner: https://github.com/tonyandrewmeyer
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yaml@4fcfcdc880ef044f479ddbc58d9b4c893731f573 -
Trigger Event:
push
-
Statement type:
File details
Details for the file pebble_shimmer-1.0.0a4-py3-none-any.whl.
File metadata
- Download URL: pebble_shimmer-1.0.0a4-py3-none-any.whl
- Upload date:
- Size: 18.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.12.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
36f2ba7ba1d09896609ce838571975c06f72c809c4eb81f64886e9d8efb07779
|
|
| MD5 |
e80358f2bb212ebf0c454d24fb071006
|
|
| BLAKE2b-256 |
a931a2f3aaf39fb3cba1fd17f8ca8c738ff363250f977314d3d2a0cf66f78730
|
Provenance
The following attestation bundles were made for pebble_shimmer-1.0.0a4-py3-none-any.whl:
Publisher:
publish.yaml on tonyandrewmeyer/shimmer
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pebble_shimmer-1.0.0a4-py3-none-any.whl -
Subject digest:
36f2ba7ba1d09896609ce838571975c06f72c809c4eb81f64886e9d8efb07779 - Sigstore transparency entry: 311565286
- Sigstore integration time:
-
Permalink:
tonyandrewmeyer/shimmer@4fcfcdc880ef044f479ddbc58d9b4c893731f573 -
Branch / Tag:
refs/tags/v1.0.0a4 - Owner: https://github.com/tonyandrewmeyer
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yaml@4fcfcdc880ef044f479ddbc58d9b4c893731f573 -
Trigger Event:
push
-
Statement type: