boxyard 0.2.0__tar.gz → 0.3.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 (45) hide show
  1. {boxyard-0.2.0 → boxyard-0.3.0}/.gitignore +3 -0
  2. boxyard-0.3.0/PKG-INFO +171 -0
  3. boxyard-0.3.0/README.md +156 -0
  4. {boxyard-0.2.0 → boxyard-0.3.0}/pyproject.toml +7 -5
  5. {boxyard-0.2.0 → boxyard-0.3.0}/src/boxyard/_cli/app.py +1 -1
  6. boxyard-0.3.0/src/boxyard/_cli/main.py +2727 -0
  7. {boxyard-0.2.0 → boxyard-0.3.0}/src/boxyard/_cli/multi_sync.py +80 -51
  8. boxyard-0.3.0/src/boxyard/_cli/path_tui.py +176 -0
  9. {boxyard-0.2.0 → boxyard-0.3.0}/src/boxyard/_enums.py +1 -1
  10. boxyard-0.3.0/src/boxyard/_fast.py +274 -0
  11. {boxyard-0.2.0 → boxyard-0.3.0}/src/boxyard/_models.py +206 -70
  12. {boxyard-0.2.0 → boxyard-0.3.0}/src/boxyard/_remote_index.py +1 -16
  13. boxyard-0.3.0/src/boxyard/_shell_helper.py +94 -0
  14. {boxyard-0.2.0 → boxyard-0.3.0}/src/boxyard/_tombstones.py +1 -17
  15. {boxyard-0.2.0 → boxyard-0.3.0}/src/boxyard/_utils/base.py +63 -38
  16. {boxyard-0.2.0 → boxyard-0.3.0}/src/boxyard/_utils/locking.py +35 -27
  17. {boxyard-0.2.0 → boxyard-0.3.0}/src/boxyard/_utils/logical_expressions.py +13 -8
  18. boxyard-0.3.0/src/boxyard/_utils/perms.py +124 -0
  19. {boxyard-0.2.0 → boxyard-0.3.0}/src/boxyard/_utils/rclone.py +132 -55
  20. {boxyard-0.2.0 → boxyard-0.3.0}/src/boxyard/_utils/sync_helper.py +57 -51
  21. {boxyard-0.2.0 → boxyard-0.3.0}/src/boxyard/cmds/__init__.py +1 -0
  22. {boxyard-0.2.0 → boxyard-0.3.0}/src/boxyard/cmds/_copy_from_remote.py +28 -21
  23. {boxyard-0.2.0 → boxyard-0.3.0}/src/boxyard/cmds/_create_user_symlinks.py +3 -4
  24. {boxyard-0.2.0 → boxyard-0.3.0}/src/boxyard/cmds/_delete_box.py +27 -14
  25. boxyard-0.3.0/src/boxyard/cmds/_doctor.py +518 -0
  26. {boxyard-0.2.0 → boxyard-0.3.0}/src/boxyard/cmds/_exclude_box.py +23 -10
  27. {boxyard-0.2.0 → boxyard-0.3.0}/src/boxyard/cmds/_force_push_to_remote.py +32 -27
  28. {boxyard-0.2.0 → boxyard-0.3.0}/src/boxyard/cmds/_get_box_sync_status.py +12 -8
  29. {boxyard-0.2.0 → boxyard-0.3.0}/src/boxyard/cmds/_include_box.py +9 -8
  30. {boxyard-0.2.0 → boxyard-0.3.0}/src/boxyard/cmds/_init_boxyard.py +19 -11
  31. {boxyard-0.2.0 → boxyard-0.3.0}/src/boxyard/cmds/_modify_boxmeta.py +44 -13
  32. {boxyard-0.2.0 → boxyard-0.3.0}/src/boxyard/cmds/_new_box.py +36 -33
  33. {boxyard-0.2.0 → boxyard-0.3.0}/src/boxyard/cmds/_rename_box.py +35 -36
  34. {boxyard-0.2.0 → boxyard-0.3.0}/src/boxyard/cmds/_sync_box.py +66 -48
  35. {boxyard-0.2.0 → boxyard-0.3.0}/src/boxyard/cmds/_sync_missing_boxmetas.py +30 -22
  36. {boxyard-0.2.0 → boxyard-0.3.0}/src/boxyard/cmds/_sync_name.py +12 -13
  37. {boxyard-0.2.0 → boxyard-0.3.0}/src/boxyard/config.py +28 -19
  38. {boxyard-0.2.0 → boxyard-0.3.0}/src/boxyard/const.py +8 -24
  39. boxyard-0.2.0/PKG-INFO +0 -65
  40. boxyard-0.2.0/README.md +0 -51
  41. boxyard-0.2.0/src/boxyard/_cli/main.py +0 -1656
  42. {boxyard-0.2.0 → boxyard-0.3.0}/LICENSE +0 -0
  43. {boxyard-0.2.0 → boxyard-0.3.0}/src/boxyard/__init__.py +0 -0
  44. {boxyard-0.2.0 → boxyard-0.3.0}/src/boxyard/_cli/__init__.py +0 -0
  45. {boxyard-0.2.0 → boxyard-0.3.0}/src/boxyard/_utils/__init__.py +0 -0
@@ -184,3 +184,6 @@ cython_debug/
184
184
  # and can be added to the global gitignore or merged into this file. For a more nuclear
185
185
  # option (not recommended) you can uncomment the following to ignore the entire idea folder.
186
186
  #.idea/
187
+
188
+ # Personal agent notes (not shared)
189
+ AGENTS.local.md
boxyard-0.3.0/PKG-INFO ADDED
@@ -0,0 +1,171 @@
1
+ Metadata-Version: 2.4
2
+ Name: boxyard
3
+ Version: 0.3.0
4
+ Summary: Add your description here
5
+ License-File: LICENSE
6
+ Requires-Python: >=3.11
7
+ Requires-Dist: filelock>=3.12.0
8
+ Requires-Dist: pydantic>=2.12.4
9
+ Requires-Dist: python-ulid[pydantic]>=3.1.0
10
+ Requires-Dist: rich>=14.2.0
11
+ Requires-Dist: textual>=1.0.0
12
+ Requires-Dist: toml>=0.10.2
13
+ Requires-Dist: typer>=0.20.0
14
+ Description-Content-Type: text/markdown
15
+
16
+ # boxyard
17
+
18
+ A CLI tool for managing and syncing folders ("boxes") across local and remote storage using [rclone](https://rclone.org/). Track metadata, organize boxes into groups, and keep everything in sync with conflict detection.
19
+
20
+ ## Install
21
+
22
+ ```bash
23
+ pip install boxyard
24
+ # or
25
+ uv pip install boxyard
26
+ ```
27
+
28
+ Requires [rclone](https://rclone.org/downloads/) to be installed and configured.
29
+
30
+ ## Quick start
31
+
32
+ ```bash
33
+ # Initialize boxyard (creates config and data directories)
34
+ boxyard init
35
+
36
+ # Create a new box from an existing folder
37
+ boxyard new ~/projects/my-project
38
+
39
+ # Sync a box to remote storage
40
+ boxyard sync my-project
41
+
42
+ # Check sync status
43
+ boxyard yard-status
44
+
45
+ # List all boxes
46
+ boxyard list
47
+ ```
48
+
49
+ ## What it does
50
+
51
+ Boxyard manages folders (called "boxes") that you want to keep synced between your local machine and remote storage (S3, SFTP, or any rclone-supported backend).
52
+
53
+ Each box has:
54
+ - **Data** (`data/`) - the actual folder contents
55
+ - **Metadata** (`boxmeta.toml`) - name, groups, storage location, creation info
56
+ - **Config** (`conf/`) - optional per-box configuration that controls how data is synced
57
+ - **Sync records** - track what's been synced and when, enabling conflict detection
58
+
59
+ ### The `conf/` folder
60
+
61
+ Each box can optionally have a `conf/` folder containing rclone filter files that customize which files are included or excluded when syncing the box's data:
62
+
63
+ - `.rclone_include` - only sync files matching these patterns
64
+ - `.rclone_exclude` - skip files matching these patterns (if absent, the global default exclude list is used)
65
+ - `.rclone_filters` - combined include/exclude filter rules
66
+
67
+ During sync, the `conf/` folder is synced *before* the data, ensuring filter rules are up-to-date before they're applied. This means filter rules travel with the box across remotes -- if you want a box to always exclude `.venv/` or only include `*.csv`, put that in its `conf/` folder and it will apply everywhere the box is synced.
68
+
69
+ Boxes are identified by a unique ID (`{timestamp}_{subid}`, e.g. `20251122_143022_a7kx9`) and organized into groups via symlinks.
70
+
71
+ ## Commands
72
+
73
+ | Command | Description |
74
+ |---------|-------------|
75
+ | `init` | Initialize boxyard config and data directories |
76
+ | `new` | Create a new box from a folder |
77
+ | `sync` | Sync a box with remote storage |
78
+ | `multi-sync` | Sync multiple boxes concurrently |
79
+ | `list` | List all boxes |
80
+ | `box-status` | Show sync status of a box |
81
+ | `yard-status` | Show sync status of all boxes |
82
+ | `doctor` | Read-only health check of the machine's boxyard state |
83
+ | `include` | Include a remote box in the local store |
84
+ | `exclude` | Exclude a box from the local store (keeps remote) |
85
+ | `delete` | Delete a box locally and/or remotely |
86
+ | `rename` | Rename a box locally, remotely, or both |
87
+ | `copy` | Copy a remote box to a local path without including it |
88
+ | `force-push` | Force push a local folder to a box's remote |
89
+ | `add-to-group` | Add a box to a group |
90
+ | `remove-from-group` | Remove a box from a group |
91
+ | `path` | Get the local path of a box |
92
+ | `which` | Identify which box a path belongs to |
93
+
94
+ ### `doctor`
95
+
96
+ `boxyard doctor` runs a strictly read-only health check of the machine's boxyard state, so misuse and drift get caught mechanically. It never mutates or auto-fixes anything, and exits with code 0 when healthy and 1 when there is any finding — so it can run under cron/supervisors and be asserted by scripts and agents.
97
+
98
+ ```bash
99
+ boxyard doctor # full check, including remote storage
100
+ boxyard doctor --no-remote # offline: skip checks that access remote storage
101
+ boxyard doctor -o json # machine-readable report
102
+ ```
103
+
104
+ Checks:
105
+
106
+ | Check | What it flags |
107
+ |-------|---------------|
108
+ | `unregistered-folder` | Directories in `user_boxes_path` that are not registered boxes (e.g. hand-created instead of via `boxyard new`) |
109
+ | `malformed-name` | Entries in `user_boxes_path` whose names don't parse as `<timestamp>_<subid>__<name>` (legacy formats are accepted) |
110
+ | `broken-registration` | `local_store` registrations missing `boxmeta.toml`, or with one that fails to parse/validate |
111
+ | `duplicate-box-id` | The same box id registered more than once |
112
+ | `stale-cache` | `boxyard_meta.json` disagreeing with a fresh scan of `local_store` |
113
+ | `dangling-symlinks` | Group symlinks whose targets don't exist |
114
+ | `group-tree-debris` | Real (non-symlink) files in the group tree, which make `create-user-symlinks` raise |
115
+ | `orphaned-sync-records` | `sync_records/<index>/` with no matching registration |
116
+ | `interrupted-sync` | Local sync records left incomplete by an interrupted sync (the local copy may be incomplete), or that fail to parse |
117
+ | `unknown-storage-location` | `local_store` dirs and remote-index caches left over from removed/renamed storage locations |
118
+ | `rclone-config` | Unresolvable rclone binary, rclone storage locations with no remote in `boxyard_rclone.conf`, or a missing default exclude file |
119
+ | `stale-meta-mirror` | Remote boxmetas not mirrored locally (what `sync-missing-meta` would fetch); skipped with `--no-remote` |
120
+ | `tombstoned-box` | Locally registered boxes that were deleted (tombstoned) on the remote from another machine; skipped with `--no-remote` |
121
+ | `tree-orphans` | Boxmeta `parents` referencing unknown box ids |
122
+
123
+ Every finding comes with a one-line hint on how to fix it.
124
+
125
+ ## Configuration
126
+
127
+ Config file: `~/.config/boxyard/config.toml`
128
+
129
+ ```toml
130
+ default_storage_location = "my-remote"
131
+ boxyard_data_path = "~/.boxyard"
132
+ user_boxes_path = "~/boxes"
133
+ user_box_groups_path = "~/box-groups"
134
+
135
+ [storage_locations.my-remote]
136
+ storage_type = "rclone"
137
+ store_path = "boxyard"
138
+ ```
139
+
140
+ Storage locations are defined as rclone remotes. Boxyard uses its own rclone config at `~/.config/boxyard/boxyard_rclone.conf`.
141
+
142
+ ## Directory layout
143
+
144
+ ```
145
+ ~/.config/boxyard/
146
+ config.toml # Main config
147
+ boxyard_rclone.conf # rclone config for remotes
148
+
149
+ ~/.boxyard/
150
+ local_store/{remote}/ # Local copies of box data
151
+ sync_records/ # Per-box sync state
152
+ locks/ # File locks for concurrent operations
153
+
154
+ ~/boxes/ # Symlinks to box data folders
155
+ ~/box-groups/ # Group symlinks (e.g. ~/box-groups/work/my-project)
156
+ ```
157
+
158
+ ## Development
159
+
160
+ Boxyard uses [nblite](https://github.com/lukastk/nblite) for notebook-first development. Source files in `src/boxyard/` are autogenerated -- edit the `.pct.py` files in `pts/` instead.
161
+
162
+ ```bash
163
+ uv sync # Install dependencies
164
+ nbl export --reverse # Sync pts -> nbs (after editing .pct.py files)
165
+ nbl export # Export nbs -> src/boxyard/
166
+ pytest src/tests/ # Run tests
167
+ ```
168
+
169
+ ## License
170
+
171
+ MIT
@@ -0,0 +1,156 @@
1
+ # boxyard
2
+
3
+ A CLI tool for managing and syncing folders ("boxes") across local and remote storage using [rclone](https://rclone.org/). Track metadata, organize boxes into groups, and keep everything in sync with conflict detection.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install boxyard
9
+ # or
10
+ uv pip install boxyard
11
+ ```
12
+
13
+ Requires [rclone](https://rclone.org/downloads/) to be installed and configured.
14
+
15
+ ## Quick start
16
+
17
+ ```bash
18
+ # Initialize boxyard (creates config and data directories)
19
+ boxyard init
20
+
21
+ # Create a new box from an existing folder
22
+ boxyard new ~/projects/my-project
23
+
24
+ # Sync a box to remote storage
25
+ boxyard sync my-project
26
+
27
+ # Check sync status
28
+ boxyard yard-status
29
+
30
+ # List all boxes
31
+ boxyard list
32
+ ```
33
+
34
+ ## What it does
35
+
36
+ Boxyard manages folders (called "boxes") that you want to keep synced between your local machine and remote storage (S3, SFTP, or any rclone-supported backend).
37
+
38
+ Each box has:
39
+ - **Data** (`data/`) - the actual folder contents
40
+ - **Metadata** (`boxmeta.toml`) - name, groups, storage location, creation info
41
+ - **Config** (`conf/`) - optional per-box configuration that controls how data is synced
42
+ - **Sync records** - track what's been synced and when, enabling conflict detection
43
+
44
+ ### The `conf/` folder
45
+
46
+ Each box can optionally have a `conf/` folder containing rclone filter files that customize which files are included or excluded when syncing the box's data:
47
+
48
+ - `.rclone_include` - only sync files matching these patterns
49
+ - `.rclone_exclude` - skip files matching these patterns (if absent, the global default exclude list is used)
50
+ - `.rclone_filters` - combined include/exclude filter rules
51
+
52
+ During sync, the `conf/` folder is synced *before* the data, ensuring filter rules are up-to-date before they're applied. This means filter rules travel with the box across remotes -- if you want a box to always exclude `.venv/` or only include `*.csv`, put that in its `conf/` folder and it will apply everywhere the box is synced.
53
+
54
+ Boxes are identified by a unique ID (`{timestamp}_{subid}`, e.g. `20251122_143022_a7kx9`) and organized into groups via symlinks.
55
+
56
+ ## Commands
57
+
58
+ | Command | Description |
59
+ |---------|-------------|
60
+ | `init` | Initialize boxyard config and data directories |
61
+ | `new` | Create a new box from a folder |
62
+ | `sync` | Sync a box with remote storage |
63
+ | `multi-sync` | Sync multiple boxes concurrently |
64
+ | `list` | List all boxes |
65
+ | `box-status` | Show sync status of a box |
66
+ | `yard-status` | Show sync status of all boxes |
67
+ | `doctor` | Read-only health check of the machine's boxyard state |
68
+ | `include` | Include a remote box in the local store |
69
+ | `exclude` | Exclude a box from the local store (keeps remote) |
70
+ | `delete` | Delete a box locally and/or remotely |
71
+ | `rename` | Rename a box locally, remotely, or both |
72
+ | `copy` | Copy a remote box to a local path without including it |
73
+ | `force-push` | Force push a local folder to a box's remote |
74
+ | `add-to-group` | Add a box to a group |
75
+ | `remove-from-group` | Remove a box from a group |
76
+ | `path` | Get the local path of a box |
77
+ | `which` | Identify which box a path belongs to |
78
+
79
+ ### `doctor`
80
+
81
+ `boxyard doctor` runs a strictly read-only health check of the machine's boxyard state, so misuse and drift get caught mechanically. It never mutates or auto-fixes anything, and exits with code 0 when healthy and 1 when there is any finding — so it can run under cron/supervisors and be asserted by scripts and agents.
82
+
83
+ ```bash
84
+ boxyard doctor # full check, including remote storage
85
+ boxyard doctor --no-remote # offline: skip checks that access remote storage
86
+ boxyard doctor -o json # machine-readable report
87
+ ```
88
+
89
+ Checks:
90
+
91
+ | Check | What it flags |
92
+ |-------|---------------|
93
+ | `unregistered-folder` | Directories in `user_boxes_path` that are not registered boxes (e.g. hand-created instead of via `boxyard new`) |
94
+ | `malformed-name` | Entries in `user_boxes_path` whose names don't parse as `<timestamp>_<subid>__<name>` (legacy formats are accepted) |
95
+ | `broken-registration` | `local_store` registrations missing `boxmeta.toml`, or with one that fails to parse/validate |
96
+ | `duplicate-box-id` | The same box id registered more than once |
97
+ | `stale-cache` | `boxyard_meta.json` disagreeing with a fresh scan of `local_store` |
98
+ | `dangling-symlinks` | Group symlinks whose targets don't exist |
99
+ | `group-tree-debris` | Real (non-symlink) files in the group tree, which make `create-user-symlinks` raise |
100
+ | `orphaned-sync-records` | `sync_records/<index>/` with no matching registration |
101
+ | `interrupted-sync` | Local sync records left incomplete by an interrupted sync (the local copy may be incomplete), or that fail to parse |
102
+ | `unknown-storage-location` | `local_store` dirs and remote-index caches left over from removed/renamed storage locations |
103
+ | `rclone-config` | Unresolvable rclone binary, rclone storage locations with no remote in `boxyard_rclone.conf`, or a missing default exclude file |
104
+ | `stale-meta-mirror` | Remote boxmetas not mirrored locally (what `sync-missing-meta` would fetch); skipped with `--no-remote` |
105
+ | `tombstoned-box` | Locally registered boxes that were deleted (tombstoned) on the remote from another machine; skipped with `--no-remote` |
106
+ | `tree-orphans` | Boxmeta `parents` referencing unknown box ids |
107
+
108
+ Every finding comes with a one-line hint on how to fix it.
109
+
110
+ ## Configuration
111
+
112
+ Config file: `~/.config/boxyard/config.toml`
113
+
114
+ ```toml
115
+ default_storage_location = "my-remote"
116
+ boxyard_data_path = "~/.boxyard"
117
+ user_boxes_path = "~/boxes"
118
+ user_box_groups_path = "~/box-groups"
119
+
120
+ [storage_locations.my-remote]
121
+ storage_type = "rclone"
122
+ store_path = "boxyard"
123
+ ```
124
+
125
+ Storage locations are defined as rclone remotes. Boxyard uses its own rclone config at `~/.config/boxyard/boxyard_rclone.conf`.
126
+
127
+ ## Directory layout
128
+
129
+ ```
130
+ ~/.config/boxyard/
131
+ config.toml # Main config
132
+ boxyard_rclone.conf # rclone config for remotes
133
+
134
+ ~/.boxyard/
135
+ local_store/{remote}/ # Local copies of box data
136
+ sync_records/ # Per-box sync state
137
+ locks/ # File locks for concurrent operations
138
+
139
+ ~/boxes/ # Symlinks to box data folders
140
+ ~/box-groups/ # Group symlinks (e.g. ~/box-groups/work/my-project)
141
+ ```
142
+
143
+ ## Development
144
+
145
+ Boxyard uses [nblite](https://github.com/lukastk/nblite) for notebook-first development. Source files in `src/boxyard/` are autogenerated -- edit the `.pct.py` files in `pts/` instead.
146
+
147
+ ```bash
148
+ uv sync # Install dependencies
149
+ nbl export --reverse # Sync pts -> nbs (after editing .pct.py files)
150
+ nbl export # Export nbs -> src/boxyard/
151
+ pytest src/tests/ # Run tests
152
+ ```
153
+
154
+ ## License
155
+
156
+ MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "boxyard"
3
- version = "0.2.0"
3
+ version = "0.3.0"
4
4
  description = "Add your description here"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -9,6 +9,7 @@ dependencies = [
9
9
  "pydantic>=2.12.4",
10
10
  "python-ulid[pydantic]>=3.1.0",
11
11
  "rich>=14.2.0",
12
+ "textual>=1.0.0",
12
13
  "toml>=0.10.2",
13
14
  "typer>=0.20.0",
14
15
  ]
@@ -28,7 +29,7 @@ dev = [
28
29
  "git-cliff>=2.10.1",
29
30
  "ipywidgets>=8.1.8",
30
31
  "jupyterlab>=4.4.0",
31
- "nblite>=1.1.10",
32
+ "nblite>=1.2.2", # 1.2.1 emits broken relative imports for function-export modules
32
33
  "pytest>=9.0.0",
33
34
  "python-dotenv>=1.2.1",
34
35
  "ruff>=0.14.13",
@@ -37,6 +38,7 @@ dev = [
37
38
 
38
39
  [project.scripts]
39
40
  boxyard = "boxyard._cli.app:app"
41
+ boxyard-shell-helper = "boxyard._shell_helper:main"
40
42
 
41
43
  [tool.pytest.ini_options]
42
44
  testpaths = ["src/tests"]
@@ -86,11 +88,11 @@ ignore = [
86
88
  # but trigger ruff warnings (unsorted imports, lambda assignments, etc.)
87
89
  "src/boxyard/**/*.py" = [
88
90
  "I001", "E731", "B904", "B905", "B018", "B006", "B007", "B020", "B023",
89
- "C401", "C408", "C416", "C419",
91
+ "C401", "C408", "C416", "C419", "C420",
90
92
  "F401", "F403", "F405", "F541", "F841",
91
- "E721",
93
+ "E703", "E721",
92
94
  "UP017", "UP024", "UP035", "UP041",
93
- "W293",
95
+ "W291", "W293",
94
96
  ]
95
97
 
96
98
  [tool.ruff.lint.isort]
@@ -1,6 +1,6 @@
1
1
  # AUTOGENERATED! DO NOT EDIT! File to edit: pts/mod/_cli/app.pct.py
2
2
 
3
- __all__ = ["app", "app_state"]
3
+ __all__ = ['app', 'app_state']
4
4
 
5
5
  # %% pts/mod/_cli/app.pct.py 3
6
6
  import typer