pam-python 0.2.6__tar.gz → 0.2.8__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 (113) hide show
  1. {pam_python-0.2.6 → pam_python-0.2.8}/PKG-INFO +4 -4
  2. {pam_python-0.2.6 → pam_python-0.2.8}/README.md +3 -3
  3. {pam_python-0.2.6 → pam_python-0.2.8}/pam/__init__.py +1 -1
  4. {pam_python-0.2.6 → pam_python-0.2.8}/pam/cli.py +29 -49
  5. {pam_python-0.2.6 → pam_python-0.2.8}/pam/templates/project/uv/Dockerfile +1 -1
  6. {pam_python-0.2.6 → pam_python-0.2.8}/pam_python.egg-info/PKG-INFO +4 -4
  7. {pam_python-0.2.6 → pam_python-0.2.8}/tests/test_cli_templates.py +30 -7
  8. {pam_python-0.2.6 → pam_python-0.2.8}/LICENSE.txt +0 -0
  9. {pam_python-0.2.6 → pam_python-0.2.8}/pam/api.py +0 -0
  10. {pam_python-0.2.6 → pam_python-0.2.8}/pam/interface_task_manager.py +0 -0
  11. {pam_python-0.2.6 → pam_python-0.2.8}/pam/logger.py +0 -0
  12. {pam_python-0.2.6 → pam_python-0.2.8}/pam/models/__init__.py +0 -0
  13. {pam_python-0.2.6 → pam_python-0.2.8}/pam/models/data_input_v2.py +0 -0
  14. {pam_python-0.2.6 → pam_python-0.2.8}/pam/models/request_command.py +0 -0
  15. {pam_python-0.2.6 → pam_python-0.2.8}/pam/models/sqlite_download.py +0 -0
  16. {pam_python-0.2.6 → pam_python-0.2.8}/pam/reports/__init__.py +0 -0
  17. {pam_python-0.2.6 → pam_python-0.2.8}/pam/reports/area.py +0 -0
  18. {pam_python-0.2.6 → pam_python-0.2.8}/pam/reports/axis.py +0 -0
  19. {pam_python-0.2.6 → pam_python-0.2.8}/pam/reports/bar.py +0 -0
  20. {pam_python-0.2.6 → pam_python-0.2.8}/pam/reports/box_plot.py +0 -0
  21. {pam_python-0.2.6 → pam_python-0.2.8}/pam/reports/bubble.py +0 -0
  22. {pam_python-0.2.6 → pam_python-0.2.8}/pam/reports/candlestick.py +0 -0
  23. {pam_python-0.2.6 → pam_python-0.2.8}/pam/reports/collection.py +0 -0
  24. {pam_python-0.2.6 → pam_python-0.2.8}/pam/reports/definition.py +0 -0
  25. {pam_python-0.2.6 → pam_python-0.2.8}/pam/reports/donut.py +0 -0
  26. {pam_python-0.2.6 → pam_python-0.2.8}/pam/reports/errors.py +0 -0
  27. {pam_python-0.2.6 → pam_python-0.2.8}/pam/reports/fields.py +0 -0
  28. {pam_python-0.2.6 → pam_python-0.2.8}/pam/reports/heatmap.py +0 -0
  29. {pam_python-0.2.6 → pam_python-0.2.8}/pam/reports/line.py +0 -0
  30. {pam_python-0.2.6 → pam_python-0.2.8}/pam/reports/manager.py +0 -0
  31. {pam_python-0.2.6 → pam_python-0.2.8}/pam/reports/metric.py +0 -0
  32. {pam_python-0.2.6 → pam_python-0.2.8}/pam/reports/pie.py +0 -0
  33. {pam_python-0.2.6 → pam_python-0.2.8}/pam/reports/polar_area.py +0 -0
  34. {pam_python-0.2.6 → pam_python-0.2.8}/pam/reports/radar.py +0 -0
  35. {pam_python-0.2.6 → pam_python-0.2.8}/pam/reports/radial_bar.py +0 -0
  36. {pam_python-0.2.6 → pam_python-0.2.8}/pam/reports/range_area.py +0 -0
  37. {pam_python-0.2.6 → pam_python-0.2.8}/pam/reports/range_bar.py +0 -0
  38. {pam_python-0.2.6 → pam_python-0.2.8}/pam/reports/range_transform.py +0 -0
  39. {pam_python-0.2.6 → pam_python-0.2.8}/pam/reports/record.py +0 -0
  40. {pam_python-0.2.6 → pam_python-0.2.8}/pam/reports/report.py +0 -0
  41. {pam_python-0.2.6 → pam_python-0.2.8}/pam/reports/scatter.py +0 -0
  42. {pam_python-0.2.6 → pam_python-0.2.8}/pam/reports/schema.py +0 -0
  43. {pam_python-0.2.6 → pam_python-0.2.8}/pam/reports/store.py +0 -0
  44. {pam_python-0.2.6 → pam_python-0.2.8}/pam/reports/table.py +0 -0
  45. {pam_python-0.2.6 → pam_python-0.2.8}/pam/reports/transport.py +0 -0
  46. {pam_python-0.2.6 → pam_python-0.2.8}/pam/reports/treemap.py +0 -0
  47. {pam_python-0.2.6 → pam_python-0.2.8}/pam/request_file_format.py +0 -0
  48. {pam_python-0.2.6 → pam_python-0.2.8}/pam/result_batch_uploader.py +0 -0
  49. {pam_python-0.2.6 → pam_python-0.2.8}/pam/server.py +0 -0
  50. {pam_python-0.2.6 → pam_python-0.2.8}/pam/service.py +0 -0
  51. {pam_python-0.2.6 → pam_python-0.2.8}/pam/sqlite.py +0 -0
  52. {pam_python-0.2.6 → pam_python-0.2.8}/pam/task_manager.py +0 -0
  53. {pam_python-0.2.6 → pam_python-0.2.8}/pam/temp_file_utils.py +0 -0
  54. {pam_python-0.2.6 → pam_python-0.2.8}/pam/templates/buildcmd/pamb +0 -0
  55. {pam_python-0.2.6 → pam_python-0.2.8}/pam/templates/buildcmd/pamb-base.sh +0 -0
  56. {pam_python-0.2.6 → pam_python-0.2.8}/pam/templates/init/AGENT.md +0 -0
  57. {pam_python-0.2.6 → pam_python-0.2.8}/pam/templates/init/REPORTS.md +0 -0
  58. {pam_python-0.2.6 → pam_python-0.2.8}/pam/templates/init/dockerignore.tmpl +0 -0
  59. {pam_python-0.2.6 → pam_python-0.2.8}/pam/templates/init/gitignore.tmpl +0 -0
  60. {pam_python-0.2.6 → pam_python-0.2.8}/pam/templates/init/main.tmpl +0 -0
  61. {pam_python-0.2.6 → pam_python-0.2.8}/pam/templates/init/pylintrc.tmpl +0 -0
  62. {pam_python-0.2.6 → pam_python-0.2.8}/pam/templates/init/run_unit_test.bat +0 -0
  63. {pam_python-0.2.6 → pam_python-0.2.8}/pam/templates/init/run_unit_test.ps1 +0 -0
  64. {pam_python-0.2.6 → pam_python-0.2.8}/pam/templates/init/run_unit_test.sh +0 -0
  65. {pam_python-0.2.6 → pam_python-0.2.8}/pam/templates/project/pip/Dockerfile +0 -0
  66. {pam_python-0.2.6 → pam_python-0.2.8}/pam/templates/project/pip/requirements.txt +0 -0
  67. {pam_python-0.2.6 → pam_python-0.2.8}/pam/templates/project/uv/pyproject.toml +0 -0
  68. {pam_python-0.2.6 → pam_python-0.2.8}/pam/templates/project/uv/python-version +0 -0
  69. {pam_python-0.2.6 → pam_python-0.2.8}/pam/templates/service/common/service.yaml +0 -0
  70. {pam_python-0.2.6 → pam_python-0.2.8}/pam/templates/service/pandas/functions.tmpl +0 -0
  71. {pam_python-0.2.6 → pam_python-0.2.8}/pam/templates/service/pandas/service.test.tmpl +0 -0
  72. {pam_python-0.2.6 → pam_python-0.2.8}/pam/templates/service/pandas/service_class.tmpl +0 -0
  73. {pam_python-0.2.6 → pam_python-0.2.8}/pam/templates/service/polars/functions.tmpl +0 -0
  74. {pam_python-0.2.6 → pam_python-0.2.8}/pam/templates/service/polars/service.test.tmpl +0 -0
  75. {pam_python-0.2.6 → pam_python-0.2.8}/pam/templates/service/polars/service_class.tmpl +0 -0
  76. {pam_python-0.2.6 → pam_python-0.2.8}/pam/tester_task.py +0 -0
  77. {pam_python-0.2.6 → pam_python-0.2.8}/pam/utils.py +0 -0
  78. {pam_python-0.2.6 → pam_python-0.2.8}/pam_python.egg-info/SOURCES.txt +0 -0
  79. {pam_python-0.2.6 → pam_python-0.2.8}/pam_python.egg-info/dependency_links.txt +0 -0
  80. {pam_python-0.2.6 → pam_python-0.2.8}/pam_python.egg-info/entry_points.txt +0 -0
  81. {pam_python-0.2.6 → pam_python-0.2.8}/pam_python.egg-info/requires.txt +0 -0
  82. {pam_python-0.2.6 → pam_python-0.2.8}/pam_python.egg-info/top_level.txt +0 -0
  83. {pam_python-0.2.6 → pam_python-0.2.8}/pyproject.toml +0 -0
  84. {pam_python-0.2.6 → pam_python-0.2.8}/setup.cfg +0 -0
  85. {pam_python-0.2.6 → pam_python-0.2.8}/tests/test_api.py +0 -0
  86. {pam_python-0.2.6 → pam_python-0.2.8}/tests/test_data_error_v2.py +0 -0
  87. {pam_python-0.2.6 → pam_python-0.2.8}/tests/test_data_input_v2.py +0 -0
  88. {pam_python-0.2.6 → pam_python-0.2.8}/tests/test_data_timeout_v2.py +0 -0
  89. {pam_python-0.2.6 → pam_python-0.2.8}/tests/test_range_transform.py +0 -0
  90. {pam_python-0.2.6 → pam_python-0.2.8}/tests/test_report_store.py +0 -0
  91. {pam_python-0.2.6 → pam_python-0.2.8}/tests/test_request_data_v2.py +0 -0
  92. {pam_python-0.2.6 → pam_python-0.2.8}/tests/test_result_batch_uploader.py +0 -0
  93. {pam_python-0.2.6 → pam_python-0.2.8}/tests/test_service_dataframe_output.py +0 -0
  94. {pam_python-0.2.6 → pam_python-0.2.8}/tests/test_sqlite_download_v2.py +0 -0
  95. {pam_python-0.2.6 → pam_python-0.2.8}/tests/test_temp_file_utils.py +0 -0
  96. {pam_python-0.2.6 → pam_python-0.2.8}/tests/test_typed_area_report.py +0 -0
  97. {pam_python-0.2.6 → pam_python-0.2.8}/tests/test_typed_bar_report.py +0 -0
  98. {pam_python-0.2.6 → pam_python-0.2.8}/tests/test_typed_box_plot_report.py +0 -0
  99. {pam_python-0.2.6 → pam_python-0.2.8}/tests/test_typed_bubble_report.py +0 -0
  100. {pam_python-0.2.6 → pam_python-0.2.8}/tests/test_typed_candlestick_report.py +0 -0
  101. {pam_python-0.2.6 → pam_python-0.2.8}/tests/test_typed_donut_report.py +0 -0
  102. {pam_python-0.2.6 → pam_python-0.2.8}/tests/test_typed_heatmap_report.py +0 -0
  103. {pam_python-0.2.6 → pam_python-0.2.8}/tests/test_typed_line_report.py +0 -0
  104. {pam_python-0.2.6 → pam_python-0.2.8}/tests/test_typed_metric_report.py +0 -0
  105. {pam_python-0.2.6 → pam_python-0.2.8}/tests/test_typed_pie_report.py +0 -0
  106. {pam_python-0.2.6 → pam_python-0.2.8}/tests/test_typed_polar_area_report.py +0 -0
  107. {pam_python-0.2.6 → pam_python-0.2.8}/tests/test_typed_radar_report.py +0 -0
  108. {pam_python-0.2.6 → pam_python-0.2.8}/tests/test_typed_radial_bar_report.py +0 -0
  109. {pam_python-0.2.6 → pam_python-0.2.8}/tests/test_typed_range_area_report.py +0 -0
  110. {pam_python-0.2.6 → pam_python-0.2.8}/tests/test_typed_range_bar_report.py +0 -0
  111. {pam_python-0.2.6 → pam_python-0.2.8}/tests/test_typed_scatter_report.py +0 -0
  112. {pam_python-0.2.6 → pam_python-0.2.8}/tests/test_typed_table_report.py +0 -0
  113. {pam_python-0.2.6 → pam_python-0.2.8}/tests/test_typed_treemap_report.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pam-python
3
- Version: 0.2.6
3
+ Version: 0.2.8
4
4
  Summary: Pam Python Library
5
5
  Author-email: Narongrit Kanhanoi <narongrit@pams.ai>
6
6
  Project-URL: Homepage, https://github.com/heart/pam-python
@@ -70,9 +70,9 @@ uv add pam-python
70
70
  uv run pam init
71
71
  ```
72
72
 
73
- `pam init` recognizes a fresh uv project, keeps its uv configuration, and adds
74
- the PAM scaffold. It refuses projects whose generated source files have already
75
- been edited.
73
+ `pam init` recognizes a fresh uv project, keeps its uv configuration, adds the
74
+ Flask and Gunicorn runtime dependencies, and adds the PAM scaffold. It refuses
75
+ projects whose generated source files have already been edited.
76
76
 
77
77
  Pip remains supported for new projects that do not use uv:
78
78
 
@@ -50,9 +50,9 @@ uv add pam-python
50
50
  uv run pam init
51
51
  ```
52
52
 
53
- `pam init` recognizes a fresh uv project, keeps its uv configuration, and adds
54
- the PAM scaffold. It refuses projects whose generated source files have already
55
- been edited.
53
+ `pam init` recognizes a fresh uv project, keeps its uv configuration, adds the
54
+ Flask and Gunicorn runtime dependencies, and adds the PAM scaffold. It refuses
55
+ projects whose generated source files have already been edited.
56
56
 
57
57
  Pip remains supported for new projects that do not use uv:
58
58
 
@@ -1,3 +1,3 @@
1
1
  """PAM Data Plugin framework."""
2
2
 
3
- __version__ = "0.2.6"
3
+ __version__ = "0.2.8"
@@ -59,6 +59,8 @@ ENGINE_DEPENDENCIES = {
59
59
  "pandas": ["pandas>=2.2.3", "pyarrow>=19.0.1"],
60
60
  }
61
61
 
62
+ UV_RUNTIME_DEPENDENCIES = ["Flask>=3.0.2", "gunicorn>=23.0.0"]
63
+
62
64
 
63
65
  def main(argv=None):
64
66
  args = list(sys.argv[1:] if argv is None else argv)
@@ -145,19 +147,10 @@ def _project_name(directory: Path) -> str:
145
147
 
146
148
 
147
149
  def _assert_new_project(directory: Path):
148
- known_project_files = set(COMMON_PROJECT_TEMPLATES.values())
149
- for templates in PACKAGE_MANAGER_TEMPLATES.values():
150
- known_project_files.update(templates.values())
151
- known_project_files.update({PROJECT_MARKER, "__init__.py", "uv.lock"})
152
- conflicts = [
153
- name for name in sorted(known_project_files)
154
- if (directory / name).exists()
155
- ]
156
- if conflicts:
157
- raise FileExistsError(
158
- "pam init only supports a new project directory; existing files: "
159
- + ", ".join(conflicts)
160
- )
150
+ # Kept for backward compatibility; collision handling is now in init_project
151
+ # via _prompt_overwrite_if_needed. Do not enforce allow-list here so
152
+ # unrelated files like .DS_Store are ignored.
153
+ return
161
154
 
162
155
 
163
156
  def _requirement_name(requirement: str) -> str:
@@ -180,45 +173,26 @@ def _assert_fresh_uv_project(directory: Path):
180
173
  "Run `uv add pam-python` before `uv run pam init`"
181
174
  )
182
175
 
183
- python_version = directory / ".python-version"
184
- if not python_version.is_file() or not python_version.read_text().strip().startswith("3.12"):
185
- raise FileExistsError(
186
- "A fresh uv project must pin Python 3.12; run `uv init --python 3.12`"
187
- )
188
176
 
189
- allowed = {
190
- ".git",
191
- ".gitignore",
192
- ".python-version",
193
- ".venv",
194
- "README.md",
195
- "main.py",
196
- "pyproject.toml",
197
- "uv.lock",
198
- }
199
- unexpected = sorted(path.name for path in directory.iterdir() if path.name not in allowed)
200
- if unexpected:
177
+ def _prompt_overwrite_if_needed(directory: Path, templates: dict[str, str]) -> bool:
178
+ dest_names = set(templates.values()) | {PROJECT_MARKER, "__init__.py"}
179
+ conflicts = sorted(name for name in dest_names if (directory / name).exists())
180
+ if not conflicts:
181
+ return True
182
+ print("The following files will be overwritten:")
183
+ for name in conflicts:
184
+ print(f" - {name}")
185
+ # Single confirmation round as requested: list collisions once, ask once.
186
+ if not sys.stdin.isatty():
201
187
  raise FileExistsError(
202
- "pam init only accepts a fresh uv project; existing files: "
203
- + ", ".join(unexpected)
204
- )
205
-
206
- readme = directory / "README.md"
207
- if readme.is_file() and readme.read_text(encoding="utf-8").strip():
208
- raise FileExistsError("pam init will not overwrite a non-empty README.md")
209
-
210
- main_file = directory / "main.py"
211
- if main_file.is_file():
212
- source = main_file.read_text(encoding="utf-8")
213
- uv_placeholder = re.fullmatch(
214
- r'def main\(\):\n'
215
- r' print\("Hello from [a-z0-9-]+!"\)\n\n\n'
216
- r'if __name__ == "__main__":\n'
217
- r' main\(\)\n?',
218
- source,
188
+ "pam init will overwrite existing files: " + ", ".join(conflicts) + ". "
189
+ "Run in an interactive terminal and confirm to overwrite."
219
190
  )
220
- if uv_placeholder is None:
221
- raise FileExistsError("pam init will not overwrite an edited main.py")
191
+ response = input("มีไฟล์ที่จะถูกเขียนทับตามรายการข้างบน ดำเนินการต่อหรือไม่? (y/N): ").strip().lower()
192
+ if response != "y":
193
+ print("Cancelled.")
194
+ return False
195
+ return True
222
196
 
223
197
 
224
198
  def _run_uv(args: list[str], cwd: Path):
@@ -263,6 +237,12 @@ def init_project(package_manager: str | None = None):
263
237
  if dest not in preserved_uv_files
264
238
  }
265
239
 
240
+ if not _prompt_overwrite_if_needed(destination, templates):
241
+ return
242
+
243
+ if preinitialized_uv:
244
+ _run_uv(["add", *UV_RUNTIME_DEPENDENCIES], destination)
245
+
266
246
  with tempfile.TemporaryDirectory(prefix=".pam-init-", dir=destination) as temp_dir:
267
247
  staging = Path(temp_dir)
268
248
  for src, dest in templates.items():
@@ -17,4 +17,4 @@ RUN mkdir -p "$DATA_FOLDER"
17
17
  EXPOSE 8000
18
18
 
19
19
  # Keep one worker: service lifecycle state is local to the process.
20
- CMD ["gunicorn", "-w", "1", "-b", "0.0.0.0:8000", "main:app"]
20
+ CMD ["uv", "run", "--no-sync", "gunicorn", "-w", "1", "-b", "0.0.0.0:8000", "main:app"]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pam-python
3
- Version: 0.2.6
3
+ Version: 0.2.8
4
4
  Summary: Pam Python Library
5
5
  Author-email: Narongrit Kanhanoi <narongrit@pams.ai>
6
6
  Project-URL: Homepage, https://github.com/heart/pam-python
@@ -70,9 +70,9 @@ uv add pam-python
70
70
  uv run pam init
71
71
  ```
72
72
 
73
- `pam init` recognizes a fresh uv project, keeps its uv configuration, and adds
74
- the PAM scaffold. It refuses projects whose generated source files have already
75
- been edited.
73
+ `pam init` recognizes a fresh uv project, keeps its uv configuration, adds the
74
+ Flask and Gunicorn runtime dependencies, and adds the PAM scaffold. It refuses
75
+ projects whose generated source files have already been edited.
76
76
 
77
77
  Pip remains supported for new projects that do not use uv:
78
78
 
@@ -151,15 +151,23 @@ class CLITemplateTest(unittest.TestCase):
151
151
 
152
152
  root = Path(temp_dir)
153
153
  self.assertTrue((root / "pyproject.toml").is_file())
154
+ self.assertIn(
155
+ '"gunicorn>=23.0.0"',
156
+ (root / "pyproject.toml").read_text(),
157
+ )
154
158
  self.assertTrue((root / "uv.lock").is_file())
155
159
  self.assertFalse((root / "requirements.txt").exists())
156
160
  self.assertFalse((root / "run_unit_test.sh").exists())
161
+ self.assertIn(
162
+ 'CMD ["uv", "run", "--no-sync", "gunicorn"',
163
+ (root / "Dockerfile").read_text(),
164
+ )
157
165
  self.assertEqual(
158
166
  [["lock"], ["add", "polars>=1.43.0"]],
159
167
  calls,
160
168
  )
161
169
 
162
- def test_init_adopts_a_fresh_uv_project_without_rewriting_uv_files(self):
170
+ def test_init_adopts_a_fresh_uv_project_and_adds_runtime_dependencies(self):
163
171
  calls = []
164
172
  original_pyproject = """[project]
165
173
  name = "customer-plugin"
@@ -186,6 +194,8 @@ if __name__ == "__main__":
186
194
 
187
195
  with mock.patch.object(cli.shutil, "which", return_value="/usr/bin/uv"), mock.patch.object(
188
196
  cli, "_run_uv", side_effect=lambda args, cwd: calls.append(list(args))
197
+ ), mock.patch.object(cli.sys.stdin, "isatty", return_value=True), mock.patch(
198
+ "builtins.input", return_value="y"
189
199
  ):
190
200
  cli.init_project()
191
201
 
@@ -194,8 +204,15 @@ if __name__ == "__main__":
194
204
  self.assertEqual("version = 1\n", (root / "uv.lock").read_text())
195
205
  self.assertEqual(".venv\n", (root / ".gitignore").read_text())
196
206
  self.assertIn("plugin_server = Server", (root / "main.py").read_text())
207
+ self.assertIn(
208
+ 'CMD ["uv", "run", "--no-sync", "gunicorn"',
209
+ (root / "Dockerfile").read_text(),
210
+ )
197
211
  self.assertEqual("uv", json.loads((root / ".pam-project").read_text())["package_manager"])
198
- self.assertEqual([], calls)
212
+ self.assertEqual(
213
+ [["add", "Flask>=3.0.2", "gunicorn>=23.0.0"]],
214
+ calls,
215
+ )
199
216
 
200
217
  def test_init_adds_gitignore_when_fresh_uv_project_does_not_have_one(self):
201
218
  uv_main = '''def main():
@@ -217,7 +234,11 @@ if __name__ == "__main__":
217
234
  (root / "README.md").touch()
218
235
  (root / "main.py").write_text(uv_main)
219
236
 
220
- with mock.patch.object(cli.shutil, "which", return_value="/usr/bin/uv"):
237
+ with mock.patch.object(cli.shutil, "which", return_value="/usr/bin/uv"), mock.patch.object(
238
+ cli, "_run_uv"
239
+ ), mock.patch.object(
240
+ cli.sys.stdin, "isatty", return_value=True
241
+ ), mock.patch("builtins.input", return_value="y"):
221
242
  cli.init_project()
222
243
 
223
244
  gitignore = (root / ".gitignore").read_text()
@@ -242,15 +263,17 @@ if __name__ == "__main__":
242
263
  (root / ".python-version").write_text("3.12\n")
243
264
  (root / "main.py").write_text("print('real code')\n")
244
265
 
245
- with self.assertRaisesRegex(FileExistsError, "edited main.py"):
246
- cli.init_project()
266
+ with mock.patch.object(cli.sys.stdin, "isatty", return_value=False):
267
+ with self.assertRaisesRegex(FileExistsError, "will overwrite existing files: main.py"):
268
+ cli.init_project()
247
269
 
248
270
  def test_init_rejects_an_existing_project_without_overwriting(self):
249
271
  with tempfile.TemporaryDirectory() as temp_dir, WorkingDirectory(temp_dir):
250
272
  original = Path("main.py")
251
273
  original.write_text("keep me\n")
252
- with self.assertRaises(FileExistsError):
253
- cli.init_project("pip")
274
+ with mock.patch.object(cli.sys.stdin, "isatty", return_value=False):
275
+ with self.assertRaises(FileExistsError):
276
+ cli.init_project("pip")
254
277
  self.assertEqual("keep me\n", original.read_text())
255
278
 
256
279
  def test_service_name_validation_rejects_paths_and_hyphens(self):
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