drybag 0.7.1__py3-none-any.whl

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 (152) hide show
  1. drybag/__init__.py +9 -0
  2. drybag/__main__.py +3 -0
  3. drybag/areas.py +114 -0
  4. drybag/attach/__init__.py +18 -0
  5. drybag/attach/api.py +75 -0
  6. drybag/attach/binding.py +497 -0
  7. drybag/attach/check.py +91 -0
  8. drybag/attach/checkout.py +167 -0
  9. drybag/attach/commands.py +180 -0
  10. drybag/attach/doctor.py +639 -0
  11. drybag/attach/exclude.py +432 -0
  12. drybag/attach/hooks.py +270 -0
  13. drybag/attach/items.py +85 -0
  14. drybag/attach/permissions.py +308 -0
  15. drybag/attach/render.py +353 -0
  16. drybag/attach/rules.py +269 -0
  17. drybag/attach/tags.py +51 -0
  18. drybag/attach/write.py +1396 -0
  19. drybag/binding.py +207 -0
  20. drybag/cli.py +176 -0
  21. drybag/command.py +99 -0
  22. drybag/config/__init__.py +5 -0
  23. drybag/config/classes.py +220 -0
  24. drybag/config/layout.py +23 -0
  25. drybag/config/loader.py +692 -0
  26. drybag/config/machine.py +75 -0
  27. drybag/config/overlay.py +44 -0
  28. drybag/config/overlay_layers.py +191 -0
  29. drybag/config/owner.py +247 -0
  30. drybag/config/paths.py +302 -0
  31. drybag/config/plates.py +161 -0
  32. drybag/config/reading.py +234 -0
  33. drybag/config/schema.py +161 -0
  34. drybag/config/session.py +397 -0
  35. drybag/docs/__init__.py +5 -0
  36. drybag/docs/api.py +44 -0
  37. drybag/docs/commands.py +148 -0
  38. drybag/docs/hygiene.py +195 -0
  39. drybag/docs/plans.py +492 -0
  40. drybag/docs/themes.py +153 -0
  41. drybag/docs/trail.py +392 -0
  42. drybag/doctor/__init__.py +26 -0
  43. drybag/doctor/api.py +67 -0
  44. drybag/doctor/checks.py +775 -0
  45. drybag/doctor/commands.py +81 -0
  46. drybag/doctor/model.py +201 -0
  47. drybag/doctor/registry.py +174 -0
  48. drybag/errors.py +37 -0
  49. drybag/findings.py +56 -0
  50. drybag/fsops.py +866 -0
  51. drybag/gitenv.py +631 -0
  52. drybag/grammar.py +45 -0
  53. drybag/guards/__init__.py +5 -0
  54. drybag/guards/api.py +42 -0
  55. drybag/guards/commands.py +189 -0
  56. drybag/guards/commit.py +574 -0
  57. drybag/guards/githooks.py +275 -0
  58. drybag/harnesses.py +321 -0
  59. drybag/hints/__init__.py +6 -0
  60. drybag/hints/bashscan.py +782 -0
  61. drybag/hints/bgcleanup.py +679 -0
  62. drybag/hints/hooks.py +119 -0
  63. drybag/hints/hygiene.py +126 -0
  64. drybag/hints/pipes.py +76 -0
  65. drybag/hints/runners.py +103 -0
  66. drybag/hooks/__init__.py +1 -0
  67. drybag/hooks/api.py +186 -0
  68. drybag/hooks/commands.py +89 -0
  69. drybag/hooks/dispatch.py +210 -0
  70. drybag/hooks/registry.py +30 -0
  71. drybag/hooks/sink.py +201 -0
  72. drybag/jsonobject.py +145 -0
  73. drybag/memory/__init__.py +1 -0
  74. drybag/memory/answers.py +61 -0
  75. drybag/memory/api.py +133 -0
  76. drybag/memory/bundles.py +271 -0
  77. drybag/memory/commands.py +373 -0
  78. drybag/memory/composed.py +155 -0
  79. drybag/memory/doctor.py +215 -0
  80. drybag/memory/hooks.py +109 -0
  81. drybag/memory/index.py +560 -0
  82. drybag/memory/inventory.py +86 -0
  83. drybag/memory/notes.py +463 -0
  84. drybag/memory/reviewed.py +93 -0
  85. drybag/memory/store.py +657 -0
  86. drybag/memory/trust.py +46 -0
  87. drybag/overlay/__init__.py +1 -0
  88. drybag/overlay/api.py +83 -0
  89. drybag/overlay/commands.py +51 -0
  90. drybag/overlay/create.py +360 -0
  91. drybag/overlay/doctor.py +224 -0
  92. drybag/overlay/identity.py +103 -0
  93. drybag/overlay/layout.py +61 -0
  94. drybag/overlay/requires.py +63 -0
  95. drybag/overlay/sync.py +37 -0
  96. drybag/overlay/template.py +78 -0
  97. drybag/overlay/upgrade.py +98 -0
  98. drybag/presets/__init__.py +74 -0
  99. drybag/presets/recommended.toml +104 -0
  100. drybag/printed.py +86 -0
  101. drybag/processes.py +105 -0
  102. drybag/prose.py +175 -0
  103. drybag/release/__init__.py +2 -0
  104. drybag/release/api.py +20 -0
  105. drybag/release/pins.py +15 -0
  106. drybag/result.py +13 -0
  107. drybag/runner.py +141 -0
  108. drybag/scaffold/__init__.py +62 -0
  109. drybag/scaffold/engine.py +187 -0
  110. drybag/scaffold/entries.py +76 -0
  111. drybag/scaffold/manifest.py +171 -0
  112. drybag/scaffold/model.py +80 -0
  113. drybag/scaffold/regions.py +110 -0
  114. drybag/scaffold/report.py +30 -0
  115. drybag/semver.py +125 -0
  116. drybag/setup/__init__.py +7 -0
  117. drybag/setup/api.py +38 -0
  118. drybag/setup/commands.py +414 -0
  119. drybag/setup/machine.py +209 -0
  120. drybag/setup/restore.py +299 -0
  121. drybag/setup/run.py +683 -0
  122. drybag/sync/__init__.py +8 -0
  123. drybag/sync/doctor.py +161 -0
  124. drybag/sync/git.py +170 -0
  125. drybag/sync/hooks.py +46 -0
  126. drybag/sync/job.py +954 -0
  127. drybag/sync/rules.py +357 -0
  128. drybag/sync/scan.py +162 -0
  129. drybag/sync/state.py +342 -0
  130. drybag/sync/trigger.py +139 -0
  131. drybag/templates/__init__.py +1 -0
  132. drybag/templates/overlay/.github/dependabot.yml +15 -0
  133. drybag/templates/overlay/.github/workflows/scan.yml +58 -0
  134. drybag/templates/overlay/.gitignore +6 -0
  135. drybag/templates/overlay/.pre-commit-config.yaml +10 -0
  136. drybag/templates/overlay/README.md +56 -0
  137. drybag/templates/overlay/common/claude/hooks.json +3 -0
  138. drybag/templates/overlay/common/claude/permissions.json +3 -0
  139. drybag/templates/overlay/common/codex/common.rules +2 -0
  140. drybag/templates/overlay/common/memory/_README.md +15 -0
  141. drybag/templates/overlay/drybag-overlay.toml +2 -0
  142. drybag/templates/overlay/projects/README.md +11 -0
  143. drybag/tomlout.py +207 -0
  144. drybag/trust/__init__.py +7 -0
  145. drybag/trust/api.py +49 -0
  146. drybag/trust/boundary.py +132 -0
  147. drybag/trust/record.py +208 -0
  148. drybag-0.7.1.dist-info/METADATA +477 -0
  149. drybag-0.7.1.dist-info/RECORD +152 -0
  150. drybag-0.7.1.dist-info/WHEEL +4 -0
  151. drybag-0.7.1.dist-info/entry_points.txt +3 -0
  152. drybag-0.7.1.dist-info/licenses/LICENSE +21 -0
drybag/__init__.py ADDED
@@ -0,0 +1,9 @@
1
+ """drybag: private layers for coding agents."""
2
+
3
+ __version__ = "0.7.1"
4
+
5
+ # The public repository, spelled once — the install hints derive from it. `scripts/release.py
6
+ # check` reads `__version__` through a regex anchored on that name alone, and does not see these
7
+ # two lines.
8
+ REPOSITORY_SLUG = "dry-bag/drybag"
9
+ REPOSITORY_URL = f"https://github.com/{REPOSITORY_SLUG}"
drybag/__main__.py ADDED
@@ -0,0 +1,3 @@
1
+ from drybag.cli import main
2
+
3
+ raise SystemExit(main())
drybag/areas.py ADDED
@@ -0,0 +1,114 @@
1
+ """Areas discovered by name: the half `drybag.cli`, `drybag.hooks.registry` and
2
+ `drybag.doctor.registry` share.
3
+
4
+ Three submodules are discovered, each by the module that reads it: an area's `commands.py` gives
5
+ it a CLI group, its `hooks.py` hook handlers, and its `doctor.py` rows in `drybag doctor`'s
6
+ report. The CLI frame and the hook registry import through `area_modules`, so an area that
7
+ fails to import fails them; the report imports through `area_imports`, which hands that failure
8
+ over in the area's place, because the report must outlive any one area's broken code.
9
+
10
+ A leaf module on purpose. `cli.py` imports every area through this discovery, so an area that
11
+ imported back into `cli.py` would constrain what the frame may ever import; the aliases and
12
+ the loop live here instead, and `cli` re-exports the aliases.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import argparse
18
+ import importlib
19
+ import pkgutil
20
+ from collections.abc import Callable
21
+ from pathlib import Path
22
+ from types import ModuleType
23
+ from typing import TYPE_CHECKING
24
+
25
+ import drybag
26
+
27
+ if TYPE_CHECKING:
28
+ SubParsers = argparse._SubParsersAction[argparse.ArgumentParser]
29
+ else:
30
+ # Generic only in typeshed: subscripting the class at runtime raises TypeError.
31
+ SubParsers = argparse._SubParsersAction
32
+
33
+ Registrar = Callable[[SubParsers], None]
34
+
35
+ # The sides, by the first part of a module's path under `src/drybag/`; a module neither side
36
+ # lists is the core's. CONTRIBUTING.md, "Areas", says what each holds and the rules between them,
37
+ # and `tests/boundaries/test_delivery.py` holds those rules. They read code, so they cannot see
38
+ # `templates/overlay/`, data only the `overlay` area reads, which sits in the core.
39
+ DELIVERY_AREAS: frozenset[str] = frozenset(
40
+ {
41
+ "attach",
42
+ "hints",
43
+ "memory",
44
+ "overlay",
45
+ "setup",
46
+ "sync",
47
+ }
48
+ )
49
+ PACK_PACKAGES: frozenset[str] = frozenset(
50
+ {
51
+ "docs",
52
+ "guards",
53
+ }
54
+ )
55
+ CORE_AREAS: frozenset[str] = frozenset(
56
+ {
57
+ "doctor",
58
+ "hooks",
59
+ }
60
+ )
61
+
62
+
63
+ def _has_submodule(area: str, submodule: str) -> bool:
64
+ """A filesystem probe: it imports only the `<area>.<submodule>` modules that exist, instead
65
+ of importing every area package merely to test whether one of them does.
66
+
67
+ That saving is latent rather than realised on today's frame: `main()` runs
68
+ `discover_registrars()` to build the full argparse parser before argparse can dispatch to
69
+ `hook`, and building that parser already imports `drybag.config`, `drybag.config.schema`,
70
+ `drybag.presets`, `drybag.release` and `drybag.release.pins` — so by the time this
71
+ probe runs for `hooks`, those packages are already loaded. The probe would only pay off if
72
+ discovery ever registered just the area named on the command line instead of the full set.
73
+ """
74
+ for root in drybag.__path__:
75
+ directory = Path(root) / area
76
+ if (directory / f"{submodule}.py").is_file():
77
+ return True
78
+ if (directory / submodule / "__init__.py").is_file():
79
+ return True
80
+ return False
81
+
82
+
83
+ def _qualified(submodule: str) -> list[str]:
84
+ """The qualified name of each `drybag.<area>.<submodule>` that exists, in area-name
85
+ order: the one probe both importers below read."""
86
+ return [
87
+ f"drybag.{entry.name}.{submodule}"
88
+ for entry in sorted(pkgutil.iter_modules(drybag.__path__), key=lambda m: m.name)
89
+ if entry.ispkg and _has_submodule(entry.name, submodule)
90
+ ]
91
+
92
+
93
+ def area_modules(submodule: str) -> list[ModuleType]:
94
+ """Every `drybag.<area>.<submodule>` that exists, in area-name order, no shared registry."""
95
+ return [importlib.import_module(name) for name in _qualified(submodule)]
96
+
97
+
98
+ def area_imports(submodule: str) -> list[tuple[str, ModuleType | Exception]]:
99
+ """Every `drybag.<area>.<submodule>` that exists, in area-name order, each under its
100
+ qualified name with the module, or with the exception its import raised in the module's place.
101
+
102
+ For a reader that must outlive an area's broken code: `doctor`'s report, which is what a user
103
+ has left when everything else is broken, so one area's `doctor.py` that raises on import must
104
+ cost that area's rows and not the report. The failure is handed over rather than judged here,
105
+ because what it costs is the reader's to say. `Exception` and not `BaseException`, as for a
106
+ check that raises: a `KeyboardInterrupt` during discovery stops the command.
107
+ """
108
+ found: list[tuple[str, ModuleType | Exception]] = []
109
+ for name in _qualified(submodule):
110
+ try:
111
+ found.append((name, importlib.import_module(name)))
112
+ except Exception as exc: # an area's broken import is handed over, never raised here
113
+ found.append((name, exc))
114
+ return found
@@ -0,0 +1,18 @@
1
+ """Bind a repository to the machine owner's private overlay, and take back what binding wrote.
2
+
3
+ An area, discovered by name: `commands.py` gives it the `attach` and `detach` groups and
4
+ `api.py` is what another area may import. `detach` takes back what `attach` wrote and leaves the
5
+ overlay's record of the binding, `projects/<name>/project.toml`, where it is. It writes nothing
6
+ a repository chose — the overlay root comes from the machine file, the store is that overlay's
7
+ own directory for this project, and what the overlay grants is shown on one screen rather than
8
+ asked about.
9
+ """
10
+
11
+ # The command that binds this checkout to its share of the overlay, which every remedy of this area
12
+ # names, so a reader is never sent to two spellings of one command. No `--store`: the store is
13
+ # derived from the machine-recorded overlay, and a placeholder an agent runs as written exits 2.
14
+ # Here and not in `doctor.py` because `hooks.py` names it too, and discovery imports `hooks.py`
15
+ # unguarded for every hook: an edge to the report's module would let a defect there stop every
16
+ # hook, and load the report on each one. Not annotated `Final`: an area's `__init__.py` imports
17
+ # nothing, `typing` included (`tests/test_surfaces.py`).
18
+ ATTACH_COMMAND = "drybag attach"
drybag/attach/api.py ADDED
@@ -0,0 +1,75 @@
1
+ """The attach area's import surface: everything another area may import from it.
2
+
3
+ The list is chosen from what consumers outside this area actually reach for, and every name on
4
+ it carries the reason beside it — a surface that survives a trim with no explanation is what
5
+ made the trim necessary. An area that needs something absent from this list grows it
6
+ deliberately, in a commit that says which area and why — it does not import a private module of
7
+ this area.
8
+
9
+ No module under `src/` outside this area imports from here. `doctor` was the one consuming area,
10
+ and its `attached` row is now this area's own (`attach/doctor.py`), which reaches this area's
11
+ modules directly. What is left is what the suite
12
+ reads from outside the area:
13
+
14
+ - the ledger (`ledger`) and `AttachLedger`, because `ledger` returns it: a return
15
+ type absent from this list is a value a consumer can hold and cannot declare, and
16
+ `tests/test_surfaces.py` derives that rule rather than restating it; `Rendered`, the type of
17
+ its `render`, for the same rule;
18
+ - the binding (`read_binding`, `Binding`). `Binding.state` is one of `memory.api`'s
19
+ `BINDING_STATES`: the vocabulary and the one classifier that decides between its members are
20
+ the memory area's, so this area publishes neither a copy of the set nor its own members;
21
+ - the file an earlier release merged the overlay's hook entries into (`LOCAL_SETTINGS`), which
22
+ the doctor area's tests write those entries into;
23
+ - `attach` and `Attached`, which `setup --restore` runs on each checkout the overlay's projects
24
+ file lists (`setup.restore`), and the type it returns, with `Carried`, the type of two of its
25
+ fields (`added` and `removed`), which `tests/test_surfaces.py` derives that a surface carries.
26
+
27
+ **Trimmed when the `attached` row moved into this area: `overlay_entries`.** It was published for
28
+ `doctor`'s provenance column, and went once `attach` merged no hook entry.
29
+
30
+ **`setup` was not a consumer of `check`, and still is not.** This docstring used to say "`setup`
31
+ runs `attach --check` and reads the same `Result`"; `setup` imports nothing of `check`, and
32
+ `attach --check` is this area's own command module calling its own `attach.check`. (`setup
33
+ --restore` does import `attach` itself, which is the bullet above.) The
34
+ permission-diff half of the area — `check`, `diff_permissions` and the `PermissionDiff` it
35
+ returns — was published on the strength of that sentence and left when the sentence did.
36
+
37
+ **Trimmed once, and back for `setup --restore`: `attach` and `Attached`; `detach` and
38
+ `Detached` are trimmed again since the `uninstall` that ran `detach` was deleted.** They were
39
+ published against a second sentence of the same kind — "`tests/test_install_path.py`, the
40
+ walkthrough that runs the four packages in the order a person does, is the other consumer, and it
41
+ drives `attach` and
42
+ `detach`, whose returns are `Attached` and `Detached`". That walkthrough drives the argument
43
+ parser: `step("attach", "--store", ..., "--machine", ...)`, which is the point of it, because the
44
+ commands' argv wiring is what it exists to exercise. It imports two surfaces,
45
+ `drybag.doctor.api` and `drybag.memory.api`, and reaches no name on this list. The two verbs
46
+ are this area's own command module's, and their results are read where they are returned.
47
+
48
+ The claim is the same shape as the one this docstring corrected two paragraphs up, and as the
49
+ one `setup/api.py` corrected about its own `setup` and `SetupReport` — three of them now, all
50
+ naming a test that runs a command rather than a module that imports a name. A published surface
51
+ argued from prose is a surface nobody measured: the first count of this list's importers
52
+ covered `src/`, `scripts/` and `tests/` and never read the sentence, and the sentence is what
53
+ kept the names.
54
+
55
+ The ledger's place is the core's (`binding.LEDGER_FILE`, beside the binding id), because `doctor`
56
+ reads it too; nothing outside delivery imports this area but through the crossings pinned in
57
+ `tests/boundaries/test_delivery.py`.
58
+ """
59
+
60
+ from drybag.attach.binding import Binding, read_binding
61
+ from drybag.attach.permissions import LOCAL_SETTINGS
62
+ from drybag.attach.render import Rendered
63
+ from drybag.attach.write import Attached, AttachLedger, Carried, attach, ledger
64
+
65
+ __all__ = [
66
+ "LOCAL_SETTINGS",
67
+ "AttachLedger",
68
+ "Attached",
69
+ "Binding",
70
+ "Carried",
71
+ "Rendered",
72
+ "attach",
73
+ "ledger",
74
+ "read_binding",
75
+ ]