borgapi 0.6.1__py3-none-any.whl → 0.7.1__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.
- borgapi/__init__.py +44 -4
- borgapi/borgapi.py +616 -313
- borgapi/capture.py +416 -0
- borgapi/helpers.py +26 -0
- borgapi/options.py +137 -68
- {borgapi-0.6.1.dist-info → borgapi-0.7.1.dist-info}/METADATA +68 -44
- borgapi-0.7.1.dist-info/RECORD +10 -0
- {borgapi-0.6.1.dist-info → borgapi-0.7.1.dist-info}/WHEEL +1 -1
- {borgapi-0.6.1.dist-info → borgapi-0.7.1.dist-info}/top_level.txt +0 -1
- borgapi-0.6.1.dist-info/RECORD +0 -27
- test/__init__.py +0 -0
- test/borgapi/__init__.py +0 -0
- test/borgapi/test_01_borgapi.py +0 -226
- test/borgapi/test_02_init.py +0 -78
- test/borgapi/test_03_create.py +0 -136
- test/borgapi/test_04_extract.py +0 -52
- test/borgapi/test_05_rename.py +0 -32
- test/borgapi/test_06_list.py +0 -59
- test/borgapi/test_07_diff.py +0 -54
- test/borgapi/test_08_delete.py +0 -68
- test/borgapi/test_09_prune.py +0 -48
- test/borgapi/test_10_info.py +0 -50
- test/borgapi/test_11_mount.py +0 -50
- test/borgapi/test_12_key.py +0 -81
- test/borgapi/test_13_export_tar.py +0 -38
- test/borgapi/test_14_config.py +0 -42
- test/borgapi/test_15_benchmark_crud.py +0 -24
- test/borgapi/test_16_compact.py +0 -33
- test/test_00_options.py +0 -70
- {borgapi-0.6.1.dist-info → borgapi-0.7.1.dist-info/licenses}/LICENSE +0 -0
borgapi/capture.py
ADDED
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
"""Save Borg output to review after command call."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import sys
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from io import BytesIO, StringIO, TextIOWrapper
|
|
7
|
+
from types import TracebackType
|
|
8
|
+
from typing import Optional, Union
|
|
9
|
+
|
|
10
|
+
try:
|
|
11
|
+
from typing import Self
|
|
12
|
+
except ImportError:
|
|
13
|
+
# Self isn't added to the typing library until version 3.11
|
|
14
|
+
from typing import TypeVar
|
|
15
|
+
|
|
16
|
+
Self = TypeVar("Self", bound="OutputCapture")
|
|
17
|
+
|
|
18
|
+
from borg.logger import JsonFormatter
|
|
19
|
+
|
|
20
|
+
from .helpers import Json
|
|
21
|
+
|
|
22
|
+
__all__ = ["OutputOptions", "ListStringIO", "PersistantHandler", "BorgLogCapture", "OutputCapture"]
|
|
23
|
+
|
|
24
|
+
LOG_LVL = "warning"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class OutputOptions:
|
|
29
|
+
"""Settings for what output should be saved."""
|
|
30
|
+
|
|
31
|
+
raw_bytes: bool = False
|
|
32
|
+
log_lvl: str = LOG_LVL
|
|
33
|
+
log_json: bool = False
|
|
34
|
+
list_show: bool = False
|
|
35
|
+
list_json: bool = False
|
|
36
|
+
stats_show: bool = False
|
|
37
|
+
stats_json: bool = False
|
|
38
|
+
repo_show: bool = False
|
|
39
|
+
repo_json: bool = False
|
|
40
|
+
prog_show: bool = False
|
|
41
|
+
prog_json: bool = False
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class ListStringIO(StringIO):
|
|
45
|
+
"""Save TextIO to a list of single lines."""
|
|
46
|
+
|
|
47
|
+
def __init__(self, initial_value="", newline="\n"):
|
|
48
|
+
r"""Wrap StringIO to gobble written data and save to a list.
|
|
49
|
+
|
|
50
|
+
:param initial_value: Initial value of buffer, passed to StringIO, defaults to ''
|
|
51
|
+
:type initial_value: str, optional
|
|
52
|
+
:param newline: What character to use for newlines, passed to StringIO, defaults to '\\n'
|
|
53
|
+
:type newline: str, optional
|
|
54
|
+
"""
|
|
55
|
+
super().__init__(initial_value=initial_value, newline=newline)
|
|
56
|
+
self.values = list()
|
|
57
|
+
self.idx = 0
|
|
58
|
+
|
|
59
|
+
def write(self, s: str, /):
|
|
60
|
+
"""Gobble written data and save it to a list right away.
|
|
61
|
+
|
|
62
|
+
:param s: data to write to output
|
|
63
|
+
:type s: str
|
|
64
|
+
"""
|
|
65
|
+
super().write(s)
|
|
66
|
+
self.flush()
|
|
67
|
+
val = self.getvalue()
|
|
68
|
+
self.seek(0)
|
|
69
|
+
self.truncate()
|
|
70
|
+
dvals = val.replace("\r", "\n").splitlines(keepends=True)
|
|
71
|
+
vals = []
|
|
72
|
+
for v in dvals:
|
|
73
|
+
nv = v.rstrip()
|
|
74
|
+
if v[-1] == "\n":
|
|
75
|
+
nv = f"{nv}\n"
|
|
76
|
+
if nv:
|
|
77
|
+
vals.append(nv)
|
|
78
|
+
if vals and self.values and self.values[-1][-1] != "\n":
|
|
79
|
+
self.values[-1] = self.values[-1] + vals[0]
|
|
80
|
+
self.values.extend(vals[1:])
|
|
81
|
+
else:
|
|
82
|
+
self.values.extend(vals)
|
|
83
|
+
|
|
84
|
+
def get(self) -> str:
|
|
85
|
+
"""Get next line of output data.
|
|
86
|
+
|
|
87
|
+
:return: Next line of output, None if end of list
|
|
88
|
+
and no new lines
|
|
89
|
+
:rtype: str
|
|
90
|
+
"""
|
|
91
|
+
if self.idx >= len(self.values):
|
|
92
|
+
return None
|
|
93
|
+
rec = self.values[self.idx]
|
|
94
|
+
self.idx += 1
|
|
95
|
+
return rec
|
|
96
|
+
|
|
97
|
+
def get_all(self) -> list[str]:
|
|
98
|
+
"""Get all data that has been written so far.
|
|
99
|
+
|
|
100
|
+
:return: all lines written to output split on newlines
|
|
101
|
+
:rtype: list[str]
|
|
102
|
+
"""
|
|
103
|
+
return self.values
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class PersistantHandler(logging.Handler):
|
|
107
|
+
"""Save logged information into a list of records."""
|
|
108
|
+
|
|
109
|
+
def __init__(self, json: bool = False):
|
|
110
|
+
"""Prep handler to be attached to a :class:`logging.Logger`.
|
|
111
|
+
|
|
112
|
+
:param json: if the output should be saved as a json value
|
|
113
|
+
instead of a string, defaults to False
|
|
114
|
+
:type json: bool, optional
|
|
115
|
+
"""
|
|
116
|
+
super().__init__()
|
|
117
|
+
self.records = list()
|
|
118
|
+
self.idx = 0
|
|
119
|
+
self.closed = False
|
|
120
|
+
|
|
121
|
+
self.json = json
|
|
122
|
+
|
|
123
|
+
fmt = "%(message)s"
|
|
124
|
+
formatter = JsonFormatter(fmt) if json else logging.Formatter(fmt)
|
|
125
|
+
self.setFormatter(formatter)
|
|
126
|
+
self.setLevel("INFO")
|
|
127
|
+
|
|
128
|
+
def emit(self, record: logging.LogRecord):
|
|
129
|
+
"""Log the record to the handlers internal list.
|
|
130
|
+
|
|
131
|
+
Implements `logger.Handler.emit`. Should not be manually called.
|
|
132
|
+
|
|
133
|
+
:param record: Logging record to be saved to the list.
|
|
134
|
+
:type record: logging.LogRecord
|
|
135
|
+
"""
|
|
136
|
+
try:
|
|
137
|
+
formatted = self.format(record)
|
|
138
|
+
if not self.json:
|
|
139
|
+
formatted = formatted.rstrip()
|
|
140
|
+
if formatted:
|
|
141
|
+
self.records.append(formatted)
|
|
142
|
+
except Exception:
|
|
143
|
+
self.handleError(record)
|
|
144
|
+
|
|
145
|
+
def get(self) -> Union[str, Json]:
|
|
146
|
+
"""Retrieve the next record in the list.
|
|
147
|
+
|
|
148
|
+
:return: Next item in the list if there is one available, otherwise `None`
|
|
149
|
+
:rtype: Union[str, Json, None]
|
|
150
|
+
"""
|
|
151
|
+
if self.idx >= len(self.records):
|
|
152
|
+
return None
|
|
153
|
+
rec = self.records[self.idx]
|
|
154
|
+
self.idx += 1
|
|
155
|
+
return rec
|
|
156
|
+
|
|
157
|
+
def get_all(self) -> list[Union[str, Json]]:
|
|
158
|
+
"""Retrieve full list of records.
|
|
159
|
+
|
|
160
|
+
:return: _description_
|
|
161
|
+
:rtype: list[Union[str, Json]]
|
|
162
|
+
"""
|
|
163
|
+
return self.records
|
|
164
|
+
|
|
165
|
+
def get_rest(self) -> list[Union[str, Json]]:
|
|
166
|
+
"""Retrieve remaining records starting at current index.
|
|
167
|
+
|
|
168
|
+
:return: Unretrieved records in the list
|
|
169
|
+
:rtype: list[Union[str, Json]]
|
|
170
|
+
"""
|
|
171
|
+
if self.idx < len(self.records):
|
|
172
|
+
return self.records[self.idx :]
|
|
173
|
+
return []
|
|
174
|
+
|
|
175
|
+
def __str__(self):
|
|
176
|
+
"""Join every record saved with newlines.
|
|
177
|
+
|
|
178
|
+
:return: String of the records saved.
|
|
179
|
+
:rtype: str
|
|
180
|
+
"""
|
|
181
|
+
return "\n".join([str(r) for r in self.records])
|
|
182
|
+
|
|
183
|
+
def value(self):
|
|
184
|
+
"""Return the records based on the output type (json or string).
|
|
185
|
+
|
|
186
|
+
:return: All records in pretty-ish format
|
|
187
|
+
:rtype: Union[str, list[Json]]
|
|
188
|
+
"""
|
|
189
|
+
if self.json:
|
|
190
|
+
return self.records
|
|
191
|
+
return str(self)
|
|
192
|
+
|
|
193
|
+
def close(self):
|
|
194
|
+
"""Set a flag to know if any new records should be expected.
|
|
195
|
+
|
|
196
|
+
When `self.closed` is `True`, then no new records will be written
|
|
197
|
+
to the logger.
|
|
198
|
+
"""
|
|
199
|
+
self.closed = True
|
|
200
|
+
|
|
201
|
+
def seek(self, idx: int = 0):
|
|
202
|
+
"""Set the index for getting the next record.
|
|
203
|
+
|
|
204
|
+
Writing records always go to the end of the list.
|
|
205
|
+
|
|
206
|
+
:param idx: position to start getting records from next, defaults to 0
|
|
207
|
+
:type idx: int, optional
|
|
208
|
+
"""
|
|
209
|
+
self.idx = idx
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
class BorgLogCapture:
|
|
213
|
+
"""Capture Borgs output to review after a command call."""
|
|
214
|
+
|
|
215
|
+
def __init__(self, logger: str, log_json: bool = False):
|
|
216
|
+
"""Attach handler to specified logger to gather output data.
|
|
217
|
+
|
|
218
|
+
:param logger: Logger to get information from.
|
|
219
|
+
:type logger: str
|
|
220
|
+
:param log_json: save data as a json instead of a string, defaults to False
|
|
221
|
+
:type log_json: bool, optional
|
|
222
|
+
"""
|
|
223
|
+
self.logger = logging.getLogger(logger)
|
|
224
|
+
self.handler = PersistantHandler(log_json)
|
|
225
|
+
self.logger.addHandler(self.handler)
|
|
226
|
+
|
|
227
|
+
def get(self) -> Optional[Union[str, Json]]:
|
|
228
|
+
"""Get next value in the handler.
|
|
229
|
+
|
|
230
|
+
:return: Next value to read from the handler if it exists.
|
|
231
|
+
Otherwise will return None.
|
|
232
|
+
:rtype: Optional[str]
|
|
233
|
+
"""
|
|
234
|
+
return self.handler.get()
|
|
235
|
+
|
|
236
|
+
def get_all(self) -> list[Union[str, Json]]:
|
|
237
|
+
"""Get every logged record since the handler was attached.
|
|
238
|
+
|
|
239
|
+
:return: list of each record entry
|
|
240
|
+
:rtype: list
|
|
241
|
+
"""
|
|
242
|
+
return self.handler.get_all()
|
|
243
|
+
|
|
244
|
+
def value(self):
|
|
245
|
+
"""Get full data from handler.
|
|
246
|
+
|
|
247
|
+
:return: the full output of the data
|
|
248
|
+
:rtype: str or dict
|
|
249
|
+
"""
|
|
250
|
+
return self.handler.value()
|
|
251
|
+
|
|
252
|
+
def close(self):
|
|
253
|
+
"""Close handler and remove it from logger."""
|
|
254
|
+
self.handler.close()
|
|
255
|
+
self.logger.removeHandler(self.handler)
|
|
256
|
+
|
|
257
|
+
def __str__(self):
|
|
258
|
+
"""Join all lines together as single string block.
|
|
259
|
+
|
|
260
|
+
:return: String of all data logged to handler
|
|
261
|
+
:rtype: str
|
|
262
|
+
"""
|
|
263
|
+
return "\n".join(self.get_all())
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
class OutputCapture:
|
|
267
|
+
"""Capture stdout and stderr by redirecting to inmemory streams.
|
|
268
|
+
|
|
269
|
+
:param raw: Expecting raw bytes from stdout and stderr
|
|
270
|
+
:type raw: bool
|
|
271
|
+
"""
|
|
272
|
+
|
|
273
|
+
def __init__(self):
|
|
274
|
+
"""Create object to log Borg output."""
|
|
275
|
+
self.ready = False
|
|
276
|
+
|
|
277
|
+
def __call__(self, opts: OutputOptions) -> Self:
|
|
278
|
+
"""Create handlers to use by a context manager.
|
|
279
|
+
|
|
280
|
+
Clears out old handlers from previous calls and creates new
|
|
281
|
+
ones for next Borg command to be used.
|
|
282
|
+
|
|
283
|
+
:param opts: Display options
|
|
284
|
+
:type opts: OutputOptions
|
|
285
|
+
:return: After setup, the object needs to be pased to the context manager.
|
|
286
|
+
:rtype: Self
|
|
287
|
+
"""
|
|
288
|
+
self.ready = False
|
|
289
|
+
self.opts = opts
|
|
290
|
+
self.raw = self.opts.raw_bytes
|
|
291
|
+
self._init_stdout(self.raw)
|
|
292
|
+
self._init_stderr()
|
|
293
|
+
|
|
294
|
+
self.list_capture = None
|
|
295
|
+
if self.opts.list_show:
|
|
296
|
+
self.list_capture = BorgLogCapture("borg.output.list", self.opts.list_json)
|
|
297
|
+
|
|
298
|
+
self.stats_capture = None
|
|
299
|
+
if self.opts.stats_show:
|
|
300
|
+
self.stats_capture = BorgLogCapture("borg.output.stats", self.opts.stats_json)
|
|
301
|
+
|
|
302
|
+
self.repo_capture = None
|
|
303
|
+
if self.opts.repo_show:
|
|
304
|
+
self.repo_capture = BorgLogCapture("borg.repository", self.opts.repo_json)
|
|
305
|
+
|
|
306
|
+
self.ready = True
|
|
307
|
+
|
|
308
|
+
return self
|
|
309
|
+
|
|
310
|
+
def _init_stdout(self, raw: bool):
|
|
311
|
+
self._stdout = TextIOWrapper(BytesIO()) if raw else ListStringIO()
|
|
312
|
+
self.stdout_original = sys.stdout
|
|
313
|
+
sys.stdout = self._stdout
|
|
314
|
+
|
|
315
|
+
def _init_stderr(self):
|
|
316
|
+
self._stderr = ListStringIO()
|
|
317
|
+
self.stderr_original = sys.stderr
|
|
318
|
+
sys.stderr = self._stderr
|
|
319
|
+
|
|
320
|
+
def getvalues(self) -> Union[str, bytes]:
|
|
321
|
+
"""Get the captured values from the redirected stdout and stderr.
|
|
322
|
+
|
|
323
|
+
:return: Redirected values from stdout and stderr
|
|
324
|
+
:rtype: Union[str, bytes]
|
|
325
|
+
"""
|
|
326
|
+
output = {}
|
|
327
|
+
|
|
328
|
+
if self.raw:
|
|
329
|
+
stdout_value = self._stdout.buffer.getvalue()
|
|
330
|
+
else:
|
|
331
|
+
stdout_value = "".join(self._stdout.get_all())
|
|
332
|
+
output["stdout"] = stdout_value
|
|
333
|
+
output["stderr"] = "".join(self._stderr.get_all())
|
|
334
|
+
|
|
335
|
+
if self.opts.list_show:
|
|
336
|
+
output["list"] = self.list_capture.value()
|
|
337
|
+
if self.opts.stats_show:
|
|
338
|
+
output["stats"] = self.stats_capture.value()
|
|
339
|
+
if self.opts.repo_show:
|
|
340
|
+
output["repo"] = self.repo_capture.value()
|
|
341
|
+
|
|
342
|
+
return output
|
|
343
|
+
|
|
344
|
+
def close(self):
|
|
345
|
+
"""Close the underlying IO streams and reset stdout and stderr."""
|
|
346
|
+
try:
|
|
347
|
+
if not self.raw:
|
|
348
|
+
self._stdout.close()
|
|
349
|
+
self._stderr.close()
|
|
350
|
+
if self.list_capture:
|
|
351
|
+
self.list_capture.close()
|
|
352
|
+
if self.stats_capture:
|
|
353
|
+
self.stats_capture.close()
|
|
354
|
+
if self.repo_capture:
|
|
355
|
+
self.repo_capture.close()
|
|
356
|
+
finally:
|
|
357
|
+
sys.stdout = self.stdout_original
|
|
358
|
+
sys.stderr = self.stderr_original
|
|
359
|
+
self.ready = False
|
|
360
|
+
|
|
361
|
+
def __enter__(self) -> Self:
|
|
362
|
+
"""Return the runtime context.
|
|
363
|
+
|
|
364
|
+
No additional work needs to be done when entering a context.
|
|
365
|
+
|
|
366
|
+
:return: Get `self` to use in a context
|
|
367
|
+
:rtype: Self
|
|
368
|
+
"""
|
|
369
|
+
return self
|
|
370
|
+
|
|
371
|
+
def __exit__(
|
|
372
|
+
self,
|
|
373
|
+
exc_type: Optional[type[BaseException]],
|
|
374
|
+
exc_value: Optional[BaseException],
|
|
375
|
+
traceback: Optional[TracebackType],
|
|
376
|
+
) -> bool:
|
|
377
|
+
"""Cleanup the capture when finished with a `with` context.
|
|
378
|
+
|
|
379
|
+
Don't want to hide any exceptions during the context, so a return
|
|
380
|
+
value of `True`
|
|
381
|
+
|
|
382
|
+
:param exc_type: exception type
|
|
383
|
+
:type exc_type: Optional[type[BaseException]]
|
|
384
|
+
:param exc_value: exception that was raised
|
|
385
|
+
:type exc_value: Optional[BaseException]
|
|
386
|
+
:param traceback: traceback of the exception that was raised
|
|
387
|
+
:type traceback: Optional[TracebackType]
|
|
388
|
+
:return: Propogates any exception that happens during runtime.
|
|
389
|
+
:rtype: bool
|
|
390
|
+
"""
|
|
391
|
+
self.close()
|
|
392
|
+
return False
|
|
393
|
+
|
|
394
|
+
def list(self):
|
|
395
|
+
"""Get buffer where list information is being logged."""
|
|
396
|
+
return self.list_capture
|
|
397
|
+
|
|
398
|
+
def stats(self):
|
|
399
|
+
"""Get buffer where stats are being logged."""
|
|
400
|
+
return self.stats_capture
|
|
401
|
+
|
|
402
|
+
def repository(self):
|
|
403
|
+
"""Get buffer where repository info is being logged."""
|
|
404
|
+
return self.repo_capture
|
|
405
|
+
|
|
406
|
+
def progress(self):
|
|
407
|
+
"""Get buffer where progress is being logged."""
|
|
408
|
+
return self._stderr
|
|
409
|
+
|
|
410
|
+
def stdout(self):
|
|
411
|
+
"""Get buffer stdout is being logged."""
|
|
412
|
+
return self._stdout.buffer if self.raw else self._stdout
|
|
413
|
+
|
|
414
|
+
def stderr(self):
|
|
415
|
+
"""Get buffer stderr is being logged."""
|
|
416
|
+
return self._stderr
|
borgapi/helpers.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Assortment of methods to help with debugging."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from typing import Any, Union
|
|
5
|
+
|
|
6
|
+
__all__ = ["Json", "Output", "Options"]
|
|
7
|
+
|
|
8
|
+
Json = Union[list, dict]
|
|
9
|
+
Output = Union[str, Json, None]
|
|
10
|
+
Options = Union[bool, str, int]
|
|
11
|
+
|
|
12
|
+
ENVIRONMENT_DEFAULTS = {
|
|
13
|
+
"BORG_EXIT_CODES": "modern",
|
|
14
|
+
"BORG_PASSPHRASE": "",
|
|
15
|
+
"BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK": "no",
|
|
16
|
+
"BORG_RELOCATED_REPO_ACCESS_IS_OK": "no",
|
|
17
|
+
"BORG_CHECK_I_KNOW_WHAT_I_AM_DOING": "NO",
|
|
18
|
+
"BORG_DELETE_I_KNOW_WHAT_I_AM_DOING": "NO",
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def force(*vals: Any) -> None:
|
|
23
|
+
"""Force print to stdout python started with."""
|
|
24
|
+
out = " ".join([str(v) for v in vals])
|
|
25
|
+
sys.__stdout__.write(out + "\n")
|
|
26
|
+
return sys.__stdout__.flush()
|