borg-space 2.2__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.
- borg_space-2.2/README.rst → borg_space-2.4/PKG-INFO +70 -5
- borg_space-2.2/PKG-INFO → borg_space-2.4/README.rst +41 -33
- {borg_space-2.2 → borg_space-2.4}/borg_space/config.py +67 -37
- {borg_space-2.2 → borg_space-2.4}/borg_space/main.py +17 -9
- {borg_space-2.2 → borg_space-2.4}/pyproject.toml +18 -4
- borg_space-2.2/borg_space/trees.py +0 -94
- {borg_space-2.2 → borg_space-2.4}/LICENSE +0 -0
- {borg_space-2.2 → borg_space-2.4}/borg_space/__init__.py +0 -0
|
@@ -1,3 +1,31 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: borg_space
|
|
3
|
+
Version: 2.4
|
|
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.
|
|
15
|
-
:Released:
|
|
42
|
+
:Version: 2.4
|
|
43
|
+
:Released: 2026-01-27
|
|
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
|
|
|
@@ -99,6 +131,7 @@ You can create a NestedText_ settings file to specify default behaviors and
|
|
|
99
131
|
define composite repositories. For example::
|
|
100
132
|
|
|
101
133
|
default repository: home
|
|
134
|
+
default path: ~{user}/.local/share/assimilate/{config}.latest.nt
|
|
102
135
|
report style: tree
|
|
103
136
|
compact format: {name}: {size:{fmt}}. Last back up: {last_create:ddd, MMM DD}. Last squeeze: {last_squeeze:ddd, MMM DD}.
|
|
104
137
|
table format: {host:<8} {user:<5} {config:<9} {size:<8.2b} {last_create:ddd, MMM DD}
|
|
@@ -114,7 +147,11 @@ define composite repositories. For example::
|
|
|
114
147
|
dev: root@dev~root
|
|
115
148
|
mail: root@mail~root
|
|
116
149
|
files: root@files~root
|
|
117
|
-
bastion:
|
|
150
|
+
bastion:
|
|
151
|
+
config: root
|
|
152
|
+
host: bastion
|
|
153
|
+
user: root
|
|
154
|
+
path: /root/.local/share/emborg/root.latest.nt
|
|
118
155
|
media: root@media~root
|
|
119
156
|
web: root@web~root
|
|
120
157
|
cluster: home@cluster
|
|
@@ -134,7 +171,17 @@ define composite repositories. For example::
|
|
|
134
171
|
children: home servers root
|
|
135
172
|
|
|
136
173
|
default repository:
|
|
137
|
-
The name of the repository to be used if none are given on the
|
|
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.
|
|
138
185
|
|
|
139
186
|
report style:
|
|
140
187
|
The report style to be used if none is specified on the command line.
|
|
@@ -230,6 +277,7 @@ repositories:
|
|
|
230
277
|
config: home
|
|
231
278
|
host: host
|
|
232
279
|
user: user
|
|
280
|
+
path: ~user/.local/share/emborg/home.latest.nt
|
|
233
281
|
|
|
234
282
|
repositiories:
|
|
235
283
|
all: home@host~user work@host~user
|
|
@@ -248,6 +296,7 @@ repositories:
|
|
|
248
296
|
config: home
|
|
249
297
|
host: host
|
|
250
298
|
user: user
|
|
299
|
+
path: ~user/.local/share/emborg/home.latest.nt
|
|
251
300
|
-
|
|
252
301
|
config: work
|
|
253
302
|
host: host
|
|
@@ -310,14 +359,30 @@ scaled nicely on the same graph::
|
|
|
310
359
|
Installation
|
|
311
360
|
------------
|
|
312
361
|
|
|
313
|
-
*Borg-Space* requires *Emborg* version 1.37 or newer
|
|
362
|
+
*Borg-Space* requires *Emborg* version 1.37 or newer or *Assimilate*.
|
|
314
363
|
|
|
315
364
|
Install with::
|
|
316
365
|
|
|
317
366
|
> pip3 install borg-space
|
|
318
367
|
|
|
319
368
|
|
|
369
|
+
Assimilate and Borg 2
|
|
370
|
+
---------------------
|
|
371
|
+
|
|
372
|
+
Borg_ 2 will be released soon, and with it will come Assimilate_, the next
|
|
373
|
+
generation of Emborg_. *Assimilate* is intended to be used with *Borg 2.0* and
|
|
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``.
|
|
381
|
+
|
|
382
|
+
.. _assimilate: https://assimilate.readthedocs.io
|
|
383
|
+
.. _borg: https://borgbackup.readthedocs.io
|
|
320
384
|
.. _emborg: https://emborg.readthedocs.io
|
|
321
385
|
.. _nestedtext: https://nestedtext.org
|
|
322
386
|
.. _arrow: https://arrow.readthedocs.io/en/latest/guide.html#supported-tokens
|
|
323
387
|
.. _quantiphy: https://quantiphy.readthedocs.io/en/stable/api.html#quantiphy.Quantity.format
|
|
388
|
+
|
|
@@ -1,30 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.1
|
|
2
|
-
Name: borg_space
|
|
3
|
-
Version: 2.2
|
|
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.
|
|
42
|
-
:Released:
|
|
14
|
+
:Version: 2.4
|
|
15
|
+
:Released: 2026-01-27
|
|
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
|
|
|
@@ -126,6 +103,7 @@ You can create a NestedText_ settings file to specify default behaviors and
|
|
|
126
103
|
define composite repositories. For example::
|
|
127
104
|
|
|
128
105
|
default repository: home
|
|
106
|
+
default path: ~{user}/.local/share/assimilate/{config}.latest.nt
|
|
129
107
|
report style: tree
|
|
130
108
|
compact format: {name}: {size:{fmt}}. Last back up: {last_create:ddd, MMM DD}. Last squeeze: {last_squeeze:ddd, MMM DD}.
|
|
131
109
|
table format: {host:<8} {user:<5} {config:<9} {size:<8.2b} {last_create:ddd, MMM DD}
|
|
@@ -141,7 +119,11 @@ define composite repositories. For example::
|
|
|
141
119
|
dev: root@dev~root
|
|
142
120
|
mail: root@mail~root
|
|
143
121
|
files: root@files~root
|
|
144
|
-
bastion:
|
|
122
|
+
bastion:
|
|
123
|
+
config: root
|
|
124
|
+
host: bastion
|
|
125
|
+
user: root
|
|
126
|
+
path: /root/.local/share/emborg/root.latest.nt
|
|
145
127
|
media: root@media~root
|
|
146
128
|
web: root@web~root
|
|
147
129
|
cluster: home@cluster
|
|
@@ -161,7 +143,17 @@ define composite repositories. For example::
|
|
|
161
143
|
children: home servers root
|
|
162
144
|
|
|
163
145
|
default repository:
|
|
164
|
-
The name of the repository to be used if none are given on the
|
|
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.
|
|
165
157
|
|
|
166
158
|
report style:
|
|
167
159
|
The report style to be used if none is specified on the command line.
|
|
@@ -257,6 +249,7 @@ repositories:
|
|
|
257
249
|
config: home
|
|
258
250
|
host: host
|
|
259
251
|
user: user
|
|
252
|
+
path: ~user/.local/share/emborg/home.latest.nt
|
|
260
253
|
|
|
261
254
|
repositiories:
|
|
262
255
|
all: home@host~user work@host~user
|
|
@@ -275,6 +268,7 @@ repositories:
|
|
|
275
268
|
config: home
|
|
276
269
|
host: host
|
|
277
270
|
user: user
|
|
271
|
+
path: ~user/.local/share/emborg/home.latest.nt
|
|
278
272
|
-
|
|
279
273
|
config: work
|
|
280
274
|
host: host
|
|
@@ -337,15 +331,29 @@ scaled nicely on the same graph::
|
|
|
337
331
|
Installation
|
|
338
332
|
------------
|
|
339
333
|
|
|
340
|
-
*Borg-Space* requires *Emborg* version 1.37 or newer
|
|
334
|
+
*Borg-Space* requires *Emborg* version 1.37 or newer or *Assimilate*.
|
|
341
335
|
|
|
342
336
|
Install with::
|
|
343
337
|
|
|
344
338
|
> pip3 install borg-space
|
|
345
339
|
|
|
346
340
|
|
|
341
|
+
Assimilate and Borg 2
|
|
342
|
+
---------------------
|
|
343
|
+
|
|
344
|
+
Borg_ 2 will be released soon, and with it will come Assimilate_, the next
|
|
345
|
+
generation of Emborg_. *Assimilate* is intended to be used with *Borg 2.0* and
|
|
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``.
|
|
353
|
+
|
|
354
|
+
.. _assimilate: https://assimilate.readthedocs.io
|
|
355
|
+
.. _borg: https://borgbackup.readthedocs.io
|
|
347
356
|
.. _emborg: https://emborg.readthedocs.io
|
|
348
357
|
.. _nestedtext: https://nestedtext.org
|
|
349
358
|
.. _arrow: https://arrow.readthedocs.io/en/latest/guide.html#supported-tokens
|
|
350
359
|
.. _quantiphy: https://quantiphy.readthedocs.io/en/stable/api.html#quantiphy.Quantity.format
|
|
351
|
-
|
|
@@ -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
|
-
|
|
51
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
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 =
|
|
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,11 +134,11 @@ class Repository:
|
|
|
109
134
|
try:
|
|
110
135
|
content = to_path(path).read_text()
|
|
111
136
|
except FileNotFoundError:
|
|
112
|
-
raise Error('
|
|
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:
|
|
116
|
-
data['size'] = Quantity(raw_data['repository size'], 'B')
|
|
141
|
+
data['size'] = Quantity(raw_data['repository size'], 'B', binary=True)
|
|
117
142
|
if 'create last run' in raw_data:
|
|
118
143
|
data['last_create'] = arrow.get(raw_data['create last run'])
|
|
119
144
|
if 'prune last run' 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
|
-
|
|
135
|
-
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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[
|
|
299
|
+
repositories[name].extend(repositories[specname])
|
|
270
300
|
else:
|
|
271
301
|
repositories[name].append(Repository(spec, alias))
|
|
272
302
|
else:
|
|
@@ -31,12 +31,11 @@ Settings are held in ~/.config/borg-space/settings.nt.
|
|
|
31
31
|
"""
|
|
32
32
|
|
|
33
33
|
# imports {{{1
|
|
34
|
-
from .config import settings, get_repos
|
|
35
|
-
from .trees import tree
|
|
34
|
+
from .config import settings, get_repos, program_name
|
|
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, terminate, 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,19 @@ 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
|
+
import os
|
|
49
|
+
|
|
48
50
|
|
|
49
51
|
# globals {{{1
|
|
50
|
-
|
|
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))
|
|
51
56
|
now = str(arrow.now())
|
|
52
57
|
Quantity.set_prefs(prec='full')
|
|
53
|
-
__version__ = "2.
|
|
54
|
-
__released__ = "
|
|
58
|
+
__version__ = "2.4"
|
|
59
|
+
__released__ = "2026-01-27"
|
|
55
60
|
date_format = settings.get('date_format', 'D MMMM YYYY')
|
|
56
61
|
size_format = settings.get('size_format', '.2b')
|
|
57
62
|
nestedtext_size_format = settings.get('nestedtext_size_format', size_format)
|
|
@@ -64,6 +69,8 @@ not_available = "⟪not available⟫"
|
|
|
64
69
|
# collect_repos() {{{1
|
|
65
70
|
def collect_repos(requests, record_size):
|
|
66
71
|
repos = {}
|
|
72
|
+
if not requests:
|
|
73
|
+
raise Error('there is no default repository.')
|
|
67
74
|
for request in requests:
|
|
68
75
|
new_repos = get_repos(request)
|
|
69
76
|
repos.update(new_repos)
|
|
@@ -73,7 +80,7 @@ def collect_repos(requests, record_size):
|
|
|
73
80
|
for name, repo in repos.items():
|
|
74
81
|
|
|
75
82
|
# read previously recorded sizes
|
|
76
|
-
data_path =
|
|
83
|
+
data_path = data_dir / f'{name}.nt'
|
|
77
84
|
try:
|
|
78
85
|
data = nt.load(data_path, top=dict)
|
|
79
86
|
except FileNotFoundError:
|
|
@@ -174,6 +181,7 @@ def generate_graph(repos, svg_file, log_scale):
|
|
|
174
181
|
|
|
175
182
|
# draw the graph {{{3
|
|
176
183
|
ax.legend(loc='upper left')
|
|
184
|
+
# labelLines(ax.get_lines())
|
|
177
185
|
if svg_file:
|
|
178
186
|
plt.savefig(svg_file)
|
|
179
187
|
else:
|
|
@@ -269,7 +277,7 @@ def print_tree_report(repos):
|
|
|
269
277
|
fmt = formatter,
|
|
270
278
|
fields = fields,
|
|
271
279
|
missing = not_available,
|
|
272
|
-
)
|
|
280
|
+
), squeeze=True
|
|
273
281
|
)
|
|
274
282
|
)
|
|
275
283
|
|
|
@@ -328,7 +336,7 @@ def main():
|
|
|
328
336
|
|
|
329
337
|
requests = cmdline['<spec>']
|
|
330
338
|
if not requests:
|
|
331
|
-
requests =
|
|
339
|
+
requests = settings.get('default_repository', '').split()
|
|
332
340
|
|
|
333
341
|
try:
|
|
334
342
|
repos = collect_repos(requests, cmdline['--record'])
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "borg_space"
|
|
3
3
|
dist-name = "borg-space"
|
|
4
|
-
version = "2.
|
|
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"
|
|
8
8
|
license = {file = "LICENSE"}
|
|
9
9
|
keywords = ["emborg", "borg", "backups"]
|
|
10
|
-
authors = [{name = "Ken Kundert", email = "
|
|
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]
|
|
@@ -39,3 +40,16 @@ borg-space = "borg_space.main:main"
|
|
|
39
40
|
[build-system]
|
|
40
41
|
requires = ["flit_core >=2,<4"]
|
|
41
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"]
|
|
@@ -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
|
|
File without changes
|