pam-python 0.2.0__py3-none-any.whl → 0.2.2__py3-none-any.whl

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 (33) hide show
  1. pam/__init__.py +1 -1
  2. pam/cli.py +353 -150
  3. pam/models/request_command.py +4 -0
  4. pam/request_file_format.py +11 -2
  5. pam/result_batch_uploader.py +99 -51
  6. pam/server.py +22 -2
  7. pam/service.py +38 -25
  8. pam/templates/init/AGENT.md +48 -68
  9. pam/templates/init/REPORTS.md +2 -2
  10. pam/templates/init/dockerignore.tmpl +3 -1
  11. pam/templates/project/pip/Dockerfile +16 -0
  12. pam/templates/{init → project/pip}/requirements.txt +0 -2
  13. pam/templates/project/uv/Dockerfile +20 -0
  14. pam/templates/project/uv/pyproject.toml +12 -0
  15. pam/templates/project/uv/python-version +1 -0
  16. pam/templates/service/pandas/functions.tmpl +13 -0
  17. pam/templates/service/{service.test.tmpl → pandas/service.test.tmpl} +31 -46
  18. pam/templates/service/pandas/service_class.tmpl +65 -0
  19. pam/templates/service/polars/functions.tmpl +11 -0
  20. pam/templates/service/polars/service.test.tmpl +122 -0
  21. pam/templates/service/polars/service_class.tmpl +59 -0
  22. pam/tester_task.py +22 -7
  23. pam/utils.py +2 -4
  24. {pam_python-0.2.0.dist-info → pam_python-0.2.2.dist-info}/METADATA +56 -60
  25. {pam_python-0.2.0.dist-info → pam_python-0.2.2.dist-info}/RECORD +30 -24
  26. pam/templates/docker/Dockerfile +0 -21
  27. pam/templates/service/functions.tmpl +0 -22
  28. pam/templates/service/service_class.tmpl +0 -81
  29. pam/templates/service/{service.yaml → common/service.yaml} +1 -1
  30. {pam_python-0.2.0.dist-info → pam_python-0.2.2.dist-info}/WHEEL +0 -0
  31. {pam_python-0.2.0.dist-info → pam_python-0.2.2.dist-info}/entry_points.txt +0 -0
  32. {pam_python-0.2.0.dist-info → pam_python-0.2.2.dist-info}/licenses/LICENSE.txt +0 -0
  33. {pam_python-0.2.0.dist-info → pam_python-0.2.2.dist-info}/top_level.txt +0 -0
pam/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  """PAM Data Plugin framework."""
2
2
 
3
- __version__ = "0.2.0"
3
+ __version__ = "0.2.2"
pam/cli.py CHANGED
@@ -1,168 +1,310 @@
1
+ import importlib.resources as resources
2
+ from importlib import metadata
3
+ import json
1
4
  import os
5
+ from pathlib import Path
2
6
  import re
3
7
  import shutil
8
+ import subprocess
4
9
  import sys
5
- import importlib.resources as resources
6
- from importlib import metadata
10
+ import tempfile
11
+ import tomllib
12
+
13
+
14
+ PROJECT_MARKER = ".pam-project"
15
+ PACKAGE_MANAGERS = {"1": "uv", "2": "pip"}
16
+ DATAFRAME_ENGINES = {"1": "polars", "2": "pandas"}
17
+
18
+ COMMON_PROJECT_TEMPLATES = {
19
+ "init/main.tmpl": "main.py",
20
+ "buildcmd/pamb": "pamb",
21
+ "buildcmd/pamb-base.sh": "pamb-base.sh",
22
+ "init/pylintrc.tmpl": ".pylintrc",
23
+ "init/REPORTS.md": "REPORTS.md",
24
+ "init/AGENT.md": "AGENT.md",
25
+ "init/gitignore.tmpl": ".gitignore",
26
+ "init/dockerignore.tmpl": ".dockerignore",
27
+ }
28
+
29
+ PACKAGE_MANAGER_TEMPLATES = {
30
+ "pip": {
31
+ "project/pip/Dockerfile": "Dockerfile",
32
+ "project/pip/requirements.txt": "requirements.txt",
33
+ "init/run_unit_test.sh": "run_unit_test.sh",
34
+ "init/run_unit_test.bat": "run_unit_test.bat",
35
+ "init/run_unit_test.ps1": "run_unit_test.ps1",
36
+ },
37
+ "uv": {
38
+ "project/uv/Dockerfile": "Dockerfile",
39
+ "project/uv/pyproject.toml": "pyproject.toml",
40
+ "project/uv/python-version": ".python-version",
41
+ },
42
+ }
43
+
44
+ SERVICE_TEMPLATES = {
45
+ "polars": {
46
+ "service/polars/service_class.tmpl": "#CLASS_NAME#.py",
47
+ "service/polars/functions.tmpl": "functions.py",
48
+ "service/polars/service.test.tmpl": "test_#MODULE_NAME#.py",
49
+ },
50
+ "pandas": {
51
+ "service/pandas/service_class.tmpl": "#CLASS_NAME#.py",
52
+ "service/pandas/functions.tmpl": "functions.py",
53
+ "service/pandas/service.test.tmpl": "test_#MODULE_NAME#.py",
54
+ },
55
+ }
56
+
57
+ ENGINE_DEPENDENCIES = {
58
+ "polars": ["polars>=1.0.0"],
59
+ "pandas": ["pandas>=2.2.3", "pyarrow>=19.0.1"],
60
+ }
7
61
 
8
62
 
9
63
  def main(argv=None):
10
64
  args = list(sys.argv[1:] if argv is None else argv)
11
- if len(args) == 0:
65
+ if not args:
12
66
  print("Usage: pam <command> [args]")
13
67
  return
14
68
 
15
69
  cmd = args[0]
16
70
 
17
71
  if cmd == "init":
72
+ if len(args) != 1:
73
+ print("Usage: pam init")
74
+ return
18
75
  init_project()
19
76
  elif cmd == "new":
20
- if len(args) < 2:
77
+ if len(args) != 3 or args[1] != "service":
21
78
  print("Usage: pam new service <service_name>")
22
79
  return
23
- create_type = args[1]
24
- if create_type == "service":
25
- if len(args) < 3:
26
- print("Usage: pam new service <service_name>")
27
- return
28
- name = args[2]
29
- create_service(name)
30
- else:
31
- print(f"Unknown new target: {create_type}")
80
+ create_service(args[2])
32
81
  else:
33
82
  print(f"Unknown command: {cmd}")
34
83
 
35
84
 
36
85
  def to_pascal_case(input_string: str) -> str:
37
- """
38
- Convert a string to PascalCase.
86
+ words = re.split(r"[^A-Za-z0-9]+", input_string)
87
+ return "".join(word[:1].upper() + word[1:] for word in words if word)
39
88
 
40
- :param input_string: The string to convert.
41
- :return: The string in PascalCase.
42
- """
43
- # Split the string into words using non-alphanumeric characters as delimiters
44
- words = re.split(r'[^A-Za-z0-9]+', input_string)
45
89
 
46
- # Capitalize each word and join them
47
- pascal_case = ''.join(word[:1].upper() + word[1:] for word in words if word)
90
+ def _template_path(src: str):
91
+ return resources.files("pam") / "templates" / src
48
92
 
49
- return pascal_case
50
93
 
94
+ def read_template_text(src: str) -> str:
95
+ with open(_template_path(src), "r", encoding="utf-8") as file:
96
+ return file.read()
51
97
 
52
- def cpy(src, dest):
53
- template_dir = resources.files("pam") / "templates"
54
- src_file = os.path.join(template_dir, src)
55
- shutil.copy(src_file, dest)
56
98
 
99
+ def _render_template(src: str, replacements: dict[str, str]) -> str:
100
+ content = read_template_text(src)
101
+ for placeholder, value in replacements.items():
102
+ content = content.replace(placeholder, value)
103
+ return content
104
+
105
+
106
+ def _write_rendered_template(src: str, dest: Path, replacements: dict[str, str]):
107
+ dest.write_text(_render_template(src, replacements), encoding="utf-8")
108
+ source_mode = os.stat(_template_path(src)).st_mode
109
+ os.chmod(dest, source_mode)
110
+
111
+
112
+ def _select(prompt: str, options: dict[str, str], default: str) -> str:
113
+ if not sys.stdin.isatty():
114
+ raise RuntimeError(f"{prompt} requires an interactive terminal")
115
+ while True:
116
+ selection = input(f"Select [1-{len(options)}] (default: {default}): ").strip()
117
+ if not selection:
118
+ selection = default
119
+ if selection in options:
120
+ return options[selection]
121
+ print(f"Invalid selection: {selection}")
122
+
123
+
124
+ def _select_package_manager() -> str:
125
+ print("Select package manager:")
126
+ print("1) uv (recommended)")
127
+ print("2) pip")
128
+ return _select("Package manager selection", PACKAGE_MANAGERS, "1")
129
+
130
+
131
+ def _select_dataframe_engine() -> str:
132
+ print("Select dataframe engine:")
133
+ print("1) Polars (recommended)")
134
+ print("2) Pandas")
135
+ return _select("Dataframe engine selection", DATAFRAME_ENGINES, "1")
136
+
137
+
138
+ def _project_name(directory: Path) -> str:
139
+ name = re.sub(r"[^a-z0-9]+", "-", directory.name.lower()).strip("-")
140
+ if not name:
141
+ name = "pam-data-plugin"
142
+ if name[0].isdigit():
143
+ name = f"pam-{name}"
144
+ return name
145
+
146
+
147
+ 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
+ )
57
161
 
58
- def read_template_text(src: str) -> str:
59
- template_dir = resources.files("pam") / "templates"
60
- src_file = os.path.join(template_dir, src)
61
- with open(src_file, "r", encoding="utf-8") as file:
62
- return file.read()
63
162
 
163
+ def _requirement_name(requirement: str) -> str:
164
+ return re.split(r"[<>=!~;\s\[]", requirement, maxsplit=1)[0].lower()
64
165
 
65
- def replace_template_content(service_name, class_name, file_name):
66
- file_path = os.path.join(service_name, file_name)
67
- with open(file_path, 'r+', encoding='utf-8') as file:
68
- filedata = file.read()
69
- updated_data = filedata.replace('#CLASS_NAME#', class_name)
70
- updated_data = updated_data.replace('#MODULE_NAME#', service_name)
71
- file.seek(0) # Move the file pointer to the beginning of the file
72
- file.write(updated_data)
73
- file.truncate() # Remove any leftover content after the replacement
74
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
+ )
75
182
 
76
- def create_service(name):
77
- if not re.fullmatch(r"[A-Za-z][A-Za-z0-9_]*", name):
78
- raise ValueError(
79
- "service_name must start with a letter and contain only letters, "
80
- "numbers, and underscores"
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`"
81
187
  )
82
- if os.path.exists(name):
83
- response = input(
84
- f"Service {name} already exists. Do you want to overwrite it? (y/N): "
85
- ).strip().lower()
86
- if response == 'y':
87
- shutil.rmtree(name)
88
- else:
89
- print("Cancelled.")
90
- return
91
188
 
92
- os.mkdir(name)
93
- open(os.path.join(name, "__init__.py"), 'a', encoding='utf-8').close()
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
+ )
94
205
 
95
- cpy(
96
- "service/service_class.tmpl",
97
- os.path.join(name, to_pascal_case(name) + "Svc.py"),
98
- )
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")
99
222
 
100
- cpy("service/service.yaml", os.path.join(name, "service.yaml"))
101
- cpy("service/functions.tmpl", os.path.join(name, "functions.py"))
102
- cpy("service/service.test.tmpl", os.path.join(name, f"test_{name}.py"))
103
223
 
104
- class_name = to_pascal_case(name) + "Svc"
105
- replace_template_content(name, class_name, class_name + ".py")
106
- replace_template_content(name, class_name, "service.yaml")
107
- replace_template_content(
108
- name, class_name, f"test_{name}.py"
109
- )
224
+ def _run_uv(args: list[str], cwd: Path):
225
+ uv = shutil.which("uv")
226
+ if uv is None:
227
+ raise RuntimeError("uv is required for this project but was not found on PATH")
228
+ subprocess.run([uv, *args], cwd=cwd, check=True)
110
229
 
111
- print(f"Service {name} created.")
112
- print(
113
- f"Run the generated run_unit_test script for your shell with `{name}` "
114
- "to test the service."
115
- )
116
230
 
231
+ def init_project(package_manager: str | None = None):
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
+
243
+ if package_manager not in PACKAGE_MANAGER_TEMPLATES:
244
+ raise ValueError(f"Unsupported package manager: {package_manager}")
245
+
246
+ if package_manager == "uv" and shutil.which("uv") is None:
247
+ raise RuntimeError("uv is required for this project but was not found on PATH")
248
+
249
+ replacements = {
250
+ "#PAM_VERSION#": _get_installed_pam_version(),
251
+ "#PROJECT_NAME#": _project_name(destination),
252
+ }
253
+ templates = {
254
+ **COMMON_PROJECT_TEMPLATES,
255
+ **PACKAGE_MANAGER_TEMPLATES[package_manager],
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
+ }
262
+
263
+ with tempfile.TemporaryDirectory(prefix=".pam-init-", dir=destination) as temp_dir:
264
+ staging = Path(temp_dir)
265
+ for src, dest in templates.items():
266
+ _write_rendered_template(src, staging / dest, replacements)
267
+ (staging / "__init__.py").touch()
268
+ (staging / PROJECT_MARKER).write_text(
269
+ json.dumps(
270
+ {
271
+ "template_version": 1,
272
+ "package_manager": package_manager,
273
+ },
274
+ indent=2,
275
+ sort_keys=True,
276
+ ) + "\n",
277
+ encoding="utf-8",
278
+ )
279
+
280
+ if package_manager == "uv" and not preinitialized_uv:
281
+ _run_uv(["lock"], staging)
117
282
 
118
- def init_project():
119
- cpy("init/main.tmpl", "main.py")
120
- cpy("docker/Dockerfile", "Dockerfile")
121
- cpy("buildcmd/pamb", "pamb")
122
- cpy("buildcmd/pamb-base.sh", "pamb-base.sh")
123
- cpy("init/pylintrc.tmpl", ".pylintrc")
124
- cpy("init/gitignore.tmpl", ".gitignore")
125
- cpy("init/dockerignore.tmpl", ".dockerignore")
126
- cpy("init/run_unit_test.sh", "run_unit_test.sh")
127
- cpy("init/run_unit_test.bat", "run_unit_test.bat")
128
- cpy("init/run_unit_test.ps1", "run_unit_test.ps1")
129
- cpy("init/AGENT.md", "AGENT.md")
130
- cpy("init/REPORTS.md", "REPORTS.md")
131
-
132
- requirements_path = "requirements.txt"
133
- has_requirements = os.path.exists(requirements_path)
134
- template_requirements = _build_project_requirements().splitlines()
135
-
136
- mode = "overwrite"
137
- if has_requirements:
138
- print("requirements.txt already exists. Choose how to proceed:")
139
- print("1) overwrite")
140
- print("2) keep")
141
- print("3) merge")
142
- selection = input("Select [1-3] (default: 1): ").strip()
143
- if selection == "2":
144
- mode = "keep"
145
- elif selection == "3":
146
- mode = "merge"
147
-
148
- if mode == "keep":
149
- print("Keeping existing requirements.txt")
283
+ for path in staging.iterdir():
284
+ os.replace(path, destination / path.name)
285
+
286
+ print(f"--- Welcome to PAM ({package_manager}) ---\n")
287
+ if package_manager == "uv":
288
+ print("Next:\n uv sync\n uv run pam new service <service_name>\n")
150
289
  else:
151
- if mode == "overwrite" or not has_requirements:
152
- with open(requirements_path, "w", encoding="utf-8") as f:
153
- f.write(_build_project_requirements())
154
- elif mode == "merge":
155
- with open(requirements_path, "r", encoding="utf-8") as f:
156
- existing_lines = f.read().splitlines()
157
- merged_lines = _merge_requirements(existing_lines, template_requirements)
158
- with open(requirements_path, "w", encoding="utf-8") as f:
159
- f.write("\n".join(merged_lines) + ("\n" if merged_lines else ""))
290
+ print(
291
+ "Next:\n python -m pip install -r requirements.txt\n"
292
+ " pam new service <service_name>\n"
293
+ )
160
294
 
161
- if not os.path.exists("__init__.py"):
162
- open("__init__.py", 'a', encoding='utf-8').close()
163
295
 
164
- print("--- Welcome to PAM ---\n")
165
- print("To create a new service run\n`pam new service <service_name>`\n\n")
296
+ def _load_project_config(directory: Path) -> dict:
297
+ marker = directory / PROJECT_MARKER
298
+ if not marker.is_file():
299
+ raise RuntimeError("This is not a PAM project created by the current `pam init`")
300
+ try:
301
+ config = json.loads(marker.read_text(encoding="utf-8"))
302
+ except (OSError, json.JSONDecodeError) as exc:
303
+ raise RuntimeError(f"Invalid {PROJECT_MARKER}: {exc}") from exc
304
+ package_manager = config.get("package_manager")
305
+ if package_manager not in PACKAGE_MANAGER_TEMPLATES:
306
+ raise RuntimeError(f"Invalid package manager in {PROJECT_MARKER}")
307
+ return config
166
308
 
167
309
 
168
310
  def _parse_requirement_name(line: str) -> str:
@@ -172,15 +314,103 @@ def _parse_requirement_name(line: str) -> str:
172
314
  if line.startswith("-"):
173
315
  return line
174
316
  line = line.split(";", 1)[0].strip()
175
- # Normalize common requirement formats like pkg==1.2, pkg>=1.2, pkg[extra]==1.2
176
- splitters = ["==", ">=", "<=", "~=", ">", "<"]
177
- for splitter in splitters:
317
+ for splitter in ["==", ">=", "<=", "~=", ">", "<"]:
178
318
  if splitter in line:
179
319
  line = line.split(splitter, 1)[0].strip()
180
320
  break
181
321
  return line.split("[", 1)[0].strip().lower()
182
322
 
183
323
 
324
+ def _add_pip_dependencies(dependencies: list[str], directory: Path):
325
+ requirements_path = directory / "requirements.txt"
326
+ if not requirements_path.is_file():
327
+ raise RuntimeError("Pip PAM project is missing requirements.txt")
328
+ existing = requirements_path.read_text(encoding="utf-8").splitlines()
329
+ existing_names = {_parse_requirement_name(line) for line in existing}
330
+ additions = [
331
+ dependency for dependency in dependencies
332
+ if _parse_requirement_name(dependency) not in existing_names
333
+ ]
334
+ if additions:
335
+ content = requirements_path.read_text(encoding="utf-8")
336
+ if content and not content.endswith("\n"):
337
+ content += "\n"
338
+ requirements_path.write_text(
339
+ content + "\n".join(additions) + "\n",
340
+ encoding="utf-8",
341
+ )
342
+
343
+
344
+ def _add_engine_dependencies(
345
+ engine: str,
346
+ package_manager: str,
347
+ directory: Path,
348
+ ):
349
+ dependencies = ENGINE_DEPENDENCIES[engine]
350
+ if package_manager == "uv":
351
+ _run_uv(["add", *dependencies], directory)
352
+ else:
353
+ _add_pip_dependencies(dependencies, directory)
354
+
355
+
356
+ def create_service(name, dataframe_engine: str | None = None):
357
+ if not re.fullmatch(r"[A-Za-z][A-Za-z0-9_]*", name):
358
+ raise ValueError(
359
+ "service_name must start with a letter and contain only letters, "
360
+ "numbers, and underscores"
361
+ )
362
+
363
+ service_path = Path(name)
364
+ if service_path.exists():
365
+ response = input(
366
+ f"Service {name} already exists. Do you want to overwrite it? (y/N): "
367
+ ).strip().lower()
368
+ if response != "y":
369
+ print("Cancelled.")
370
+ return
371
+
372
+ dataframe_engine = dataframe_engine or _select_dataframe_engine()
373
+ if dataframe_engine not in SERVICE_TEMPLATES:
374
+ raise ValueError(f"Unsupported dataframe engine: {dataframe_engine}")
375
+
376
+ project_config = _load_project_config(Path.cwd())
377
+ _add_engine_dependencies(
378
+ dataframe_engine,
379
+ project_config["package_manager"],
380
+ Path.cwd(),
381
+ )
382
+
383
+ if service_path.exists():
384
+ shutil.rmtree(service_path)
385
+ service_path.mkdir()
386
+ (service_path / "__init__.py").touch()
387
+
388
+ class_name = to_pascal_case(name) + "Svc"
389
+ replacements = {
390
+ "#CLASS_NAME#": class_name,
391
+ "#MODULE_NAME#": name,
392
+ }
393
+ for src, destination_template in SERVICE_TEMPLATES[dataframe_engine].items():
394
+ destination_name = destination_template
395
+ for placeholder, value in replacements.items():
396
+ destination_name = destination_name.replace(placeholder, value)
397
+ _write_rendered_template(
398
+ src,
399
+ service_path / destination_name,
400
+ replacements,
401
+ )
402
+ _write_rendered_template(
403
+ "service/common/service.yaml",
404
+ service_path / "service.yaml",
405
+ replacements,
406
+ )
407
+
408
+ print(f"Service {name} created with {dataframe_engine}.")
409
+ if project_config["package_manager"] == "pip":
410
+ print("Install updated dependencies with `python -m pip install -r requirements.txt`.")
411
+ print(f"Run the generated service test for `{name}` before editing the template.")
412
+
413
+
184
414
  def _get_installed_pam_version() -> str:
185
415
  try:
186
416
  from pam import __version__
@@ -195,32 +425,5 @@ def _get_installed_pam_version() -> str:
195
425
  return "0.0.0"
196
426
 
197
427
 
198
- def _build_project_requirements() -> str:
199
- pam_version = _get_installed_pam_version()
200
- template = read_template_text("init/requirements.txt")
201
- rendered = template.replace("#PAM_VERSION#", pam_version)
202
- if not rendered.endswith("\n"):
203
- rendered += "\n"
204
- return rendered
205
-
206
-
207
- def _merge_requirements(existing_lines, frozen_lines):
208
- """
209
- Merge frozen requirements into existing lines while keeping existing entries.
210
- """
211
- existing_keys = {_parse_requirement_name(line) for line in existing_lines}
212
- existing_keys.discard("")
213
-
214
- merged = list(existing_lines)
215
- for line in frozen_lines:
216
- key = _parse_requirement_name(line)
217
- if not key:
218
- continue
219
- if key not in existing_keys:
220
- merged.append(line)
221
- existing_keys.add(key)
222
- return merged
223
-
224
-
225
428
  if __name__ == "__main__":
226
429
  main()
@@ -26,6 +26,7 @@ import zipfile
26
26
  from flask import Request
27
27
  from pam.utils import log
28
28
  from pam.temp_file_utils import TempfileUtils
29
+ from pam.request_file_format import RequestFileFormat
29
30
 
30
31
  @dataclass
31
32
  class RequestCommand:
@@ -43,6 +44,7 @@ class RequestCommand:
43
44
  next: str
44
45
  input_files: list[str]
45
46
  service_name: str
47
+ file_format: RequestFileFormat = RequestFileFormat.CSV
46
48
  log_api: str = ""
47
49
  sqlite_download_v2: str = ""
48
50
 
@@ -152,6 +154,7 @@ class RequestCommand:
152
154
 
153
155
  is_end = RequestCommand.__safe_str_to_bool(params.pop("is_end", "true"))
154
156
  next_page = params.pop("next", "")
157
+ file_format = RequestFileFormat.parse(params.pop("file_format", "csv"))
155
158
 
156
159
  input_files = []
157
160
  error_message = ""
@@ -179,6 +182,7 @@ class RequestCommand:
179
182
  next=next_page,
180
183
  input_files=input_files,
181
184
  service_name=service_name,
185
+ file_format=file_format,
182
186
  log_api=log_api,
183
187
  sqlite_download_v2=sqlite_download_v2
184
188
  ), error_message
@@ -1,5 +1,14 @@
1
1
  from enum import Enum
2
2
 
3
3
  class RequestFileFormat(Enum):
4
- CSV="csv"
5
- PARQUET="parquet"
4
+ CSV = "csv"
5
+ PARQUET = "parquet"
6
+
7
+ @classmethod
8
+ def parse(cls, value):
9
+ if isinstance(value, cls):
10
+ return value
11
+ try:
12
+ return cls(str(value).strip().lower())
13
+ except ValueError:
14
+ return cls.CSV