borg-space 2.3__tar.gz → 2.5__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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: borg_space
3
- Version: 2.3
3
+ Version: 2.5
4
4
  Summary: Accessory for Emborg used to report and track the size of your Borg repositories
5
5
  Keywords: emborg,borg,backups
6
6
  Author-email: Ken Kundert <borg-space@nurdletech.com>
@@ -39,11 +39,12 @@ Borg-Space — Report and track the size of your Emborg repositories
39
39
  :target: https://pypi.python.org/pypi/borg-space/
40
40
 
41
41
  :Author: Ken Kundert
42
- :Version: 2.3
43
- :Released: 2025-05-11
42
+ :Version: 2.5
43
+ :Released: 2026-06-28
44
44
 
45
- *Borg-Space* is an accessory for Emborg_. It reports on the space consumed by
46
- your *BorgBackup* repositories. You can get this information using the
45
+ *Borg-Space* is an accessory for Emborg_ and Assimilate_. It reports on the
46
+ space consumed by your *BorgBackup* repositories. You can get this information
47
+ using the
47
48
  ``emborg info`` command, but there are several reasons to prefer *Borg-Space*.
48
49
 
49
50
  #. *Borg-Space* is capable of reporting on many repositories at once.
@@ -131,6 +132,7 @@ You can create a NestedText_ settings file to specify default behaviors and
131
132
  define composite repositories. For example::
132
133
 
133
134
  default repository: home
135
+ default path: ~{user}/.local/share/assimilate/{config}.latest.nt
134
136
  report style: tree
135
137
  compact format: {name}: {size:{fmt}}. Last back up: {last_create:ddd, MMM DD}. Last squeeze: {last_squeeze:ddd, MMM DD}.
136
138
  table format: {host:<8} {user:<5} {config:<9} {size:<8.2b} {last_create:ddd, MMM DD}
@@ -146,7 +148,11 @@ define composite repositories. For example::
146
148
  dev: root@dev~root
147
149
  mail: root@mail~root
148
150
  files: root@files~root
149
- bastion: root@bastion~root
151
+ bastion:
152
+ config: root
153
+ host: bastion
154
+ user: root
155
+ path: /root/.local/share/emborg/root.latest.nt
150
156
  media: root@media~root
151
157
  web: root@web~root
152
158
  cluster: home@cluster
@@ -166,7 +172,17 @@ define composite repositories. For example::
166
172
  children: home servers root
167
173
 
168
174
  default repository:
169
- The name of the repository to be used if none are given on the command line.
175
+ The name (or names) of the repository to be used if none are given on the
176
+ command line.
177
+
178
+ default path:
179
+ The path to the *Emborg* or *Assimilate* generated latest.nt files. If not
180
+ give, it defaults to::
181
+
182
+ ~{user}/.local/share/emborg/{config}.latest.nt
183
+
184
+ ``{config}`` and ``{user}`` are placeholders that are replaced by the
185
+ corresponding component of the repository specification.
170
186
 
171
187
  report style:
172
188
  The report style to be used if none is specified on the command line.
@@ -262,6 +278,7 @@ repositories:
262
278
  config: home
263
279
  host: host
264
280
  user: user
281
+ path: ~user/.local/share/emborg/home.latest.nt
265
282
 
266
283
  repositiories:
267
284
  all: home@host~user work@host~user
@@ -280,6 +297,7 @@ repositories:
280
297
  config: home
281
298
  host: host
282
299
  user: user
300
+ path: ~user/.local/share/emborg/home.latest.nt
283
301
  -
284
302
  config: work
285
303
  host: host
@@ -342,30 +360,25 @@ scaled nicely on the same graph::
342
360
  Installation
343
361
  ------------
344
362
 
345
- *Borg-Space* requires *Emborg* version 1.37 or newer.
363
+ *Borg-Space* requires *Emborg* version 1.37 or newer or *Assimilate*.
346
364
 
347
365
  Install with::
348
366
 
349
367
  > pip3 install borg-space
350
368
 
351
369
 
352
- Borg 2
353
- ------
370
+ Assimilate and Borg 2
371
+ ---------------------
354
372
 
355
373
  Borg_ 2 will be released soon, and with it will come Assimilate_, the next
356
374
  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 .
375
+ beyond while *Emborg* would be used with older versions of *Borg*. To use
376
+ *Assimilate* you should set the *default path* accordingly. To support both
377
+ *Emborg* and *Assimilate* simultaneously, you should set *default path* for one
378
+ and then use *path* overrides for individual repositories.
379
+
380
+ *Assimilate* only saves the space used by the repository when running
381
+ a *compact* command and only if the *get_repo_size* is set to ``'yes``.
369
382
 
370
383
  .. _assimilate: https://assimilate.readthedocs.io
371
384
  .. _borg: https://borgbackup.readthedocs.io
@@ -11,11 +11,12 @@ Borg-Space — Report and track the size of your Emborg repositories
11
11
  :target: https://pypi.python.org/pypi/borg-space/
12
12
 
13
13
  :Author: Ken Kundert
14
- :Version: 2.3
15
- :Released: 2025-05-11
14
+ :Version: 2.5
15
+ :Released: 2026-06-28
16
16
 
17
- *Borg-Space* is an accessory for Emborg_. It reports on the space consumed by
18
- your *BorgBackup* repositories. You can get this information using the
17
+ *Borg-Space* is an accessory for Emborg_ and Assimilate_. It reports on the
18
+ space consumed by your *BorgBackup* repositories. You can get this information
19
+ using the
19
20
  ``emborg info`` command, but there are several reasons to prefer *Borg-Space*.
20
21
 
21
22
  #. *Borg-Space* is capable of reporting on many repositories at once.
@@ -103,6 +104,7 @@ You can create a NestedText_ settings file to specify default behaviors and
103
104
  define composite repositories. For example::
104
105
 
105
106
  default repository: home
107
+ default path: ~{user}/.local/share/assimilate/{config}.latest.nt
106
108
  report style: tree
107
109
  compact format: {name}: {size:{fmt}}. Last back up: {last_create:ddd, MMM DD}. Last squeeze: {last_squeeze:ddd, MMM DD}.
108
110
  table format: {host:<8} {user:<5} {config:<9} {size:<8.2b} {last_create:ddd, MMM DD}
@@ -118,7 +120,11 @@ define composite repositories. For example::
118
120
  dev: root@dev~root
119
121
  mail: root@mail~root
120
122
  files: root@files~root
121
- bastion: root@bastion~root
123
+ bastion:
124
+ config: root
125
+ host: bastion
126
+ user: root
127
+ path: /root/.local/share/emborg/root.latest.nt
122
128
  media: root@media~root
123
129
  web: root@web~root
124
130
  cluster: home@cluster
@@ -138,7 +144,17 @@ define composite repositories. For example::
138
144
  children: home servers root
139
145
 
140
146
  default repository:
141
- The name of the repository to be used if none are given on the command line.
147
+ The name (or names) of the repository to be used if none are given on the
148
+ command line.
149
+
150
+ default path:
151
+ The path to the *Emborg* or *Assimilate* generated latest.nt files. If not
152
+ give, it defaults to::
153
+
154
+ ~{user}/.local/share/emborg/{config}.latest.nt
155
+
156
+ ``{config}`` and ``{user}`` are placeholders that are replaced by the
157
+ corresponding component of the repository specification.
142
158
 
143
159
  report style:
144
160
  The report style to be used if none is specified on the command line.
@@ -234,6 +250,7 @@ repositories:
234
250
  config: home
235
251
  host: host
236
252
  user: user
253
+ path: ~user/.local/share/emborg/home.latest.nt
237
254
 
238
255
  repositiories:
239
256
  all: home@host~user work@host~user
@@ -252,6 +269,7 @@ repositories:
252
269
  config: home
253
270
  host: host
254
271
  user: user
272
+ path: ~user/.local/share/emborg/home.latest.nt
255
273
  -
256
274
  config: work
257
275
  host: host
@@ -314,30 +332,25 @@ scaled nicely on the same graph::
314
332
  Installation
315
333
  ------------
316
334
 
317
- *Borg-Space* requires *Emborg* version 1.37 or newer.
335
+ *Borg-Space* requires *Emborg* version 1.37 or newer or *Assimilate*.
318
336
 
319
337
  Install with::
320
338
 
321
339
  > pip3 install borg-space
322
340
 
323
341
 
324
- Borg 2
325
- ------
342
+ Assimilate and Borg 2
343
+ ---------------------
326
344
 
327
345
  Borg_ 2 will be released soon, and with it will come Assimilate_, the next
328
346
  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 .
347
+ beyond while *Emborg* would be used with older versions of *Borg*. To use
348
+ *Assimilate* you should set the *default path* accordingly. To support both
349
+ *Emborg* and *Assimilate* simultaneously, you should set *default path* for one
350
+ and then use *path* overrides for individual repositories.
351
+
352
+ *Assimilate* only saves the space used by the repository when running
353
+ a *compact* command and only if the *get_repo_size* is set to ``'yes``.
341
354
 
342
355
  .. _assimilate: https://assimilate.readthedocs.io
343
356
  .. _borg: https://borgbackup.readthedocs.io
@@ -32,7 +32,6 @@ if new_home: # pragma: no cover
32
32
 
33
33
  # GLOBALS {{{1
34
34
  set_prefs(use_inform=True)
35
- settings_file = to_path(user_config_dir('borg-space')) / 'settings.nt'
36
35
  voluptuous_error_msg_mappings = {
37
36
  "extra keys not allowed": ("unknown key", "key"),
38
37
  "expected a dictionary": ("expected key:value pair", "value"),
@@ -41,24 +40,43 @@ voluptuous_key_prefix = "key contains"
41
40
  hostname = socket.gethostname().split('.')[0]
42
41
  # version of the hostname (the hostname without any domain name)
43
42
  username = pwd.getpwuid(os.getuid()).pw_name
43
+ program_name = 'borg-space'
44
+ if 'XDG_CONFIG_HOME' in os.environ:
45
+ config_dir = os.sep.join([os.environ['XDG_CONFIG_HOME'], program_name])
46
+ else:
47
+ config_dir = user_config_dir(program_name)
48
+ settings_file = to_path(config_dir) / 'settings.nt'
44
49
 
45
50
 
46
51
  # REPOSITORY {{{1
47
52
  # Repository class {{{2
48
53
  class Repository:
49
54
  def __init__(self, spec, name=None):
50
- prefix, _, user = spec.partition('~')
51
- config, _, host = prefix.partition('@')
55
+ if is_str(spec):
56
+ prefix, _, user = spec.partition('~')
57
+ config, _, host = prefix.partition('@')
58
+ path = None
59
+ else:
60
+ user = spec.get('user')
61
+ config = spec.get('config')
62
+ host = spec.get('host')
63
+ path = spec.get('path')
64
+ spec = spec.get('spec')
65
+
52
66
  if not config:
53
67
  raise Error("spec is missing Emborg config name.", culprit=spec)
54
68
  if not name:
55
69
  name = spec
56
70
 
57
- self.spec = spec
58
- self.name = name
59
- self.config = a_name(config)
60
- self.host = a_name(host) or hostname
61
- self.user = a_name(user) or username
71
+ try:
72
+ self.spec = spec
73
+ self.name = name
74
+ self.config = a_name(config)
75
+ self.host = a_name(host) or hostname
76
+ self.user = a_name(user) or username
77
+ self.path = path
78
+ except Invalid as e:
79
+ raise Error(e, culprit=spec)
62
80
  self.latest = None
63
81
 
64
82
  def __str__(self):
@@ -87,14 +105,21 @@ class Repository:
87
105
  user = self.user,
88
106
  full_spec = str(self)
89
107
  )
90
- info.update(self.latest)
108
+ if self.latest:
109
+ info.update(self.latest)
91
110
  return info
92
111
 
93
112
  def get_path(self):
94
113
  user = self.user if self.user else getpass.getuser()
95
114
  config = self.config
96
115
  assert config
97
- path = f"~{user}/.local/share/emborg/{config}.latest.nt"
116
+ path = self.path
117
+ if not path:
118
+ path = settings.get(
119
+ 'default_path',
120
+ "~{user}/.local/share/emborg/{config}.latest.nt"
121
+ )
122
+ path = path.format(user=user, config=config)
98
123
  return (self.host, path)
99
124
 
100
125
  def get_latest(self):
@@ -109,7 +134,7 @@ class Repository:
109
134
  try:
110
135
  content = to_path(path).read_text()
111
136
  except FileNotFoundError:
112
- raise Error('unknown repository.', culprit=str(self))
137
+ raise Error(f'repository not found: {path}', culprit=str(self))
113
138
  raw_data = nt.loads(content)
114
139
  self.latest = data = {}
115
140
  if 'repository size' in raw_data:
@@ -128,27 +153,27 @@ class Repository:
128
153
  def get_repos(spec):
129
154
  if not spec:
130
155
  spec = settings.get('default_repository')
131
- if not spec:
132
- raise Error('there is no default repository.')
133
156
 
134
- try:
135
- children = repositories[spec]
136
- except (TypeError, KeyError):
137
- # not found in repositories specified in settings file.
138
- # see if it exists on local machine
139
- children = [Repository(spec)]
140
-
141
- results = {}
142
- for child in children:
143
- host, path = child.get_path()
144
- name = str(child)
157
+ specs = spec.split()
158
+ for spec in specs:
145
159
  try:
146
- child.get_latest()
147
- results[name] = child
148
- except Error as e:
149
- e.report(culprit=name)
150
- except OSError as e:
151
- error(os_error(e), culprit=name)
160
+ children = repositories[spec]
161
+ except (TypeError, KeyError):
162
+ # not found in repositories specified in settings file.
163
+ # see if it exists on local machine
164
+ children = [Repository(spec)]
165
+
166
+ results = {}
167
+ for child in children:
168
+ # host, path = child.get_path()
169
+ name = str(child)
170
+ try:
171
+ child.get_latest()
172
+ results[name] = child
173
+ except Error as e:
174
+ e.report(culprit=name)
175
+ except OSError as e:
176
+ error(os_error(e), culprit=name)
152
177
  return results
153
178
 
154
179
 
@@ -168,7 +193,7 @@ def to_list(args):
168
193
  if is_str(args):
169
194
  args = args.split()
170
195
  if is_mapping(args):
171
- raise Invalid(f"expected a list or string")
196
+ raise Invalid("expected a list or string")
172
197
  return args
173
198
 
174
199
  # a_name() {{{2
@@ -200,7 +225,7 @@ def a_spec(arg):
200
225
  if is_str(arg):
201
226
  return arg
202
227
  if is_mapping(arg):
203
- unknown_keys = arg.keys() - set(['config', 'host', 'user'])
228
+ unknown_keys = arg.keys() - set(['config', 'host', 'user', 'path'])
204
229
  if unknown_keys:
205
230
  raise Invalid(f"unknown {plural(unknown_keys):key}: {conjoin(unknown_keys)}.")
206
231
  if 'config' not in arg:
@@ -210,7 +235,8 @@ def a_spec(arg):
210
235
  spec = f"{spec}@{arg.get('host')}"
211
236
  if arg.get('user'):
212
237
  spec = f"{spec}~{arg.get('user')}"
213
- return spec
238
+ arg['spec'] = spec
239
+ return arg
214
240
  raise Invalid("expected a specification")
215
241
 
216
242
  # to_specs() {{{2
@@ -218,7 +244,7 @@ def to_specs(arg):
218
244
  if is_str(arg):
219
245
  return [a_spec(r) for r in arg.split()]
220
246
  if is_mapping(arg):
221
- unknown_keys = arg.keys() - set(['config', 'host', 'user'])
247
+ unknown_keys = arg.keys() - set(['config', 'host', 'user', 'path'])
222
248
  if unknown_keys:
223
249
  raise Invalid(f"unknown {plural(unknown_keys):key}: {conjoin(unknown_keys)}.")
224
250
  return [a_spec(arg)]
@@ -230,6 +256,7 @@ def to_specs(arg):
230
256
  validate_settings = Schema({
231
257
  'repositories': {key_as_name: to_specs},
232
258
  'default_repository': str,
259
+ 'default_path': str,
233
260
  'report_style': str,
234
261
  'compact_format': str,
235
262
  'table_format': str,
@@ -264,9 +291,12 @@ try:
264
291
  repositories[name] = []
265
292
  alias = name if len(specs) <= 1 else None
266
293
  for spec in specs:
267
- if spec in repositories and spec != name:
294
+ specname = spec
295
+ if is_mapping(spec):
296
+ specname = spec['spec']
297
+ if specname in repositories and specname != name:
268
298
  # this is a known (previously defined) repository
269
- repositories[name].extend(repositories[spec])
299
+ repositories[name].extend(repositories[specname])
270
300
  else:
271
301
  repositories[name].append(Repository(spec, alias))
272
302
  else:
@@ -292,7 +322,7 @@ except MultipleInvalid as e: # report schema violations
292
322
  flag = 'key'
293
323
  loc = keymap.get(tuple(err.path))
294
324
  codicil = loc.as_line(flag) if loc else None
295
- keys = nt.join_keys(err.path, keymap=keymap)
325
+ keys = nt.get_keys(err.path, keymap=keymap, sep=', ')
296
326
  error(
297
327
  full_stop(msg),
298
328
  culprit = (settings_file, keys),
@@ -31,7 +31,7 @@ Settings are held in ~/.config/borg-space/settings.nt.
31
31
  """
32
32
 
33
33
  # imports {{{1
34
- from .config import settings, get_repos
34
+ from .config import settings, get_repos, program_name
35
35
  import arrow
36
36
  from appdirs import user_data_dir
37
37
  from docopt import docopt
@@ -45,14 +45,18 @@ import matplotlib.pyplot as plt
45
45
  from matplotlib.dates import AutoDateFormatter, AutoDateLocator
46
46
  from matplotlib.ticker import FuncFormatter
47
47
  # from labellines import labelLines
48
+ import os
48
49
 
49
50
 
50
51
  # globals {{{1
51
- data_dir = Path(user_data_dir('borg-space'))
52
+ if 'XDG_DATA_HOME' in os.environ:
53
+ data_dir = Path(os.sep.join([os.environ['XDG_DATA_HOME'], program_name]))
54
+ else:
55
+ data_dir = Path(user_data_dir(program_name))
52
56
  now = str(arrow.now())
53
57
  Quantity.set_prefs(prec='full')
54
- __version__ = "2.3"
55
- __released__ = "2025-05-11"
58
+ __version__ = "2.5"
59
+ __released__ = "2026-06-28"
56
60
  date_format = settings.get('date_format', 'D MMMM YYYY')
57
61
  size_format = settings.get('size_format', '.2b')
58
62
  nestedtext_size_format = settings.get('nestedtext_size_format', size_format)
@@ -65,6 +69,8 @@ not_available = "⟪not available⟫"
65
69
  # collect_repos() {{{1
66
70
  def collect_repos(requests, record_size):
67
71
  repos = {}
72
+ if not requests:
73
+ raise Error('there is no default repository.')
68
74
  for request in requests:
69
75
  new_repos = get_repos(request)
70
76
  repos.update(new_repos)
@@ -74,7 +80,7 @@ def collect_repos(requests, record_size):
74
80
  for name, repo in repos.items():
75
81
 
76
82
  # read previously recorded sizes
77
- data_path = Path(data_dir / f'{name}.nt')
83
+ data_path = data_dir / f'{name}.nt'
78
84
  try:
79
85
  data = nt.load(data_path, top=dict)
80
86
  except FileNotFoundError:
@@ -330,7 +336,7 @@ def main():
330
336
 
331
337
  requests = cmdline['<spec>']
332
338
  if not requests:
333
- requests = [''] # this gets the default config
339
+ requests = settings.get('default_repository', '').split()
334
340
 
335
341
  try:
336
342
  repos = collect_repos(requests, cmdline['--record'])
@@ -1,7 +1,7 @@
1
1
  [project]
2
2
  name = "borg_space"
3
3
  dist-name = "borg-space"
4
- version = "2.3"
4
+ version = "2.5"
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"
@@ -38,5 +38,18 @@ changelog = "https://github.com/KenKundert/ntlog/blob/master/CHANGELOG.rst"
38
38
  borg-space = "borg_space.main:main"
39
39
 
40
40
  [build-system]
41
- requires = ["flit_core >=2,<4"]
41
+ requires = ["flit_core >=2"]
42
42
  build-backend = "flit_core.buildapi"
43
+
44
+ [tool.pytest.ini_options]
45
+ addopts = "--tb=short"
46
+
47
+ [tool.ruff]
48
+ exclude = [".tox", "doc", "Diffs"]
49
+
50
+ [tool.ruff.lint]
51
+ select = ["F"]
52
+ ignore = []
53
+
54
+ [tool.ruff.lint.per-file-ignores]
55
+ "borg_space/__init__.py" = ["F401"]
File without changes