Metadata-Version: 2.4
Name: systemd-unit-edit
Version: 0.1.0
Classifier: Programming Language :: Rust
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Programming Language :: Python :: Implementation :: PyPy
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Topic :: System :: Systems Administration
Classifier: Operating System :: POSIX :: Linux
License-File: LICENSE
Summary: Python bindings for lossless systemd unit file editing
Keywords: systemd,unit,parser,lossless
Author-email: Jelmer Vernooij <jelmer@debian.org>
License-Expression: Apache-2.0
Requires-Python: >=3.8
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
Project-URL: Repository, https://github.com/jelmer/systemd-unit-edit

# systemd-unit-edit

Python bindings for the [systemd-unit-edit](https://github.com/jelmer/systemd-unit-edit) Rust library — a lossless parser and editor for systemd unit files.

All whitespace, comments, and formatting are preserved during parsing and modification.

## Installation

```bash
pip install systemd-unit-edit
```

Requires Python 3.8+ and a supported platform (Linux x86_64, etc.).

## Quick Start

```python
from systemd_unit_edit import SystemdUnit

# Parse from a string
unit = SystemdUnit("""[Unit]
Description=Test Service
After=network.target

[Service]
Type=simple
ExecStart=/usr/bin/test
""")

# Read sections
for section in unit.sections():
    print(section.name())

# Get a specific section
svc = unit.get_section("Service")
print(svc.get("Type"))  # "simple"

# Modify values
svc.set("Type", "forking")

# Add new entries
svc.add("Restart", "on-failure")

# Write back
print(unit.text())  # lossless roundtrip
```

## Loading and Saving Files

```python
# Load from file
unit = SystemdUnit.from_file("/etc/systemd/system/foo.service")

# Load with drop-in overrides merged (e.g. foo.service.d/*.conf)
unit = SystemdUnit.from_file_with_dropins("/etc/systemd/system/foo.service")

# Write to file
unit.write_to_file("/tmp/foo.service")
```

## API Reference

### `SystemdUnit`

The root type representing a parsed systemd unit file.

| Method | Description |
|---|---|
| `SystemdUnit(text)` | Parse from string. Raises `ValueError` on syntax errors. |
| `SystemdUnit.from_file(path)` | Load from a file. Raises `OSError` / `ValueError`. |
| `SystemdUnit.from_file_with_dropins(path)` | Load with drop-in `.conf` files merged. |
| `.sections()` | List of all `Section` objects. |
| `.get_section(name)` | Get a `Section` by name, or `None`. |
| `.add_section(name)` | Append a new empty section. |
| `.text()` | Serialize back to string (lossless). |
| `.write_to_file(path)` | Write to a file. |
| `.merge_dropin(dropin)` | Merge another `SystemdUnit` as a drop-in. |

### `Section`

Represents a section (e.g. `[Unit]`, `[Service]`).

| Method | Description |
|---|---|
| `.name()` | Section name string. |
| `.entries()` | List of all `Entry` objects. |
| `.get(key)` | First value for a key, or `None`. |
| `.get_all(key)` | All values for a key (for multi-entry keys). |
| `.set(key, value)` | Set a value (replaces first occurrence, or adds). |
| `.add(key, value)` | Append a new entry (always adds, even if key exists). |
| `.insert_at(index, key, value)` | Insert at position among entries. |
| `.insert_before(existing_key, key, value)` | Insert before the first entry with `existing_key`. |
| `.insert_after(existing_key, key, value)` | Insert after the first entry with `existing_key`. |
| `.remove(key)` | Remove the first entry with the key. |
| `.remove_all(key)` | Remove all entries with the key. |
| `.remove_value(key, value)` | Remove a specific value from space-separated entries. |
| `.get_list(key)` | Get value as a space-separated list. |
| `.set_list(key, values)` | Set a space-separated list value. |
| `.get_bool(key)` | Get value as `bool` (`yes`/`no`/`true`/`false`/`1`/`0`/`on`/`off`). |
| `.set_bool(key, value)` | Set a boolean value (writes `"yes"` / `"no"`). |

### `Entry`

Represents a key=value entry in a section.

| Method | Description |
|---|---|
| `.key()` | Key name string. |
| `.value()` | Value with line continuations resolved. |
| `.raw_value()` | Raw value as it appears in the file. |
| `.set_value(new_value)` | Replace the value in place. |
| `.value_as_list()` | Value split on whitespace. |
| `.value_as_bool()` | Value parsed as boolean. |
| `.is_quoted()` | `True` if value is surrounded by `"..."` or `'...'`. |
| `.unquoted_value()` | Value with surrounding quotes removed. |
| `.unescape_value()` | Value with C-style escapes processed (`\n`, `\t`, etc.). |
| `.expand_specifiers(ctx)` | Expand `%`-specifiers using a `SpecifierContext`. |
| `Entry.escape_value(s)` | (static) Escape a string for systemd values. |
| `Entry.format_bool(b)` | (static) Format bool as `"yes"` / `"no"`. |

### `SpecifierContext`

Context for expanding systemd `%`-specifiers.

```python
from systemd_unit_edit import SpecifierContext

# Manual setup
ctx = SpecifierContext()
ctx.set("i", "myinstance")
ctx.expand("/var/lib/%i")  # "/var/lib/myinstance"

# From a unit name (sets %N, %n, %p, %i automatically)
ctx = SpecifierContext.with_unit_name("foo@bar.service")
ctx.expand("%p: %i")  # "foo: bar"
```

### Module Functions

| Function | Description |
|---|---|
| `parse_timespan(s)` | Parse a systemd time span to seconds (float). e.g. `"1h 30min"` → `5400.0` |
| `is_accumulating_directive(key)` | `True` if the directive accumulates values in drop-ins (e.g. `Wants`, `After`). |

## Running Tests

```bash
pip install pytest
maturin develop
pytest tests/
```

## License

Apache-2.0


<a href="https://www.buymeacoffee.com/bhoehn" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/default-orange.png" alt="Buy Me A Coffee" height="41" width="174"></a>

