backupchan-client-lib 0.1.0__py3-none-any.whl

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.
backupchan/__init__.py ADDED
@@ -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"]
backupchan/api.py ADDED
@@ -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
backupchan/models.py ADDED
@@ -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,9 @@
1
+ backupchan/__init__.py,sha256=jhwUn-HVSPLU8JO7aFSxggnUDvh2jfVPLyBIyzqqxA8,241
2
+ backupchan/api.py,sha256=1MtoEKrffNcIILBsf_9mw5O3_v6KgAhUzthQzu_hlnM,4668
3
+ backupchan/connection.py,sha256=9T-0YHWAN_lPKzxF9NDUPS9kmJdfRqm5tms0gIOEjTw,2150
4
+ backupchan/models.py,sha256=s_7a2bITQ-EaPJYwA1B4rcqyXJzod-ZpdaNbp-Vg4gE,1346
5
+ backupchan_client_lib-0.1.0.dist-info/licenses/LICENSE,sha256=cddrZI_oMZyNm2uGZRY6oBNcPO4dm6vVz5yG5CuVi2c,1456
6
+ backupchan_client_lib-0.1.0.dist-info/METADATA,sha256=iU6T2l8Ya7NIsYlqM8c-ak0uwk7XzqVBzZ0s8AFcoWo,1606
7
+ backupchan_client_lib-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
8
+ backupchan_client_lib-0.1.0.dist-info/top_level.txt,sha256=eaLLvj7unYp2MDP291KmY-V6ELzb-oWc5lyIgoj8ciM,11
9
+ backupchan_client_lib-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -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 @@
1
+ backupchan