pam-python 0.2.1__tar.gz → 0.2.2__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 (106) hide show
  1. {pam_python-0.2.1/pam_python.egg-info → pam_python-0.2.2}/PKG-INFO +17 -21
  2. {pam_python-0.2.1 → pam_python-0.2.2}/README.md +16 -20
  3. {pam_python-0.2.1 → pam_python-0.2.2}/pam/__init__.py +1 -1
  4. {pam_python-0.2.1 → pam_python-0.2.2}/pam/cli.py +79 -4
  5. {pam_python-0.2.1 → pam_python-0.2.2/pam_python.egg-info}/PKG-INFO +17 -21
  6. {pam_python-0.2.1 → pam_python-0.2.2}/tests/test_cli_templates.py +52 -0
  7. {pam_python-0.2.1 → pam_python-0.2.2}/LICENSE.txt +0 -0
  8. {pam_python-0.2.1 → pam_python-0.2.2}/pam/api.py +0 -0
  9. {pam_python-0.2.1 → pam_python-0.2.2}/pam/interface_task_manager.py +0 -0
  10. {pam_python-0.2.1 → pam_python-0.2.2}/pam/logger.py +0 -0
  11. {pam_python-0.2.1 → pam_python-0.2.2}/pam/models/__init__.py +0 -0
  12. {pam_python-0.2.1 → pam_python-0.2.2}/pam/models/request_command.py +0 -0
  13. {pam_python-0.2.1 → pam_python-0.2.2}/pam/models/sqlite_download.py +0 -0
  14. {pam_python-0.2.1 → pam_python-0.2.2}/pam/reports/__init__.py +0 -0
  15. {pam_python-0.2.1 → pam_python-0.2.2}/pam/reports/area.py +0 -0
  16. {pam_python-0.2.1 → pam_python-0.2.2}/pam/reports/axis.py +0 -0
  17. {pam_python-0.2.1 → pam_python-0.2.2}/pam/reports/bar.py +0 -0
  18. {pam_python-0.2.1 → pam_python-0.2.2}/pam/reports/box_plot.py +0 -0
  19. {pam_python-0.2.1 → pam_python-0.2.2}/pam/reports/bubble.py +0 -0
  20. {pam_python-0.2.1 → pam_python-0.2.2}/pam/reports/candlestick.py +0 -0
  21. {pam_python-0.2.1 → pam_python-0.2.2}/pam/reports/collection.py +0 -0
  22. {pam_python-0.2.1 → pam_python-0.2.2}/pam/reports/definition.py +0 -0
  23. {pam_python-0.2.1 → pam_python-0.2.2}/pam/reports/donut.py +0 -0
  24. {pam_python-0.2.1 → pam_python-0.2.2}/pam/reports/errors.py +0 -0
  25. {pam_python-0.2.1 → pam_python-0.2.2}/pam/reports/fields.py +0 -0
  26. {pam_python-0.2.1 → pam_python-0.2.2}/pam/reports/heatmap.py +0 -0
  27. {pam_python-0.2.1 → pam_python-0.2.2}/pam/reports/line.py +0 -0
  28. {pam_python-0.2.1 → pam_python-0.2.2}/pam/reports/manager.py +0 -0
  29. {pam_python-0.2.1 → pam_python-0.2.2}/pam/reports/metric.py +0 -0
  30. {pam_python-0.2.1 → pam_python-0.2.2}/pam/reports/pie.py +0 -0
  31. {pam_python-0.2.1 → pam_python-0.2.2}/pam/reports/polar_area.py +0 -0
  32. {pam_python-0.2.1 → pam_python-0.2.2}/pam/reports/radar.py +0 -0
  33. {pam_python-0.2.1 → pam_python-0.2.2}/pam/reports/radial_bar.py +0 -0
  34. {pam_python-0.2.1 → pam_python-0.2.2}/pam/reports/range_area.py +0 -0
  35. {pam_python-0.2.1 → pam_python-0.2.2}/pam/reports/range_bar.py +0 -0
  36. {pam_python-0.2.1 → pam_python-0.2.2}/pam/reports/range_transform.py +0 -0
  37. {pam_python-0.2.1 → pam_python-0.2.2}/pam/reports/record.py +0 -0
  38. {pam_python-0.2.1 → pam_python-0.2.2}/pam/reports/report.py +0 -0
  39. {pam_python-0.2.1 → pam_python-0.2.2}/pam/reports/scatter.py +0 -0
  40. {pam_python-0.2.1 → pam_python-0.2.2}/pam/reports/schema.py +0 -0
  41. {pam_python-0.2.1 → pam_python-0.2.2}/pam/reports/store.py +0 -0
  42. {pam_python-0.2.1 → pam_python-0.2.2}/pam/reports/table.py +0 -0
  43. {pam_python-0.2.1 → pam_python-0.2.2}/pam/reports/transport.py +0 -0
  44. {pam_python-0.2.1 → pam_python-0.2.2}/pam/reports/treemap.py +0 -0
  45. {pam_python-0.2.1 → pam_python-0.2.2}/pam/request_file_format.py +0 -0
  46. {pam_python-0.2.1 → pam_python-0.2.2}/pam/result_batch_uploader.py +0 -0
  47. {pam_python-0.2.1 → pam_python-0.2.2}/pam/server.py +0 -0
  48. {pam_python-0.2.1 → pam_python-0.2.2}/pam/service.py +0 -0
  49. {pam_python-0.2.1 → pam_python-0.2.2}/pam/sqlite.py +0 -0
  50. {pam_python-0.2.1 → pam_python-0.2.2}/pam/task_manager.py +0 -0
  51. {pam_python-0.2.1 → pam_python-0.2.2}/pam/temp_file_utils.py +0 -0
  52. {pam_python-0.2.1 → pam_python-0.2.2}/pam/templates/buildcmd/pamb +0 -0
  53. {pam_python-0.2.1 → pam_python-0.2.2}/pam/templates/buildcmd/pamb-base.sh +0 -0
  54. {pam_python-0.2.1 → pam_python-0.2.2}/pam/templates/init/AGENT.md +0 -0
  55. {pam_python-0.2.1 → pam_python-0.2.2}/pam/templates/init/REPORTS.md +0 -0
  56. {pam_python-0.2.1 → pam_python-0.2.2}/pam/templates/init/dockerignore.tmpl +0 -0
  57. {pam_python-0.2.1 → pam_python-0.2.2}/pam/templates/init/gitignore.tmpl +0 -0
  58. {pam_python-0.2.1 → pam_python-0.2.2}/pam/templates/init/main.tmpl +0 -0
  59. {pam_python-0.2.1 → pam_python-0.2.2}/pam/templates/init/pylintrc.tmpl +0 -0
  60. {pam_python-0.2.1 → pam_python-0.2.2}/pam/templates/init/run_unit_test.bat +0 -0
  61. {pam_python-0.2.1 → pam_python-0.2.2}/pam/templates/init/run_unit_test.ps1 +0 -0
  62. {pam_python-0.2.1 → pam_python-0.2.2}/pam/templates/init/run_unit_test.sh +0 -0
  63. {pam_python-0.2.1 → pam_python-0.2.2}/pam/templates/project/pip/Dockerfile +0 -0
  64. {pam_python-0.2.1 → pam_python-0.2.2}/pam/templates/project/pip/requirements.txt +0 -0
  65. {pam_python-0.2.1 → pam_python-0.2.2}/pam/templates/project/uv/Dockerfile +0 -0
  66. {pam_python-0.2.1 → pam_python-0.2.2}/pam/templates/project/uv/pyproject.toml +0 -0
  67. {pam_python-0.2.1 → pam_python-0.2.2}/pam/templates/project/uv/python-version +0 -0
  68. {pam_python-0.2.1 → pam_python-0.2.2}/pam/templates/service/common/service.yaml +0 -0
  69. {pam_python-0.2.1 → pam_python-0.2.2}/pam/templates/service/pandas/functions.tmpl +0 -0
  70. {pam_python-0.2.1 → pam_python-0.2.2}/pam/templates/service/pandas/service.test.tmpl +0 -0
  71. {pam_python-0.2.1 → pam_python-0.2.2}/pam/templates/service/pandas/service_class.tmpl +0 -0
  72. {pam_python-0.2.1 → pam_python-0.2.2}/pam/templates/service/polars/functions.tmpl +0 -0
  73. {pam_python-0.2.1 → pam_python-0.2.2}/pam/templates/service/polars/service.test.tmpl +0 -0
  74. {pam_python-0.2.1 → pam_python-0.2.2}/pam/templates/service/polars/service_class.tmpl +0 -0
  75. {pam_python-0.2.1 → pam_python-0.2.2}/pam/tester_task.py +0 -0
  76. {pam_python-0.2.1 → pam_python-0.2.2}/pam/utils.py +0 -0
  77. {pam_python-0.2.1 → pam_python-0.2.2}/pam_python.egg-info/SOURCES.txt +0 -0
  78. {pam_python-0.2.1 → pam_python-0.2.2}/pam_python.egg-info/dependency_links.txt +0 -0
  79. {pam_python-0.2.1 → pam_python-0.2.2}/pam_python.egg-info/entry_points.txt +0 -0
  80. {pam_python-0.2.1 → pam_python-0.2.2}/pam_python.egg-info/requires.txt +0 -0
  81. {pam_python-0.2.1 → pam_python-0.2.2}/pam_python.egg-info/top_level.txt +0 -0
  82. {pam_python-0.2.1 → pam_python-0.2.2}/pyproject.toml +0 -0
  83. {pam_python-0.2.1 → pam_python-0.2.2}/setup.cfg +0 -0
  84. {pam_python-0.2.1 → pam_python-0.2.2}/tests/test_range_transform.py +0 -0
  85. {pam_python-0.2.1 → pam_python-0.2.2}/tests/test_report_store.py +0 -0
  86. {pam_python-0.2.1 → pam_python-0.2.2}/tests/test_result_batch_uploader.py +0 -0
  87. {pam_python-0.2.1 → pam_python-0.2.2}/tests/test_service_dataframe_output.py +0 -0
  88. {pam_python-0.2.1 → pam_python-0.2.2}/tests/test_sqlite_download_v2.py +0 -0
  89. {pam_python-0.2.1 → pam_python-0.2.2}/tests/test_typed_area_report.py +0 -0
  90. {pam_python-0.2.1 → pam_python-0.2.2}/tests/test_typed_bar_report.py +0 -0
  91. {pam_python-0.2.1 → pam_python-0.2.2}/tests/test_typed_box_plot_report.py +0 -0
  92. {pam_python-0.2.1 → pam_python-0.2.2}/tests/test_typed_bubble_report.py +0 -0
  93. {pam_python-0.2.1 → pam_python-0.2.2}/tests/test_typed_candlestick_report.py +0 -0
  94. {pam_python-0.2.1 → pam_python-0.2.2}/tests/test_typed_donut_report.py +0 -0
  95. {pam_python-0.2.1 → pam_python-0.2.2}/tests/test_typed_heatmap_report.py +0 -0
  96. {pam_python-0.2.1 → pam_python-0.2.2}/tests/test_typed_line_report.py +0 -0
  97. {pam_python-0.2.1 → pam_python-0.2.2}/tests/test_typed_metric_report.py +0 -0
  98. {pam_python-0.2.1 → pam_python-0.2.2}/tests/test_typed_pie_report.py +0 -0
  99. {pam_python-0.2.1 → pam_python-0.2.2}/tests/test_typed_polar_area_report.py +0 -0
  100. {pam_python-0.2.1 → pam_python-0.2.2}/tests/test_typed_radar_report.py +0 -0
  101. {pam_python-0.2.1 → pam_python-0.2.2}/tests/test_typed_radial_bar_report.py +0 -0
  102. {pam_python-0.2.1 → pam_python-0.2.2}/tests/test_typed_range_area_report.py +0 -0
  103. {pam_python-0.2.1 → pam_python-0.2.2}/tests/test_typed_range_bar_report.py +0 -0
  104. {pam_python-0.2.1 → pam_python-0.2.2}/tests/test_typed_scatter_report.py +0 -0
  105. {pam_python-0.2.1 → pam_python-0.2.2}/tests/test_typed_table_report.py +0 -0
  106. {pam_python-0.2.1 → pam_python-0.2.2}/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.1
3
+ Version: 0.2.2
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
@@ -58,46 +58,42 @@ This README is a practical, step-by-step guide you can follow to create and run
58
58
 
59
59
  ---
60
60
 
61
- **Install**
62
- Recommended installation:
61
+ **Create a uv project**
62
+
63
+ Install PAM inside each project instead of installing the CLI globally:
63
64
 
64
65
  ```bash
65
66
  mkdir my_data_plugin
66
67
  cd my_data_plugin
67
- uv tool install pam-python
68
+ uv init --python 3.12
69
+ uv add pam-python
70
+ uv run pam init
68
71
  ```
69
72
 
70
- Pip remains supported:
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.
76
+
77
+ Pip remains supported for new projects that do not use uv:
71
78
 
72
79
  ```bash
80
+ mkdir my_data_plugin
81
+ cd my_data_plugin
73
82
  python3 -m venv venv
74
83
  source venv/bin/activate
75
84
  python -m pip install pam-python
76
- ```
77
-
78
- ---
79
-
80
- **Initialize a Project**
81
- This creates a runnable project with templates, including `AGENT.md` for runtime
82
- rules and `REPORTS.md` for the managed-report public API and copyable examples.
83
-
84
- ```bash
85
85
  pam init
86
86
  ```
87
87
 
88
- `pam init` asks you to select `uv` (default) or `pip`. A uv project contains
89
- `pyproject.toml`, `.python-version`, and `uv.lock`; a pip project contains
90
- `requirements.txt`. The two templates are independent and are never mixed.
91
-
92
- Initialization only supports a new project directory and will not migrate or
93
- overwrite an existing project.
88
+ When run in an empty directory, `pam init` still asks whether to create a uv or
89
+ pip project. PAM never migrates an existing application.
94
90
 
95
91
  ---
96
92
 
97
93
  **Create a Service**
98
94
 
99
95
  ```bash
100
- pam new service rfm_segment
96
+ uv run pam new service rfm_segment
101
97
  ```
102
98
 
103
99
  The generator asks you to select Polars (default) or Pandas. Polars services use
@@ -38,46 +38,42 @@ This README is a practical, step-by-step guide you can follow to create and run
38
38
 
39
39
  ---
40
40
 
41
- **Install**
42
- Recommended installation:
41
+ **Create a uv project**
42
+
43
+ Install PAM inside each project instead of installing the CLI globally:
43
44
 
44
45
  ```bash
45
46
  mkdir my_data_plugin
46
47
  cd my_data_plugin
47
- uv tool install pam-python
48
+ uv init --python 3.12
49
+ uv add pam-python
50
+ uv run pam init
48
51
  ```
49
52
 
50
- Pip remains supported:
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.
56
+
57
+ Pip remains supported for new projects that do not use uv:
51
58
 
52
59
  ```bash
60
+ mkdir my_data_plugin
61
+ cd my_data_plugin
53
62
  python3 -m venv venv
54
63
  source venv/bin/activate
55
64
  python -m pip install pam-python
56
- ```
57
-
58
- ---
59
-
60
- **Initialize a Project**
61
- This creates a runnable project with templates, including `AGENT.md` for runtime
62
- rules and `REPORTS.md` for the managed-report public API and copyable examples.
63
-
64
- ```bash
65
65
  pam init
66
66
  ```
67
67
 
68
- `pam init` asks you to select `uv` (default) or `pip`. A uv project contains
69
- `pyproject.toml`, `.python-version`, and `uv.lock`; a pip project contains
70
- `requirements.txt`. The two templates are independent and are never mixed.
71
-
72
- Initialization only supports a new project directory and will not migrate or
73
- overwrite an existing project.
68
+ When run in an empty directory, `pam init` still asks whether to create a uv or
69
+ pip project. PAM never migrates an existing application.
74
70
 
75
71
  ---
76
72
 
77
73
  **Create a Service**
78
74
 
79
75
  ```bash
80
- pam new service rfm_segment
76
+ uv run pam new service rfm_segment
81
77
  ```
82
78
 
83
79
  The generator asks you to select Polars (default) or Pandas. Polars services use
@@ -1,3 +1,3 @@
1
1
  """PAM Data Plugin framework."""
2
2
 
3
- __version__ = "0.2.1"
3
+ __version__ = "0.2.2"
@@ -8,6 +8,7 @@ import shutil
8
8
  import subprocess
9
9
  import sys
10
10
  import tempfile
11
+ import tomllib
11
12
 
12
13
 
13
14
  PROJECT_MARKER = ".pam-project"
@@ -159,6 +160,67 @@ def _assert_new_project(directory: Path):
159
160
  )
160
161
 
161
162
 
163
+ def _requirement_name(requirement: str) -> str:
164
+ return re.split(r"[<>=!~;\s\[]", requirement, maxsplit=1)[0].lower()
165
+
166
+
167
+ def _assert_fresh_uv_project(directory: Path):
168
+ pyproject_path = directory / "pyproject.toml"
169
+ try:
170
+ project = tomllib.loads(pyproject_path.read_text(encoding="utf-8"))["project"]
171
+ except (OSError, tomllib.TOMLDecodeError, KeyError, TypeError) as exc:
172
+ raise FileExistsError(f"Invalid fresh uv project: {exc}") from exc
173
+
174
+ dependencies = project.get("dependencies", [])
175
+ if not isinstance(dependencies, list) or not any(
176
+ isinstance(item, str) and _requirement_name(item) == "pam-python"
177
+ for item in dependencies
178
+ ):
179
+ raise FileExistsError(
180
+ "Run `uv add pam-python` before `uv run pam init`"
181
+ )
182
+
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
+
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:
201
+ 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,
219
+ )
220
+ if uv_placeholder is None:
221
+ raise FileExistsError("pam init will not overwrite an edited main.py")
222
+
223
+
162
224
  def _run_uv(args: list[str], cwd: Path):
163
225
  uv = shutil.which("uv")
164
226
  if uv is None:
@@ -167,12 +229,20 @@ def _run_uv(args: list[str], cwd: Path):
167
229
 
168
230
 
169
231
  def init_project(package_manager: str | None = None):
170
- package_manager = package_manager or _select_package_manager()
232
+ destination = Path.cwd()
233
+ preinitialized_uv = (destination / "pyproject.toml").is_file()
234
+ if preinitialized_uv:
235
+ if package_manager not in (None, "uv"):
236
+ raise FileExistsError("A uv project cannot be initialized as a pip project")
237
+ _assert_fresh_uv_project(destination)
238
+ package_manager = "uv"
239
+ else:
240
+ package_manager = package_manager or _select_package_manager()
241
+ _assert_new_project(destination)
242
+
171
243
  if package_manager not in PACKAGE_MANAGER_TEMPLATES:
172
244
  raise ValueError(f"Unsupported package manager: {package_manager}")
173
245
 
174
- destination = Path.cwd()
175
- _assert_new_project(destination)
176
246
  if package_manager == "uv" and shutil.which("uv") is None:
177
247
  raise RuntimeError("uv is required for this project but was not found on PATH")
178
248
 
@@ -184,6 +254,11 @@ def init_project(package_manager: str | None = None):
184
254
  **COMMON_PROJECT_TEMPLATES,
185
255
  **PACKAGE_MANAGER_TEMPLATES[package_manager],
186
256
  }
257
+ if preinitialized_uv:
258
+ templates = {
259
+ src: dest for src, dest in templates.items()
260
+ if dest not in {".gitignore", ".python-version", "pyproject.toml"}
261
+ }
187
262
 
188
263
  with tempfile.TemporaryDirectory(prefix=".pam-init-", dir=destination) as temp_dir:
189
264
  staging = Path(temp_dir)
@@ -202,7 +277,7 @@ def init_project(package_manager: str | None = None):
202
277
  encoding="utf-8",
203
278
  )
204
279
 
205
- if package_manager == "uv":
280
+ if package_manager == "uv" and not preinitialized_uv:
206
281
  _run_uv(["lock"], staging)
207
282
 
208
283
  for path in staging.iterdir():
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pam-python
3
- Version: 0.2.1
3
+ Version: 0.2.2
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
@@ -58,46 +58,42 @@ This README is a practical, step-by-step guide you can follow to create and run
58
58
 
59
59
  ---
60
60
 
61
- **Install**
62
- Recommended installation:
61
+ **Create a uv project**
62
+
63
+ Install PAM inside each project instead of installing the CLI globally:
63
64
 
64
65
  ```bash
65
66
  mkdir my_data_plugin
66
67
  cd my_data_plugin
67
- uv tool install pam-python
68
+ uv init --python 3.12
69
+ uv add pam-python
70
+ uv run pam init
68
71
  ```
69
72
 
70
- Pip remains supported:
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.
76
+
77
+ Pip remains supported for new projects that do not use uv:
71
78
 
72
79
  ```bash
80
+ mkdir my_data_plugin
81
+ cd my_data_plugin
73
82
  python3 -m venv venv
74
83
  source venv/bin/activate
75
84
  python -m pip install pam-python
76
- ```
77
-
78
- ---
79
-
80
- **Initialize a Project**
81
- This creates a runnable project with templates, including `AGENT.md` for runtime
82
- rules and `REPORTS.md` for the managed-report public API and copyable examples.
83
-
84
- ```bash
85
85
  pam init
86
86
  ```
87
87
 
88
- `pam init` asks you to select `uv` (default) or `pip`. A uv project contains
89
- `pyproject.toml`, `.python-version`, and `uv.lock`; a pip project contains
90
- `requirements.txt`. The two templates are independent and are never mixed.
91
-
92
- Initialization only supports a new project directory and will not migrate or
93
- overwrite an existing project.
88
+ When run in an empty directory, `pam init` still asks whether to create a uv or
89
+ pip project. PAM never migrates an existing application.
94
90
 
95
91
  ---
96
92
 
97
93
  **Create a Service**
98
94
 
99
95
  ```bash
100
- pam new service rfm_segment
96
+ uv run pam new service rfm_segment
101
97
  ```
102
98
 
103
99
  The generator asks you to select Polars (default) or Pandas. Polars services use
@@ -1,3 +1,4 @@
1
+ import json
1
2
  import os
2
3
  from pathlib import Path
3
4
  import sys
@@ -114,6 +115,57 @@ class CLITemplateTest(unittest.TestCase):
114
115
  calls,
115
116
  )
116
117
 
118
+ def test_init_adopts_a_fresh_uv_project_without_rewriting_uv_files(self):
119
+ calls = []
120
+ original_pyproject = """[project]
121
+ name = "customer-plugin"
122
+ version = "0.1.0"
123
+ requires-python = ">=3.12"
124
+ dependencies = ["pam-python>=0.2.1"]
125
+ """
126
+ uv_main = '''def main():
127
+ print("Hello from customer-plugin!")
128
+
129
+
130
+ if __name__ == "__main__":
131
+ main()
132
+ '''
133
+
134
+ with tempfile.TemporaryDirectory() as temp_dir, WorkingDirectory(temp_dir):
135
+ root = Path(temp_dir)
136
+ (root / "pyproject.toml").write_text(original_pyproject)
137
+ (root / ".python-version").write_text("3.12\n")
138
+ (root / "uv.lock").write_text("version = 1\n")
139
+ (root / ".gitignore").write_text(".venv\n")
140
+ (root / "README.md").touch()
141
+ (root / "main.py").write_text(uv_main)
142
+
143
+ with mock.patch.object(cli.shutil, "which", return_value="/usr/bin/uv"), mock.patch.object(
144
+ cli, "_run_uv", side_effect=lambda args, cwd: calls.append(list(args))
145
+ ):
146
+ cli.init_project()
147
+
148
+ self.assertEqual(original_pyproject, (root / "pyproject.toml").read_text())
149
+ self.assertEqual("3.12\n", (root / ".python-version").read_text())
150
+ self.assertEqual("version = 1\n", (root / "uv.lock").read_text())
151
+ self.assertEqual(".venv\n", (root / ".gitignore").read_text())
152
+ self.assertIn("plugin_server = Server", (root / "main.py").read_text())
153
+ self.assertEqual("uv", json.loads((root / ".pam-project").read_text())["package_manager"])
154
+ self.assertEqual([], calls)
155
+
156
+ def test_init_rejects_an_edited_uv_project(self):
157
+ with tempfile.TemporaryDirectory() as temp_dir, WorkingDirectory(temp_dir):
158
+ root = Path(temp_dir)
159
+ (root / "pyproject.toml").write_text(
160
+ '[project]\nname = "plugin"\nversion = "0.1.0"\n'
161
+ 'dependencies = ["pam-python"]\n'
162
+ )
163
+ (root / ".python-version").write_text("3.12\n")
164
+ (root / "main.py").write_text("print('real code')\n")
165
+
166
+ with self.assertRaisesRegex(FileExistsError, "edited main.py"):
167
+ cli.init_project()
168
+
117
169
  def test_init_rejects_an_existing_project_without_overwriting(self):
118
170
  with tempfile.TemporaryDirectory() as temp_dir, WorkingDirectory(temp_dir):
119
171
  original = Path("main.py")
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