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/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()