Skip to content

Commit 3b89ecd

Browse files
committed
Add new typed api
1 parent 425cb82 commit 3b89ecd

16 files changed

Lines changed: 3053 additions & 574 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ urbackup_api/__pycache__/*
55
# IDE
66
.idea
77
tests/__pycache__
8+
test/__pycache__

README.md

Lines changed: 137 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# urbackup-server-web-api-wrapper
2-
Python wrapper to access and control an UrBackup server
2+
Python wrapper to access and control an UrBackup server.
3+
4+
All responses are returned as **typed dataclasses** (e.g. `StatusResult`,
5+
`Backups`, `UserListItem`) so you get IDE autocompletion and type checking
6+
out of the box.
37

48
## Installation
59

@@ -9,34 +13,150 @@ Install with:
913

1014
## Usage
1115

12-
Start a full file backup:
16+
### Connect and log in
1317

1418
```python
15-
import urbackup_api
19+
from urbackup_api import urbackup_server_typed
20+
21+
server = urbackup_server_typed("http://127.0.0.1:55414/x", "admin", "foo")
22+
server.login()
23+
```
1624

17-
server = urbackup_api.urbackup_server("http://127.0.0.1:55414/x", "admin", "foo")
25+
### Start a backup
1826

19-
server.start_full_file_backup("testclient0")
27+
```python
28+
from urbackup_api import BackupType
29+
30+
# Get the client ID from the status list
31+
status = server.get_status_result()
32+
client = [c for c in status.status if c.name == "testclient0"][0]
33+
34+
results = server.start_backup([client.id], BackupType.FULL_FILE)
35+
for r in results:
36+
print(f"client {r.clientid}: start_ok={r.start_ok}")
2037
```
2138

22-
List clients with no file backup in the last three days:
39+
### List clients with no file backup in the last three days
2340

2441
```python
25-
import urbackup_api
26-
import time
2742
import datetime
28-
server = urbackup_api.urbackup_server("http://127.0.0.1:55414/x", "admin", "foo")
29-
clients = server.get_status()
30-
diff_time = 3*24*60*60 # 3 days
31-
for client in clients:
32-
if client["lastbackup"]=="-" or client["lastbackup"] < time.time() - diff_time:
43+
import time
44+
45+
status = server.get_status_result()
46+
diff_time = 3 * 24 * 60 * 60 # 3 days
3347

34-
if client["lastbackup"]=="-" or client["lastbackup"]==0:
48+
for client in status.status:
49+
if client.lastbackup == "-" or client.lastbackup < time.time() - diff_time:
50+
if client.lastbackup == "-" or client.lastbackup == 0:
3551
lastbackup = "Never"
3652
else:
37-
lastbackup = datetime.datetime.fromtimestamp(client["lastbackup"]).strftime("%x %X")
53+
lastbackup = datetime.datetime.fromtimestamp(client.lastbackup).strftime("%x %X")
54+
55+
print(f"Last file backup at {lastbackup} of client {client.name} is older than three days")
56+
```
57+
58+
### Browse backups
59+
60+
```python
61+
from urbackup_api import Backups
62+
63+
backups = server.get_backups(client.id)
64+
for b in backups.backups:
65+
print(f"Backup {b.id}: {b.size_bytes} bytes, incremental={b.incremental}")
66+
67+
# Browse files inside a backup
68+
files = server.get_files(client.id, backups.backups[0].id, path="/")
69+
for f in files.files:
70+
print(f" {'[dir]' if f.dir else ' '} {f.name}")
71+
```
72+
73+
### Monitor progress
74+
75+
```python
76+
from urbackup_api import ProgressResult
77+
78+
progress = server.get_progress(with_last_activities=True)
79+
for p in progress.progress:
80+
print(f"Client {p.name}: {p.pcdone:.1f}% done")
81+
82+
for a in progress.lastacts or []:
83+
print(f" Last activity: {a.name} ({a.duration}s)")
84+
```
85+
86+
### Manage settings
87+
88+
```python
89+
# Global settings
90+
settings = server.get_general_settings_result()
91+
print(settings["settings"]["max_file_incr"]["value"])
92+
93+
server.save_general_settings({"max_file_incr": "15"})
3894

39-
print("Last file backup at {lastbackup} of client {clientname} is older than three days".format(
40-
lastbackup=lastbackup, clientname=client["name"] ) )
95+
# Client settings (by ID)
96+
client_settings = server.get_client_settings_by_id(client.id)
97+
server.save_client_settings_by_id(client.id, {"internet_authkey": "mykey", "overwrite": "true"})
98+
99+
# Mail / LDAP
100+
server.save_mail_settings({"mail_servername": "smtp.example.com", "mail_serverport": "587"})
101+
ldap = server.get_ldap_settings()
102+
```
103+
104+
### User management
105+
106+
```python
107+
from urbackup_api import UserListItem
108+
109+
# List users
110+
users = server.get_user_list()
111+
for u in users:
112+
print(f"{u.name} (id={u.id})")
113+
114+
# Create / remove / change password
115+
server.create_user("operator", "s3cret")
116+
server.change_user_rights(users[0].id, "all=all")
117+
server.change_user_password(users[0].id, "newpassword")
118+
server.remove_user(users[0].id)
119+
```
120+
121+
### Logs
122+
123+
```python
124+
from urbackup_api import LogLevel
125+
126+
logs = server.get_logs(log_level=LogLevel.WARNING)
127+
for entry in logs:
128+
print(f"[{entry.name}] errors={entry.errors} warnings={entry.warnings}")
129+
130+
# Detailed log entries
131+
details = server.get_log(logs[0].id)
132+
for row in details:
133+
print(f" [{row.level}] {row.message}")
134+
```
135+
136+
### Extra clients
137+
138+
```python
139+
# Extra clients are included in the status result
140+
status = server.get_status_result()
141+
for ec in status.extra_clients:
142+
print(ec["hostname"])
143+
144+
server.add_extra_client("10.0.1.100")
145+
server.remove_extra_client(ec_id)
146+
```
147+
148+
### Remove / stop-remove clients
149+
150+
```python
151+
server.remove_client(client.id)
152+
server.stop_remove_client(client.id) # cancel pending removal
153+
```
154+
155+
### Usage statistics
156+
157+
```python
158+
usage = server.get_usage_stats()
159+
for u in usage:
160+
print(f"{u.name}: files={u.files}, images={u.images}, used={u.used}")
41161
```
42162

tests/test_new_backups.py

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
"""Tests for new typed backup-related operations."""
2+
3+
import pytest
4+
5+
from urbackup_api import (
6+
BackupType,
7+
Backups,
8+
ClientProcessActionTypes,
9+
ProgressResult,
10+
StartBackupResultItem,
11+
)
12+
13+
14+
class TestBackupList:
15+
16+
def test_get_backups_returns_typed(self, server):
17+
server.add_client("pytest-new-backup-client")
18+
status = server.get_client_status("pytest-new-backup-client")
19+
result = server.get_backups(status["id"])
20+
assert isinstance(result, Backups)
21+
assert isinstance(result.backups, list)
22+
assert result.backups == []
23+
24+
def test_get_backups_has_image_backups(self, server):
25+
server.add_client("pytest-new-imgbackup-client")
26+
status = server.get_client_status("pytest-new-imgbackup-client")
27+
result = server.get_backups(status["id"])
28+
assert isinstance(result, Backups)
29+
assert isinstance(result.backup_images, list)
30+
assert result.backup_images == []
31+
32+
def test_get_backups_has_client_info(self, server):
33+
server.add_client("pytest-new-bkp-info")
34+
status = server.get_client_status("pytest-new-bkp-info")
35+
result = server.get_backups(status["id"])
36+
assert result.clientid == status["id"]
37+
assert result.clientname == "pytest-new-bkp-info"
38+
39+
40+
class TestStartBackup:
41+
42+
@pytest.fixture(autouse=True)
43+
def _create_client(self, server):
44+
server.add_client("pytest-new-startbackup")
45+
self._status = server.get_client_status("pytest-new-startbackup")
46+
47+
def test_start_incr_file_backup_no_online_client(self, server):
48+
results = server.start_backup(
49+
[self._status["id"]], BackupType.INCR_FILE
50+
)
51+
assert isinstance(results, list)
52+
for r in results:
53+
assert isinstance(r, StartBackupResultItem)
54+
# Client not online, so start_ok should be False
55+
assert r.start_ok is False
56+
57+
def test_start_full_file_backup_no_online_client(self, server):
58+
results = server.start_backup(
59+
[self._status["id"]], BackupType.FULL_FILE
60+
)
61+
assert isinstance(results, list)
62+
for r in results:
63+
assert isinstance(r, StartBackupResultItem)
64+
assert r.start_ok is False
65+
66+
def test_start_incr_image_backup_no_online_client(self, server):
67+
results = server.start_backup(
68+
[self._status["id"]], BackupType.INCR_IMAGE
69+
)
70+
assert isinstance(results, list)
71+
for r in results:
72+
assert isinstance(r, StartBackupResultItem)
73+
assert r.start_ok is False
74+
75+
def test_start_full_image_backup_no_online_client(self, server):
76+
results = server.start_backup(
77+
[self._status["id"]], BackupType.FULL_IMAGE
78+
)
79+
assert isinstance(results, list)
80+
for r in results:
81+
assert isinstance(r, StartBackupResultItem)
82+
assert r.start_ok is False
83+
84+
def test_start_backup_returns_client_id(self, server):
85+
results = server.start_backup(
86+
[self._status["id"]], BackupType.INCR_FILE
87+
)
88+
assert len(results) == 1
89+
assert results[0].clientid == self._status["id"]
90+
91+
def test_start_backup_returns_start_type(self, server):
92+
results = server.start_backup(
93+
[self._status["id"]], BackupType.FULL_FILE
94+
)
95+
assert len(results) == 1
96+
assert results[0].start_type == "full_file"
97+
98+
99+
class TestProgressTyped:
100+
101+
def test_get_progress_empty(self, server):
102+
result = server.get_progress()
103+
assert isinstance(result, ProgressResult)
104+
assert result.progress == []
105+
106+
def test_stop_process_invalid(self, server):
107+
# Stopping a nonexistent process should still return a result
108+
result = server.stop_process(clientid=99999, process_id=99999)
109+
assert isinstance(result, ProgressResult)
110+
111+
112+
class TestProcessActionTypes:
113+
114+
def test_action_types_enum_values(self):
115+
assert ClientProcessActionTypes.NONE == 0
116+
assert ClientProcessActionTypes.INCR_FILE == 1
117+
assert ClientProcessActionTypes.FULL_FILE == 2
118+
assert ClientProcessActionTypes.INCR_IMAGE == 3
119+
assert ClientProcessActionTypes.FULL_IMAGE == 4
120+
assert ClientProcessActionTypes.RESUME_INCR_FILE == 5
121+
assert ClientProcessActionTypes.RESUME_FULL_FILE == 6
122+
assert ClientProcessActionTypes.RESTORE_FILE == 8
123+
assert ClientProcessActionTypes.RESTORE_IMAGE == 9
124+
assert ClientProcessActionTypes.UPDATE == 10
125+
assert ClientProcessActionTypes.CHECK_INTEGRITY == 11
126+
assert ClientProcessActionTypes.BACKUP_DATABASE == 12
127+
assert ClientProcessActionTypes.RECALCULATE_STATISTICS == 13
128+
129+
def test_backup_type_enum_values(self):
130+
assert BackupType.INCR_FILE.value == "incr_file"
131+
assert BackupType.FULL_FILE.value == "full_file"
132+
assert BackupType.INCR_IMAGE.value == "incr_image"
133+
assert BackupType.FULL_IMAGE.value == "full_image"

0 commit comments

Comments
 (0)