path-sync 0.3.3__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 CHANGED
@@ -3,7 +3,7 @@
3
3
  from path_sync import copy
4
4
  from path_sync import config
5
5
 
6
- VERSION = "0.3.3"
6
+ VERSION = "0.3.4"
7
7
  __all__ = [
8
8
  "copy",
9
9
  "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
- @dataclass
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 _sync_path(
315
+ def _iter_sync_files(
316
316
  mapping: PathMapping,
317
317
  src_root: Path,
318
318
  dest_root: Path,
319
- dest: Destination,
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
- changes += _copy_file(
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
- changes += _copy_file(
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
- dest_path = dest_root / dest_base
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
- src_pattern,
364
+ src_path,
375
365
  dest_path,
376
366
  dest,
377
- dest_base,
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"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: path-sync
3
- Version: 0.3.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 (`src_path` required, `dest_path` optional) |
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=2ju41FHOI1W8Ewd9lcAf-v3Mp7vRXFeD72hSCyPlOoI,153
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=-iqEoAGTA2oBeR8HXbJfzJ0hmnmtq27LBCeSgVbOo9k,18635
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=okHJkM2NsJ6Xe2HjJw6XqBTnIzCTQGniJRou6wra56g,4459
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.3.dist-info/METADATA,sha256=E0c9qnwBL5yTRW7XXcH6yvFaHjQiM7ieV_xpEwXiBtI,8231
18
- path_sync-0.3.3.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
19
- path_sync-0.3.3.dist-info/entry_points.txt,sha256=jTsL0c-9gP-4_Jt3EPgihtpLcwQR0AFAf1AUpD50AlI,54
20
- path_sync-0.3.3.dist-info/licenses/LICENSE,sha256=OphKV48tcMv6ep-7j-8T6nycykPT0g8ZlMJ9zbGvdPs,1066
21
- path_sync-0.3.3.dist-info/RECORD,,
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,,
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2025 Your Name
3
+ Copyright (c) 2026 Espen Albert
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal