backupchan-client-lib 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,6 @@
1
+ __pycache__/
2
+ *.egg-info
3
+ *.swp
4
+ build/
5
+ *.bak
6
+ dist/
@@ -0,0 +1,11 @@
1
+ Copyright 2025 moltony
2
+
3
+ Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
4
+
5
+ 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
6
+
7
+ 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
8
+
9
+ 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
10
+
11
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,69 @@
1
+ Metadata-Version: 2.4
2
+ Name: backupchan-client-lib
3
+ Version: 0.1.0
4
+ Summary: Library for interfacing with Backup-chan.
5
+ Author-email: Moltony <koronavirusnyj@gmail.com>
6
+ License: BSD-3-Clause
7
+ Classifier: Development Status :: 4 - Beta
8
+ Classifier: Environment :: Console
9
+ Classifier: License :: OSI Approved :: BSD License
10
+ Classifier: Natural Language :: English
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python :: 3 :: Only
13
+ Classifier: Topic :: System :: Archiving :: Backup
14
+ Classifier: Topic :: Software Development :: Libraries
15
+ Classifier: Typing :: Typed
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Requires-Dist: requests
19
+ Provides-Extra: dev
20
+ Requires-Dist: pytest; extra == "dev"
21
+ Requires-Dist: requests-mock; extra == "dev"
22
+ Dynamic: license-file
23
+
24
+ # Backup-chan client library
25
+
26
+ This is the Python library for interfacing with a Backup-chan server.
27
+
28
+ ## Installing
29
+
30
+ ```bash
31
+ # Normal install:
32
+ pip install .
33
+ # With extra dependencies for development:
34
+ pip install .[dev]
35
+ ```
36
+
37
+ For instructions on setting up the server, refer to Backup-chan server's README.
38
+
39
+ ## Testing
40
+
41
+ ```
42
+ pytest
43
+ ```
44
+
45
+ ## Example
46
+
47
+ ```python
48
+ from backupchan import *
49
+
50
+ api = API("http://192.168.1.43", 5000, "your api key")
51
+
52
+ targets = api.list_targets()
53
+ for target in targets:
54
+ print(target)
55
+
56
+ target_id = api.new_target(
57
+ "the waifu collection",
58
+ BackupType.MULTI,
59
+ BackupRecycleCriteria.AGE,
60
+ 10,
61
+ BackupRecycleAction.RECYCLE,
62
+ "/var/backups/waifu",
63
+ "wf-$I_$D",
64
+ False,
65
+ None
66
+ )
67
+ target = api.get_target(target_id)
68
+ print(f"Created new target: {target}")
69
+ ```
@@ -0,0 +1,46 @@
1
+ # Backup-chan client library
2
+
3
+ This is the Python library for interfacing with a Backup-chan server.
4
+
5
+ ## Installing
6
+
7
+ ```bash
8
+ # Normal install:
9
+ pip install .
10
+ # With extra dependencies for development:
11
+ pip install .[dev]
12
+ ```
13
+
14
+ For instructions on setting up the server, refer to Backup-chan server's README.
15
+
16
+ ## Testing
17
+
18
+ ```
19
+ pytest
20
+ ```
21
+
22
+ ## Example
23
+
24
+ ```python
25
+ from backupchan import *
26
+
27
+ api = API("http://192.168.1.43", 5000, "your api key")
28
+
29
+ targets = api.list_targets()
30
+ for target in targets:
31
+ print(target)
32
+
33
+ target_id = api.new_target(
34
+ "the waifu collection",
35
+ BackupType.MULTI,
36
+ BackupRecycleCriteria.AGE,
37
+ 10,
38
+ BackupRecycleAction.RECYCLE,
39
+ "/var/backups/waifu",
40
+ "wf-$I_$D",
41
+ False,
42
+ None
43
+ )
44
+ target = api.get_target(target_id)
45
+ print(f"Created new target: {target}")
46
+ ```
@@ -0,0 +1,5 @@
1
+ from .connection import Connection
2
+ from .models import *
3
+ from .api import API, BackupchanAPIError
4
+
5
+ __all__ = ["Connection", "BackupRecycleCriteria", "BackupRecycleAction", "BackupType", "BackupTarget", "Backup", "API", "BackupchanAPIError"]
@@ -0,0 +1,121 @@
1
+ import io
2
+ from .connection import Connection
3
+ from .models import Backup, BackupTarget, BackupRecycleCriteria, BackupRecycleAction, BackupType
4
+
5
+ class BackupchanAPIError(Exception):
6
+ def __init__(self, message: str, status_code: int | None = None):
7
+ super().__init__(message)
8
+ self.status_code = status_code
9
+
10
+ def check_success(response: tuple[dict, int]) -> dict:
11
+ data, status = response
12
+ if not data.get("success", False):
13
+ raise BackupchanAPIError(f"Server returned error: {data} (code {status})", status)
14
+ return data
15
+
16
+ class API:
17
+ def __init__(self, host: str, port: int, api_key: str):
18
+ self.connection = Connection(host, port, api_key)
19
+
20
+ def list_targets(self, page: int = 1) -> list[BackupTarget]:
21
+ response = self.connection.get(f"target?page={page}")
22
+ targets = response[0]["targets"]
23
+ return [BackupTarget.from_dict(target) for target in targets]
24
+
25
+ def new_target(self, name: str, backup_type: BackupType, recycle_criteria: BackupRecycleCriteria, recycle_value: int, recycle_action: BackupRecycleAction, location: str, name_template: str, deduplicate: bool, alias: str | None) -> str:
26
+ """
27
+ Returns ID of new target.
28
+ """
29
+ data = {
30
+ "name": name,
31
+ "backup_type": backup_type,
32
+ "recycle_criteria": recycle_criteria,
33
+ "recycle_value": recycle_value,
34
+ "recycle_action": recycle_action,
35
+ "location": location,
36
+ "name_template": name_template,
37
+ "deduplicate": deduplicate,
38
+ "alias": alias
39
+ }
40
+ resp_json = check_success(self.connection.post("target", data))
41
+ return resp_json["id"]
42
+
43
+ def upload_backup(self, target_id: str, file: io.IOBase, filename: str, manual: bool) -> str:
44
+ """
45
+ Returns ID of new backup.
46
+ """
47
+ data = {
48
+ "manual": int(manual)
49
+ }
50
+
51
+ files = {
52
+ "backup_file": (filename, file)
53
+ }
54
+
55
+ response = self.connection.post_form(f"target/{target_id}/upload", data=data, files=files)
56
+ resp_json = check_success(response)
57
+ return resp_json["id"]
58
+
59
+ def get_target(self, id: str) -> tuple[BackupTarget, list[Backup]]:
60
+ response = self.connection.get(f"target/{id}")
61
+ resp_json = check_success(response)
62
+ return BackupTarget.from_dict(resp_json["target"]), [Backup.from_dict(backup) for backup in resp_json["backups"]]
63
+
64
+ def edit_target(self, id: str, name: str, recycle_criteria: BackupRecycleCriteria, recycle_value: int, recycle_action: BackupRecycleAction, location: str, name_template: str, deduplicate: bool, alias: str | None):
65
+ data = {
66
+ "name": name,
67
+ "recycle_criteria": recycle_criteria,
68
+ "recycle_value": recycle_value,
69
+ "recycle_action": recycle_action,
70
+ "location": location,
71
+ "name_template": name_template,
72
+ "deduplicate": deduplicate,
73
+ "alias": alias
74
+ }
75
+ response = self.connection.patch(f"target/{id}", data=data)
76
+ check_success(response)
77
+
78
+ def delete_target(self, id: str, delete_files: bool):
79
+ data = {
80
+ "delete_files": delete_files
81
+ }
82
+ response = self.connection.delete(f"target/{id}", data=data)
83
+ check_success(response)
84
+
85
+ def delete_target_backups(self, id: str, delete_files: bool):
86
+ data = {
87
+ "delete_files": delete_files
88
+ }
89
+ response = self.connection.delete(f"target/{id}/all", data=data)
90
+ check_success(response)
91
+
92
+ def delete_backup(self, id: str, delete_files: bool):
93
+ data = {
94
+ "delete_files": delete_files
95
+ }
96
+ response = self.connection.delete(f"backup/{id}", data=data)
97
+ check_success(response)
98
+
99
+ def recycle_backup(self, id: str, is_recycled: bool):
100
+ data = {
101
+ "is_recycled": is_recycled
102
+ }
103
+ response = self.connection.patch(f"backup/{id}", data=data)
104
+ check_success(response)
105
+
106
+ def list_recycled_backups(self) -> list[Backup]:
107
+ response = self.connection.get("recycle_bin")
108
+ resp_json = check_success(response)
109
+ return [Backup.from_dict(backup) for backup in resp_json["backups"]]
110
+
111
+ def clear_recycle_bin(self, delete_files: bool):
112
+ data = {
113
+ "delete_files": delete_files
114
+ }
115
+ response = self.connection.delete("recycle_bin", data=data)
116
+ check_success(response)
117
+
118
+ def get_log(self, tail: int) -> str:
119
+ response = self.connection.get(f"log?tail={tail}")
120
+ resp_json = check_success(response)
121
+ return resp_json["log"]
@@ -0,0 +1,49 @@
1
+ import requests
2
+ import json
3
+
4
+ class Connection:
5
+ def __init__(self, host: str, port: int, api_key: str):
6
+ # TODO check these
7
+ self.api_key = api_key
8
+
9
+ if host.startswith("http://") or host.startswith("https://"):
10
+ server_host = host.rstrip("/")
11
+ else:
12
+ server_host = f"http://{host.rstrip('/')}"
13
+ self.base_url = f"{server_host}:{port}"
14
+
15
+ def endpoint_url(self, endpoint: str) -> str:
16
+ return f"{self.base_url}/api/{endpoint.rstrip('/')}"
17
+
18
+ def headers(self) -> dict:
19
+ return {"Authorization": f"Bearer {self.api_key}"}
20
+
21
+ def get(self, endpoint: str, raise_on_error=False) -> tuple[dict, int]:
22
+ response = requests.get(self.endpoint_url(endpoint), headers=self.headers())
23
+ if raise_on_error:
24
+ response.raise_for_status()
25
+ return response.json(), response.status_code
26
+
27
+ def post(self, endpoint: str, data: dict, raise_on_error=False) -> tuple[dict, int]:
28
+ response = requests.post(self.endpoint_url(endpoint), headers=self.headers(), json=data)
29
+ if raise_on_error:
30
+ response.raise_for_status()
31
+ return response.json(), response.status_code
32
+
33
+ def post_form(self, endpoint: str, data: dict, files: dict, raise_on_error=False) -> tuple[dict, int]:
34
+ response = requests.post(self.endpoint_url(endpoint), headers=self.headers(), data=data, files=files)
35
+ if raise_on_error:
36
+ response.raise_for_status()
37
+ return response.json(), response.status_code
38
+
39
+ def patch(self, endpoint: str, data: dict, raise_on_error=False) -> tuple[dict, int]:
40
+ response = requests.patch(self.endpoint_url(endpoint), headers=self.headers(), json=data)
41
+ if raise_on_error:
42
+ response.raise_for_status()
43
+ return response.json(), response.status_code
44
+
45
+ def delete(self, endpoint: str, data: dict, raise_on_error=False) -> tuple[dict, int]:
46
+ response = requests.delete(self.endpoint_url(endpoint), headers=self.headers(), json=data)
47
+ if raise_on_error:
48
+ response.raise_for_status()
49
+ return response.json(), response.status_code
@@ -0,0 +1,50 @@
1
+ from enum import Enum
2
+ from dataclasses import dataclass
3
+ from typing import Optional
4
+ from datetime import datetime
5
+
6
+ class BackupRecycleCriteria(str, Enum):
7
+ NONE = "none"
8
+ COUNT = "count"
9
+ AGE = "age"
10
+
11
+ class BackupRecycleAction(str, Enum):
12
+ DELETE = "delete"
13
+ RECYCLE = "recycle"
14
+
15
+ class BackupType(str, Enum):
16
+ SINGLE = "single"
17
+ MULTI = "multi"
18
+
19
+ @dataclass
20
+ class BackupTarget:
21
+ id: str
22
+ name: str
23
+ target_type: BackupType
24
+ recycle_criteria: BackupRecycleCriteria
25
+ recycle_value: Optional[int]
26
+ recycle_action: BackupRecycleAction
27
+ location: str
28
+ name_template: str
29
+ deduplicate: bool
30
+ alias: str | None
31
+
32
+ @staticmethod
33
+ def from_dict(d: dict) -> "BackupTarget":
34
+ return BackupTarget(d["id"], d["name"], d["target_type"], d["recycle_criteria"], d["recycle_value"], d["recycle_action"], d["location"], d["name_template"], d["deduplicate"], d["alias"])
35
+
36
+ @dataclass
37
+ class Backup:
38
+ id: str
39
+ target_id: str
40
+ created_at: datetime
41
+ manual: bool
42
+ is_recycled: bool
43
+ filesize: int
44
+
45
+ def pretty_created_at(self) -> str:
46
+ return self.created_at.strftime("%B %d, %Y %H:%M")
47
+
48
+ @staticmethod
49
+ def from_dict(d: dict) -> "Backup":
50
+ return Backup(d["id"], d["target_id"], datetime.fromisoformat(d["created_at"]), d["manual"], d["is_recycled"], d["filesize"])
@@ -0,0 +1,69 @@
1
+ Metadata-Version: 2.4
2
+ Name: backupchan-client-lib
3
+ Version: 0.1.0
4
+ Summary: Library for interfacing with Backup-chan.
5
+ Author-email: Moltony <koronavirusnyj@gmail.com>
6
+ License: BSD-3-Clause
7
+ Classifier: Development Status :: 4 - Beta
8
+ Classifier: Environment :: Console
9
+ Classifier: License :: OSI Approved :: BSD License
10
+ Classifier: Natural Language :: English
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python :: 3 :: Only
13
+ Classifier: Topic :: System :: Archiving :: Backup
14
+ Classifier: Topic :: Software Development :: Libraries
15
+ Classifier: Typing :: Typed
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Requires-Dist: requests
19
+ Provides-Extra: dev
20
+ Requires-Dist: pytest; extra == "dev"
21
+ Requires-Dist: requests-mock; extra == "dev"
22
+ Dynamic: license-file
23
+
24
+ # Backup-chan client library
25
+
26
+ This is the Python library for interfacing with a Backup-chan server.
27
+
28
+ ## Installing
29
+
30
+ ```bash
31
+ # Normal install:
32
+ pip install .
33
+ # With extra dependencies for development:
34
+ pip install .[dev]
35
+ ```
36
+
37
+ For instructions on setting up the server, refer to Backup-chan server's README.
38
+
39
+ ## Testing
40
+
41
+ ```
42
+ pytest
43
+ ```
44
+
45
+ ## Example
46
+
47
+ ```python
48
+ from backupchan import *
49
+
50
+ api = API("http://192.168.1.43", 5000, "your api key")
51
+
52
+ targets = api.list_targets()
53
+ for target in targets:
54
+ print(target)
55
+
56
+ target_id = api.new_target(
57
+ "the waifu collection",
58
+ BackupType.MULTI,
59
+ BackupRecycleCriteria.AGE,
60
+ 10,
61
+ BackupRecycleAction.RECYCLE,
62
+ "/var/backups/waifu",
63
+ "wf-$I_$D",
64
+ False,
65
+ None
66
+ )
67
+ target = api.get_target(target_id)
68
+ print(f"Created new target: {target}")
69
+ ```
@@ -0,0 +1,16 @@
1
+ .gitignore
2
+ LICENSE
3
+ README.md
4
+ pyproject.toml
5
+ backupchan/__init__.py
6
+ backupchan/api.py
7
+ backupchan/connection.py
8
+ backupchan/models.py
9
+ backupchan_client_lib.egg-info/PKG-INFO
10
+ backupchan_client_lib.egg-info/SOURCES.txt
11
+ backupchan_client_lib.egg-info/dependency_links.txt
12
+ backupchan_client_lib.egg-info/requires.txt
13
+ backupchan_client_lib.egg-info/top_level.txt
14
+ tests/conftest.py
15
+ tests/test_connection.py
16
+ tests/test_models.py
@@ -0,0 +1,5 @@
1
+ requests
2
+
3
+ [dev]
4
+ pytest
5
+ requests-mock
@@ -0,0 +1,35 @@
1
+ [project]
2
+ name = "backupchan-client-lib"
3
+ version = "0.1.0"
4
+ description="Library for interfacing with Backup-chan."
5
+ authors = [
6
+ { name="Moltony", email="koronavirusnyj@gmail.com" } # but I probably won't respond...
7
+ ]
8
+ dependencies = [
9
+ "requests"
10
+ ]
11
+
12
+ readme = "README.md"
13
+ license = {text = "BSD-3-Clause"}
14
+
15
+ classifiers = [
16
+ "Development Status :: 4 - Beta",
17
+ "Environment :: Console",
18
+ "License :: OSI Approved :: BSD License",
19
+ "Natural Language :: English",
20
+ "Operating System :: OS Independent",
21
+ "Programming Language :: Python :: 3 :: Only",
22
+ "Topic :: System :: Archiving :: Backup",
23
+ "Topic :: Software Development :: Libraries",
24
+ "Typing :: Typed"
25
+ ]
26
+
27
+ [build-system]
28
+ requires = ["setuptools", "wheel"]
29
+ build-backend = "setuptools.build_meta"
30
+
31
+ [project.optional-dependencies]
32
+ dev = [
33
+ "pytest",
34
+ "requests-mock"
35
+ ]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,8 @@
1
+ import pytest
2
+ from backupchan import Connection
3
+
4
+ TEST_TOKEN = "test-token"
5
+
6
+ @pytest.fixture
7
+ def conn():
8
+ return Connection("http://localhost", 5000, TEST_TOKEN)
@@ -0,0 +1,144 @@
1
+ import uuid
2
+ import io
3
+ import pytest
4
+ import requests_mock
5
+ from backupchan import Connection
6
+
7
+ # example responses all taken straight from the api docs
8
+
9
+ NULL_UUID = "00000000-0000-0000-0000-000000000000"
10
+
11
+ def check_request(mock: requests_mock.Mocker, conn: Connection, method: str, payload: None | dict = None):
12
+ last_request = mock.last_request
13
+ assert mock.called
14
+ assert last_request.method == method
15
+ assert last_request.headers["Authorization"] == conn.headers()["Authorization"]
16
+
17
+ if payload is not None:
18
+ if "application/json" in last_request.headers["Content-Type"]:
19
+ assert last_request.json() == payload
20
+ else:
21
+ assert last_request.text is not None
22
+
23
+ def test_get(conn):
24
+ mock_response = {
25
+ "success": True,
26
+ "targets": [
27
+ {
28
+ "id": NULL_UUID,
29
+ "name": "My backup",
30
+ "target_type": "multi",
31
+ "recycle_criteria": "count",
32
+ "recycle_value": 10,
33
+ "recycle_action": "recycle",
34
+ "location": "/var/backups/MyBackup",
35
+ "name_template": "backup-$I-$D"
36
+ }
37
+ ]
38
+ }
39
+
40
+ with requests_mock.Mocker() as m:
41
+ m.get("http://localhost:5000/api/target", json=mock_response, status_code=200)
42
+
43
+ result, status = conn.get("target")
44
+
45
+ check_request(m, conn, "GET")
46
+
47
+ assert status == 200
48
+ assert result["success"] is True
49
+ assert len(result["targets"]) == 1
50
+ assert result["targets"][0]["name"] == "My backup"
51
+
52
+ def test_post(conn):
53
+ mock_response = {
54
+ "success": True,
55
+ "id": NULL_UUID
56
+ }
57
+
58
+ with requests_mock.Mocker() as m:
59
+ m.post("http://localhost:5000/api/target", json=mock_response, status_code=201)
60
+
61
+ payload = {
62
+ "name": "Backupy",
63
+ "backup_type": "multi",
64
+ "recycle_criteria": "count",
65
+ "recycle_value": 10,
66
+ "recycle_action": "recycle",
67
+ "location": "/bakupy",
68
+ "name_template": "bkp-$I"
69
+ }
70
+
71
+ result, status = conn.post("target", payload)
72
+
73
+ check_request(m, conn, "POST", payload)
74
+
75
+ assert status == 201
76
+ assert result["success"] is True
77
+ assert result["id"] == NULL_UUID
78
+
79
+ def test_post_form(conn):
80
+ mock_response = {
81
+ "success": True,
82
+ "id": NULL_UUID
83
+ }
84
+
85
+ test_uuid = str(uuid.uuid4())
86
+
87
+ with requests_mock.Mocker() as m:
88
+ m.post(f"http://localhost:5000/api/target/{test_uuid}/upload", json=mock_response, status_code=200)
89
+
90
+ payload = {
91
+ "manual": False
92
+ }
93
+
94
+ files = {
95
+ "backup_file": io.BytesIO(b"i am file")
96
+ }
97
+
98
+ result, status = conn.post_form(f"target/{test_uuid}/upload", data=payload, files=files)
99
+
100
+ last_request = m.last_request
101
+ check_request(m, conn, "POST", payload)
102
+ assert "multipart/form-data" in last_request.headers["Content-Type"]
103
+
104
+ assert status == 200
105
+ assert result["success"] is True
106
+ assert result["id"] == NULL_UUID
107
+
108
+ def test_delete(conn):
109
+ mock_response = {
110
+ "success": True
111
+ }
112
+
113
+ with requests_mock.Mocker() as m:
114
+ m.delete(f"http://localhost:5000/api/target/{NULL_UUID}", json=mock_response, status_code=200)
115
+
116
+ payload = {
117
+ "delete_files": True
118
+ }
119
+
120
+ result, status = conn.delete(f"target/{NULL_UUID}", data=payload)
121
+
122
+ check_request(m, conn, "DELETE", payload)
123
+
124
+ assert status == 200
125
+ assert result["success"] is True
126
+
127
+ def test_patch(conn):
128
+ mock_response = {
129
+ "success": True
130
+ }
131
+
132
+ with requests_mock.Mocker() as m:
133
+ m.patch(f"http://localhost:5000/api/backup/{NULL_UUID}", json=mock_response, status_code=200)
134
+
135
+ payload = {
136
+ "is_recycled": True
137
+ }
138
+
139
+ result, status = conn.patch(f"backup/{NULL_UUID}", data=payload)
140
+
141
+ check_request(m, conn, "PATCH", payload)
142
+
143
+ assert status == 200
144
+ assert result["success"] is True
@@ -0,0 +1,47 @@
1
+ import datetime
2
+ from backupchan.models import Backup, BackupType, BackupRecycleAction, BackupRecycleCriteria, BackupTarget
3
+
4
+ def test_target_from_dict():
5
+ json_target = {
6
+ "id": "deadbeef-dead-beef-dead-beefdeadbeef",
7
+ "name": "touhoku kiritest",
8
+ "target_type": "multi",
9
+ "recycle_criteria": "count",
10
+ "recycle_value": 13,
11
+ "recycle_action": "recycle",
12
+ "location": "/var/backups/touhoku",
13
+ "name_template": "$I_kiritanpo",
14
+ "deduplicate": False,
15
+ "alias": None
16
+ }
17
+
18
+ target = BackupTarget.from_dict(json_target)
19
+ assert target.id == json_target["id"]
20
+ assert target.name == json_target["name"]
21
+ assert target.target_type == BackupType.MULTI
22
+ assert target.recycle_criteria == BackupRecycleCriteria.COUNT
23
+ assert target.recycle_value == json_target["recycle_value"]
24
+ assert target.recycle_action == BackupRecycleAction.RECYCLE
25
+ assert target.location == json_target["location"]
26
+ assert target.name_template == json_target["name_template"]
27
+ assert target.deduplicate == json_target["deduplicate"]
28
+ assert target.alias == json_target["alias"]
29
+
30
+ def test_backup_from_dict():
31
+ created_at = datetime.datetime.now()
32
+ json_backup = {
33
+ "id": "d0d0caca-d0d0-caca-d0d0-cacad0d0caca",
34
+ "target_id": "deadbeef-dead-beef-dead-beefdeadbeef",
35
+ "created_at": created_at.isoformat(),
36
+ "manual": False,
37
+ "is_recycled": True,
38
+ "filesize": 123456
39
+ }
40
+
41
+ backup = Backup.from_dict(json_backup)
42
+ assert backup.id == json_backup["id"]
43
+ assert backup.target_id == json_backup["target_id"]
44
+ assert backup.created_at == created_at
45
+ assert backup.manual == json_backup["manual"]
46
+ assert backup.is_recycled == json_backup["is_recycled"]
47
+ assert backup.filesize == json_backup["filesize"]