bundlewrap 5.1.0__tar.gz → 5.2.0__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.
Files changed (98) hide show
  1. {bundlewrap-5.1.0 → bundlewrap-5.2.0}/CHANGELOG.md +9 -0
  2. {bundlewrap-5.1.0 → bundlewrap-5.2.0}/PKG-INFO +1 -1
  3. {bundlewrap-5.1.0 → bundlewrap-5.2.0}/bundlewrap/__init__.py +1 -1
  4. {bundlewrap-5.1.0 → bundlewrap-5.2.0}/bundlewrap/cmdline/parser.py +4 -2
  5. {bundlewrap-5.1.0 → bundlewrap-5.2.0}/bundlewrap/items/directories.py +14 -0
  6. {bundlewrap-5.1.0 → bundlewrap-5.2.0}/bundlewrap/node.py +33 -26
  7. {bundlewrap-5.1.0 → bundlewrap-5.2.0}/bundlewrap/repo.py +13 -6
  8. bundlewrap-5.2.0/bundlewrap/utils/node_lambda.py +34 -0
  9. {bundlewrap-5.1.0 → bundlewrap-5.2.0}/bundlewrap.egg-info/PKG-INFO +1 -1
  10. {bundlewrap-5.1.0 → bundlewrap-5.2.0}/setup.py +1 -1
  11. bundlewrap-5.1.0/bundlewrap/utils/node_lambda.py +0 -55
  12. {bundlewrap-5.1.0 → bundlewrap-5.2.0}/AUTHORS +0 -0
  13. {bundlewrap-5.1.0 → bundlewrap-5.2.0}/LICENSE +0 -0
  14. {bundlewrap-5.1.0 → bundlewrap-5.2.0}/MANIFEST.in +0 -0
  15. {bundlewrap-5.1.0 → bundlewrap-5.2.0}/README.md +0 -0
  16. {bundlewrap-5.1.0 → bundlewrap-5.2.0}/bundlewrap/bundle.py +0 -0
  17. {bundlewrap-5.1.0 → bundlewrap-5.2.0}/bundlewrap/cmdline/__init__.py +0 -0
  18. {bundlewrap-5.1.0 → bundlewrap-5.2.0}/bundlewrap/cmdline/apply.py +0 -0
  19. {bundlewrap-5.1.0 → bundlewrap-5.2.0}/bundlewrap/cmdline/debug.py +0 -0
  20. {bundlewrap-5.1.0 → bundlewrap-5.2.0}/bundlewrap/cmdline/diff.py +0 -0
  21. {bundlewrap-5.1.0 → bundlewrap-5.2.0}/bundlewrap/cmdline/generate_completions.py +0 -0
  22. {bundlewrap-5.1.0 → bundlewrap-5.2.0}/bundlewrap/cmdline/groups.py +0 -0
  23. {bundlewrap-5.1.0 → bundlewrap-5.2.0}/bundlewrap/cmdline/hash.py +0 -0
  24. {bundlewrap-5.1.0 → bundlewrap-5.2.0}/bundlewrap/cmdline/ipmi.py +0 -0
  25. {bundlewrap-5.1.0 → bundlewrap-5.2.0}/bundlewrap/cmdline/items.py +0 -0
  26. {bundlewrap-5.1.0 → bundlewrap-5.2.0}/bundlewrap/cmdline/lock.py +0 -0
  27. {bundlewrap-5.1.0 → bundlewrap-5.2.0}/bundlewrap/cmdline/metadata.py +0 -0
  28. {bundlewrap-5.1.0 → bundlewrap-5.2.0}/bundlewrap/cmdline/nodes.py +0 -0
  29. {bundlewrap-5.1.0 → bundlewrap-5.2.0}/bundlewrap/cmdline/plot.py +0 -0
  30. {bundlewrap-5.1.0 → bundlewrap-5.2.0}/bundlewrap/cmdline/pw.py +0 -0
  31. {bundlewrap-5.1.0 → bundlewrap-5.2.0}/bundlewrap/cmdline/repo.py +0 -0
  32. {bundlewrap-5.1.0 → bundlewrap-5.2.0}/bundlewrap/cmdline/run.py +0 -0
  33. {bundlewrap-5.1.0 → bundlewrap-5.2.0}/bundlewrap/cmdline/stats.py +0 -0
  34. {bundlewrap-5.1.0 → bundlewrap-5.2.0}/bundlewrap/cmdline/test.py +0 -0
  35. {bundlewrap-5.1.0 → bundlewrap-5.2.0}/bundlewrap/cmdline/verify.py +0 -0
  36. {bundlewrap-5.1.0 → bundlewrap-5.2.0}/bundlewrap/cmdline/zen.py +0 -0
  37. {bundlewrap-5.1.0 → bundlewrap-5.2.0}/bundlewrap/concurrency.py +0 -0
  38. {bundlewrap-5.1.0 → bundlewrap-5.2.0}/bundlewrap/deps.py +0 -0
  39. {bundlewrap-5.1.0 → bundlewrap-5.2.0}/bundlewrap/exceptions.py +0 -0
  40. {bundlewrap-5.1.0 → bundlewrap-5.2.0}/bundlewrap/group.py +0 -0
  41. {bundlewrap-5.1.0 → bundlewrap-5.2.0}/bundlewrap/itemqueue.py +0 -0
  42. {bundlewrap-5.1.0 → bundlewrap-5.2.0}/bundlewrap/items/__init__.py +0 -0
  43. {bundlewrap-5.1.0 → bundlewrap-5.2.0}/bundlewrap/items/actions.py +0 -0
  44. {bundlewrap-5.1.0 → bundlewrap-5.2.0}/bundlewrap/items/dconf.py +0 -0
  45. {bundlewrap-5.1.0 → bundlewrap-5.2.0}/bundlewrap/items/files.py +0 -0
  46. {bundlewrap-5.1.0 → bundlewrap-5.2.0}/bundlewrap/items/git_deploy.py +0 -0
  47. {bundlewrap-5.1.0 → bundlewrap-5.2.0}/bundlewrap/items/groups.py +0 -0
  48. {bundlewrap-5.1.0 → bundlewrap-5.2.0}/bundlewrap/items/kubernetes.py +0 -0
  49. {bundlewrap-5.1.0 → bundlewrap-5.2.0}/bundlewrap/items/pkg.py +0 -0
  50. {bundlewrap-5.1.0 → bundlewrap-5.2.0}/bundlewrap/items/pkg_apk.py +0 -0
  51. {bundlewrap-5.1.0 → bundlewrap-5.2.0}/bundlewrap/items/pkg_apt.py +0 -0
  52. {bundlewrap-5.1.0 → bundlewrap-5.2.0}/bundlewrap/items/pkg_dnf.py +0 -0
  53. {bundlewrap-5.1.0 → bundlewrap-5.2.0}/bundlewrap/items/pkg_freebsd.py +0 -0
  54. {bundlewrap-5.1.0 → bundlewrap-5.2.0}/bundlewrap/items/pkg_openbsd.py +0 -0
  55. {bundlewrap-5.1.0 → bundlewrap-5.2.0}/bundlewrap/items/pkg_opkg.py +0 -0
  56. {bundlewrap-5.1.0 → bundlewrap-5.2.0}/bundlewrap/items/pkg_pacman.py +0 -0
  57. {bundlewrap-5.1.0 → bundlewrap-5.2.0}/bundlewrap/items/pkg_pamac.py +0 -0
  58. {bundlewrap-5.1.0 → bundlewrap-5.2.0}/bundlewrap/items/pkg_pip.py +0 -0
  59. {bundlewrap-5.1.0 → bundlewrap-5.2.0}/bundlewrap/items/pkg_snap.py +0 -0
  60. {bundlewrap-5.1.0 → bundlewrap-5.2.0}/bundlewrap/items/pkg_yum.py +0 -0
  61. {bundlewrap-5.1.0 → bundlewrap-5.2.0}/bundlewrap/items/pkg_zypper.py +0 -0
  62. {bundlewrap-5.1.0 → bundlewrap-5.2.0}/bundlewrap/items/postgres_dbs.py +0 -0
  63. {bundlewrap-5.1.0 → bundlewrap-5.2.0}/bundlewrap/items/postgres_roles.py +0 -0
  64. {bundlewrap-5.1.0 → bundlewrap-5.2.0}/bundlewrap/items/routeros.py +0 -0
  65. {bundlewrap-5.1.0 → bundlewrap-5.2.0}/bundlewrap/items/svc_freebsd.py +0 -0
  66. {bundlewrap-5.1.0 → bundlewrap-5.2.0}/bundlewrap/items/svc_openbsd.py +0 -0
  67. {bundlewrap-5.1.0 → bundlewrap-5.2.0}/bundlewrap/items/svc_openrc.py +0 -0
  68. {bundlewrap-5.1.0 → bundlewrap-5.2.0}/bundlewrap/items/svc_systemd.py +0 -0
  69. {bundlewrap-5.1.0 → bundlewrap-5.2.0}/bundlewrap/items/svc_systemv.py +0 -0
  70. {bundlewrap-5.1.0 → bundlewrap-5.2.0}/bundlewrap/items/symlinks.py +0 -0
  71. {bundlewrap-5.1.0 → bundlewrap-5.2.0}/bundlewrap/items/users.py +0 -0
  72. {bundlewrap-5.1.0 → bundlewrap-5.2.0}/bundlewrap/items/zfs_dataset.py +0 -0
  73. {bundlewrap-5.1.0 → bundlewrap-5.2.0}/bundlewrap/items/zfs_pool.py +0 -0
  74. {bundlewrap-5.1.0 → bundlewrap-5.2.0}/bundlewrap/lock.py +0 -0
  75. {bundlewrap-5.1.0 → bundlewrap-5.2.0}/bundlewrap/metadata.py +0 -0
  76. {bundlewrap-5.1.0 → bundlewrap-5.2.0}/bundlewrap/metagen.py +0 -0
  77. {bundlewrap-5.1.0 → bundlewrap-5.2.0}/bundlewrap/operations.py +0 -0
  78. {bundlewrap-5.1.0 → bundlewrap-5.2.0}/bundlewrap/secrets.py +0 -0
  79. {bundlewrap-5.1.0 → bundlewrap-5.2.0}/bundlewrap/utils/__init__.py +0 -0
  80. {bundlewrap-5.1.0 → bundlewrap-5.2.0}/bundlewrap/utils/cmdline.py +0 -0
  81. {bundlewrap-5.1.0 → bundlewrap-5.2.0}/bundlewrap/utils/crypto.py +0 -0
  82. {bundlewrap-5.1.0 → bundlewrap-5.2.0}/bundlewrap/utils/dicts.py +0 -0
  83. {bundlewrap-5.1.0 → bundlewrap-5.2.0}/bundlewrap/utils/magic_strings.py +0 -0
  84. {bundlewrap-5.1.0 → bundlewrap-5.2.0}/bundlewrap/utils/metastack.py +0 -0
  85. {bundlewrap-5.1.0 → bundlewrap-5.2.0}/bundlewrap/utils/plot.py +0 -0
  86. {bundlewrap-5.1.0 → bundlewrap-5.2.0}/bundlewrap/utils/remote.py +0 -0
  87. {bundlewrap-5.1.0 → bundlewrap-5.2.0}/bundlewrap/utils/scm.py +0 -0
  88. {bundlewrap-5.1.0 → bundlewrap-5.2.0}/bundlewrap/utils/table.py +0 -0
  89. {bundlewrap-5.1.0 → bundlewrap-5.2.0}/bundlewrap/utils/testing.py +0 -0
  90. {bundlewrap-5.1.0 → bundlewrap-5.2.0}/bundlewrap/utils/text.py +0 -0
  91. {bundlewrap-5.1.0 → bundlewrap-5.2.0}/bundlewrap/utils/ui.py +0 -0
  92. {bundlewrap-5.1.0 → bundlewrap-5.2.0}/bundlewrap.egg-info/SOURCES.txt +0 -0
  93. {bundlewrap-5.1.0 → bundlewrap-5.2.0}/bundlewrap.egg-info/dependency_links.txt +0 -0
  94. {bundlewrap-5.1.0 → bundlewrap-5.2.0}/bundlewrap.egg-info/entry_points.txt +0 -0
  95. {bundlewrap-5.1.0 → bundlewrap-5.2.0}/bundlewrap.egg-info/not-zip-safe +0 -0
  96. {bundlewrap-5.1.0 → bundlewrap-5.2.0}/bundlewrap.egg-info/requires.txt +0 -0
  97. {bundlewrap-5.1.0 → bundlewrap-5.2.0}/bundlewrap.egg-info/top_level.txt +0 -0
  98. {bundlewrap-5.1.0 → bundlewrap-5.2.0}/setup.cfg +0 -0
@@ -1,3 +1,12 @@
1
+ # 5.2.0
2
+
3
+ 2026-10-07
4
+
5
+ * added `$BW_LOCK_ADD_SKIP_ITEM_VERIFICATION`: This sets a default for the `--skip-item-verification` option of `bw lock`
6
+ * `bw verify` will skip items when Faults are unavailable (instead of showing long stack traces), now matches the behavior of `bw apply`
7
+ * added `pre_purge_command` to `directory` items
8
+ * fixed `repo.nodes_matching()`: No longer swallows errors and now raises `NoSuchTarget` as documented
9
+
1
10
  # 5.1.0
2
11
 
3
12
  2026-06-18
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bundlewrap
3
- Version: 5.1.0
3
+ Version: 5.2.0
4
4
  Summary: Config management with Python
5
5
  Home-page: http://bundlewrap.org
6
6
  Author: Torsten Rehn
@@ -1,2 +1,2 @@
1
- VERSION = (5, 1, 0)
1
+ VERSION = (5, 2, 0)
2
2
  VERSION_STRING = ".".join([str(v) for v in VERSION])
@@ -1,4 +1,5 @@
1
- from argparse import ArgumentParser, RawTextHelpFormatter, SUPPRESS
1
+ from argparse import (ArgumentParser, BooleanOptionalAction,
2
+ RawTextHelpFormatter, SUPPRESS)
2
3
  from os import environ, getcwd
3
4
  from os.path import join
4
5
 
@@ -581,7 +582,8 @@ bundle:my_bundle # items in this bundle
581
582
  )
582
583
  parser_lock_add.add_argument(
583
584
  "--skip-item-verification",
584
- action='store_true',
585
+ action=BooleanOptionalAction,
586
+ default=bool(int(environ.get('BW_LOCK_ADD_SKIP_ITEM_VERIFICATION', 0))),
585
587
  dest='skip_item_verification',
586
588
  help=_("disable verification of item selector (-i)"),
587
589
  )
@@ -49,6 +49,7 @@ class Directory(Item):
49
49
  'mode': "0755",
50
50
  'owner': "root",
51
51
  'purge': False,
52
+ 'pre_purge_command': None,
52
53
  }
53
54
  ITEM_TYPE_NAME = "directory"
54
55
 
@@ -98,6 +99,11 @@ class Directory(Item):
98
99
  return
99
100
 
100
101
  for path in status.actual_state.get('paths_to_purge', set()):
102
+ if self.attributes['pre_purge_command']:
103
+ self.run(self.attributes['pre_purge_command'].format(
104
+ path=quote(path),
105
+ file=quote(path.split('/')[-1]),
106
+ ))
101
107
  self.run("rm -rf -- {}".format(quote(path)))
102
108
 
103
109
  for fix_type in ('mode', 'owner', 'group'):
@@ -280,6 +286,14 @@ class Directory(Item):
280
286
  for key, value in attributes.items():
281
287
  ATTRIBUTE_VALIDATORS[key](item_id, value)
282
288
 
289
+ if attributes.get('pre_purge_command') and not attributes.get('purge'):
290
+ raise BundleError(_(
291
+ "{b} {i} cannot use pre_purge_command if not purging"
292
+ ).format(
293
+ n=bundle.name,
294
+ i=item_id,
295
+ ))
296
+
283
297
  @classmethod
284
298
  def validate_name(cls, bundle, name):
285
299
  if normpath(name) != name:
@@ -13,6 +13,7 @@ from .concurrency import WorkerPool
13
13
  from .deps import find_item, ItemDependencyLoop
14
14
  from .exceptions import (
15
15
  BundleError,
16
+ FaultUnavailable,
16
17
  GracefulApplyException,
17
18
  ItemSkipped,
18
19
  NodeLockedException,
@@ -1145,38 +1146,44 @@ def verify_items(
1145
1146
  return bool(items)
1146
1147
 
1147
1148
  def next_task():
1148
- while True:
1149
- try:
1150
- item = items.pop()
1151
- except IndexError:
1152
- return None
1153
- if item._faults_missing_for_attributes:
1154
- if item.error_on_missing_fault:
1155
- item._raise_for_faults()
1156
- else:
1157
- io.progress_advance()
1158
- io.stdout(_("{x} {node} {bundle} {item} ({msg})").format(
1159
- bundle=bold(item.bundle.name),
1160
- item=item.id,
1161
- msg=yellow(_("Fault unavailable")),
1162
- node=bold(node.name),
1163
- x=yellow("»"),
1164
- ))
1165
- else:
1166
- return {
1167
- 'task_id': node.name + ":" + item.bundle.name + ":" + item.id,
1168
- 'target': item.verify,
1169
- 'kwargs': {
1170
- 'autoskip_selector': autoskip_selector,
1171
- 'autoonly_selector': autoonly_selector,
1172
- },
1173
- }
1149
+ try:
1150
+ item = items.pop()
1151
+ except IndexError:
1152
+ return None
1153
+ return {
1154
+ 'task_id': node.name + ":" + item.bundle.name + ":" + item.id,
1155
+ 'target': item.verify,
1156
+ 'kwargs': {
1157
+ 'autoskip_selector': autoskip_selector,
1158
+ 'autoonly_selector': autoonly_selector,
1159
+ },
1160
+ }
1174
1161
 
1175
1162
  def handle_exception(task_id, exception, traceback):
1176
1163
  node_name, bundle_name, item_id = task_id.split(":", 2)
1177
1164
  io.progress_advance()
1178
1165
  if isinstance(exception, (ItemSkipped, NotImplementedError)):
1179
1166
  pass
1167
+ elif isinstance(exception, FaultUnavailable):
1168
+ item = node.get_item(item_id)
1169
+ if item.error_on_missing_fault:
1170
+ io.stderr(_("{x} {node} {bundle} {item} ({msg})").format(
1171
+ bundle=bold(bundle_name),
1172
+ item=item_id,
1173
+ msg=red(_("Fault unavailable")),
1174
+ node=bold(node_name),
1175
+ x=red("!"),
1176
+ ))
1177
+ return False
1178
+ else:
1179
+ io.stdout(_("{x} {node} {bundle} {item} ({msg})").format(
1180
+ bundle=bold(bundle_name),
1181
+ item=item_id,
1182
+ msg=yellow(_("Fault unavailable")),
1183
+ node=bold(node_name),
1184
+ x=yellow("»"),
1185
+ ))
1186
+ return None
1180
1187
  else:
1181
1188
  # Unlike with `bw apply`, it is OK for `bw verify` to encounter
1182
1189
  # exceptions when getting an item's status. `bw verify` doesn't
@@ -696,28 +696,35 @@ class Repository(MetadataGenerator):
696
696
  targets = set()
697
697
  for name in target_strings:
698
698
  name = name.strip()
699
+ targets_for_name = set()
699
700
  if name.startswith("bundle:"):
700
701
  bundle_name = name.split(":", 1)[1]
701
- targets.update(self.nodes_with_bundle(bundle_name))
702
+ targets_for_name.update(self.nodes_with_bundle(bundle_name))
702
703
  elif name.startswith("!bundle:"):
703
704
  bundle_name = name.split(":", 1)[1]
704
- targets.update(self.nodes_without_bundle(bundle_name))
705
+ targets_for_name.update(self.nodes_without_bundle(bundle_name))
705
706
  elif name.startswith("!group:"):
706
707
  group_name = name.split(":", 1)[1]
707
- targets.update(self.nodes_not_in_group(group_name))
708
+ targets_for_name.update(self.nodes_not_in_group(group_name))
708
709
  elif name.startswith("lambda:"):
709
710
  lambda_str = name.split(":", 1)[1]
710
- targets.update(self.nodes_matching_lambda(lambda_str, lambda_workers))
711
+ targets_for_name.update(self.nodes_matching_lambda(lambda_str, lambda_workers))
711
712
  else:
712
713
  try:
713
- targets.add(self.get_node(name))
714
+ targets_for_name.add(self.get_node(name))
714
715
  except NoSuchNode:
715
716
  try:
716
717
  group = self.get_group(name)
717
- targets.update(group.nodes)
718
+ targets_for_name.update(group.nodes)
718
719
  except NoSuchGroup:
719
720
  raise NoSuchTarget(name)
720
721
 
722
+ # Puplic API docs demand this.
723
+ if not targets_for_name:
724
+ raise NoSuchTarget(name)
725
+
726
+ targets |= targets_for_name
727
+
721
728
  return list(targets)
722
729
 
723
730
  def metadata_hash(self):
@@ -0,0 +1,34 @@
1
+ from bundlewrap.concurrency import WorkerPool
2
+
3
+
4
+ def parallel_node_eval(
5
+ nodes,
6
+ expression,
7
+ workers,
8
+ ):
9
+ nodes = set(nodes)
10
+
11
+ def tasks_available():
12
+ return bool(nodes)
13
+
14
+ def next_task():
15
+ node = nodes.pop()
16
+
17
+ def get_values():
18
+ return eval("lambda node: " + expression)(node)
19
+
20
+ return {
21
+ 'task_id': node.name,
22
+ 'target': get_values,
23
+ }
24
+
25
+ def handle_result(task_id, result, _duration):
26
+ return task_id, result
27
+
28
+ worker_pool = WorkerPool(
29
+ tasks_available,
30
+ next_task,
31
+ handle_result=handle_result,
32
+ workers=workers,
33
+ )
34
+ return dict(worker_pool.run())
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bundlewrap
3
- Version: 5.1.0
3
+ Version: 5.2.0
4
4
  Summary: Config management with Python
5
5
  Home-page: http://bundlewrap.org
6
6
  Author: Torsten Rehn
@@ -2,7 +2,7 @@ from setuptools import find_packages, setup
2
2
 
3
3
  setup(
4
4
  name="bundlewrap",
5
- version="5.1.0",
5
+ version="5.2.0",
6
6
  description="Config management with Python",
7
7
  long_description=(
8
8
  "By allowing for easy and low-overhead config management, BundleWrap fills the gap between complex deployments using Chef or Puppet and old school system administration over SSH.\n"
@@ -1,55 +0,0 @@
1
- from traceback import format_exc
2
-
3
- from bundlewrap.concurrency import WorkerPool
4
- from bundlewrap.exceptions import RepositoryError
5
- from bundlewrap.utils.text import red, bold, prefix_lines, _
6
- from bundlewrap.utils.ui import io
7
-
8
-
9
- def parallel_node_eval(
10
- nodes,
11
- expression,
12
- workers,
13
- ):
14
- nodes = set(nodes)
15
-
16
- def tasks_available():
17
- return bool(nodes)
18
-
19
- def next_task():
20
- node = nodes.pop()
21
-
22
- def get_values():
23
- try:
24
- return eval("lambda node: " + expression)(node)
25
- except RepositoryError:
26
- raise
27
- except Exception:
28
- traceback = format_exc()
29
- io.stderr(_(
30
- "{x} {node} Exception while evaluating `{expression}`, returning as None:\n{traceback}"
31
- ).format(
32
- x=red("✘"),
33
- node=bold(node),
34
- expression=expression,
35
- traceback=prefix_lines("\n" + traceback, f"{red('│')} ") + red("╵"),
36
- ))
37
- # Returning None here is kinda meh. But it's the only alternative
38
- # to failing hard by re-raising, which would be very annoying.
39
- return None
40
-
41
- return {
42
- 'task_id': node.name,
43
- 'target': get_values,
44
- }
45
-
46
- def handle_result(task_id, result, _duration):
47
- return task_id, result
48
-
49
- worker_pool = WorkerPool(
50
- tasks_available,
51
- next_task,
52
- handle_result=handle_result,
53
- workers=workers,
54
- )
55
- return dict(worker_pool.run())
File without changes
File without changes
File without changes
File without changes
File without changes