gitacross 2.0.0__tar.gz → 2.1.0__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.
Files changed (42) hide show
  1. {gitacross-2.0.0/src/gitacross.egg-info → gitacross-2.1.0}/PKG-INFO +11 -1
  2. {gitacross-2.0.0 → gitacross-2.1.0}/README.md +9 -0
  3. {gitacross-2.0.0 → gitacross-2.1.0}/pyproject.toml +14 -1
  4. {gitacross-2.0.0 → gitacross-2.1.0}/src/gitacross/__init__.py +2 -1
  5. {gitacross-2.0.0 → gitacross-2.1.0}/src/gitacross/__main__.py +0 -1
  6. gitacross-2.1.0/src/gitacross/cli.py +105 -0
  7. {gitacross-2.0.0 → gitacross-2.1.0}/src/gitacross/config.py +8 -1
  8. gitacross-2.1.0/src/gitacross/linter/__init__.py +26 -0
  9. gitacross-2.1.0/src/gitacross/linter/fixer.py +282 -0
  10. gitacross-2.1.0/src/gitacross/linter/keys.py +39 -0
  11. gitacross-2.1.0/src/gitacross/linter/models.py +145 -0
  12. gitacross-2.0.0/src/gitacross/linter.py → gitacross-2.1.0/src/gitacross/linter/validator.py +24 -444
  13. {gitacross-2.0.0 → gitacross-2.1.0}/src/gitacross/main.py +232 -284
  14. {gitacross-2.0.0 → gitacross-2.1.0}/src/gitacross/providers/__init__.py +16 -3
  15. {gitacross-2.0.0 → gitacross-2.1.0}/src/gitacross/providers/base.py +8 -1
  16. {gitacross-2.0.0 → gitacross-2.1.0}/src/gitacross/providers/gitea.py +10 -5
  17. {gitacross-2.0.0 → gitacross-2.1.0}/src/gitacross/providers/github.py +0 -1
  18. {gitacross-2.0.0 → gitacross-2.1.0}/src/gitacross/renderer.py +8 -10
  19. {gitacross-2.0.0 → gitacross-2.1.0}/src/gitacross/source.py +10 -9
  20. {gitacross-2.0.0 → gitacross-2.1.0}/src/gitacross/target.py +6 -6
  21. {gitacross-2.0.0 → gitacross-2.1.0/src/gitacross.egg-info}/PKG-INFO +11 -1
  22. {gitacross-2.0.0 → gitacross-2.1.0}/src/gitacross.egg-info/SOURCES.txt +16 -2
  23. {gitacross-2.0.0 → gitacross-2.1.0}/src/gitacross.egg-info/requires.txt +1 -0
  24. gitacross-2.1.0/tests/test_config.py +488 -0
  25. gitacross-2.1.0/tests/test_git.py +341 -0
  26. gitacross-2.1.0/tests/test_integration.py +1205 -0
  27. gitacross-2.1.0/tests/test_linter.py +1030 -0
  28. gitacross-2.1.0/tests/test_providers.py +643 -0
  29. gitacross-2.1.0/tests/test_renderer.py +648 -0
  30. gitacross-2.1.0/tests/test_source.py +779 -0
  31. gitacross-2.1.0/tests/test_state.py +77 -0
  32. gitacross-2.1.0/tests/test_sync_api.py +1142 -0
  33. gitacross-2.1.0/tests/test_target.py +223 -0
  34. gitacross-2.0.0/tests/test_sync.py +0 -4560
  35. {gitacross-2.0.0 → gitacross-2.1.0}/LICENSE +0 -0
  36. {gitacross-2.0.0 → gitacross-2.1.0}/setup.cfg +0 -0
  37. {gitacross-2.0.0 → gitacross-2.1.0}/src/gitacross/git.py +0 -0
  38. {gitacross-2.0.0 → gitacross-2.1.0}/src/gitacross/retry.py +0 -0
  39. {gitacross-2.0.0 → gitacross-2.1.0}/src/gitacross/state.py +0 -0
  40. {gitacross-2.0.0 → gitacross-2.1.0}/src/gitacross.egg-info/dependency_links.txt +0 -0
  41. {gitacross-2.0.0 → gitacross-2.1.0}/src/gitacross.egg-info/entry_points.txt +0 -0
  42. {gitacross-2.0.0 → gitacross-2.1.0}/src/gitacross.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gitacross
3
- Version: 2.0.0
3
+ Version: 2.1.0
4
4
  Summary: Mirror releases and git commits across platforms (Gitea, GitHub, local) with transform pipelines.
5
5
  Author: Matthew Deik
6
6
  License-Expression: MIT
@@ -22,6 +22,7 @@ License-File: LICENSE
22
22
  Requires-Dist: PyYAML<7.0,>=6.0
23
23
  Provides-Extra: dev
24
24
  Requires-Dist: pytest>=7.0; extra == "dev"
25
+ Requires-Dist: pytest-cov>=4.0; extra == "dev"
25
26
  Requires-Dist: build; extra == "dev"
26
27
  Requires-Dist: twine; extra == "dev"
27
28
  Dynamic: license-file
@@ -77,6 +78,15 @@ For development, install the local checkout:
77
78
  pip install -e .
78
79
  ```
79
80
 
81
+ ### Tests
82
+
83
+ ```bash
84
+ pip install -e .[dev] # pytest + pytest-cov
85
+ pytest # run the test suite
86
+ pytest --cov=gitacross # run with a coverage floor (90% line + branch)
87
+ ruff check src tests # lint
88
+ ```
89
+
80
90
  ### Config
81
91
 
82
92
  GitAcross reads a YAML file listing the mirrors you want. Each entry in the `projects` list is a **project**: it has a `source` (where releases come from) and a `target` (where they go).
@@ -49,6 +49,15 @@ For development, install the local checkout:
49
49
  pip install -e .
50
50
  ```
51
51
 
52
+ ### Tests
53
+
54
+ ```bash
55
+ pip install -e .[dev] # pytest + pytest-cov
56
+ pytest # run the test suite
57
+ pytest --cov=gitacross # run with a coverage floor (90% line + branch)
58
+ ruff check src tests # lint
59
+ ```
60
+
52
61
  ### Config
53
62
 
54
63
  GitAcross reads a YAML file listing the mirrors you want. Each entry in the `projects` list is a **project**: it has a `source` (where releases come from) and a `target` (where they go).
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "gitacross"
7
- version = "2.0.0"
7
+ version = "2.1.0"
8
8
  description = "Mirror releases and git commits across platforms (Gitea, GitHub, local) with transform pipelines."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
@@ -33,6 +33,7 @@ dependencies = [
33
33
  [project.optional-dependencies]
34
34
  dev = [
35
35
  "pytest>=7.0",
36
+ "pytest-cov>=4.0",
36
37
  "build",
37
38
  "twine",
38
39
  ]
@@ -46,3 +47,15 @@ where = ["src"]
46
47
  [tool.pytest.ini_options]
47
48
  pythonpath = ["src"]
48
49
  testpaths = ["tests"]
50
+
51
+ [tool.coverage.run]
52
+ source = ["gitacross"]
53
+ # Keep branch measurement on for the report, but don't gate on it —
54
+ # a 100% branch gate incentivizes line-touching tests over behavior tests.
55
+ branch = true
56
+
57
+ [tool.coverage.report]
58
+ # Enforcement floor with headroom: catches regressions without demanding
59
+ # 100% (which encourages tests-for-the-sake-of-it on defensive branches).
60
+ fail_under = 90
61
+ show_missing = true
@@ -28,6 +28,7 @@ from importlib.metadata import PackageNotFoundError
28
28
  from importlib.metadata import version as _installed_version
29
29
  from pathlib import Path
30
30
 
31
+ from .cli import main # the CLI lives in cli.py; main.py re-exports it too
31
32
  from .config import Config, ProjectConfig
32
33
  from .linter import (
33
34
  ConfigFixer,
@@ -40,7 +41,7 @@ from .linter import (
40
41
  fix_config,
41
42
  lint_config,
42
43
  )
43
- from .main import main, run, sync_project
44
+ from .main import run, sync_project
44
45
 
45
46
 
46
47
  def _package_version() -> str:
@@ -1,7 +1,6 @@
1
1
  """Entry point when invoked with `python -m gitacross`."""
2
2
  from __future__ import annotations
3
3
 
4
-
5
4
  from .main import main
6
5
 
7
6
  if __name__ == "__main__":
@@ -0,0 +1,105 @@
1
+ """Command-line interface for GitAcross.
2
+
3
+ The sync logic lives in :mod:`gitacross.main`; this module only parses
4
+ ``argv`` and maps results to process exit codes.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import argparse
9
+ import logging
10
+ import sys
11
+ from pathlib import Path
12
+ from typing import cast
13
+
14
+
15
+ def _setup_logging(verbose):
16
+ level = logging.DEBUG if verbose else logging.INFO
17
+ logging.basicConfig(
18
+ level=level,
19
+ format="%(asctime)s [%(levelname)s] %(message)s",
20
+ datefmt="%H:%M:%S",
21
+ )
22
+
23
+
24
+ def main():
25
+ from .main import DEFAULT_WORK_DIR, run
26
+
27
+ parser = argparse.ArgumentParser(description="Sync releases from source to target")
28
+ _ = parser.add_argument("--config", required=True, help="Path to config.yml")
29
+ _ = parser.add_argument("--project", help="Sync only this project (by name)")
30
+ _ = parser.add_argument(
31
+ "--dry-run", action="store_true", help="Print changes without pushing"
32
+ )
33
+ _ = parser.add_argument(
34
+ "--reset",
35
+ action="store_true",
36
+ help="Clear state and cache before running (fresh start)",
37
+ )
38
+ _ = parser.add_argument(
39
+ "--lint",
40
+ action="store_true",
41
+ help="Lint config file for YAML errors, invalid settings, and redundant options",
42
+ )
43
+ _ = parser.add_argument(
44
+ "--fix",
45
+ action="store_true",
46
+ help="Automatically fix misplaced keys and remove redundant options in config file",
47
+ )
48
+ _ = parser.add_argument(
49
+ "--workdir",
50
+ default=DEFAULT_WORK_DIR,
51
+ help="Directory for state.yml and cache/ (default: .gitsync)",
52
+ )
53
+ _ = parser.add_argument("-v", "--verbose", action="store_true", help="Debug logging")
54
+ args = parser.parse_args()
55
+
56
+ # Convert argparse's Any values into concrete types (cast is the escape hatch).
57
+ verbose = cast(bool, args.verbose)
58
+ config_path = Path(cast(str, args.config))
59
+ fix_flag = cast(bool, args.fix)
60
+ lint_flag = cast(bool, args.lint)
61
+ project_filter = None if args.project is None else cast(str, args.project)
62
+ dry_run_flag = cast(bool, args.dry_run)
63
+ reset_flag = cast(bool, args.reset)
64
+ workdir = cast(str, args.workdir)
65
+
66
+ _setup_logging(verbose)
67
+
68
+ if fix_flag:
69
+ if not config_path.exists():
70
+ sys.exit(f"Config file not found: {config_path}")
71
+ from .linter import fix_config, lint_config
72
+
73
+ fix_report = fix_config(
74
+ config_path, write_back=not dry_run_flag, dry_run=dry_run_flag
75
+ )
76
+ if not fix_report.is_valid:
77
+ sys.exit(1)
78
+ if lint_flag:
79
+ print()
80
+ lint_report = lint_config(config_path)
81
+ sys.exit(0 if lint_report.is_valid else 1)
82
+ sys.exit(0)
83
+
84
+ if lint_flag:
85
+ if not config_path.exists():
86
+ sys.exit(f"Config file not found: {config_path}")
87
+ from .linter import lint_config
88
+
89
+ report = lint_config(config_path)
90
+ sys.exit(0 if report.is_valid else 1)
91
+
92
+ try:
93
+ results = run(
94
+ config_path,
95
+ project=project_filter,
96
+ dry_run=dry_run_flag,
97
+ reset=reset_flag,
98
+ work_dir=workdir,
99
+ )
100
+ except (FileNotFoundError, ValueError) as exc:
101
+ sys.exit(str(exc))
102
+
103
+ if any(not r["synced"] for r in results):
104
+ sys.exit(1)
105
+
@@ -7,6 +7,8 @@ from typing import final
7
7
 
8
8
  import yaml
9
9
 
10
+ from .providers import REMOTE_TYPES
11
+
10
12
  logger = logging.getLogger(__name__)
11
13
 
12
14
  VALID_PROJECT_KEYS = {
@@ -164,7 +166,12 @@ class _EndpointConfig:
164
166
 
165
167
  @property
166
168
  def is_remote(self):
167
- return self.type in ("gitea", "github")
169
+ return self.type in REMOTE_TYPES
170
+
171
+ @property
172
+ def repo_slug(self):
173
+ """Filesystem-safe repo identifier (``owner/name`` → ``owner_name``)."""
174
+ return self.repo.replace("/", "_")
168
175
 
169
176
  @property
170
177
  def owner(self):
@@ -0,0 +1,26 @@
1
+ """Config linter and fixer for GitAcross.
2
+
3
+ ``lint_config`` validates a config; ``fix_config`` rewrites it in place to
4
+ remove redundant and misplaced options.
5
+
6
+ Implementation lives in three modules:
7
+
8
+ * :mod:`gitacross.linter.models` — report/issue data types and YAML dump helper
9
+ * :mod:`gitacross.linter.validator` — :class:`ConfigLinter` (the checks)
10
+ * :mod:`gitacross.linter.fixer` — :class:`ConfigFixer` (the rewrites)
11
+ """
12
+ from .fixer import ConfigFixer, fix_config
13
+ from .models import FixIssue, FixReport, LintIssue, LintReport, LintSeverity
14
+ from .validator import ConfigLinter, lint_config
15
+
16
+ __all__ = [
17
+ "ConfigFixer",
18
+ "ConfigLinter",
19
+ "FixIssue",
20
+ "FixReport",
21
+ "LintIssue",
22
+ "LintReport",
23
+ "LintSeverity",
24
+ "fix_config",
25
+ "lint_config",
26
+ ]
@@ -0,0 +1,282 @@
1
+ """Config fixer — rewrites configs to remove redundant/misplaced options.
2
+ """
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ import yaml
9
+
10
+ from ..config import KNOWN_SOURCE_KEYS, VALID_PROJECT_KEYS
11
+ from .models import FixIssue, FixReport, _CleanDumper
12
+
13
+
14
+ class ConfigFixer:
15
+ def __init__(self):
16
+ self.fixes: list[FixIssue] = []
17
+
18
+ def __repr__(self):
19
+ return f"ConfigFixer(fixes={len(self.fixes)})"
20
+
21
+ def _add(self, message: str, project: str | None = None):
22
+ self.fixes.append(FixIssue(message=message, project=project))
23
+
24
+ def fix_yaml_string(self, content: str) -> FixReport:
25
+ # Fresh accumulator — reusing an instance must not leak prior fixes
26
+ self.fixes = []
27
+ try:
28
+ data = yaml.safe_load(content)
29
+ except yaml.YAMLError as e:
30
+ return FixReport(fixes=[], content=content, is_valid=False, error=str(e))
31
+
32
+ if data is None:
33
+ return FixReport(fixes=[], content=content, is_valid=True)
34
+
35
+ is_dict_wrapper = isinstance(data, dict) and "projects" in data
36
+ projects_list = data["projects"] if is_dict_wrapper else data
37
+
38
+ if not isinstance(projects_list, list):
39
+ return FixReport(
40
+ fixes=[],
41
+ content=content,
42
+ is_valid=False,
43
+ error="Top-level YAML structure must be a list of projects or a map with a 'projects' list.",
44
+ )
45
+
46
+ for _idx, p in enumerate(projects_list):
47
+ if isinstance(p, dict):
48
+ self._fix_project(p)
49
+
50
+ fixed_yaml = yaml.dump(
51
+ data,
52
+ Dumper=_CleanDumper,
53
+ sort_keys=False,
54
+ default_flow_style=False,
55
+ )
56
+ return FixReport(fixes=self.fixes, content=fixed_yaml, is_valid=True)
57
+
58
+ def _fix_project(self, p: dict[str, Any]):
59
+ p_name = p.get("name") or "<unnamed>"
60
+
61
+ # 1. Misplaced endpoint keys at project level -> move to source
62
+ for k in list(p.keys()):
63
+ if k in KNOWN_SOURCE_KEYS:
64
+ val = p.pop(k)
65
+ if "source" not in p or not isinstance(p["source"], dict):
66
+ p["source"] = {}
67
+ if k not in p["source"]:
68
+ p["source"][k] = val
69
+ self._add(
70
+ f"Moved '{k}: {val}' from project level into 'source:'",
71
+ project=p_name,
72
+ )
73
+ else:
74
+ self._add(
75
+ f"Removed duplicate project-level '{k}' (already present under source)",
76
+ project=p_name,
77
+ )
78
+
79
+ # 2. Misplaced project-level keys inside source -> move to project level
80
+ if isinstance(p.get("source"), dict):
81
+ for k in list(p["source"].keys()):
82
+ if k in VALID_PROJECT_KEYS and k != "type":
83
+ val = p["source"].pop(k)
84
+ if k not in p:
85
+ p[k] = val
86
+ self._add(
87
+ f"Moved '{k}' from 'source:' to project level",
88
+ project=p_name,
89
+ )
90
+ else:
91
+ self._add(
92
+ f"Removed duplicate '{k}' from 'source:' (already present at project level)",
93
+ project=p_name,
94
+ )
95
+
96
+ # 3. Misplaced project keys in target -> move to project level
97
+ # and misplaced source keys in target -> move to source
98
+ if isinstance(p.get("target"), dict):
99
+ for k in list(p["target"].keys()):
100
+ if k in VALID_PROJECT_KEYS and k != "type":
101
+ val = p["target"].pop(k)
102
+ if k not in p:
103
+ p[k] = val
104
+ self._add(
105
+ f"Moved '{k}' from 'target:' to project level",
106
+ project=p_name,
107
+ )
108
+ else:
109
+ self._add(
110
+ f"Removed duplicate '{k}' from 'target:' (already present at project level)",
111
+ project=p_name,
112
+ )
113
+ elif k in KNOWN_SOURCE_KEYS:
114
+ val = p["target"].pop(k)
115
+ if "source" not in p or not isinstance(p["source"], dict):
116
+ p["source"] = {}
117
+ if k not in p["source"]:
118
+ p["source"][k] = val
119
+ self._add(
120
+ f"Moved '{k}: {val}' from 'target:' into 'source:'",
121
+ project=p_name,
122
+ )
123
+ else:
124
+ self._add(
125
+ f"Removed duplicate '{k}' from 'target:' (already present under source)",
126
+ project=p_name,
127
+ )
128
+
129
+ # 4. Redundant project-level options
130
+ if p.get("enabled") is True:
131
+ del p["enabled"]
132
+ self._add("Removed redundant 'enabled: true'", project=p_name)
133
+
134
+ for key in ("preserve_description",):
135
+ if p.get(key) is True:
136
+ del p[key]
137
+ self._add(f"Removed redundant '{key}: true'", project=p_name)
138
+
139
+ for key in ("sync_assets",):
140
+ if p.get(key) is False:
141
+ del p[key]
142
+ self._add(f"Removed redundant '{key}: false'", project=p_name)
143
+
144
+ if p.get("stream_assets") is False:
145
+ del p["stream_assets"]
146
+ self._add("Removed redundant 'stream_assets: false'", project=p_name)
147
+
148
+ # 3. Source endpoint fixes
149
+ if isinstance(p.get("source"), dict):
150
+ src = p["source"]
151
+ if src.get("mode") == "release":
152
+ del src["mode"]
153
+ self._add(
154
+ "Removed redundant 'source.mode: release'", project=p_name
155
+ )
156
+ if src.get("include_prereleases") is False:
157
+ del src["include_prereleases"]
158
+ self._add(
159
+ "Removed redundant 'source.include_prereleases: false'",
160
+ project=p_name,
161
+ )
162
+ if src.get("include_drafts") is False:
163
+ del src["include_drafts"]
164
+ self._add(
165
+ "Removed redundant 'source.include_drafts: false'",
166
+ project=p_name,
167
+ )
168
+ if src.get("sync_from") == "":
169
+ del src["sync_from"]
170
+ self._add(
171
+ "Removed redundant 'source.sync_from: \"\"'",
172
+ project=p_name,
173
+ )
174
+ if src.get("tag_pattern") == "*":
175
+ del src["tag_pattern"]
176
+ self._add(
177
+ "Removed redundant 'source.tag_pattern: \"*\"'",
178
+ project=p_name,
179
+ )
180
+
181
+ # 4. Target endpoint fixes
182
+ if isinstance(p.get("target"), dict):
183
+ tgt = p["target"]
184
+ if tgt.get("branch") == "main":
185
+ del tgt["branch"]
186
+ self._add(
187
+ "Removed redundant 'target.branch: main'", project=p_name
188
+ )
189
+
190
+ # 5. Retry fixes
191
+ if isinstance(p.get("retry"), dict):
192
+ ret = p["retry"]
193
+ if ret.get("max_attempts") == 3:
194
+ del ret["max_attempts"]
195
+ if ret.get("backoff_seconds") == 2:
196
+ del ret["backoff_seconds"]
197
+ if len(ret) == 0:
198
+ del p["retry"]
199
+ self._add(
200
+ "Removed default 'retry' configuration block",
201
+ project=p_name,
202
+ )
203
+ else:
204
+ self._add(
205
+ "Removed redundant retry defaults", project=p_name
206
+ )
207
+
208
+ # 6. Renderer fixes
209
+ if isinstance(p.get("renderer"), dict):
210
+ ren = p["renderer"]
211
+ if isinstance(ren.get("operations"), list):
212
+ for op in ren["operations"]:
213
+ if not isinstance(op, dict):
214
+ continue
215
+ for op_type, items in op.items():
216
+ if isinstance(items, list):
217
+ for item in items:
218
+ if not isinstance(item, dict):
219
+ continue
220
+ if item.get("pattern") == "literal":
221
+ del item["pattern"]
222
+ self._add(
223
+ f"Removed redundant 'pattern: literal' in {op_type} operation",
224
+ project=p_name,
225
+ )
226
+ if op_type == "replace":
227
+ if item.get("case_sensitive") is True:
228
+ del item["case_sensitive"]
229
+ self._add(
230
+ "Removed redundant 'case_sensitive: true' in replace operation",
231
+ project=p_name,
232
+ )
233
+ if item.get("match_case") is False:
234
+ del item["match_case"]
235
+ self._add(
236
+ "Removed redundant 'match_case: false' in replace operation",
237
+ project=p_name,
238
+ )
239
+ if op_type == "validate" and item.get(
240
+ "case_sensitive"
241
+ ) is True:
242
+ del item["case_sensitive"]
243
+ self._add(
244
+ "Removed redundant 'case_sensitive: true' in validate operation",
245
+ project=p_name,
246
+ )
247
+
248
+
249
+
250
+ def fix_config(
251
+ config: str | Path,
252
+ write_back: bool = True,
253
+ print_output: bool = True,
254
+ dry_run: bool = False,
255
+ ) -> FixReport:
256
+ """Fix misplaced and redundant options in config file.
257
+
258
+ Path-only: unlike :func:`lint_config`, this writes the fixed YAML back to
259
+ the file (when *write_back* is true), so it requires a real file path.
260
+ Pass ``dry_run=True`` to preview the fixes without writing anything.
261
+ """
262
+ path = Path(config)
263
+ if not path.exists():
264
+ report = FixReport(
265
+ fixes=[], content="", is_valid=False, error=f"File not found: {path}"
266
+ )
267
+ if print_output:
268
+ print(report.format_text())
269
+ return report
270
+
271
+ content = path.read_text(encoding="utf-8")
272
+ fixer = ConfigFixer()
273
+ report = fixer.fix_yaml_string(content)
274
+ if report.is_valid and write_back and not dry_run and report.fixes:
275
+ _ = path.write_text(report.content, encoding="utf-8")
276
+ if print_output:
277
+ if dry_run and report.fixes:
278
+ print(f"[DRY-RUN] Would apply {len(report.fixes)} fix(es):")
279
+ print(report.format_text())
280
+ return report
281
+
282
+
@@ -0,0 +1,39 @@
1
+ """Valid key sets for source/target endpoints.
2
+
3
+ Separate from the validator so the fixer never pulls in validation code.
4
+ """
5
+ from __future__ import annotations
6
+
7
+ _VALID_SOURCE_KEYS_REMOTE = {
8
+ "type",
9
+ "repo",
10
+ "api",
11
+ "token",
12
+ "mode",
13
+ "branch",
14
+ "include_prereleases",
15
+ "include_drafts",
16
+ "sync_from",
17
+ }
18
+
19
+ _VALID_SOURCE_KEYS_LOCAL = {
20
+ "type",
21
+ "path",
22
+ "tag_pattern",
23
+ "sync_from",
24
+ }
25
+
26
+ _VALID_TARGET_KEYS_REMOTE = {
27
+ "type",
28
+ "repo",
29
+ "api",
30
+ "token",
31
+ "branch",
32
+ }
33
+
34
+ _VALID_TARGET_KEYS_LOCAL = {
35
+ "type",
36
+ "path",
37
+ "branch",
38
+ }
39
+