path-sync 0.3.3__tar.gz → 0.3.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.
- {path_sync-0.3.3 → path_sync-0.3.4}/.gitignore +1 -0
- {path_sync-0.3.3 → path_sync-0.3.4}/LICENSE +1 -1
- {path_sync-0.3.3 → path_sync-0.3.4}/PKG-INFO +50 -2
- {path_sync-0.3.3 → path_sync-0.3.4}/README.md +49 -1
- {path_sync-0.3.3 → path_sync-0.3.4}/path_sync/__init__.py +1 -1
- {path_sync-0.3.3 → path_sync-0.3.4}/path_sync/_internal/cmd_copy.py +29 -41
- {path_sync-0.3.3 → path_sync-0.3.4}/path_sync/_internal/models.py +4 -1
- {path_sync-0.3.3 → path_sync-0.3.4}/pyproject.toml +1 -1
- {path_sync-0.3.3 → path_sync-0.3.4}/path_sync/__main__.py +0 -0
- {path_sync-0.3.3 → path_sync-0.3.4}/path_sync/_internal/__init__.py +0 -0
- {path_sync-0.3.3 → path_sync-0.3.4}/path_sync/_internal/cmd_boot.py +0 -0
- {path_sync-0.3.3 → path_sync-0.3.4}/path_sync/_internal/cmd_validate.py +0 -0
- {path_sync-0.3.3 → path_sync-0.3.4}/path_sync/_internal/file_utils.py +0 -0
- {path_sync-0.3.3 → path_sync-0.3.4}/path_sync/_internal/git_ops.py +0 -0
- {path_sync-0.3.3 → path_sync-0.3.4}/path_sync/_internal/header.py +0 -0
- {path_sync-0.3.3 → path_sync-0.3.4}/path_sync/_internal/typer_app.py +0 -0
- {path_sync-0.3.3 → path_sync-0.3.4}/path_sync/_internal/validation.py +0 -0
- {path_sync-0.3.3 → path_sync-0.3.4}/path_sync/_internal/yaml_utils.py +0 -0
- {path_sync-0.3.3 → path_sync-0.3.4}/path_sync/config.py +0 -0
- {path_sync-0.3.3 → path_sync-0.3.4}/path_sync/copy.py +0 -0
- {path_sync-0.3.3 → path_sync-0.3.4}/path_sync/sections.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: path-sync
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.4
|
|
4
4
|
Summary: Sync files from a source repo to multiple destination repos
|
|
5
5
|
Author-email: EspenAlbert <espen.albert1@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -133,6 +133,26 @@ destinations:
|
|
|
133
133
|
skip_sections:
|
|
134
134
|
justfile: [coverage] # keep local coverage recipe
|
|
135
135
|
```
|
|
136
|
+
|
|
137
|
+
## Skipping Files per Destination
|
|
138
|
+
|
|
139
|
+
Use `skip_file_patterns` to exclude files for specific destinations. Patterns match against the **destination path** (after `dest_path` remapping):
|
|
140
|
+
|
|
141
|
+
```yaml
|
|
142
|
+
paths:
|
|
143
|
+
- src_path: scripts/
|
|
144
|
+
dest_path: tools/ # remapped in destination
|
|
145
|
+
destinations:
|
|
146
|
+
- name: dest1
|
|
147
|
+
dest_path_relative: ../dest1
|
|
148
|
+
skip_file_patterns:
|
|
149
|
+
- "tools/internal/*" # matches destination path, not src
|
|
150
|
+
- "*.test.py"
|
|
151
|
+
- "docs/draft.md"
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Patterns use [fnmatch](https://docs.python.org/3/library/fnmatch.html) syntax (`*` matches any characters, `?` matches single character).
|
|
155
|
+
|
|
136
156
|
## Config Reference
|
|
137
157
|
|
|
138
158
|
**Source config** (`.github/{name}.src.yaml`):
|
|
@@ -145,6 +165,10 @@ paths:
|
|
|
145
165
|
- src_path: .cursor/**/*.mdc
|
|
146
166
|
- src_path: templates/justfile
|
|
147
167
|
dest_path: justfile
|
|
168
|
+
- src_path: scripts/
|
|
169
|
+
exclude_file_patterns:
|
|
170
|
+
- "*.pyc"
|
|
171
|
+
- "test_*.py"
|
|
148
172
|
destinations:
|
|
149
173
|
- name: dest1
|
|
150
174
|
repo_url: https://github.com/user/dest1
|
|
@@ -153,6 +177,8 @@ destinations:
|
|
|
153
177
|
default_branch: main
|
|
154
178
|
skip_sections:
|
|
155
179
|
justfile: [coverage]
|
|
180
|
+
skip_file_patterns:
|
|
181
|
+
- "scripts/internal/*"
|
|
156
182
|
```
|
|
157
183
|
|
|
158
184
|
| Field | Description |
|
|
@@ -160,11 +186,33 @@ destinations:
|
|
|
160
186
|
| `name` | Config identifier |
|
|
161
187
|
| `src_repo_url` | Source repo URL (auto-detected from git remote) |
|
|
162
188
|
| `schedule` | Cron for scheduled sync workflow |
|
|
163
|
-
| `paths` | Files/globs to sync (
|
|
189
|
+
| `paths` | Files/globs to sync (see path options below) |
|
|
164
190
|
| `destinations` | Target repos with sync settings |
|
|
165
191
|
| `header_config` | Comment style per extension (has defaults) |
|
|
166
192
|
| `pr_defaults` | PR title, labels, reviewers, assignees |
|
|
167
193
|
|
|
194
|
+
**Path options**:
|
|
195
|
+
|
|
196
|
+
| Field | Description |
|
|
197
|
+
|-------|-------------|
|
|
198
|
+
| `src_path` | Source file, directory, or glob pattern (required) |
|
|
199
|
+
| `dest_path` | Destination path (defaults to `src_path`) |
|
|
200
|
+
| `sync_mode` | `sync` (default), `replace`, or `scaffold` |
|
|
201
|
+
| `exclude_dirs` | Directory names to skip (defaults: `__pycache__`, `.git`, `.venv`, etc.) |
|
|
202
|
+
| `exclude_file_patterns` | Filename patterns to skip, supports globs (`*.pyc`, `test_*.py`) |
|
|
203
|
+
|
|
204
|
+
**Destination options**:
|
|
205
|
+
|
|
206
|
+
| Field | Description |
|
|
207
|
+
|-------|-------------|
|
|
208
|
+
| `name` | Destination identifier (required) |
|
|
209
|
+
| `repo_url` | Repo URL for cloning if not found locally |
|
|
210
|
+
| `dest_path_relative` | Path to destination repo relative to source (required) |
|
|
211
|
+
| `copy_branch` | Branch for sync (defaults to `sync/{config_name}`) |
|
|
212
|
+
| `default_branch` | Default branch to compare against (defaults to `main`) |
|
|
213
|
+
| `skip_sections` | Map of `{dest_path: [section_ids]}` to preserve locally |
|
|
214
|
+
| `skip_file_patterns` | Patterns to skip for this destination (matches dest path, fnmatch syntax) |
|
|
215
|
+
|
|
168
216
|
## Header Format
|
|
169
217
|
|
|
170
218
|
Synced files have a header comment identifying the source config:
|
|
@@ -115,6 +115,26 @@ destinations:
|
|
|
115
115
|
skip_sections:
|
|
116
116
|
justfile: [coverage] # keep local coverage recipe
|
|
117
117
|
```
|
|
118
|
+
|
|
119
|
+
## Skipping Files per Destination
|
|
120
|
+
|
|
121
|
+
Use `skip_file_patterns` to exclude files for specific destinations. Patterns match against the **destination path** (after `dest_path` remapping):
|
|
122
|
+
|
|
123
|
+
```yaml
|
|
124
|
+
paths:
|
|
125
|
+
- src_path: scripts/
|
|
126
|
+
dest_path: tools/ # remapped in destination
|
|
127
|
+
destinations:
|
|
128
|
+
- name: dest1
|
|
129
|
+
dest_path_relative: ../dest1
|
|
130
|
+
skip_file_patterns:
|
|
131
|
+
- "tools/internal/*" # matches destination path, not src
|
|
132
|
+
- "*.test.py"
|
|
133
|
+
- "docs/draft.md"
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Patterns use [fnmatch](https://docs.python.org/3/library/fnmatch.html) syntax (`*` matches any characters, `?` matches single character).
|
|
137
|
+
|
|
118
138
|
## Config Reference
|
|
119
139
|
|
|
120
140
|
**Source config** (`.github/{name}.src.yaml`):
|
|
@@ -127,6 +147,10 @@ paths:
|
|
|
127
147
|
- src_path: .cursor/**/*.mdc
|
|
128
148
|
- src_path: templates/justfile
|
|
129
149
|
dest_path: justfile
|
|
150
|
+
- src_path: scripts/
|
|
151
|
+
exclude_file_patterns:
|
|
152
|
+
- "*.pyc"
|
|
153
|
+
- "test_*.py"
|
|
130
154
|
destinations:
|
|
131
155
|
- name: dest1
|
|
132
156
|
repo_url: https://github.com/user/dest1
|
|
@@ -135,6 +159,8 @@ destinations:
|
|
|
135
159
|
default_branch: main
|
|
136
160
|
skip_sections:
|
|
137
161
|
justfile: [coverage]
|
|
162
|
+
skip_file_patterns:
|
|
163
|
+
- "scripts/internal/*"
|
|
138
164
|
```
|
|
139
165
|
|
|
140
166
|
| Field | Description |
|
|
@@ -142,11 +168,33 @@ destinations:
|
|
|
142
168
|
| `name` | Config identifier |
|
|
143
169
|
| `src_repo_url` | Source repo URL (auto-detected from git remote) |
|
|
144
170
|
| `schedule` | Cron for scheduled sync workflow |
|
|
145
|
-
| `paths` | Files/globs to sync (
|
|
171
|
+
| `paths` | Files/globs to sync (see path options below) |
|
|
146
172
|
| `destinations` | Target repos with sync settings |
|
|
147
173
|
| `header_config` | Comment style per extension (has defaults) |
|
|
148
174
|
| `pr_defaults` | PR title, labels, reviewers, assignees |
|
|
149
175
|
|
|
176
|
+
**Path options**:
|
|
177
|
+
|
|
178
|
+
| Field | Description |
|
|
179
|
+
|-------|-------------|
|
|
180
|
+
| `src_path` | Source file, directory, or glob pattern (required) |
|
|
181
|
+
| `dest_path` | Destination path (defaults to `src_path`) |
|
|
182
|
+
| `sync_mode` | `sync` (default), `replace`, or `scaffold` |
|
|
183
|
+
| `exclude_dirs` | Directory names to skip (defaults: `__pycache__`, `.git`, `.venv`, etc.) |
|
|
184
|
+
| `exclude_file_patterns` | Filename patterns to skip, supports globs (`*.pyc`, `test_*.py`) |
|
|
185
|
+
|
|
186
|
+
**Destination options**:
|
|
187
|
+
|
|
188
|
+
| Field | Description |
|
|
189
|
+
|-------|-------------|
|
|
190
|
+
| `name` | Destination identifier (required) |
|
|
191
|
+
| `repo_url` | Repo URL for cloning if not found locally |
|
|
192
|
+
| `dest_path_relative` | Path to destination repo relative to source (required) |
|
|
193
|
+
| `copy_branch` | Branch for sync (defaults to `sync/{config_name}`) |
|
|
194
|
+
| `default_branch` | Default branch to compare against (defaults to `main`) |
|
|
195
|
+
| `skip_sections` | Map of `{dest_path: [section_ids]}` to preserve locally |
|
|
196
|
+
| `skip_file_patterns` | Patterns to skip for this destination (matches dest path, fnmatch syntax) |
|
|
197
|
+
|
|
150
198
|
## Header Format
|
|
151
199
|
|
|
152
200
|
Synced files have a header comment identifying the source config:
|
|
@@ -8,6 +8,7 @@ from dataclasses import dataclass, field
|
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
|
|
10
10
|
import typer
|
|
11
|
+
from pydantic import BaseModel
|
|
11
12
|
|
|
12
13
|
from path_sync import sections
|
|
13
14
|
from path_sync._internal import git_ops, header
|
|
@@ -68,8 +69,7 @@ def capture_sync_log(dest_name: str):
|
|
|
68
69
|
root_logger.removeHandler(file_handler)
|
|
69
70
|
|
|
70
71
|
|
|
71
|
-
|
|
72
|
-
class CopyOptions:
|
|
72
|
+
class CopyOptions(BaseModel):
|
|
73
73
|
dry_run: bool = False
|
|
74
74
|
force_overwrite: bool = False
|
|
75
75
|
no_checkout: bool = False
|
|
@@ -312,19 +312,12 @@ def _sync_paths(
|
|
|
312
312
|
return result
|
|
313
313
|
|
|
314
314
|
|
|
315
|
-
def
|
|
315
|
+
def _iter_sync_files(
|
|
316
316
|
mapping: PathMapping,
|
|
317
317
|
src_root: Path,
|
|
318
318
|
dest_root: Path,
|
|
319
|
-
|
|
320
|
-
config_name: str,
|
|
321
|
-
dry_run: bool,
|
|
322
|
-
force_overwrite: bool,
|
|
323
|
-
) -> tuple[int, set[Path]]:
|
|
319
|
+
):
|
|
324
320
|
src_pattern = src_root / mapping.src_path
|
|
325
|
-
changes = 0
|
|
326
|
-
synced: set[Path] = set()
|
|
327
|
-
sync_mode = mapping.sync_mode
|
|
328
321
|
|
|
329
322
|
if "*" in mapping.src_path:
|
|
330
323
|
glob_prefix = mapping.src_path.split("*")[0].rstrip("/")
|
|
@@ -336,53 +329,48 @@ def _sync_path(
|
|
|
336
329
|
src_path = Path(src_file)
|
|
337
330
|
if src_path.is_file() and not mapping.is_excluded(src_path):
|
|
338
331
|
rel = src_path.relative_to(src_root / glob_prefix)
|
|
339
|
-
dest_path = dest_root / dest_base / rel
|
|
340
332
|
dest_key = str(Path(dest_base) / rel)
|
|
341
|
-
|
|
342
|
-
src_path,
|
|
343
|
-
dest_path,
|
|
344
|
-
dest,
|
|
345
|
-
dest_key,
|
|
346
|
-
config_name,
|
|
347
|
-
sync_mode,
|
|
348
|
-
dry_run,
|
|
349
|
-
force_overwrite,
|
|
350
|
-
)
|
|
351
|
-
synced.add(dest_path)
|
|
333
|
+
yield src_path, dest_key, dest_root / dest_base / rel
|
|
352
334
|
elif src_pattern.is_dir():
|
|
353
335
|
dest_base = mapping.resolved_dest_path()
|
|
354
336
|
for src_file in src_pattern.rglob("*"):
|
|
355
337
|
if src_file.is_file() and not mapping.is_excluded(src_file):
|
|
356
338
|
rel = src_file.relative_to(src_pattern)
|
|
357
|
-
dest_path = dest_root / dest_base / rel
|
|
358
339
|
dest_key = str(Path(dest_base) / rel)
|
|
359
|
-
|
|
360
|
-
src_file,
|
|
361
|
-
dest_path,
|
|
362
|
-
dest,
|
|
363
|
-
dest_key,
|
|
364
|
-
config_name,
|
|
365
|
-
sync_mode,
|
|
366
|
-
dry_run,
|
|
367
|
-
force_overwrite,
|
|
368
|
-
)
|
|
369
|
-
synced.add(dest_path)
|
|
340
|
+
yield src_file, dest_key, dest_root / dest_base / rel
|
|
370
341
|
elif src_pattern.is_file():
|
|
371
342
|
dest_base = mapping.resolved_dest_path()
|
|
372
|
-
|
|
343
|
+
yield src_pattern, dest_base, dest_root / dest_base
|
|
344
|
+
else:
|
|
345
|
+
logger.warning(f"Source not found: {mapping.src_path}")
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def _sync_path(
|
|
349
|
+
mapping: PathMapping,
|
|
350
|
+
src_root: Path,
|
|
351
|
+
dest_root: Path,
|
|
352
|
+
dest: Destination,
|
|
353
|
+
config_name: str,
|
|
354
|
+
dry_run: bool,
|
|
355
|
+
force_overwrite: bool,
|
|
356
|
+
) -> tuple[int, set[Path]]:
|
|
357
|
+
changes = 0
|
|
358
|
+
synced: set[Path] = set()
|
|
359
|
+
|
|
360
|
+
for src_path, dest_key, dest_path in _iter_sync_files(mapping, src_root, dest_root):
|
|
361
|
+
if dest.is_skipped(dest_key):
|
|
362
|
+
continue
|
|
373
363
|
changes += _copy_file(
|
|
374
|
-
|
|
364
|
+
src_path,
|
|
375
365
|
dest_path,
|
|
376
366
|
dest,
|
|
377
|
-
|
|
367
|
+
dest_key,
|
|
378
368
|
config_name,
|
|
379
|
-
sync_mode,
|
|
369
|
+
mapping.sync_mode,
|
|
380
370
|
dry_run,
|
|
381
371
|
force_overwrite,
|
|
382
372
|
)
|
|
383
373
|
synced.add(dest_path)
|
|
384
|
-
else:
|
|
385
|
-
logger.warning(f"Source not found: {mapping.src_path}")
|
|
386
374
|
|
|
387
375
|
return changes, synced
|
|
388
376
|
|
|
@@ -121,11 +121,14 @@ class Destination(BaseModel):
|
|
|
121
121
|
copy_branch: str = ""
|
|
122
122
|
default_branch: str = "main"
|
|
123
123
|
skip_sections: dict[str, list[str]] = Field(default_factory=dict)
|
|
124
|
+
skip_file_patterns: set[str] = Field(default_factory=set)
|
|
124
125
|
|
|
125
126
|
def resolved_copy_branch(self, config_name: str) -> str:
|
|
126
|
-
"""Returns branch name, defaulting to sync/{config_name} if not set."""
|
|
127
127
|
return self.copy_branch or f"sync/{config_name}"
|
|
128
128
|
|
|
129
|
+
def is_skipped(self, dest_key: str) -> bool:
|
|
130
|
+
return any(fnmatch.fnmatch(dest_key, pat) for pat in self.skip_file_patterns)
|
|
131
|
+
|
|
129
132
|
|
|
130
133
|
class SrcConfig(BaseModel):
|
|
131
134
|
CONFIG_EXT: ClassVar[str] = ".src.yaml"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|