path-sync 0.7.5__tar.gz → 0.7.7__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.7.5 → path_sync-0.7.7}/PKG-INFO +1 -1
- {path_sync-0.7.5 → path_sync-0.7.7}/path_sync/__init__.py +1 -1
- {path_sync-0.7.5 → path_sync-0.7.7}/path_sync/_internal/auto_merge.py +31 -11
- {path_sync-0.7.5 → path_sync-0.7.7}/path_sync/_internal/cmd_copy.py +20 -7
- {path_sync-0.7.5 → path_sync-0.7.7}/path_sync/_internal/cmd_dep_update.py +5 -2
- {path_sync-0.7.5 → path_sync-0.7.7}/path_sync/_internal/git_ops.py +35 -3
- {path_sync-0.7.5 → path_sync-0.7.7}/pyproject.toml +24 -6
- {path_sync-0.7.5 → path_sync-0.7.7}/.gitignore +0 -0
- {path_sync-0.7.5 → path_sync-0.7.7}/LICENSE +0 -0
- {path_sync-0.7.5 → path_sync-0.7.7}/README.md +0 -0
- {path_sync-0.7.5 → path_sync-0.7.7}/path_sync/__main__.py +0 -0
- {path_sync-0.7.5 → path_sync-0.7.7}/path_sync/_internal/__init__.py +0 -0
- {path_sync-0.7.5 → path_sync-0.7.7}/path_sync/_internal/cmd_boot.py +0 -0
- {path_sync-0.7.5 → path_sync-0.7.7}/path_sync/_internal/cmd_options.py +0 -0
- {path_sync-0.7.5 → path_sync-0.7.7}/path_sync/_internal/cmd_validate.py +0 -0
- {path_sync-0.7.5 → path_sync-0.7.7}/path_sync/_internal/file_utils.py +0 -0
- {path_sync-0.7.5 → path_sync-0.7.7}/path_sync/_internal/header.py +0 -0
- {path_sync-0.7.5 → path_sync-0.7.7}/path_sync/_internal/log_capture.py +0 -0
- {path_sync-0.7.5 → path_sync-0.7.7}/path_sync/_internal/models.py +0 -0
- {path_sync-0.7.5 → path_sync-0.7.7}/path_sync/_internal/models_dep.py +0 -0
- {path_sync-0.7.5 → path_sync-0.7.7}/path_sync/_internal/prompt_utils.py +0 -0
- {path_sync-0.7.5 → path_sync-0.7.7}/path_sync/_internal/repo_utils.py +0 -0
- {path_sync-0.7.5 → path_sync-0.7.7}/path_sync/_internal/typer_app.py +0 -0
- {path_sync-0.7.5 → path_sync-0.7.7}/path_sync/_internal/validation.py +0 -0
- {path_sync-0.7.5 → path_sync-0.7.7}/path_sync/_internal/verify.py +0 -0
- {path_sync-0.7.5 → path_sync-0.7.7}/path_sync/_internal/yaml_utils.py +0 -0
- {path_sync-0.7.5 → path_sync-0.7.7}/path_sync/config.py +0 -0
- {path_sync-0.7.5 → path_sync-0.7.7}/path_sync/copy.py +0 -0
- {path_sync-0.7.5 → path_sync-0.7.7}/path_sync/dep_update.py +0 -0
- {path_sync-0.7.5 → path_sync-0.7.7}/path_sync/sections.py +0 -0
- {path_sync-0.7.5 → path_sync-0.7.7}/path_sync/validate_no_changes.py +0 -0
|
@@ -38,6 +38,8 @@ COMPLETED_CHECK_STATES: frozenset[str] = frozenset(FAILED_CHECK_STATES | {"SUCCE
|
|
|
38
38
|
class CheckRun(BaseModel):
|
|
39
39
|
name: str
|
|
40
40
|
state: str
|
|
41
|
+
workflow: str = ""
|
|
42
|
+
link: str = ""
|
|
41
43
|
|
|
42
44
|
@property
|
|
43
45
|
def failed(self) -> bool:
|
|
@@ -70,19 +72,20 @@ class PRMergeResult(BaseModel):
|
|
|
70
72
|
return [c for c in self.checks if c.pending]
|
|
71
73
|
|
|
72
74
|
|
|
73
|
-
def enable_auto_merge(repo_path: Path, pr_ref: str, config: AutoMergeConfig) -> None:
|
|
75
|
+
def enable_auto_merge(repo_path: Path, pr_ref: str, config: AutoMergeConfig, dest_name: str = "") -> None:
|
|
76
|
+
label = dest_name or pr_ref
|
|
74
77
|
cmd = ["gh", "pr", "merge", "--auto", f"--{config.method}", pr_ref]
|
|
75
78
|
if config.delete_branch:
|
|
76
79
|
cmd.append("--delete-branch")
|
|
77
80
|
result = subprocess.run(cmd, cwd=repo_path, capture_output=True, text=True)
|
|
78
81
|
if result.returncode != 0:
|
|
79
|
-
logger.warning(f"
|
|
82
|
+
logger.warning(f" {label}: auto-merge enable failed: {result.stderr.strip()}")
|
|
80
83
|
else:
|
|
81
|
-
logger.info(f"
|
|
84
|
+
logger.info(f" {label}: enabled auto-merge ({config.method})")
|
|
82
85
|
|
|
83
86
|
|
|
84
87
|
def get_pr_checks(repo_path: Path, pr_ref: str) -> list[CheckRun]:
|
|
85
|
-
cmd = ["gh", "pr", "checks", pr_ref, "--json", "name,state"]
|
|
88
|
+
cmd = ["gh", "pr", "checks", pr_ref, "--json", "name,state,workflow,link"]
|
|
86
89
|
result = subprocess.run(cmd, cwd=repo_path, capture_output=True, text=True)
|
|
87
90
|
if result.returncode != 0:
|
|
88
91
|
logger.warning(f"Failed to get checks for {pr_ref}: {result.stderr.strip()}")
|
|
@@ -103,6 +106,7 @@ def get_pr_url(repo_path: Path, pr_ref: str) -> str:
|
|
|
103
106
|
cmd = ["gh", "pr", "view", pr_ref, "--json", "url", "-q", ".url"]
|
|
104
107
|
result = subprocess.run(cmd, cwd=repo_path, capture_output=True, text=True)
|
|
105
108
|
if result.returncode != 0:
|
|
109
|
+
logger.warning(f"Failed to get PR URL for {pr_ref}: {result.stderr.strip()}")
|
|
106
110
|
return pr_ref
|
|
107
111
|
return result.stdout.strip() or pr_ref
|
|
108
112
|
|
|
@@ -110,21 +114,31 @@ def get_pr_url(repo_path: Path, pr_ref: str) -> str:
|
|
|
110
114
|
def wait_for_merge(repo_path: Path, pr_ref: str, config: AutoMergeConfig, dest_name: str = "") -> PRMergeResult:
|
|
111
115
|
pr_url = get_pr_url(repo_path, pr_ref)
|
|
112
116
|
deadline = time.monotonic() + config.timeout_seconds
|
|
117
|
+
label = dest_name or pr_ref
|
|
118
|
+
poll_count = 0
|
|
113
119
|
|
|
114
120
|
while time.monotonic() < deadline:
|
|
115
121
|
state = get_pr_state(repo_path, pr_ref)
|
|
116
122
|
if state == PRState.MERGED:
|
|
123
|
+
logger.info(f"{label}: merged ({pr_url})")
|
|
117
124
|
return PRMergeResult(dest_name=dest_name, pr_url=pr_url, branch=pr_ref, state=PRState.MERGED)
|
|
118
125
|
if state == PRState.CLOSED:
|
|
119
126
|
checks = get_pr_checks(repo_path, pr_ref)
|
|
127
|
+
logger.warning(f"{label}: PR closed without merging ({pr_url})")
|
|
120
128
|
return PRMergeResult(dest_name=dest_name, pr_url=pr_url, branch=pr_ref, state=PRState.CLOSED, checks=checks)
|
|
129
|
+
poll_count += 1
|
|
130
|
+
elapsed = int(config.timeout_seconds - (deadline - time.monotonic()))
|
|
131
|
+
logger.info(f"{label}: still open (poll #{poll_count}, {elapsed}s elapsed)")
|
|
121
132
|
time.sleep(config.poll_interval_seconds)
|
|
122
133
|
|
|
123
134
|
checks = get_pr_checks(repo_path, pr_ref)
|
|
124
|
-
logger.warning(f"Timeout waiting for {
|
|
135
|
+
logger.warning(f"Timeout waiting for {label} after {config.timeout_seconds}s ({pr_url})")
|
|
125
136
|
return PRMergeResult(dest_name=dest_name, pr_url=pr_url, branch=pr_ref, state=PRState.OPEN, checks=checks)
|
|
126
137
|
|
|
127
138
|
|
|
139
|
+
SEPARATOR_WIDTH = 40
|
|
140
|
+
|
|
141
|
+
|
|
128
142
|
def handle_auto_merge(
|
|
129
143
|
pr_refs: list[PRRef],
|
|
130
144
|
config: AutoMergeConfig,
|
|
@@ -133,22 +147,27 @@ def handle_auto_merge(
|
|
|
133
147
|
if not pr_refs:
|
|
134
148
|
return []
|
|
135
149
|
|
|
150
|
+
line = "─" * SEPARATOR_WIDTH
|
|
151
|
+
logger.info(f"\n{line}")
|
|
152
|
+
logger.info(" Auto-merge")
|
|
153
|
+
logger.info(line)
|
|
154
|
+
|
|
136
155
|
pending_refs: list[PRRef] = []
|
|
137
156
|
for ref in pr_refs:
|
|
138
157
|
state = get_pr_state(ref.repo_path, ref.branch_or_url)
|
|
139
158
|
if state == PRState.MERGED:
|
|
140
|
-
logger.info(f"{ref.dest_name}: already merged")
|
|
159
|
+
logger.info(f" {ref.dest_name}: already merged")
|
|
141
160
|
continue
|
|
142
|
-
enable_auto_merge(ref.repo_path, ref.branch_or_url, config)
|
|
161
|
+
enable_auto_merge(ref.repo_path, ref.branch_or_url, config, dest_name=ref.dest_name)
|
|
143
162
|
pending_refs.append(ref)
|
|
144
163
|
|
|
145
164
|
if no_wait:
|
|
146
|
-
logger.info("--no-wait: skipping merge polling")
|
|
165
|
+
logger.info(" --no-wait: skipping merge polling")
|
|
147
166
|
return []
|
|
148
167
|
|
|
149
168
|
results: list[PRMergeResult] = []
|
|
150
169
|
for ref in pending_refs:
|
|
151
|
-
logger.info(f"Waiting for {ref.dest_name} to merge...")
|
|
170
|
+
logger.info(f" Waiting for {ref.dest_name} to merge...")
|
|
152
171
|
result = wait_for_merge(ref.repo_path, ref.branch_or_url, config, dest_name=ref.dest_name)
|
|
153
172
|
results.append(result)
|
|
154
173
|
|
|
@@ -160,9 +179,10 @@ def _log_summary(results: list[PRMergeResult]) -> None:
|
|
|
160
179
|
if not results:
|
|
161
180
|
return
|
|
162
181
|
max_name = max(len(r.dest_name) for r in results)
|
|
163
|
-
|
|
182
|
+
max_url = max(len(r.pr_url) for r in results)
|
|
183
|
+
header = f"{'Repo':<{max_name}} {'PR':<{max_url}} State Failed Checks"
|
|
164
184
|
logger.info(header)
|
|
165
185
|
logger.info("-" * len(header))
|
|
166
186
|
for r in results:
|
|
167
187
|
failed = ", ".join(c.name for c in r.failed_checks)
|
|
168
|
-
logger.info(f"{r.dest_name:<{max_name}} {r.state:<8} {failed}")
|
|
188
|
+
logger.info(f"{r.dest_name:<{max_name}} {r.pr_url:<{max_url}} {r.state:<8} {failed}")
|
|
@@ -258,8 +258,10 @@ def _sync_destination(
|
|
|
258
258
|
dest_root = resolve_repo_path(dest, src_root, opts.work_dir)
|
|
259
259
|
dest_repo = ensure_repo(dest, dest_root, dry_run=opts.dry_run)
|
|
260
260
|
copy_branch = dest.resolved_copy_branch(config.name)
|
|
261
|
+
_print_dest_header(dest)
|
|
261
262
|
|
|
262
263
|
if _skip_already_synced(dest.name, dest_root, copy_branch, commit_ts, opts, config):
|
|
264
|
+
typer.echo(" (already synced, skipped)", err=True)
|
|
263
265
|
return 0, None
|
|
264
266
|
|
|
265
267
|
if not opts.no_checkout and prompt_utils.prompt_confirm(f"Switch {dest.name} to {copy_branch}?", opts.no_prompt):
|
|
@@ -270,10 +272,10 @@ def _sync_destination(
|
|
|
270
272
|
from_default=opts.checkout_from_default,
|
|
271
273
|
)
|
|
272
274
|
result = _sync_paths(config, dest, src_root, dest_root, opts)
|
|
273
|
-
_print_sync_summary(
|
|
275
|
+
_print_sync_summary(result)
|
|
274
276
|
|
|
275
277
|
if result.total == 0:
|
|
276
|
-
|
|
278
|
+
typer.echo(" No changes", err=True)
|
|
277
279
|
_close_stale_pr(dest_root, copy_branch, opts, config)
|
|
278
280
|
return 0, None
|
|
279
281
|
|
|
@@ -304,14 +306,23 @@ def _sync_destination(
|
|
|
304
306
|
return result.total, pr_ref
|
|
305
307
|
|
|
306
308
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
+
SEPARATOR_WIDTH = 40
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def _print_dest_header(dest: Destination) -> None:
|
|
313
|
+
line = "─" * SEPARATOR_WIDTH
|
|
314
|
+
typer.echo(f"\n{line}", err=True)
|
|
315
|
+
typer.echo(f" {dest.name}", err=True)
|
|
316
|
+
typer.echo(line, err=True)
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def _print_sync_summary(result: SyncResult) -> None:
|
|
309
320
|
if result.content_changes > 0:
|
|
310
321
|
typer.echo(f" [{result.content_changes} files synced]", err=True)
|
|
311
322
|
if result.orphans_deleted > 0:
|
|
312
323
|
typer.echo(f" [-] {result.orphans_deleted} orphans deleted", err=True)
|
|
313
324
|
if result.total > 0:
|
|
314
|
-
typer.echo(f"
|
|
325
|
+
typer.echo(f" {result.total} changes ready.", err=True)
|
|
315
326
|
|
|
316
327
|
|
|
317
328
|
def _sync_paths(
|
|
@@ -596,8 +607,10 @@ def _push_and_pr(
|
|
|
596
607
|
if not prompt_utils.prompt_confirm(f"Push {dest.name} to origin?", opts.no_prompt):
|
|
597
608
|
return None
|
|
598
609
|
|
|
599
|
-
git_ops.push_branch(repo, copy_branch, force=True)
|
|
600
|
-
|
|
610
|
+
if git_ops.push_branch(repo, copy_branch, force=True):
|
|
611
|
+
typer.echo(f" Pushed: {copy_branch} (force)", err=True)
|
|
612
|
+
else:
|
|
613
|
+
typer.echo(f" Skipped push for {copy_branch}: content unchanged on remote", err=True)
|
|
601
614
|
|
|
602
615
|
if opts.no_pr or not prompt_utils.prompt_confirm(f"Create PR for {dest.name}?", opts.no_prompt):
|
|
603
616
|
return None
|
|
@@ -201,9 +201,12 @@ def _create_prs(config: DepConfig, results: list[RepoResult], opts: DepUpdateOpt
|
|
|
201
201
|
continue
|
|
202
202
|
|
|
203
203
|
repo = git_ops.get_repo(result.repo_path)
|
|
204
|
-
git_ops.push_branch(repo, config.pr.branch, force=True)
|
|
205
|
-
|
|
206
204
|
body = _build_pr_body(result.log_content, result.failures)
|
|
205
|
+
|
|
206
|
+
if not git_ops.push_branch(repo, config.pr.branch, force=True):
|
|
207
|
+
git_ops.update_pr_body(result.repo_path, config.pr.branch, body)
|
|
208
|
+
continue
|
|
209
|
+
|
|
207
210
|
pr_url = git_ops.create_or_update_pr(
|
|
208
211
|
result.repo_path,
|
|
209
212
|
config.pr.branch,
|
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
4
|
import os
|
|
5
|
+
import re
|
|
5
6
|
import subprocess
|
|
6
7
|
from contextlib import suppress
|
|
7
8
|
from pathlib import Path
|
|
@@ -10,6 +11,20 @@ from git import GitCommandError, InvalidGitRepositoryError, NoSuchPathError, Rep
|
|
|
10
11
|
|
|
11
12
|
logger = logging.getLogger(__name__)
|
|
12
13
|
|
|
14
|
+
GH_PR_BODY_MAX_CHARS = 64536 # real limit is 65536, but we leave some buffer
|
|
15
|
+
_TRUNCATION_NOTICE = "\n\n... (truncated, output too long for PR body)"
|
|
16
|
+
_CODE_FENCE_RE = re.compile(r"^`{3,}", re.MULTILINE)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _truncate_body(body: str) -> str:
|
|
20
|
+
if len(body) <= GH_PR_BODY_MAX_CHARS:
|
|
21
|
+
return body
|
|
22
|
+
cut = GH_PR_BODY_MAX_CHARS - len(_TRUNCATION_NOTICE)
|
|
23
|
+
truncated = body[:cut]
|
|
24
|
+
fence_count = len(_CODE_FENCE_RE.findall(truncated))
|
|
25
|
+
suffix = "\n```" + _TRUNCATION_NOTICE if fence_count % 2 else _TRUNCATION_NOTICE
|
|
26
|
+
return truncated[: GH_PR_BODY_MAX_CHARS - len(suffix)] + suffix
|
|
27
|
+
|
|
13
28
|
|
|
14
29
|
def _auth_url(url: str) -> str:
|
|
15
30
|
"""Inject GH_TOKEN into HTTPS URL for authentication."""
|
|
@@ -145,10 +160,24 @@ def _ensure_git_user(repo: Repo) -> None:
|
|
|
145
160
|
repo.config_writer().set_value("user", "email", "path-sync[bot]@users.noreply.github.com").release()
|
|
146
161
|
|
|
147
162
|
|
|
148
|
-
def
|
|
163
|
+
def remote_branch_has_same_content(repo: Repo, branch: str) -> bool:
|
|
164
|
+
"""Check if origin/{branch} has identical file content (tree) as local {branch}."""
|
|
165
|
+
with suppress(GitCommandError):
|
|
166
|
+
local_tree = repo.git.rev_parse(f"{branch}^{{tree}}")
|
|
167
|
+
remote_tree = repo.git.rev_parse(f"origin/{branch}^{{tree}}")
|
|
168
|
+
return local_tree == remote_tree
|
|
169
|
+
return False
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def push_branch(repo: Repo, branch: str, force: bool = True) -> bool:
|
|
173
|
+
"""Push branch to origin. Returns False if skipped (content unchanged on remote)."""
|
|
174
|
+
if force and remote_branch_has_same_content(repo, branch):
|
|
175
|
+
logger.info(f"Skipping push for {branch}: content unchanged on remote")
|
|
176
|
+
return False
|
|
149
177
|
logger.info(f"Pushing {branch}" + (" (force)" if force else ""))
|
|
150
178
|
args = ["--force", "-u", "origin", branch] if force else ["-u", "origin", branch]
|
|
151
179
|
repo.git.push(*args)
|
|
180
|
+
return True
|
|
152
181
|
|
|
153
182
|
|
|
154
183
|
def _get_repo_full_name(repo_path: Path) -> str | None:
|
|
@@ -189,6 +218,7 @@ def update_pr_body(repo_path: Path, branch: str, body: str) -> bool:
|
|
|
189
218
|
if not pr_number:
|
|
190
219
|
return False
|
|
191
220
|
|
|
221
|
+
body = _truncate_body(body)
|
|
192
222
|
cmd = [
|
|
193
223
|
"gh",
|
|
194
224
|
"api",
|
|
@@ -202,7 +232,8 @@ def update_pr_body(repo_path: Path, branch: str, body: str) -> bool:
|
|
|
202
232
|
if result.returncode != 0:
|
|
203
233
|
logger.warning(f"Failed to update PR body: {result.stderr}")
|
|
204
234
|
return False
|
|
205
|
-
|
|
235
|
+
pr_url = f"https://github.com/{repo_full}/pull/{pr_number}"
|
|
236
|
+
logger.info(f"Updated PR body: {pr_url}")
|
|
206
237
|
return True
|
|
207
238
|
|
|
208
239
|
|
|
@@ -242,8 +273,9 @@ def create_or_update_pr(
|
|
|
242
273
|
reviewers: list[str] | None = None,
|
|
243
274
|
assignees: list[str] | None = None,
|
|
244
275
|
) -> str:
|
|
276
|
+
body = _truncate_body(body) if body else ""
|
|
245
277
|
cmd = ["gh", "pr", "create", "--head", branch, "--title", title]
|
|
246
|
-
cmd.extend(["--body", body
|
|
278
|
+
cmd.extend(["--body", body])
|
|
247
279
|
if labels:
|
|
248
280
|
cmd.extend(["--label", ",".join(labels)])
|
|
249
281
|
if reviewers:
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[project]
|
|
4
4
|
name = "path-sync"
|
|
5
|
-
version = "0.7.
|
|
5
|
+
version = "0.7.7"
|
|
6
6
|
description = "Sync files from a source repo to multiple destination repos"
|
|
7
7
|
requires-python = ">=3.13"
|
|
8
8
|
license = "MIT"
|
|
@@ -68,12 +68,13 @@ target-version = "py313"
|
|
|
68
68
|
# UP046: use type parameters on functions instead of TypeVar
|
|
69
69
|
# UP047: use type parameters instead of `Generic[T]` subclass
|
|
70
70
|
extend-ignore = ["E501", "UP006", "UP007", "UP035", "UP040", "UP046", "UP047"]
|
|
71
|
-
extend-select = ["Q", "RUF100", "C90", "UP", "I", "T"]
|
|
71
|
+
extend-select = ["Q", "RUF100", "C90", "UP", "I", "T", "FURB"]
|
|
72
72
|
# === OK_EDIT: path-sync ruff ===
|
|
73
73
|
|
|
74
74
|
# === DO_NOT_EDIT: path-sync pytest ===
|
|
75
75
|
[tool.pytest.ini_options]
|
|
76
|
-
addopts = "-s -vv --log-cli-level=INFO"
|
|
76
|
+
addopts = "-s -vv --log-cli-level=INFO -m 'not manual'"
|
|
77
|
+
markers = ["manual: test requires manual env setup (not run in CI)"]
|
|
77
78
|
python_files = ["*_test.py", "test_*.py"]
|
|
78
79
|
asyncio_mode = "auto"
|
|
79
80
|
# === OK_EDIT: path-sync pytest ===
|
|
@@ -84,16 +85,33 @@ omit = ["*_test.py"]
|
|
|
84
85
|
exclude_lines = ["no cov", "if __name__ == .__main__.:"]
|
|
85
86
|
# === OK_EDIT: path-sync coverage ===
|
|
86
87
|
|
|
87
|
-
# === DO_NOT_EDIT: path-sync
|
|
88
|
+
# === DO_NOT_EDIT: path-sync vulture ===
|
|
89
|
+
[tool.vulture]
|
|
90
|
+
min_confidence = 80
|
|
91
|
+
ignore_names = ["mock_*", "Mock*", "*_mock"]
|
|
92
|
+
exclude = [".venv/", "*test.py"]
|
|
93
|
+
# === OK_EDIT: path-sync vulture ===
|
|
94
|
+
|
|
95
|
+
# === DO_NOT_EDIT: path-sync dev-lint ===
|
|
88
96
|
[dependency-groups]
|
|
89
|
-
dev = [
|
|
97
|
+
dev-lint = [
|
|
90
98
|
"pyright>=1.1",
|
|
99
|
+
"ruff>=0.14",
|
|
100
|
+
"vulture>=2.14",
|
|
101
|
+
]
|
|
102
|
+
# === OK_EDIT: path-sync dev-lint ===
|
|
103
|
+
|
|
104
|
+
# === DO_NOT_EDIT: path-sync dev-test ===
|
|
105
|
+
dev-test = [
|
|
91
106
|
"pytest>=9.0",
|
|
92
107
|
"pytest-asyncio>=0.24",
|
|
93
108
|
"pytest-cov>=7.0",
|
|
94
109
|
"pytest-regressions>=2.6",
|
|
95
|
-
"ruff>=0.14",
|
|
96
110
|
]
|
|
111
|
+
# === OK_EDIT: path-sync dev-test ===
|
|
112
|
+
|
|
113
|
+
# === DO_NOT_EDIT: path-sync dev ===
|
|
114
|
+
dev = [{include-group = "dev-lint"}, {include-group = "dev-test"}]
|
|
97
115
|
# === OK_EDIT: path-sync dev ===
|
|
98
116
|
|
|
99
117
|
# === DO_NOT_EDIT: path-sync docs ===
|
|
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
|
|
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
|