path-sync 0.3.2__py3-none-any.whl → 0.3.4__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.
- path_sync/__init__.py +1 -1
- path_sync/_internal/cmd_copy.py +29 -41
- path_sync/_internal/models.py +9 -2
- {path_sync-0.3.2.dist-info → path_sync-0.3.4.dist-info}/METADATA +50 -2
- {path_sync-0.3.2.dist-info → path_sync-0.3.4.dist-info}/RECORD +8 -8
- {path_sync-0.3.2.dist-info → path_sync-0.3.4.dist-info}/licenses/LICENSE +1 -1
- {path_sync-0.3.2.dist-info → path_sync-0.3.4.dist-info}/WHEEL +0 -0
- {path_sync-0.3.2.dist-info → path_sync-0.3.4.dist-info}/entry_points.txt +0 -0
path_sync/__init__.py
CHANGED
path_sync/_internal/cmd_copy.py
CHANGED
|
@@ -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
|
|
path_sync/_internal/models.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import fnmatch
|
|
3
4
|
import glob as glob_mod
|
|
4
5
|
from enum import StrEnum
|
|
5
6
|
from pathlib import Path
|
|
@@ -39,12 +40,15 @@ class PathMapping(BaseModel):
|
|
|
39
40
|
dest_path: str = ""
|
|
40
41
|
sync_mode: SyncMode = SyncMode.SYNC
|
|
41
42
|
exclude_dirs: set[str] = Field(default_factory=_default_exclude_dirs)
|
|
43
|
+
exclude_file_patterns: set[str] = Field(default_factory=set)
|
|
42
44
|
|
|
43
45
|
def resolved_dest_path(self) -> str:
|
|
44
46
|
return self.dest_path or self.src_path
|
|
45
47
|
|
|
46
48
|
def is_excluded(self, path: Path) -> bool:
|
|
47
|
-
|
|
49
|
+
if self.exclude_dirs & set(path.parts):
|
|
50
|
+
return True
|
|
51
|
+
return any(fnmatch.fnmatch(path.name, pat) for pat in self.exclude_file_patterns)
|
|
48
52
|
|
|
49
53
|
def expand_dest_paths(self, repo_root: Path) -> list[Path]:
|
|
50
54
|
dest_path = self.resolved_dest_path()
|
|
@@ -117,11 +121,14 @@ class Destination(BaseModel):
|
|
|
117
121
|
copy_branch: str = ""
|
|
118
122
|
default_branch: str = "main"
|
|
119
123
|
skip_sections: dict[str, list[str]] = Field(default_factory=dict)
|
|
124
|
+
skip_file_patterns: set[str] = Field(default_factory=set)
|
|
120
125
|
|
|
121
126
|
def resolved_copy_branch(self, config_name: str) -> str:
|
|
122
|
-
"""Returns branch name, defaulting to sync/{config_name} if not set."""
|
|
123
127
|
return self.copy_branch or f"sync/{config_name}"
|
|
124
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
|
+
|
|
125
132
|
|
|
126
133
|
class SrcConfig(BaseModel):
|
|
127
134
|
CONFIG_EXT: ClassVar[str] = ".src.yaml"
|
|
@@ -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:
|
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
path_sync/__init__.py,sha256=
|
|
1
|
+
path_sync/__init__.py,sha256=jOWYUecn-jrXFa3Zq7wW_EzKuqrJN6kQ1E_PY4rPGf4,153
|
|
2
2
|
path_sync/__main__.py,sha256=HDj3qgijDcK8k976qsAvKMvUq9fZEJTK-dZldUc5-no,326
|
|
3
3
|
path_sync/config.py,sha256=XYuEK_bjSpAk_nZN0oxpEA-S3t9F6Qn0yznYLoQHIw8,568
|
|
4
4
|
path_sync/copy.py,sha256=BpflW4086XJFSHHK4taYPgXtF07xHB8qrgm8TYqdM4E,120
|
|
5
5
|
path_sync/sections.py,sha256=lUFGO3gqpGr7msztDudUrejmfghrfOPWiOMKBP6YotE,1927
|
|
6
6
|
path_sync/_internal/__init__.py,sha256=iPkMhrpiyXBijo2Hp-y_2zEYxAXnHLnStKM0X0HHd4U,56
|
|
7
7
|
path_sync/_internal/cmd_boot.py,sha256=cFomUyPOhNX9fi5_BYL2Sm7O1oq7vntD8b7clQ7mL1E,3104
|
|
8
|
-
path_sync/_internal/cmd_copy.py,sha256
|
|
8
|
+
path_sync/_internal/cmd_copy.py,sha256=FR5wHZ7co4Bt1t20nnCdl4s05doApxzpkrXzM9xHlQA,18241
|
|
9
9
|
path_sync/_internal/cmd_validate.py,sha256=e6m-JZlXAGr0ZRqfLhhrlmjs4w79p2WWnZo4I05PGpo,1798
|
|
10
10
|
path_sync/_internal/file_utils.py,sha256=5C33qzKFQdwChi5YwUWBujj126t0P6dbGSU_5hWExpE,194
|
|
11
11
|
path_sync/_internal/git_ops.py,sha256=rpG_r7VNH1KlBgqM9mz7xop0mpdy76Vs3rzCoxE1dIQ,5895
|
|
12
12
|
path_sync/_internal/header.py,sha256=evgY2q_gfDdEytEt_jyJ7M_KdGzCpfdKBUnoh3v-0Go,2593
|
|
13
|
-
path_sync/_internal/models.py,sha256=
|
|
13
|
+
path_sync/_internal/models.py,sha256=IA6lb_BFXntcZnn9bWJZYenlnDvk9ddNBA67l5qFrmA,4577
|
|
14
14
|
path_sync/_internal/typer_app.py,sha256=lEGMRXql3Se3VbmwAohvpUaL2cbY-RwhPUq8kL7bPbc,177
|
|
15
15
|
path_sync/_internal/validation.py,sha256=qhEha-pJiM5zkZlr4sj2I4ZqvqcWMEfL4IZu_LGatLI,2226
|
|
16
16
|
path_sync/_internal/yaml_utils.py,sha256=yj6Bl54EltjLEcVKaiA5Ahb9byT6OUMh0xIEzTsrvnQ,498
|
|
17
|
-
path_sync-0.3.
|
|
18
|
-
path_sync-0.3.
|
|
19
|
-
path_sync-0.3.
|
|
20
|
-
path_sync-0.3.
|
|
21
|
-
path_sync-0.3.
|
|
17
|
+
path_sync-0.3.4.dist-info/METADATA,sha256=pNmrWhqEKTMg8c8mG7yuaX4-fsQiA2uBCkgQwVDOiwI,10000
|
|
18
|
+
path_sync-0.3.4.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
19
|
+
path_sync-0.3.4.dist-info/entry_points.txt,sha256=jTsL0c-9gP-4_Jt3EPgihtpLcwQR0AFAf1AUpD50AlI,54
|
|
20
|
+
path_sync-0.3.4.dist-info/licenses/LICENSE,sha256=MnHjsc6ccjI5Iiw2R3jLEAApIcrEpLdIcZxkilhSPxc,1069
|
|
21
|
+
path_sync-0.3.4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|