backer-cli 0.1.1.dev0__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.
- backer_cli-0.1.1.dev0/PKG-INFO +32 -0
- backer_cli-0.1.1.dev0/README.md +1 -0
- backer_cli-0.1.1.dev0/pyproject.toml +50 -0
- backer_cli-0.1.1.dev0/setup.cfg +4 -0
- backer_cli-0.1.1.dev0/src/backer/__init__.py +1 -0
- backer_cli-0.1.1.dev0/src/backer/__main__.py +4 -0
- backer_cli-0.1.1.dev0/src/backer/apitypes.py +21 -0
- backer_cli-0.1.1.dev0/src/backer/cli.py +82 -0
- backer_cli-0.1.1.dev0/src/backer/http.py +124 -0
- backer_cli-0.1.1.dev0/src/backer/jobs.py +9 -0
- backer_cli-0.1.1.dev0/src/backer/version.py +100 -0
- backer_cli-0.1.1.dev0/src/backer_cli.egg-info/PKG-INFO +32 -0
- backer_cli-0.1.1.dev0/src/backer_cli.egg-info/SOURCES.txt +15 -0
- backer_cli-0.1.1.dev0/src/backer_cli.egg-info/dependency_links.txt +1 -0
- backer_cli-0.1.1.dev0/src/backer_cli.egg-info/entry_points.txt +2 -0
- backer_cli-0.1.1.dev0/src/backer_cli.egg-info/requires.txt +8 -0
- backer_cli-0.1.1.dev0/src/backer_cli.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: backer-cli
|
|
3
|
+
Version: 0.1.1.dev0
|
|
4
|
+
Summary: Backup control automation tool
|
|
5
|
+
Author-email: lmriccardo <rlmarca.dev@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/lmriccardo/backer
|
|
8
|
+
Project-URL: Repository, https://github.com/lmriccardo/backer
|
|
9
|
+
Project-URL: Issues, https://github.com/lmriccardo/backer/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/lmriccardo/backer/releases
|
|
11
|
+
Keywords: backup,rsync,automation,cron,nas
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: System Administrators
|
|
14
|
+
Classifier: Topic :: System :: Archiving :: Backup
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
21
|
+
Requires-Python: >=3.12
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
Requires-Dist: pydantic[email]>=2.6
|
|
24
|
+
Requires-Dist: croniter>=2.0
|
|
25
|
+
Requires-Dist: PyYAML>=6.0
|
|
26
|
+
Requires-Dist: tabulate>=0.9
|
|
27
|
+
Requires-Dist: requests>=2.32
|
|
28
|
+
Requires-Dist: rich>=13.7
|
|
29
|
+
Requires-Dist: aiohttp>=3.13
|
|
30
|
+
Requires-Dist: packaging>=26.0
|
|
31
|
+
|
|
32
|
+
# The Backer CLI application
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# The Backer CLI application
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "backer-cli"
|
|
7
|
+
version = "0.1.1-dev"
|
|
8
|
+
description = "Backup control automation tool"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.12"
|
|
11
|
+
keywords = ["backup","rsync","automation","cron","nas"]
|
|
12
|
+
authors = [{ name = "lmriccardo", email = "rlmarca.dev@gmail.com" }]
|
|
13
|
+
license = { text = "MIT" }
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Intended Audience :: System Administrators",
|
|
17
|
+
"Topic :: System :: Archiving :: Backup",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Operating System :: POSIX :: Linux"
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
dependencies = [
|
|
27
|
+
"pydantic[email]>=2.6",
|
|
28
|
+
"croniter>=2.0",
|
|
29
|
+
"PyYAML>=6.0",
|
|
30
|
+
"tabulate>=0.9",
|
|
31
|
+
"requests>=2.32",
|
|
32
|
+
"rich>=13.7",
|
|
33
|
+
"aiohttp>=3.13",
|
|
34
|
+
"packaging>=26.0"
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
[project.urls]
|
|
38
|
+
Homepage = "https://github.com/lmriccardo/backer"
|
|
39
|
+
Repository = "https://github.com/lmriccardo/backer"
|
|
40
|
+
Issues = "https://github.com/lmriccardo/backer/issues"
|
|
41
|
+
Changelog = "https://github.com/lmriccardo/backer/releases"
|
|
42
|
+
|
|
43
|
+
[project.scripts]
|
|
44
|
+
backer = "backer.cli:main"
|
|
45
|
+
|
|
46
|
+
[tool.setuptools]
|
|
47
|
+
package-dir = {"" = "src"}
|
|
48
|
+
|
|
49
|
+
[tool.setuptools.packages.find]
|
|
50
|
+
where=["src"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0-dev"
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from pydantic import BaseModel
|
|
2
|
+
|
|
3
|
+
class ResponseModel(BaseModel): ...
|
|
4
|
+
|
|
5
|
+
class VersionResponse(ResponseModel):
|
|
6
|
+
api : int
|
|
7
|
+
commit : str
|
|
8
|
+
date : str
|
|
9
|
+
go_version : str
|
|
10
|
+
version : str
|
|
11
|
+
|
|
12
|
+
def __str__(self) -> str:
|
|
13
|
+
return (
|
|
14
|
+
f"Daemon Version: backerd {self.version} " +
|
|
15
|
+
f"({self.commit}) built {self.date} " +
|
|
16
|
+
f"go={self.go_version}"
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
class HealthzResponse(ResponseModel):
|
|
20
|
+
ok: bool
|
|
21
|
+
version: VersionResponse
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import asyncio
|
|
3
|
+
import http
|
|
4
|
+
import aiohttp.client_exceptions as httpexcept
|
|
5
|
+
import backer.jobs as jobsfn
|
|
6
|
+
import backer.http as httputils
|
|
7
|
+
import backer.version as version
|
|
8
|
+
|
|
9
|
+
CLIENT : httputils.BackerClient | None = None
|
|
10
|
+
|
|
11
|
+
class UnreachableDaemonError(Exception):
|
|
12
|
+
def __init__(self) -> None: super().__init__()
|
|
13
|
+
def __str__(self) -> str:
|
|
14
|
+
return "Backer daemon is not reachable or it is not running"
|
|
15
|
+
|
|
16
|
+
async def _exec(parser: argparse.ArgumentParser) -> None:
|
|
17
|
+
# Parse the arguments
|
|
18
|
+
args = parser.parse_args()
|
|
19
|
+
|
|
20
|
+
global CLIENT
|
|
21
|
+
CLIENT = httputils.BackerClient(sock_path=httputils.get_unix_socket())
|
|
22
|
+
await CLIENT.create() # Create the session with the connector
|
|
23
|
+
|
|
24
|
+
# Before performing any command, check if the daemon
|
|
25
|
+
# is reachable by performing a simple healtz request.
|
|
26
|
+
resp = await CLIENT.make_request(httputils.Endpoints.Healthz)
|
|
27
|
+
if resp.code != http.HTTPStatus.OK or not resp.body.ok:
|
|
28
|
+
raise UnreachableDaemonError()
|
|
29
|
+
|
|
30
|
+
# In the health message there is also the api version
|
|
31
|
+
endpoints = httputils.Endpoints()
|
|
32
|
+
endpoints.version(resp.body.version.api)
|
|
33
|
+
|
|
34
|
+
# If the --version option is given just returns the version
|
|
35
|
+
if args.version:
|
|
36
|
+
# Format the version and returns
|
|
37
|
+
print(resp.body.version)
|
|
38
|
+
await version.format_version()
|
|
39
|
+
return
|
|
40
|
+
|
|
41
|
+
# If the version is not given, but there is no
|
|
42
|
+
# command as well, just prints out the help msg
|
|
43
|
+
if args.command is None:
|
|
44
|
+
parser.print_help()
|
|
45
|
+
return 1
|
|
46
|
+
|
|
47
|
+
_ = await args.func(args, CLIENT, endpoints)
|
|
48
|
+
return 0
|
|
49
|
+
|
|
50
|
+
def def_jobs_command(parser: argparse._SubParsersAction) -> None:
|
|
51
|
+
# Top-level subparser
|
|
52
|
+
jobs_parser = parser.add_parser("jobs", help="Manage jobs")
|
|
53
|
+
jobs_subparser = jobs_parser.add_subparsers(dest="jobs_command", required=True)
|
|
54
|
+
|
|
55
|
+
# Jobs create command
|
|
56
|
+
create_parser = jobs_subparser.add_parser("create", help="Create a job")
|
|
57
|
+
create_parser.add_argument("config", help="YAML configuration file")
|
|
58
|
+
create_parser.set_defaults(func=jobsfn.handle_jobs_create)
|
|
59
|
+
|
|
60
|
+
async def async_main() -> int:
|
|
61
|
+
parser = argparse.ArgumentParser(prog="backer", description="Backup automation control tool")
|
|
62
|
+
subparsers = parser.add_subparsers(dest="command")
|
|
63
|
+
|
|
64
|
+
# First add --version option to base parser
|
|
65
|
+
parser.add_argument("--version", required=False, action="store_true", help="Print out the version")
|
|
66
|
+
|
|
67
|
+
def_jobs_command(subparsers)
|
|
68
|
+
|
|
69
|
+
exit_code = 1
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
exit_code = await _exec(parser)
|
|
73
|
+
except (httpexcept.ClientConnectorError, UnreachableDaemonError):
|
|
74
|
+
print(UnreachableDaemonError())
|
|
75
|
+
except Exception as e:
|
|
76
|
+
print(f"Error: {e} (type: {type(e)})")
|
|
77
|
+
|
|
78
|
+
await CLIENT.release()
|
|
79
|
+
return exit_code
|
|
80
|
+
|
|
81
|
+
def main() -> int:
|
|
82
|
+
return asyncio.run(async_main())
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module with some useful functions to communicate
|
|
3
|
+
with the backerd (the backer daemon) using its HTTP
|
|
4
|
+
REST API.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import aiohttp
|
|
10
|
+
import aiohttp.typedefs
|
|
11
|
+
import backer.apitypes as apitypes
|
|
12
|
+
|
|
13
|
+
from enum import StrEnum
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
|
|
17
|
+
from typing import (
|
|
18
|
+
overload, Dict, Any, TypedDict,
|
|
19
|
+
Unpack, TypeVar, Optional,
|
|
20
|
+
Type
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
_T = TypeVar('_T')
|
|
24
|
+
|
|
25
|
+
SOCK_NAME = "backerd.sock"
|
|
26
|
+
|
|
27
|
+
class _RequestOptions(TypedDict, total=False):
|
|
28
|
+
params: aiohttp.typedefs.Query
|
|
29
|
+
headers: Dict[str, Any]
|
|
30
|
+
body: Dict[str,Any]
|
|
31
|
+
|
|
32
|
+
class HttpMethod(StrEnum):
|
|
33
|
+
NULL = ""
|
|
34
|
+
GET = "GET"
|
|
35
|
+
POST = "POST"
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True)
|
|
38
|
+
class Endpoint:
|
|
39
|
+
route : str
|
|
40
|
+
method : HttpMethod = HttpMethod.NULL
|
|
41
|
+
model : Optional[Type[apitypes.ResponseModel]] = None
|
|
42
|
+
|
|
43
|
+
@overload
|
|
44
|
+
def __truediv__(self, other: str) -> 'Endpoint': ...
|
|
45
|
+
@overload
|
|
46
|
+
def __truediv__(self, other: 'Endpoint') -> 'Endpoint': ...
|
|
47
|
+
|
|
48
|
+
def __truediv__(self, other) -> 'Endpoint':
|
|
49
|
+
if not (isinstance(other, str) or isinstance(other, Endpoint)):
|
|
50
|
+
return NotImplemented()
|
|
51
|
+
|
|
52
|
+
if isinstance(other, str):
|
|
53
|
+
# Input strings usually is for a route group, therefore
|
|
54
|
+
# we can leave the model and method to default None, NULL
|
|
55
|
+
return Endpoint(self.route + other)
|
|
56
|
+
|
|
57
|
+
return Endpoint(self.route + other.route, other.method, other.model)
|
|
58
|
+
|
|
59
|
+
def __eq__(self, other: 'Endpoint') -> bool:
|
|
60
|
+
return self.route == other.route \
|
|
61
|
+
and self.method == other.method
|
|
62
|
+
|
|
63
|
+
class Endpoints:
|
|
64
|
+
_Root : Endpoint = Endpoint("/api")
|
|
65
|
+
Version : Endpoint = _Root / Endpoint("/version", HttpMethod.GET, apitypes.VersionResponse)
|
|
66
|
+
Healthz : Endpoint = _Root / Endpoint("/healthz", HttpMethod.GET, apitypes.HealthzResponse)
|
|
67
|
+
|
|
68
|
+
def __init__(self): self._version = "/v1"
|
|
69
|
+
def version(self, v: int) -> None:
|
|
70
|
+
self._version = f"/v{v}"
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def _RootVers(self) -> Endpoint: return self._Root / self._version
|
|
74
|
+
@property
|
|
75
|
+
def Jobs(self) -> Endpoint: return self._RootVers / Endpoint("/jobs", HttpMethod.GET)
|
|
76
|
+
@property
|
|
77
|
+
def CreateJob(self) -> Endpoint: return self.Jobs / Endpoint("/create", HttpMethod.POST)
|
|
78
|
+
|
|
79
|
+
@dataclass(frozen=True)
|
|
80
|
+
class _Response[_T]:
|
|
81
|
+
code : int
|
|
82
|
+
body : _T
|
|
83
|
+
|
|
84
|
+
def get_unix_socket() -> str:
|
|
85
|
+
""" Returns the unix socket for backerd """
|
|
86
|
+
basedir = os.getenv("XDG_RUNTIME_DIR")
|
|
87
|
+
return str(Path(basedir) / SOCK_NAME)
|
|
88
|
+
|
|
89
|
+
class BackerClient:
|
|
90
|
+
def __init__(self, sock_path: str) -> None:
|
|
91
|
+
self._sock_path = sock_path
|
|
92
|
+
self._session: aiohttp.ClientSession | None = None
|
|
93
|
+
|
|
94
|
+
async def make_request(
|
|
95
|
+
self, endpoint: Endpoint, **kwargs: Unpack[_RequestOptions]
|
|
96
|
+
) -> _Response[_T | Dict[str,Any]]:
|
|
97
|
+
assert self._session is not None, "backer client must have a session"
|
|
98
|
+
|
|
99
|
+
resp = await self._session.request(
|
|
100
|
+
method=endpoint.method.value, url=f"http://localhost{endpoint.route}", **kwargs
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
data = await resp.json() # Get the json body from the response
|
|
104
|
+
data = endpoint.model.model_validate(data)
|
|
105
|
+
|
|
106
|
+
custom_resp = _Response(resp.status, data)
|
|
107
|
+
resp.close() # Close the response before leaving
|
|
108
|
+
return custom_resp
|
|
109
|
+
|
|
110
|
+
async def create(self) -> None:
|
|
111
|
+
connector = aiohttp.UnixConnector(path=self._sock_path)
|
|
112
|
+
self._session = aiohttp.ClientSession(connector=connector)
|
|
113
|
+
|
|
114
|
+
async def release(self) -> None:
|
|
115
|
+
if self._session is None: return
|
|
116
|
+
await self._session.close()
|
|
117
|
+
self._session = None
|
|
118
|
+
|
|
119
|
+
async def __aenter__(self) -> 'BackerClient':
|
|
120
|
+
await self.create()
|
|
121
|
+
return self
|
|
122
|
+
|
|
123
|
+
async def __aexit__(self, *args):
|
|
124
|
+
await self.release()
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import backer.http as httputils
|
|
3
|
+
|
|
4
|
+
async def handle_jobs_create(
|
|
5
|
+
args: argparse.Namespace, client: httputils.BackerClient,
|
|
6
|
+
endpoints: httputils.Endpoints
|
|
7
|
+
) -> None:
|
|
8
|
+
""" Handle the `backer jobs create <config>` command """
|
|
9
|
+
print("Create job called")
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import aiohttp
|
|
4
|
+
|
|
5
|
+
from packaging.version import Version
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import List, Tuple, TypeAlias
|
|
9
|
+
from backer import __version__ as __in_version__
|
|
10
|
+
from importlib.metadata import version, PackageNotFoundError
|
|
11
|
+
|
|
12
|
+
__version__ = __in_version__
|
|
13
|
+
try:
|
|
14
|
+
__version__ = version("backer-cli")
|
|
15
|
+
except PackageNotFoundError:
|
|
16
|
+
...
|
|
17
|
+
|
|
18
|
+
RELEASE_API_URL = "https://pypi.org/simple/backer-cli/"
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class RemoteFileInfo:
|
|
22
|
+
name: str
|
|
23
|
+
upload_time: datetime
|
|
24
|
+
|
|
25
|
+
VersionList : TypeAlias = List[Version]
|
|
26
|
+
FileList : TypeAlias = List[RemoteFileInfo]
|
|
27
|
+
|
|
28
|
+
async def _get_all_versions() -> Tuple[VersionList, FileList] | None:
|
|
29
|
+
""" Get the lastest version of the backupctl project """
|
|
30
|
+
headers = {'Host': 'pypi.org', 'Accept': 'application/vnd.pypi.simple.v1+json'}
|
|
31
|
+
timeout = aiohttp.ClientTimeout(total=10)
|
|
32
|
+
|
|
33
|
+
# Perform the async fetch operation and get back the payload
|
|
34
|
+
async with aiohttp.ClientSession(timeout=timeout, headers=headers) as session:
|
|
35
|
+
async with session.get(RELEASE_API_URL) as resp:
|
|
36
|
+
if resp.status < 200 or resp.status >= 300: return
|
|
37
|
+
payload = await resp.json()
|
|
38
|
+
|
|
39
|
+
if not isinstance(payload, dict): return
|
|
40
|
+
if not "versions" in payload: return
|
|
41
|
+
if not isinstance(payload["versions"], list): return
|
|
42
|
+
if not "files" in payload: return
|
|
43
|
+
|
|
44
|
+
# First parse all files
|
|
45
|
+
version_list = list(map(Version, payload.get('versions')))
|
|
46
|
+
file_list = []
|
|
47
|
+
for file_data in payload.get("files"):
|
|
48
|
+
if not isinstance(file_data, dict): continue
|
|
49
|
+
|
|
50
|
+
file_name = file_data.get('filename')
|
|
51
|
+
upload_time = file_data.get('upload-time')
|
|
52
|
+
|
|
53
|
+
if not (isinstance(file_name, str) and isinstance(upload_time, str)):
|
|
54
|
+
continue
|
|
55
|
+
|
|
56
|
+
upload_time = datetime.fromisoformat(upload_time.replace("Z", "+00:00"))
|
|
57
|
+
file_list.append(RemoteFileInfo( file_name, upload_time ))
|
|
58
|
+
|
|
59
|
+
return version_list, file_list
|
|
60
|
+
|
|
61
|
+
def _get_latest_release( versions: VersionList ) -> Version:
|
|
62
|
+
""" Returns the lastest release """
|
|
63
|
+
return max( versions )
|
|
64
|
+
|
|
65
|
+
def _get_release_time( version: Version, files: FileList ) -> datetime | None:
|
|
66
|
+
""" Return the date when the version has been released """
|
|
67
|
+
version_str = str(version) # Convert back to string
|
|
68
|
+
for fileitem in files:
|
|
69
|
+
if fileitem.name.startswith(f"backer-{version_str}"):
|
|
70
|
+
return fileitem.upload_time
|
|
71
|
+
|
|
72
|
+
return None
|
|
73
|
+
|
|
74
|
+
async def format_version() -> None:
|
|
75
|
+
""" Format the version and get latest version """
|
|
76
|
+
curr_version = Version(__version__)
|
|
77
|
+
result = await _get_all_versions()
|
|
78
|
+
curr_version_time = None
|
|
79
|
+
|
|
80
|
+
if result is not None:
|
|
81
|
+
versions, files = result
|
|
82
|
+
curr_version_time = _get_release_time( curr_version, files )
|
|
83
|
+
|
|
84
|
+
# Print the current version
|
|
85
|
+
print(f"CLI Version: {curr_version} (", end="")
|
|
86
|
+
curr_t = "Not Yet Released" if curr_version_time is None else str(curr_version_time)
|
|
87
|
+
print(f"{curr_t})")
|
|
88
|
+
|
|
89
|
+
if result is not None:
|
|
90
|
+
last_version = _get_latest_release( versions )
|
|
91
|
+
last_version_time = _get_release_time( last_version, files )
|
|
92
|
+
|
|
93
|
+
# Print if there is a more recent version
|
|
94
|
+
if curr_version < last_version:
|
|
95
|
+
print(
|
|
96
|
+
"!! A new version is available - " +\
|
|
97
|
+
f"{last_version} ({last_version_time}) !!"
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
print("")
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: backer-cli
|
|
3
|
+
Version: 0.1.1.dev0
|
|
4
|
+
Summary: Backup control automation tool
|
|
5
|
+
Author-email: lmriccardo <rlmarca.dev@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/lmriccardo/backer
|
|
8
|
+
Project-URL: Repository, https://github.com/lmriccardo/backer
|
|
9
|
+
Project-URL: Issues, https://github.com/lmriccardo/backer/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/lmriccardo/backer/releases
|
|
11
|
+
Keywords: backup,rsync,automation,cron,nas
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: System Administrators
|
|
14
|
+
Classifier: Topic :: System :: Archiving :: Backup
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
21
|
+
Requires-Python: >=3.12
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
Requires-Dist: pydantic[email]>=2.6
|
|
24
|
+
Requires-Dist: croniter>=2.0
|
|
25
|
+
Requires-Dist: PyYAML>=6.0
|
|
26
|
+
Requires-Dist: tabulate>=0.9
|
|
27
|
+
Requires-Dist: requests>=2.32
|
|
28
|
+
Requires-Dist: rich>=13.7
|
|
29
|
+
Requires-Dist: aiohttp>=3.13
|
|
30
|
+
Requires-Dist: packaging>=26.0
|
|
31
|
+
|
|
32
|
+
# The Backer CLI application
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
src/backer/__init__.py
|
|
4
|
+
src/backer/__main__.py
|
|
5
|
+
src/backer/apitypes.py
|
|
6
|
+
src/backer/cli.py
|
|
7
|
+
src/backer/http.py
|
|
8
|
+
src/backer/jobs.py
|
|
9
|
+
src/backer/version.py
|
|
10
|
+
src/backer_cli.egg-info/PKG-INFO
|
|
11
|
+
src/backer_cli.egg-info/SOURCES.txt
|
|
12
|
+
src/backer_cli.egg-info/dependency_links.txt
|
|
13
|
+
src/backer_cli.egg-info/entry_points.txt
|
|
14
|
+
src/backer_cli.egg-info/requires.txt
|
|
15
|
+
src/backer_cli.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
backer
|