borg-space 2.1__tar.gz → 2.3__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.
@@ -1,3 +1,31 @@
1
+ Metadata-Version: 2.4
2
+ Name: borg_space
3
+ Version: 2.3
4
+ Summary: Accessory for Emborg used to report and track the size of your Borg repositories
5
+ Keywords: emborg,borg,backups
6
+ Author-email: Ken Kundert <borg-space@nurdletech.com>
7
+ Requires-Python: >=3.6
8
+ Description-Content-Type: text/x-rst
9
+ Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
10
+ Classifier: Natural Language :: English
11
+ Classifier: Operating System :: POSIX :: Linux
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Topic :: Utilities
14
+ License-File: LICENSE
15
+ Requires-Dist: appdirs
16
+ Requires-Dist: arrow
17
+ Requires-Dist: docopt
18
+ Requires-Dist: inform>=1.34
19
+ Requires-Dist: matplotlib
20
+ Requires-Dist: nestedtext
21
+ Requires-Dist: quantiphy
22
+ Requires-Dist: shlib
23
+ Requires-Dist: voluptuous>=0.14
24
+ Project-URL: changelog, https://github.com/KenKundert/ntlog/blob/master/CHANGELOG.rst
25
+ Project-URL: documentation, https://github.com/KenKundert/borg-space/blob/master/README.rst
26
+ Project-URL: homepage, https://github.com/kenkundert/borg-space
27
+ Project-URL: repository, https://github.com/kenkundert/borg-space
28
+
1
29
  Borg-Space — Report and track the size of your Emborg repositories
2
30
  ==================================================================
3
31
 
@@ -11,8 +39,8 @@ Borg-Space — Report and track the size of your Emborg repositories
11
39
  :target: https://pypi.python.org/pypi/borg-space/
12
40
 
13
41
  :Author: Ken Kundert
14
- :Version: 2.1
15
- :Released: 2023-06-09
42
+ :Version: 2.3
43
+ :Released: 2025-05-11
16
44
 
17
45
  *Borg-Space* is an accessory for Emborg_. It reports on the space consumed by
18
46
  your *BorgBackup* repositories. You can get this information using the
@@ -32,6 +60,10 @@ To show the size of one or more repositories, simply run::
32
60
  # borg-space home
33
61
  home: 12.81 GB
34
62
 
63
+ This reports on the latest repository size and, of course, assumes that you have
64
+ already run *emborg create*. You must not use the ``--fast`` command line
65
+ option when running *create*.
66
+
35
67
  You can specify any number of repositories, and they can be composites. In the
36
68
  following example, *home* is an alias that expands to *borgbase* and *rsync*::
37
69
 
@@ -317,7 +349,28 @@ Install with::
317
349
  > pip3 install borg-space
318
350
 
319
351
 
352
+ Borg 2
353
+ ------
354
+
355
+ Borg_ 2 will be released soon, and with it will come Assimilate_, the next
356
+ generation of Emborg_. *Assimilate* is intended to be used with *Borg 2.0* and
357
+ beyond while *Emborg* would be used with older versions of *Borg*. Currently
358
+ *Borg-Space* does not support *Assimilate* directly, but the *latest.nt* files
359
+ produced by *Assimilate* are compatible with *Borg-Space*, only their location
360
+ differs. You can get the current version of *Borg-Space* to read *Assimilate*
361
+ *latest.nt* files by simply creating a symbolic link from the expected location
362
+ to the actual location. For example, if you convert your *home* repository from
363
+ *Emborg* to *Assimilate*, you can use the following commands to get *Borg-Space*
364
+ to use the *latest.nt* file produced by *Assimilate*::
365
+
366
+ cd ~/.local/share/emborg
367
+ rm home.latest.nt
368
+ ln -s ../assimilate/home.latest.nt .
369
+
370
+ .. _assimilate: https://assimilate.readthedocs.io
371
+ .. _borg: https://borgbackup.readthedocs.io
320
372
  .. _emborg: https://emborg.readthedocs.io
321
373
  .. _nestedtext: https://nestedtext.org
322
374
  .. _arrow: https://arrow.readthedocs.io/en/latest/guide.html#supported-tokens
323
375
  .. _quantiphy: https://quantiphy.readthedocs.io/en/stable/api.html#quantiphy.Quantity.format
376
+
@@ -1,30 +1,3 @@
1
- Metadata-Version: 2.1
2
- Name: borg_space
3
- Version: 2.1
4
- Summary: Accessory for Emborg used to report and track the size of your Borg repositories
5
- Keywords: emborg,borg,backups
6
- Author-email: Ken Kundert <emborg@nurdletech.com>
7
- Requires-Python: >=3.6
8
- Description-Content-Type: text/x-rst
9
- Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
10
- Classifier: Natural Language :: English
11
- Classifier: Operating System :: POSIX :: Linux
12
- Classifier: Programming Language :: Python :: 3
13
- Classifier: Topic :: Utilities
14
- Requires-Dist: appdirs
15
- Requires-Dist: arrow
16
- Requires-Dist: docopt
17
- Requires-Dist: inform
18
- Requires-Dist: matplotlib
19
- Requires-Dist: nestedtext
20
- Requires-Dist: quantiphy
21
- Requires-Dist: shlib
22
- Requires-Dist: voluptuous
23
- Project-URL: changelog, https://github.com/KenKundert/ntlog/blob/master/CHANGELOG.rst
24
- Project-URL: documentation, https://github.com/KenKundert/borg-space/blob/master/README.rst
25
- Project-URL: homepage, https://github.com/kenkundert/borg-space
26
- Project-URL: repository, https://github.com/kenkundert/borg-space
27
-
28
1
  Borg-Space — Report and track the size of your Emborg repositories
29
2
  ==================================================================
30
3
 
@@ -38,8 +11,8 @@ Borg-Space — Report and track the size of your Emborg repositories
38
11
  :target: https://pypi.python.org/pypi/borg-space/
39
12
 
40
13
  :Author: Ken Kundert
41
- :Version: 2.1
42
- :Released: 2023-06-09
14
+ :Version: 2.3
15
+ :Released: 2025-05-11
43
16
 
44
17
  *Borg-Space* is an accessory for Emborg_. It reports on the space consumed by
45
18
  your *BorgBackup* repositories. You can get this information using the
@@ -59,6 +32,10 @@ To show the size of one or more repositories, simply run::
59
32
  # borg-space home
60
33
  home: 12.81 GB
61
34
 
35
+ This reports on the latest repository size and, of course, assumes that you have
36
+ already run *emborg create*. You must not use the ``--fast`` command line
37
+ option when running *create*.
38
+
62
39
  You can specify any number of repositories, and they can be composites. In the
63
40
  following example, *home* is an alias that expands to *borgbase* and *rsync*::
64
41
 
@@ -344,8 +321,27 @@ Install with::
344
321
  > pip3 install borg-space
345
322
 
346
323
 
324
+ Borg 2
325
+ ------
326
+
327
+ Borg_ 2 will be released soon, and with it will come Assimilate_, the next
328
+ generation of Emborg_. *Assimilate* is intended to be used with *Borg 2.0* and
329
+ beyond while *Emborg* would be used with older versions of *Borg*. Currently
330
+ *Borg-Space* does not support *Assimilate* directly, but the *latest.nt* files
331
+ produced by *Assimilate* are compatible with *Borg-Space*, only their location
332
+ differs. You can get the current version of *Borg-Space* to read *Assimilate*
333
+ *latest.nt* files by simply creating a symbolic link from the expected location
334
+ to the actual location. For example, if you convert your *home* repository from
335
+ *Emborg* to *Assimilate*, you can use the following commands to get *Borg-Space*
336
+ to use the *latest.nt* file produced by *Assimilate*::
337
+
338
+ cd ~/.local/share/emborg
339
+ rm home.latest.nt
340
+ ln -s ../assimilate/home.latest.nt .
341
+
342
+ .. _assimilate: https://assimilate.readthedocs.io
343
+ .. _borg: https://borgbackup.readthedocs.io
347
344
  .. _emborg: https://emborg.readthedocs.io
348
345
  .. _nestedtext: https://nestedtext.org
349
346
  .. _arrow: https://arrow.readthedocs.io/en/latest/guide.html#supported-tokens
350
347
  .. _quantiphy: https://quantiphy.readthedocs.io/en/stable/api.html#quantiphy.Quantity.format
351
-
@@ -3,8 +3,8 @@
3
3
  # IMPORTS {{{1
4
4
  from appdirs import user_config_dir
5
5
  from inform import (
6
- Error, conjoin, error, fatal, full_stop,
7
- is_str, is_mapping, is_collection, plural, os_error, terminate
6
+ Error, conjoin, error, full_stop,
7
+ is_str, is_mapping, is_collection, plural, os_error, terminate_if_errors
8
8
  )
9
9
  from quantiphy import Quantity
10
10
  from shlib import to_path, Run, set_prefs
@@ -37,6 +37,7 @@ voluptuous_error_msg_mappings = {
37
37
  "extra keys not allowed": ("unknown key", "key"),
38
38
  "expected a dictionary": ("expected key:value pair", "value"),
39
39
  }
40
+ voluptuous_key_prefix = "key contains"
40
41
  hostname = socket.gethostname().split('.')[0]
41
42
  # version of the hostname (the hostname without any domain name)
42
43
  username = pwd.getpwuid(os.getuid()).pw_name
@@ -112,7 +113,7 @@ class Repository:
112
113
  raw_data = nt.loads(content)
113
114
  self.latest = data = {}
114
115
  if 'repository size' in raw_data:
115
- data['size'] = Quantity(raw_data['repository size'], 'B')
116
+ data['size'] = Quantity(raw_data['repository size'], 'B', binary=True)
116
117
  if 'create last run' in raw_data:
117
118
  data['last_create'] = arrow.get(raw_data['create last run'])
118
119
  if 'prune last run' in raw_data:
@@ -167,21 +168,33 @@ def to_list(args):
167
168
  if is_str(args):
168
169
  args = args.split()
169
170
  if is_mapping(args):
170
- raise Invalid(f"{args}: expected list or string")
171
+ raise Invalid(f"expected a list or string")
171
172
  return args
172
173
 
173
174
  # a_name() {{{2
174
- def a_name(arg):
175
+ def a_name(arg, is_key=False):
175
176
  # names are expected to be identifiers except that dashes are allowed
177
+ # also allow names starting with a digit
176
178
  if not arg:
177
179
  return arg
178
180
  if not is_str(arg):
179
- raise Invalid("expected string")
180
- cleaned = arg.replace('-', '0')
181
+ raise Invalid("expected a string")
182
+ cleaned = '_' + arg.replace('-', '_')
183
+ # add prefix to allow leading digits, replace '-' to allow dashes
181
184
  if not cleaned.isidentifier():
182
- raise Invalid(f"{arg}: expected a name")
185
+ from string import ascii_letters as letters, digits
186
+ invalid = ''.join(sorted(set(cleaned) - set(letters + digits + '-_')))
187
+ desc = f"{plural(invalid):/an invalid character/invalid characters}"
188
+ if is_key:
189
+ raise Invalid(f"key contains {desc}: ‘{invalid}’")
190
+ else:
191
+ raise Invalid(f"value contains {desc}: ‘{invalid}’")
183
192
  return arg
184
193
 
194
+ # key_as_name() {{{2
195
+ def key_as_name(arg):
196
+ return a_name(arg, is_key=True)
197
+
185
198
  # a_spec() {{{2
186
199
  def a_spec(arg):
187
200
  if is_str(arg):
@@ -215,7 +228,7 @@ def to_specs(arg):
215
228
 
216
229
  # validate_settings {{{2
217
230
  validate_settings = Schema({
218
- 'repositories': {a_name: to_specs},
231
+ 'repositories': {key_as_name: to_specs},
219
232
  'default_repository': str,
220
233
  'report_style': str,
221
234
  'compact_format': str,
@@ -245,31 +258,38 @@ try:
245
258
 
246
259
  # convert from specifications to Repository objects
247
260
  repositories = {}
248
- for name, specs in specifications.items():
249
- if specs:
250
- repositories[name] = []
251
- alias = name if len(specs) <= 1 else None
252
- for spec in specs:
253
- if spec in repositories and spec != name:
254
- # this is a known (previously defined) repository
255
- repositories[name].extend(repositories[spec])
256
- else:
257
- repositories[name].append(Repository(spec, alias))
258
- else:
259
- repositories[name] = [Repository(name)]
261
+ try:
262
+ for name, specs in specifications.items():
263
+ if specs:
264
+ repositories[name] = []
265
+ alias = name if len(specs) <= 1 else None
266
+ for spec in specs:
267
+ if spec in repositories and spec != name:
268
+ # this is a known (previously defined) repository
269
+ repositories[name].extend(repositories[spec])
270
+ else:
271
+ repositories[name].append(Repository(spec, alias))
272
+ else:
273
+ repositories[name] = [Repository(name)]
274
+ except Invalid as e:
275
+ raise Error(e, culprit=name)
260
276
 
261
277
  except nt.NestedTextError as e:
262
- e.terminate()
278
+ e.report()
279
+ except Error as e:
280
+ e.report(culprit=(settings_file,) + e.culprit)
263
281
  except FileNotFoundError:
264
282
  settings = {}
265
283
  repositories = {}
266
284
  except OSError as e:
267
- fatal(os_error(e), culprit=settings_file)
285
+ error(os_error(e))
268
286
  except MultipleInvalid as e: # report schema violations
269
287
  for err in e.errors:
270
288
  msg, flag = voluptuous_error_msg_mappings.get(
271
289
  err.msg, (err.msg, 'value')
272
290
  )
291
+ if msg.startswith(voluptuous_key_prefix):
292
+ flag = 'key'
273
293
  loc = keymap.get(tuple(err.path))
274
294
  codicil = loc.as_line(flag) if loc else None
275
295
  keys = nt.join_keys(err.path, keymap=keymap)
@@ -278,4 +298,4 @@ except MultipleInvalid as e: # report schema violations
278
298
  culprit = (settings_file, keys),
279
299
  codicil = codicil
280
300
  )
281
- terminate()
301
+ terminate_if_errors()
@@ -32,11 +32,10 @@ Settings are held in ~/.config/borg-space/settings.nt.
32
32
 
33
33
  # imports {{{1
34
34
  from .config import settings, get_repos
35
- from .trees import tree
36
35
  import arrow
37
36
  from appdirs import user_data_dir
38
37
  from docopt import docopt
39
- from inform import Error, display, error, os_error, warn
38
+ from inform import Error, display, error, os_error, terminate, warn, tree
40
39
  from pathlib import Path
41
40
  from quantiphy import Quantity
42
41
  import json
@@ -45,13 +44,15 @@ import matplotlib
45
44
  import matplotlib.pyplot as plt
46
45
  from matplotlib.dates import AutoDateFormatter, AutoDateLocator
47
46
  from matplotlib.ticker import FuncFormatter
47
+ # from labellines import labelLines
48
+
48
49
 
49
50
  # globals {{{1
50
51
  data_dir = Path(user_data_dir('borg-space'))
51
52
  now = str(arrow.now())
52
53
  Quantity.set_prefs(prec='full')
53
- __version__ = "2.1"
54
- __released__ = "2023-06-09"
54
+ __version__ = "2.3"
55
+ __released__ = "2025-05-11"
55
56
  date_format = settings.get('date_format', 'D MMMM YYYY')
56
57
  size_format = settings.get('size_format', '.2b')
57
58
  nestedtext_size_format = settings.get('nestedtext_size_format', size_format)
@@ -151,6 +152,8 @@ def generate_graph(repos, svg_file, log_scale):
151
152
  ax.xaxis.set_major_locator(locator)
152
153
  ax.xaxis.set_major_formatter(AutoDateFormatter(locator))
153
154
 
155
+ fig.autofmt_xdate()
156
+
154
157
  # add traces in order of last size, largest to smallest {{{3
155
158
  largest = 0
156
159
  smallest = 1e100
@@ -163,7 +166,7 @@ def generate_graph(repos, svg_file, log_scale):
163
166
 
164
167
  # use SI scale factors on Y-axis
165
168
  def bytes(value, pos=None):
166
- return Quantity(value, 'B').render()
169
+ return Quantity(value, 'B').render(prec=3)
167
170
  ax.yaxis.set_major_formatter(FuncFormatter(bytes))
168
171
  if largest / smallest > 10:
169
172
  ax.yaxis.set_minor_formatter("")
@@ -172,6 +175,7 @@ def generate_graph(repos, svg_file, log_scale):
172
175
 
173
176
  # draw the graph {{{3
174
177
  ax.legend(loc='upper left')
178
+ # labelLines(ax.get_lines())
175
179
  if svg_file:
176
180
  plt.savefig(svg_file)
177
181
  else:
@@ -267,7 +271,7 @@ def print_tree_report(repos):
267
271
  fmt = formatter,
268
272
  fields = fields,
269
273
  missing = not_available,
270
- )
274
+ ), squeeze=True
271
275
  )
272
276
  )
273
277
 
@@ -341,3 +345,4 @@ def main():
341
345
  error(os_error(e))
342
346
  except KeyboardInterrupt:
343
347
  pass
348
+ terminate()
@@ -1,13 +1,13 @@
1
1
  [project]
2
2
  name = "borg_space"
3
3
  dist-name = "borg-space"
4
- version = "2.1"
4
+ version = "2.3"
5
5
  description = "Accessory for Emborg used to report and track the size of your Borg repositories"
6
6
  readme = "README.rst"
7
7
  requires-python = ">=3.6"
8
8
  license = {file = "LICENSE"}
9
9
  keywords = ["emborg", "borg", "backups"]
10
- authors = [{name = "Ken Kundert", email = "emborg@nurdletech.com"}]
10
+ authors = [{name = "Ken Kundert", email = "borg-space@nurdletech.com"}]
11
11
  classifiers = [
12
12
  "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
13
13
  "Natural Language :: English",
@@ -19,12 +19,13 @@ dependencies = [
19
19
  "appdirs",
20
20
  "arrow",
21
21
  "docopt",
22
- "inform",
22
+ "inform>=1.34",
23
23
  "matplotlib",
24
+ # "matplotlib-label-lines",
24
25
  "nestedtext",
25
26
  "quantiphy",
26
27
  "shlib",
27
- "voluptuous",
28
+ "voluptuous>=0.14",
28
29
  ]
29
30
 
30
31
  [project.urls]
@@ -1,94 +0,0 @@
1
- # Format a Tree
2
- # Description {{{1
3
- # Given a data hierarchy consisting of zero or more levels of dictionaries with
4
- # lists as leaf values, where each dictionary key and list value is a string,
5
- # this function creates a Unicode diagram of that tree.
6
- #
7
- # For example, here is a filesystem sub-hierarchy:
8
- #
9
- # tests/
10
- # ├── examples/
11
- # │ ├── test_example_01.py
12
- # │ ├── test_example_02.py
13
- # │ └── test_example_03.py
14
- # ├── foobar/
15
- # │ ├── test_foobar_01.py
16
- # │ ├── test_foobar_02.py
17
- # │ └── test_foobar_03.py
18
- # └── hello/
19
- # └── world/
20
- # ├── test_world_01.py
21
- # ├── test_world_02.py
22
- # └── test_world_03.py
23
-
24
- # Imports {{{1
25
- from inform import Info, conjoin, is_collection
26
-
27
- def gen_connectors(width):
28
- space = " " # This is a non-breaking space, needed with variable width fonts
29
- line = "─" # This is horizontal rule
30
- connector_seeds = dict(
31
- item = "├",
32
- last_item = "└",
33
- lead = "│",
34
- last_lead = space,
35
- )
36
- pad = space if width > 1 else ''
37
-
38
- def extend(seed):
39
- fill = space if seed in [space, "│"] else line
40
- return seed + (width - 2)*fill + pad
41
-
42
- return Info(**{k: extend(v) for k, v in connector_seeds.items()})
43
-
44
- connectors = gen_connectors(4)
45
-
46
- def tree(data, key_suffix=''):
47
- return _tree(data, key_suffix, top=True)
48
-
49
- def _tree(data, key_suffix, top=False, leader=''):
50
- lines = []
51
- if hasattr(data, 'items'):
52
- last = len(data) - 1
53
- for i, item in enumerate(data.items()):
54
- key, value = item
55
- # determine key-leader-supplement and item-leader-supplement
56
- if top:
57
- kls = ''
58
- ils = ''
59
- elif i < last:
60
- kls = connectors.item
61
- ils = connectors.lead
62
- else:
63
- kls = connectors.last_item
64
- ils = connectors.last_lead
65
-
66
- if is_collection(value):
67
- # append dictionary to those already processed
68
- lines += [
69
- leader + kls + key + key_suffix,
70
- _tree(value, key_suffix, leader = leader + ils) if value else None
71
- ]
72
- else:
73
- # the value is a scalar, so squeeze key & value on one line
74
- lines += [
75
- leader + kls + key + ': ' + value,
76
- ]
77
- return '\n'.join(l for l in lines if l)
78
-
79
- elif not is_collection(data):
80
- data = [str(data)]
81
-
82
- if top:
83
- joiner = '\n'
84
- terminator = '\n'
85
- items = conjoin(data, sep='\n', conj='\n')
86
- else:
87
- joiner = '\n' + leader + connectors.item
88
- terminator = '\n' + leader + connectors.last_item
89
- connector = connectors.item if len(data) > 1 else connectors.last_item
90
- items = leader + connector + conjoin(data, sep=joiner, conj=terminator)
91
-
92
- if items:
93
- lines.append(items)
94
- return '\n'.join(lines)
File without changes