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.
@@ -33,6 +33,7 @@ coverage.xml
33
33
  # pkg-ext dev mode files
34
34
  .groups-dev.yaml
35
35
  CHANGELOG-dev.md
36
+ *.api-dev.yaml
36
37
 
37
38
  # MkDocs build output
38
39
  site/
@@ -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
@@ -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:
@@ -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 (`src_path` required, `dest_path` optional) |
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:
@@ -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"
@@ -3,7 +3,7 @@
3
3
  # === OK_EDIT: path-sync header ===
4
4
  [project]
5
5
  name = "path-sync"
6
- version = "0.3.3"
6
+ version = "0.3.4"
7
7
  description = "Sync files from a source repo to multiple destination repos"
8
8
  requires-python = ">=3.13"
9
9
  license = "MIT"
File without changes
File without changes