borg-space 2.3__tar.gz → 2.4__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.4
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,8 +39,8 @@ 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.4
43
+ :Released: 2026-01-27
44
44
 
45
45
  *Borg-Space* is an accessory for Emborg_. It reports on the space consumed by
46
46
  your *BorgBackup* repositories. You can get this information using the
@@ -131,6 +131,7 @@ You can create a NestedText_ settings file to specify default behaviors and
131
131
  define composite repositories. For example::
132
132
 
133
133
  default repository: home
134
+ default path: ~{user}/.local/share/assimilate/{config}.latest.nt
134
135
  report style: tree
135
136
  compact format: {name}: {size:{fmt}}. Last back up: {last_create:ddd, MMM DD}. Last squeeze: {last_squeeze:ddd, MMM DD}.
136
137
  table format: {host:<8} {user:<5} {config:<9} {size:<8.2b} {last_create:ddd, MMM DD}
@@ -146,7 +147,11 @@ define composite repositories. For example::
146
147
  dev: root@dev~root
147
148
  mail: root@mail~root
148
149
  files: root@files~root
149
- bastion: root@bastion~root
150
+ bastion:
151
+ config: root
152
+ host: bastion
153
+ user: root
154
+ path: /root/.local/share/emborg/root.latest.nt
150
155
  media: root@media~root
151
156
  web: root@web~root
152
157
  cluster: home@cluster
@@ -166,7 +171,17 @@ define composite repositories. For example::
166
171
  children: home servers root
167
172
 
168
173
  default repository:
169
- The name of the repository to be used if none are given on the command line.
174
+ The name (or names) of the repository to be used if none are given on the
175
+ command line.
176
+
177
+ default path:
178
+ The path to the *Emborg* or *Assimilate* generated latest.nt files. If not
179
+ give, it defaults to::
180
+
181
+ ~{user}/.local/share/emborg/{config}.latest.nt
182
+
183
+ ``{config}`` and ``{user}`` are placeholders that are replaced by the
184
+ corresponding component of the repository specification.
170
185
 
171
186
  report style:
172
187
  The report style to be used if none is specified on the command line.
@@ -262,6 +277,7 @@ repositories:
262
277
  config: home
263
278
  host: host
264
279
  user: user
280
+ path: ~user/.local/share/emborg/home.latest.nt
265
281
 
266
282
  repositiories:
267
283
  all: home@host~user work@host~user
@@ -280,6 +296,7 @@ repositories:
280
296
  config: home
281
297
  host: host
282
298
  user: user
299
+ path: ~user/.local/share/emborg/home.latest.nt
283
300
  -
284
301
  config: work
285
302
  host: host
@@ -342,30 +359,25 @@ scaled nicely on the same graph::
342
359
  Installation
343
360
  ------------
344
361
 
345
- *Borg-Space* requires *Emborg* version 1.37 or newer.
362
+ *Borg-Space* requires *Emborg* version 1.37 or newer or *Assimilate*.
346
363
 
347
364
  Install with::
348
365
 
349
366
  > pip3 install borg-space
350
367
 
351
368
 
352
- Borg 2
353
- ------
369
+ Assimilate and Borg 2
370
+ ---------------------
354
371
 
355
372
  Borg_ 2 will be released soon, and with it will come Assimilate_, the next
356
373
  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 .
374
+ beyond while *Emborg* would be used with older versions of *Borg*. To use
375
+ *Assimilate* you should set the *default path* accordingly. To support both
376
+ *Emborg* and *Assimilate* simultaneously, you should set *default path* for one
377
+ and then use *path* overrides for individual repositories.
378
+
379
+ *Assimilate* only saves the space used by the repository when running
380
+ a *compact* command and only if the *get_repo_size* is set to ``'yes``.
369
381
 
370
382
  .. _assimilate: https://assimilate.readthedocs.io
371
383
  .. _borg: https://borgbackup.readthedocs.io
@@ -11,8 +11,8 @@ 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.4
15
+ :Released: 2026-01-27
16
16
 
17
17
  *Borg-Space* is an accessory for Emborg_. It reports on the space consumed by
18
18
  your *BorgBackup* repositories. You can get this information using the
@@ -103,6 +103,7 @@ You can create a NestedText_ settings file to specify default behaviors and
103
103
  define composite repositories. For example::
104
104
 
105
105
  default repository: home
106
+ default path: ~{user}/.local/share/assimilate/{config}.latest.nt
106
107
  report style: tree
107
108
  compact format: {name}: {size:{fmt}}. Last back up: {last_create:ddd, MMM DD}. Last squeeze: {last_squeeze:ddd, MMM DD}.
108
109
  table format: {host:<8} {user:<5} {config:<9} {size:<8.2b} {last_create:ddd, MMM DD}
@@ -118,7 +119,11 @@ define composite repositories. For example::
118
119
  dev: root@dev~root
119
120
  mail: root@mail~root
120
121
  files: root@files~root
121
- bastion: root@bastion~root
122
+ bastion:
123
+ config: root
124
+ host: bastion
125
+ user: root
126
+ path: /root/.local/share/emborg/root.latest.nt
122
127
  media: root@media~root
123
128
  web: root@web~root
124
129
  cluster: home@cluster
@@ -138,7 +143,17 @@ define composite repositories. For example::
138
143
  children: home servers root
139
144
 
140
145
  default repository:
141
- The name of the repository to be used if none are given on the command line.
146
+ The name (or names) of the repository to be used if none are given on the
147
+ command line.
148
+
149
+ default path:
150
+ The path to the *Emborg* or *Assimilate* generated latest.nt files. If not
151
+ give, it defaults to::
152
+
153
+ ~{user}/.local/share/emborg/{config}.latest.nt
154
+
155
+ ``{config}`` and ``{user}`` are placeholders that are replaced by the
156
+ corresponding component of the repository specification.
142
157
 
143
158
  report style:
144
159
  The report style to be used if none is specified on the command line.
@@ -234,6 +249,7 @@ repositories:
234
249
  config: home
235
250
  host: host
236
251
  user: user
252
+ path: ~user/.local/share/emborg/home.latest.nt
237
253
 
238
254
  repositiories:
239
255
  all: home@host~user work@host~user
@@ -252,6 +268,7 @@ repositories:
252
268
  config: home
253
269
  host: host
254
270
  user: user
271
+ path: ~user/.local/share/emborg/home.latest.nt
255
272
  -
256
273
  config: work
257
274
  host: host
@@ -314,30 +331,25 @@ scaled nicely on the same graph::
314
331
  Installation
315
332
  ------------
316
333
 
317
- *Borg-Space* requires *Emborg* version 1.37 or newer.
334
+ *Borg-Space* requires *Emborg* version 1.37 or newer or *Assimilate*.
318
335
 
319
336
  Install with::
320
337
 
321
338
  > pip3 install borg-space
322
339
 
323
340
 
324
- Borg 2
325
- ------
341
+ Assimilate and Borg 2
342
+ ---------------------
326
343
 
327
344
  Borg_ 2 will be released soon, and with it will come Assimilate_, the next
328
345
  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 .
346
+ beyond while *Emborg* would be used with older versions of *Borg*. To use
347
+ *Assimilate* you should set the *default path* accordingly. To support both
348
+ *Emborg* and *Assimilate* simultaneously, you should set *default path* for one
349
+ and then use *path* overrides for individual repositories.
350
+
351
+ *Assimilate* only saves the space used by the repository when running
352
+ a *compact* command and only if the *get_repo_size* is set to ``'yes``.
341
353
 
342
354
  .. _assimilate: https://assimilate.readthedocs.io
343
355
  .. _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:
@@ -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.4"
59
+ __released__ = "2026-01-27"
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.4"
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"
@@ -40,3 +40,16 @@ borg-space = "borg_space.main:main"
40
40
  [build-system]
41
41
  requires = ["flit_core >=2,<4"]
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