deploynator 1.3.1__tar.gz → 1.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.
Files changed (42) hide show
  1. {deploynator-1.3.1 → deploynator-1.3.4}/PKG-INFO +1 -1
  2. {deploynator-1.3.1 → deploynator-1.3.4}/deploynator/__init__.py +1 -1
  3. {deploynator-1.3.1 → deploynator-1.3.4}/deploynator/__main__.py +5 -0
  4. {deploynator-1.3.1 → deploynator-1.3.4}/deploynator/_app.py +32 -1
  5. {deploynator-1.3.1 → deploynator-1.3.4}/deploynator/_deploy.py +6 -4
  6. {deploynator-1.3.1 → deploynator-1.3.4}/deploynator/_pypi.py +13 -3
  7. {deploynator-1.3.1 → deploynator-1.3.4}/deploynator/_toml.py +57 -10
  8. {deploynator-1.3.1 → deploynator-1.3.4}/tests/test_toml.py +55 -0
  9. {deploynator-1.3.1 → deploynator-1.3.4}/.gitignore +0 -0
  10. {deploynator-1.3.1 → deploynator-1.3.4}/LICENSE +0 -0
  11. {deploynator-1.3.1 → deploynator-1.3.4}/README.md +0 -0
  12. {deploynator-1.3.1 → deploynator-1.3.4}/deploynator/_changelog.py +0 -0
  13. {deploynator-1.3.1 → deploynator-1.3.4}/deploynator/_config.py +0 -0
  14. {deploynator-1.3.1 → deploynator-1.3.4}/deploynator/_docs.py +0 -0
  15. {deploynator-1.3.1 → deploynator-1.3.4}/deploynator/_git.py +0 -0
  16. {deploynator-1.3.1 → deploynator-1.3.4}/deploynator/_mcp_server.py +0 -0
  17. {deploynator-1.3.1 → deploynator-1.3.4}/deploynator/_models.py +0 -0
  18. {deploynator-1.3.1 → deploynator-1.3.4}/deploynator/_platforms.py +0 -0
  19. {deploynator-1.3.1 → deploynator-1.3.4}/deploynator/_readme.py +0 -0
  20. {deploynator-1.3.1 → deploynator-1.3.4}/deploynator/_runner.py +0 -0
  21. {deploynator-1.3.1 → deploynator-1.3.4}/deploynator/_semver.py +0 -0
  22. {deploynator-1.3.1 → deploynator-1.3.4}/example.deploynator.toml +0 -0
  23. {deploynator-1.3.1 → deploynator-1.3.4}/projects/deploynator.yaml +0 -0
  24. {deploynator-1.3.1 → deploynator-1.3.4}/projects/lazyown.yaml +0 -0
  25. {deploynator-1.3.1 → deploynator-1.3.4}/projects/template.yaml +0 -0
  26. {deploynator-1.3.1 → deploynator-1.3.4}/pyproject.toml +0 -0
  27. {deploynator-1.3.1 → deploynator-1.3.4}/specs/deploynator_toml_pipeline.spec.md +0 -0
  28. {deploynator-1.3.1 → deploynator-1.3.4}/tests/__init__.py +0 -0
  29. {deploynator-1.3.1 → deploynator-1.3.4}/tests/bdd_test_deploy.py +0 -0
  30. {deploynator-1.3.1 → deploynator-1.3.4}/tests/test_changelog.py +0 -0
  31. {deploynator-1.3.1 → deploynator-1.3.4}/tests/test_cli_mcp.py +0 -0
  32. {deploynator-1.3.1 → deploynator-1.3.4}/tests/test_config.py +0 -0
  33. {deploynator-1.3.1 → deploynator-1.3.4}/tests/test_deploy.py +0 -0
  34. {deploynator-1.3.1 → deploynator-1.3.4}/tests/test_init.py +0 -0
  35. {deploynator-1.3.1 → deploynator-1.3.4}/tests/test_install_example.py +0 -0
  36. {deploynator-1.3.1 → deploynator-1.3.4}/tests/test_models.py +0 -0
  37. {deploynator-1.3.1 → deploynator-1.3.4}/tests/test_path_deploy.py +0 -0
  38. {deploynator-1.3.1 → deploynator-1.3.4}/tests/test_pipeline.py +0 -0
  39. {deploynator-1.3.1 → deploynator-1.3.4}/tests/test_pypi.py +0 -0
  40. {deploynator-1.3.1 → deploynator-1.3.4}/tests/test_readme_footer.py +0 -0
  41. {deploynator-1.3.1 → deploynator-1.3.4}/tests/test_runner.py +0 -0
  42. {deploynator-1.3.1 → deploynator-1.3.4}/tests/test_semver.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: deploynator
3
- Version: 1.3.1
3
+ Version: 1.3.4
4
4
  Summary: Multi-platform deployer for GitHub, GitLab, and Bitbucket
5
5
  Project-URL: Homepage, https://github.com/grisuno/deploynator
6
6
  Project-URL: Repository, https://github.com/grisuno/deploynator
@@ -1,4 +1,4 @@
1
1
  from deploynator._app import DeploynatorApp
2
2
 
3
- __version__ = "1.3.1"
3
+ __version__ = "1.3.4"
4
4
  __all__ = ["DeploynatorApp"]
@@ -28,6 +28,10 @@ def build_parser() -> argparse.ArgumentParser:
28
28
  "--workdir", type=str, default="",
29
29
  help="Working directory (default: current dir)",
30
30
  )
31
+ parser.add_argument(
32
+ "--verbose", "-v", action="store_true",
33
+ help="Verbose output (twine --verbose, full command + untruncated errors)",
34
+ )
31
35
 
32
36
  subparsers = parser.add_subparsers(dest="command", help="Available commands")
33
37
 
@@ -199,6 +203,7 @@ def main(argv: list[str] | None = None) -> NoReturn | None:
199
203
  no_test=getattr(args, "no_test", False),
200
204
  no_docs=getattr(args, "no_docs", False),
201
205
  no_pypi=getattr(args, "no_pypi", False),
206
+ verbose=getattr(args, "verbose", False),
202
207
  )
203
208
  sys.exit(0 if all(r.success for r in results) else 1)
204
209
 
@@ -128,6 +128,8 @@ ensure_pkg() {
128
128
 
129
129
  ensure_pkg deploynator deploynator "git+https://github.com/grisuno/deploynator.git"
130
130
  ensure_pkg readmenator readmenator readmenator
131
+ ensure_pkg build build build
132
+ ensure_pkg twine twine twine
131
133
 
132
134
  if ! python3 -c "import deploynator" 2>/dev/null; then
133
135
  echo "[!] deploynator not available and install failed (offline?)." >&2
@@ -195,8 +197,35 @@ version:
195
197
 
196
198
  toml_path = os.path.join(base, "deploynator.toml")
197
199
  if not os.path.isfile(toml_path):
200
+ from deploynator._toml import detect_test_commands, _has_python_tests
201
+ inferred: list[str] | None = None
202
+ try:
203
+ detected = detect_test_commands(base)
204
+ if detected is not None and not (
205
+ detected != ["make test-host"]
206
+ and detected[0].startswith("make")
207
+ and _has_python_tests(base)
208
+ ):
209
+ inferred = detected
210
+ except ValueError:
211
+ inferred = None
212
+ if inferred is not None:
213
+ commands_repr = "[" + ", ".join(f'"{c}"' for c in inferred) + "]"
214
+ timeout = 900
215
+ else:
216
+ commands_repr = '["python -m pytest tests/ -q"]'
217
+ timeout = 300
218
+ content = self.DEPLOY_TOML_TEMPLATE.format(name=project_name)
219
+ content = content.replace(
220
+ 'commands = ["python -m pytest tests/ -q"]',
221
+ f"commands = {commands_repr}",
222
+ ).replace(
223
+ "timeout_s = 300",
224
+ f"timeout_s = {timeout}",
225
+ 1,
226
+ )
198
227
  with open(toml_path, "w") as f:
199
- f.write(self.DEPLOY_TOML_TEMPLATE.format(name=project_name))
228
+ f.write(content)
200
229
  created.append(toml_path)
201
230
 
202
231
  script_path = os.path.join(base, "deploy.sh")
@@ -327,6 +356,7 @@ version:
327
356
  no_test: bool = False,
328
357
  no_docs: bool = False,
329
358
  no_pypi: bool = False,
359
+ verbose: bool = False,
330
360
  ) -> list[DeployResult]:
331
361
  base = os.getcwd()
332
362
  ctype = CommitType.from_string(commit_type)
@@ -395,6 +425,7 @@ version:
395
425
  run_docs=not no_docs,
396
426
  force_bump=bump or None,
397
427
  run_pypi=not no_pypi,
428
+ verbose=verbose,
398
429
  )
399
430
  results.append(result)
400
431
  self._print_result(result)
@@ -107,6 +107,7 @@ class DeployOrchestrator:
107
107
  run_docs: bool = True,
108
108
  force_bump: Optional[str] = None,
109
109
  run_pypi: bool = True,
110
+ verbose: bool = False,
110
111
  ) -> DeployResult:
111
112
  repo_path = self._resolve_project_path(project)
112
113
 
@@ -334,7 +335,8 @@ class DeployOrchestrator:
334
335
  print("[!] PyPI packaging detected but no credentials "
335
336
  "(TWINE_PASSWORD/PYPI_TOKEN/~/.pypirc). Skipping upload.")
336
337
  else:
337
- ok, msg = self._pypi.run(repo_path, toml_cfg, new_version_str, token=token)
338
+ ok, msg = self._pypi.run(repo_path, toml_cfg, new_version_str, token=token,
339
+ verbose=verbose)
338
340
  print(f"[{'OK' if ok else 'FAIL'}] {msg}")
339
341
  if not ok:
340
342
  return DeployResult(
@@ -343,10 +345,10 @@ class DeployOrchestrator:
343
345
  success=False,
344
346
  platforms_pushed=platforms_pushed,
345
347
  releases_created=releases_created,
346
- error=msg[:2000],
348
+ error=msg if verbose else msg[:2000],
347
349
  )
348
350
  else:
349
- ok, msg = self._pypi.run(repo_path, toml_cfg, new_version_str)
351
+ ok, msg = self._pypi.run(repo_path, toml_cfg, new_version_str, verbose=verbose)
350
352
  print(f"[{'OK' if ok else 'FAIL'}] {msg}")
351
353
  if not ok:
352
354
  return DeployResult(
@@ -355,7 +357,7 @@ class DeployOrchestrator:
355
357
  success=False,
356
358
  platforms_pushed=platforms_pushed,
357
359
  releases_created=releases_created,
358
- error=msg[:2000],
360
+ error=msg if verbose else msg[:2000],
359
361
  )
360
362
 
361
363
  return DeployResult(
@@ -105,7 +105,7 @@ class PyPIRunner:
105
105
  shutil.rmtree(target)
106
106
 
107
107
  def run(self, project_path: str, toml: TomlConfig, version: str,
108
- token: str | None = None) -> tuple[bool, str]:
108
+ token: str | None = None, verbose: bool = False) -> tuple[bool, str]:
109
109
  if not self.is_packaging_project(project_path):
110
110
  return True, "PyPI skipped: no packaging files"
111
111
  try:
@@ -120,7 +120,10 @@ class PyPIRunner:
120
120
  timeout=toml.pypi_timeout_s,
121
121
  )
122
122
  if build.returncode != 0:
123
- err = (build.stderr or build.stdout or "").strip()[-2000:]
123
+ raw = (build.stderr or build.stdout or "").strip()
124
+ if verbose and raw:
125
+ print(raw)
126
+ err = raw if verbose else raw[-2000:]
124
127
  hint = "" if "build" not in err.lower() else " (pip install build)"
125
128
  return False, f"PyPI build failed for {version}{hint}: {err}"
126
129
  dist_dir = os.path.join(project_path, "dist")
@@ -128,11 +131,15 @@ class PyPIRunner:
128
131
  if not artifacts:
129
132
  return False, f"PyPI build produced no artifacts for {version}"
130
133
  cmd = [sys.executable, "-m", "twine", "upload", "--non-interactive"]
134
+ if verbose:
135
+ cmd += ["--verbose"]
131
136
  if toml.pypi_repo == "testpypi":
132
137
  cmd += ["--repository", "testpypi"]
133
138
  elif toml.pypi_repo not in ("pypi", "testpypi"):
134
139
  cmd += ["--repository-url", toml.pypi_repo]
135
140
  cmd += [os.path.join("dist", a) for a in artifacts]
141
+ if verbose:
142
+ print(f"[...] {subprocess.list2cmdline(cmd)} (cwd={project_path})")
136
143
  env = dict(os.environ)
137
144
  token = token or os.environ.get("PYPI_TOKEN")
138
145
  if token and not os.environ.get("TWINE_PASSWORD"):
@@ -150,6 +157,9 @@ class PyPIRunner:
150
157
  except subprocess.TimeoutExpired:
151
158
  return False, f"PyPI upload timed out after {toml.pypi_timeout_s}s"
152
159
  if upload.returncode != 0:
153
- err = (upload.stderr or upload.stdout or "").strip()[-2000:]
160
+ raw = (upload.stderr or upload.stdout or "").strip()
161
+ if verbose and raw:
162
+ print(raw)
163
+ err = raw if verbose else raw[-2000:]
154
164
  return False, f"PyPI upload failed for {version}: {err}"
155
165
  return True, f"PyPI uploaded {version}: {', '.join(artifacts)}"
@@ -71,14 +71,42 @@ def _has_python_tests(project_path: str) -> bool:
71
71
  return True
72
72
  except OSError:
73
73
  return False
74
+
75
+
76
+ def looks_like_packaging_project(project_path: str) -> bool:
77
+ """Inferencia runtime (multi-proyecto): ¿es un paquete PyPI?
78
+
79
+ Espejo liviano de PyPIRunner.is_packaging_project sin importar
80
+ _pypi (evita ciclo). El gate final sigue en PyPIRunner al
81
+ desplegar; aquí solo se usa para el default de pypi_enabled.
82
+ """
83
+ pyproject = os.path.join(project_path, "pyproject.toml")
84
+ if os.path.isfile(pyproject) and not os.path.islink(pyproject):
85
+ try:
86
+ import tomllib
87
+ with open(pyproject, "rb") as f:
88
+ data = tomllib.load(f)
89
+ project = data.get("project", {})
90
+ if isinstance(project, dict) and project.get("name"):
91
+ return True
92
+ except Exception:
93
+ pass
94
+ for candidate in ("setup.py", "setup.cfg"):
95
+ candidate_path = os.path.join(project_path, candidate)
96
+ if os.path.isfile(candidate_path) and not os.path.islink(candidate_path):
97
+ return True
98
+ return False
74
99
  return False
75
100
 
76
101
 
77
102
  def detect_test_commands(project_path: str) -> list[str] | None:
78
103
  """Detecta comandos de test para proyectos C/autotools sin toml.
79
104
 
80
- Orden: Makefile (test-host > test > check > tests, o primer
81
- test* disponible) → CMake (build+ctest) → meson (setup+test).
105
+ Runtime multi-proyecto, sin hardcodear nombres de proyecto:
106
+ Makefile (test-host canónico solo; si no, TODOS los targets de
107
+ test en una sola invocación `make test test-* ...`, p. ej.
108
+ `make test test-dsp test-transport`) → CMake (build+ctest) →
109
+ meson (setup+test).
82
110
  Devuelve None si no hay nada detectable (caller usa pytest default).
83
111
  """
84
112
  try:
@@ -86,12 +114,26 @@ def detect_test_commands(project_path: str) -> list[str] | None:
86
114
  except ValueError:
87
115
  targets = []
88
116
  if targets:
89
- for preferred in MAKE_TEST_PRIORITY:
90
- if preferred in targets:
91
- return [f"make {preferred}"]
92
- test_like = sorted(t for t in targets if t.startswith("test-") or t.startswith("test_"))
93
- if test_like:
94
- return [f"make {test_like[0]}"]
117
+ # `test-host` es la suite canónica (drivers internos propios):
118
+ # va sola, manda sobre todo (incluso sobre tests/*.py).
119
+ if "test-host" in targets:
120
+ return ["make test-host"]
121
+ combined: list[str] = []
122
+ for preferred in MAKE_TEST_PRIORITY[1:]:
123
+ # `tests` desnudo suele ser helper mkdir (dependencia order-only
124
+ # `| tests`), no runner: se excluye si hay otro target de test.
125
+ if preferred == "tests" and any(
126
+ t in targets for t in ("test", "check")
127
+ ):
128
+ continue
129
+ if preferred in targets and preferred not in combined:
130
+ combined.append(preferred)
131
+ for extra in sorted(t for t in targets
132
+ if (t.startswith("test-") or t.startswith("test_"))
133
+ and t not in combined):
134
+ combined.append(extra)
135
+ if combined:
136
+ return [f"make {' '.join(combined)}"]
95
137
  # Makefile sin target de test: al menos verifica que compila.
96
138
  if "all" in targets:
97
139
  return ["make"]
@@ -108,7 +150,12 @@ def detect_test_commands(project_path: str) -> list[str] | None:
108
150
 
109
151
 
110
152
  def _auto_toml_config(project_path: str) -> TomlConfig | None:
111
- """Config auto para proyectos sin deploynator.toml (C primero)."""
153
+ """Config auto para proyectos sin deploynator.toml (C primero).
154
+
155
+ Multi-proyecto: pypi_enabled se infiere al vuelo de los ficheros
156
+ de packaging (pyproject/setup.py/setup.cfg); el gate final sigue
157
+ en PyPIRunner.is_packaging_project al desplegar.
158
+ """
112
159
  detected = detect_test_commands(project_path)
113
160
  if detected is None:
114
161
  return None
@@ -124,7 +171,7 @@ def _auto_toml_config(project_path: str) -> TomlConfig | None:
124
171
  test_commands=detected,
125
172
  test_timeout_s=C_TEST_TIMEOUT_S,
126
173
  docs_auto=True,
127
- pypi_enabled=False,
174
+ pypi_enabled=looks_like_packaging_project(project_path),
128
175
  )
129
176
 
130
177
 
@@ -159,3 +159,58 @@ class TestCMakefileDetection:
159
159
  os.symlink(real, os.path.join(tmp, "Makefile"))
160
160
  with pytest.raises(ValueError, match="Symlink"):
161
161
  list_make_targets(tmp)
162
+
163
+
164
+ class TestMultiTargetAndPyPIInference:
165
+ """Runtime multi-proyecto: todos los targets test en una invocación
166
+ y pypi_enabled inferido de los ficheros de packaging."""
167
+
168
+ def _mk(self, tmp, content):
169
+ with open(os.path.join(tmp, "Makefile"), "w") as f:
170
+ f.write(content)
171
+
172
+ def test_detect_combines_test_targets(self):
173
+ from deploynator._toml import detect_test_commands
174
+ with tempfile.TemporaryDirectory() as tmp:
175
+ self._mk(tmp, "test:\n\techo t\ntest-dsp:\n\techo d\n"
176
+ "test-transport:\n\techo r\ntests:\n\tmkdir -p tests\n")
177
+ assert detect_test_commands(tmp) == ["make test test-dsp test-transport"]
178
+
179
+ def test_detect_test_host_alone_wins(self):
180
+ from deploynator._toml import detect_test_commands
181
+ with tempfile.TemporaryDirectory() as tmp:
182
+ self._mk(tmp, "test-host:\n\techo h\ntest:\n\techo t\n"
183
+ "test-dsp:\n\techo d\n")
184
+ assert detect_test_commands(tmp) == ["make test-host"]
185
+
186
+ def test_detect_bare_tests_alone_kept(self):
187
+ from deploynator._toml import detect_test_commands
188
+ with tempfile.TemporaryDirectory() as tmp:
189
+ self._mk(tmp, "tests:\n\techo t\n")
190
+ assert detect_test_commands(tmp) == ["make tests"]
191
+
192
+ def test_auto_pypi_enabled_for_packaging(self):
193
+ from deploynator._toml import load_toml_config
194
+ with tempfile.TemporaryDirectory() as tmp:
195
+ self._mk(tmp, "test:\n\techo t\n")
196
+ with open(os.path.join(tmp, "pyproject.toml"), "w") as f:
197
+ f.write('[project]\nname = "x"\nversion = "0.1.0"\n')
198
+ cfg = load_toml_config(tmp)
199
+ assert cfg.test_commands == ["make test"]
200
+ assert cfg.pypi_enabled is True
201
+
202
+ def test_auto_pypi_disabled_for_plain_c(self):
203
+ from deploynator._toml import load_toml_config
204
+ with tempfile.TemporaryDirectory() as tmp:
205
+ self._mk(tmp, "test:\n\techo t\n")
206
+ cfg = load_toml_config(tmp)
207
+ assert cfg.test_commands == ["make test"]
208
+ assert cfg.pypi_enabled is False
209
+
210
+ def test_looks_like_packaging_setup_py(self):
211
+ from deploynator._toml import looks_like_packaging_project
212
+ with tempfile.TemporaryDirectory() as tmp:
213
+ assert looks_like_packaging_project(tmp) is False
214
+ with open(os.path.join(tmp, "setup.py"), "w") as f:
215
+ f.write("from setuptools import setup\nsetup(name='x')\n")
216
+ assert looks_like_packaging_project(tmp) is True
File without changes
File without changes
File without changes
File without changes