pam-python 0.2.0__py3-none-any.whl → 0.2.1__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.
- pam/__init__.py +1 -1
- pam/cli.py +283 -155
- pam/models/request_command.py +4 -0
- pam/request_file_format.py +11 -2
- pam/result_batch_uploader.py +99 -51
- pam/server.py +22 -2
- pam/service.py +38 -25
- pam/templates/init/AGENT.md +48 -68
- pam/templates/init/REPORTS.md +2 -2
- pam/templates/init/dockerignore.tmpl +3 -1
- pam/templates/project/pip/Dockerfile +16 -0
- pam/templates/{init → project/pip}/requirements.txt +0 -2
- pam/templates/project/uv/Dockerfile +20 -0
- pam/templates/project/uv/pyproject.toml +12 -0
- pam/templates/project/uv/python-version +1 -0
- pam/templates/service/pandas/functions.tmpl +13 -0
- pam/templates/service/{service.test.tmpl → pandas/service.test.tmpl} +31 -46
- pam/templates/service/pandas/service_class.tmpl +65 -0
- pam/templates/service/polars/functions.tmpl +11 -0
- pam/templates/service/polars/service.test.tmpl +122 -0
- pam/templates/service/polars/service_class.tmpl +59 -0
- pam/tester_task.py +22 -7
- pam/utils.py +2 -4
- {pam_python-0.2.0.dist-info → pam_python-0.2.1.dist-info}/METADATA +49 -49
- {pam_python-0.2.0.dist-info → pam_python-0.2.1.dist-info}/RECORD +30 -24
- pam/templates/docker/Dockerfile +0 -21
- pam/templates/service/functions.tmpl +0 -22
- pam/templates/service/service_class.tmpl +0 -81
- pam/templates/service/{service.yaml → common/service.yaml} +1 -1
- {pam_python-0.2.0.dist-info → pam_python-0.2.1.dist-info}/WHEEL +0 -0
- {pam_python-0.2.0.dist-info → pam_python-0.2.1.dist-info}/entry_points.txt +0 -0
- {pam_python-0.2.0.dist-info → pam_python-0.2.1.dist-info}/licenses/LICENSE.txt +0 -0
- {pam_python-0.2.0.dist-info → pam_python-0.2.1.dist-info}/top_level.txt +0 -0
pam/__init__.py
CHANGED
pam/cli.py
CHANGED
|
@@ -1,168 +1,235 @@
|
|
|
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
|
|
6
|
-
|
|
10
|
+
import tempfile
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
PROJECT_MARKER = ".pam-project"
|
|
14
|
+
PACKAGE_MANAGERS = {"1": "uv", "2": "pip"}
|
|
15
|
+
DATAFRAME_ENGINES = {"1": "polars", "2": "pandas"}
|
|
16
|
+
|
|
17
|
+
COMMON_PROJECT_TEMPLATES = {
|
|
18
|
+
"init/main.tmpl": "main.py",
|
|
19
|
+
"buildcmd/pamb": "pamb",
|
|
20
|
+
"buildcmd/pamb-base.sh": "pamb-base.sh",
|
|
21
|
+
"init/pylintrc.tmpl": ".pylintrc",
|
|
22
|
+
"init/REPORTS.md": "REPORTS.md",
|
|
23
|
+
"init/AGENT.md": "AGENT.md",
|
|
24
|
+
"init/gitignore.tmpl": ".gitignore",
|
|
25
|
+
"init/dockerignore.tmpl": ".dockerignore",
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
PACKAGE_MANAGER_TEMPLATES = {
|
|
29
|
+
"pip": {
|
|
30
|
+
"project/pip/Dockerfile": "Dockerfile",
|
|
31
|
+
"project/pip/requirements.txt": "requirements.txt",
|
|
32
|
+
"init/run_unit_test.sh": "run_unit_test.sh",
|
|
33
|
+
"init/run_unit_test.bat": "run_unit_test.bat",
|
|
34
|
+
"init/run_unit_test.ps1": "run_unit_test.ps1",
|
|
35
|
+
},
|
|
36
|
+
"uv": {
|
|
37
|
+
"project/uv/Dockerfile": "Dockerfile",
|
|
38
|
+
"project/uv/pyproject.toml": "pyproject.toml",
|
|
39
|
+
"project/uv/python-version": ".python-version",
|
|
40
|
+
},
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
SERVICE_TEMPLATES = {
|
|
44
|
+
"polars": {
|
|
45
|
+
"service/polars/service_class.tmpl": "#CLASS_NAME#.py",
|
|
46
|
+
"service/polars/functions.tmpl": "functions.py",
|
|
47
|
+
"service/polars/service.test.tmpl": "test_#MODULE_NAME#.py",
|
|
48
|
+
},
|
|
49
|
+
"pandas": {
|
|
50
|
+
"service/pandas/service_class.tmpl": "#CLASS_NAME#.py",
|
|
51
|
+
"service/pandas/functions.tmpl": "functions.py",
|
|
52
|
+
"service/pandas/service.test.tmpl": "test_#MODULE_NAME#.py",
|
|
53
|
+
},
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
ENGINE_DEPENDENCIES = {
|
|
57
|
+
"polars": ["polars>=1.0.0"],
|
|
58
|
+
"pandas": ["pandas>=2.2.3", "pyarrow>=19.0.1"],
|
|
59
|
+
}
|
|
7
60
|
|
|
8
61
|
|
|
9
62
|
def main(argv=None):
|
|
10
63
|
args = list(sys.argv[1:] if argv is None else argv)
|
|
11
|
-
if
|
|
64
|
+
if not args:
|
|
12
65
|
print("Usage: pam <command> [args]")
|
|
13
66
|
return
|
|
14
67
|
|
|
15
68
|
cmd = args[0]
|
|
16
69
|
|
|
17
70
|
if cmd == "init":
|
|
71
|
+
if len(args) != 1:
|
|
72
|
+
print("Usage: pam init")
|
|
73
|
+
return
|
|
18
74
|
init_project()
|
|
19
75
|
elif cmd == "new":
|
|
20
|
-
if len(args)
|
|
76
|
+
if len(args) != 3 or args[1] != "service":
|
|
21
77
|
print("Usage: pam new service <service_name>")
|
|
22
78
|
return
|
|
23
|
-
|
|
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}")
|
|
79
|
+
create_service(args[2])
|
|
32
80
|
else:
|
|
33
81
|
print(f"Unknown command: {cmd}")
|
|
34
82
|
|
|
35
83
|
|
|
36
84
|
def to_pascal_case(input_string: str) -> str:
|
|
37
|
-
""
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
46
|
-
# Capitalize each word and join them
|
|
47
|
-
pascal_case = ''.join(word[:1].upper() + word[1:] for word in words if word)
|
|
48
|
-
|
|
49
|
-
return pascal_case
|
|
85
|
+
words = re.split(r"[^A-Za-z0-9]+", input_string)
|
|
86
|
+
return "".join(word[:1].upper() + word[1:] for word in words if word)
|
|
50
87
|
|
|
51
88
|
|
|
52
|
-
def
|
|
53
|
-
|
|
54
|
-
src_file = os.path.join(template_dir, src)
|
|
55
|
-
shutil.copy(src_file, dest)
|
|
89
|
+
def _template_path(src: str):
|
|
90
|
+
return resources.files("pam") / "templates" / src
|
|
56
91
|
|
|
57
92
|
|
|
58
93
|
def read_template_text(src: str) -> str:
|
|
59
|
-
|
|
60
|
-
src_file = os.path.join(template_dir, src)
|
|
61
|
-
with open(src_file, "r", encoding="utf-8") as file:
|
|
94
|
+
with open(_template_path(src), "r", encoding="utf-8") as file:
|
|
62
95
|
return file.read()
|
|
63
96
|
|
|
64
97
|
|
|
65
|
-
def
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
98
|
+
def _render_template(src: str, replacements: dict[str, str]) -> str:
|
|
99
|
+
content = read_template_text(src)
|
|
100
|
+
for placeholder, value in replacements.items():
|
|
101
|
+
content = content.replace(placeholder, value)
|
|
102
|
+
return content
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _write_rendered_template(src: str, dest: Path, replacements: dict[str, str]):
|
|
106
|
+
dest.write_text(_render_template(src, replacements), encoding="utf-8")
|
|
107
|
+
source_mode = os.stat(_template_path(src)).st_mode
|
|
108
|
+
os.chmod(dest, source_mode)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _select(prompt: str, options: dict[str, str], default: str) -> str:
|
|
112
|
+
if not sys.stdin.isatty():
|
|
113
|
+
raise RuntimeError(f"{prompt} requires an interactive terminal")
|
|
114
|
+
while True:
|
|
115
|
+
selection = input(f"Select [1-{len(options)}] (default: {default}): ").strip()
|
|
116
|
+
if not selection:
|
|
117
|
+
selection = default
|
|
118
|
+
if selection in options:
|
|
119
|
+
return options[selection]
|
|
120
|
+
print(f"Invalid selection: {selection}")
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _select_package_manager() -> str:
|
|
124
|
+
print("Select package manager:")
|
|
125
|
+
print("1) uv (recommended)")
|
|
126
|
+
print("2) pip")
|
|
127
|
+
return _select("Package manager selection", PACKAGE_MANAGERS, "1")
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _select_dataframe_engine() -> str:
|
|
131
|
+
print("Select dataframe engine:")
|
|
132
|
+
print("1) Polars (recommended)")
|
|
133
|
+
print("2) Pandas")
|
|
134
|
+
return _select("Dataframe engine selection", DATAFRAME_ENGINES, "1")
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _project_name(directory: Path) -> str:
|
|
138
|
+
name = re.sub(r"[^a-z0-9]+", "-", directory.name.lower()).strip("-")
|
|
139
|
+
if not name:
|
|
140
|
+
name = "pam-data-plugin"
|
|
141
|
+
if name[0].isdigit():
|
|
142
|
+
name = f"pam-{name}"
|
|
143
|
+
return name
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _assert_new_project(directory: Path):
|
|
147
|
+
known_project_files = set(COMMON_PROJECT_TEMPLATES.values())
|
|
148
|
+
for templates in PACKAGE_MANAGER_TEMPLATES.values():
|
|
149
|
+
known_project_files.update(templates.values())
|
|
150
|
+
known_project_files.update({PROJECT_MARKER, "__init__.py", "uv.lock"})
|
|
151
|
+
conflicts = [
|
|
152
|
+
name for name in sorted(known_project_files)
|
|
153
|
+
if (directory / name).exists()
|
|
154
|
+
]
|
|
155
|
+
if conflicts:
|
|
156
|
+
raise FileExistsError(
|
|
157
|
+
"pam init only supports a new project directory; existing files: "
|
|
158
|
+
+ ", ".join(conflicts)
|
|
81
159
|
)
|
|
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
|
-
|
|
92
|
-
os.mkdir(name)
|
|
93
|
-
open(os.path.join(name, "__init__.py"), 'a', encoding='utf-8').close()
|
|
94
|
-
|
|
95
|
-
cpy(
|
|
96
|
-
"service/service_class.tmpl",
|
|
97
|
-
os.path.join(name, to_pascal_case(name) + "Svc.py"),
|
|
98
|
-
)
|
|
99
160
|
|
|
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
161
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
162
|
+
def _run_uv(args: list[str], cwd: Path):
|
|
163
|
+
uv = shutil.which("uv")
|
|
164
|
+
if uv is None:
|
|
165
|
+
raise RuntimeError("uv is required for this project but was not found on PATH")
|
|
166
|
+
subprocess.run([uv, *args], cwd=cwd, check=True)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def init_project(package_manager: str | None = None):
|
|
170
|
+
package_manager = package_manager or _select_package_manager()
|
|
171
|
+
if package_manager not in PACKAGE_MANAGER_TEMPLATES:
|
|
172
|
+
raise ValueError(f"Unsupported package manager: {package_manager}")
|
|
173
|
+
|
|
174
|
+
destination = Path.cwd()
|
|
175
|
+
_assert_new_project(destination)
|
|
176
|
+
if package_manager == "uv" and shutil.which("uv") is None:
|
|
177
|
+
raise RuntimeError("uv is required for this project but was not found on PATH")
|
|
178
|
+
|
|
179
|
+
replacements = {
|
|
180
|
+
"#PAM_VERSION#": _get_installed_pam_version(),
|
|
181
|
+
"#PROJECT_NAME#": _project_name(destination),
|
|
182
|
+
}
|
|
183
|
+
templates = {
|
|
184
|
+
**COMMON_PROJECT_TEMPLATES,
|
|
185
|
+
**PACKAGE_MANAGER_TEMPLATES[package_manager],
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
with tempfile.TemporaryDirectory(prefix=".pam-init-", dir=destination) as temp_dir:
|
|
189
|
+
staging = Path(temp_dir)
|
|
190
|
+
for src, dest in templates.items():
|
|
191
|
+
_write_rendered_template(src, staging / dest, replacements)
|
|
192
|
+
(staging / "__init__.py").touch()
|
|
193
|
+
(staging / PROJECT_MARKER).write_text(
|
|
194
|
+
json.dumps(
|
|
195
|
+
{
|
|
196
|
+
"template_version": 1,
|
|
197
|
+
"package_manager": package_manager,
|
|
198
|
+
},
|
|
199
|
+
indent=2,
|
|
200
|
+
sort_keys=True,
|
|
201
|
+
) + "\n",
|
|
202
|
+
encoding="utf-8",
|
|
203
|
+
)
|
|
110
204
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
f"Run the generated run_unit_test script for your shell with `{name}` "
|
|
114
|
-
"to test the service."
|
|
115
|
-
)
|
|
205
|
+
if package_manager == "uv":
|
|
206
|
+
_run_uv(["lock"], staging)
|
|
116
207
|
|
|
208
|
+
for path in staging.iterdir():
|
|
209
|
+
os.replace(path, destination / path.name)
|
|
117
210
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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")
|
|
211
|
+
print(f"--- Welcome to PAM ({package_manager}) ---\n")
|
|
212
|
+
if package_manager == "uv":
|
|
213
|
+
print("Next:\n uv sync\n uv run pam new service <service_name>\n")
|
|
150
214
|
else:
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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 ""))
|
|
215
|
+
print(
|
|
216
|
+
"Next:\n python -m pip install -r requirements.txt\n"
|
|
217
|
+
" pam new service <service_name>\n"
|
|
218
|
+
)
|
|
160
219
|
|
|
161
|
-
if not os.path.exists("__init__.py"):
|
|
162
|
-
open("__init__.py", 'a', encoding='utf-8').close()
|
|
163
220
|
|
|
164
|
-
|
|
165
|
-
|
|
221
|
+
def _load_project_config(directory: Path) -> dict:
|
|
222
|
+
marker = directory / PROJECT_MARKER
|
|
223
|
+
if not marker.is_file():
|
|
224
|
+
raise RuntimeError("This is not a PAM project created by the current `pam init`")
|
|
225
|
+
try:
|
|
226
|
+
config = json.loads(marker.read_text(encoding="utf-8"))
|
|
227
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
228
|
+
raise RuntimeError(f"Invalid {PROJECT_MARKER}: {exc}") from exc
|
|
229
|
+
package_manager = config.get("package_manager")
|
|
230
|
+
if package_manager not in PACKAGE_MANAGER_TEMPLATES:
|
|
231
|
+
raise RuntimeError(f"Invalid package manager in {PROJECT_MARKER}")
|
|
232
|
+
return config
|
|
166
233
|
|
|
167
234
|
|
|
168
235
|
def _parse_requirement_name(line: str) -> str:
|
|
@@ -172,15 +239,103 @@ def _parse_requirement_name(line: str) -> str:
|
|
|
172
239
|
if line.startswith("-"):
|
|
173
240
|
return line
|
|
174
241
|
line = line.split(";", 1)[0].strip()
|
|
175
|
-
|
|
176
|
-
splitters = ["==", ">=", "<=", "~=", ">", "<"]
|
|
177
|
-
for splitter in splitters:
|
|
242
|
+
for splitter in ["==", ">=", "<=", "~=", ">", "<"]:
|
|
178
243
|
if splitter in line:
|
|
179
244
|
line = line.split(splitter, 1)[0].strip()
|
|
180
245
|
break
|
|
181
246
|
return line.split("[", 1)[0].strip().lower()
|
|
182
247
|
|
|
183
248
|
|
|
249
|
+
def _add_pip_dependencies(dependencies: list[str], directory: Path):
|
|
250
|
+
requirements_path = directory / "requirements.txt"
|
|
251
|
+
if not requirements_path.is_file():
|
|
252
|
+
raise RuntimeError("Pip PAM project is missing requirements.txt")
|
|
253
|
+
existing = requirements_path.read_text(encoding="utf-8").splitlines()
|
|
254
|
+
existing_names = {_parse_requirement_name(line) for line in existing}
|
|
255
|
+
additions = [
|
|
256
|
+
dependency for dependency in dependencies
|
|
257
|
+
if _parse_requirement_name(dependency) not in existing_names
|
|
258
|
+
]
|
|
259
|
+
if additions:
|
|
260
|
+
content = requirements_path.read_text(encoding="utf-8")
|
|
261
|
+
if content and not content.endswith("\n"):
|
|
262
|
+
content += "\n"
|
|
263
|
+
requirements_path.write_text(
|
|
264
|
+
content + "\n".join(additions) + "\n",
|
|
265
|
+
encoding="utf-8",
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _add_engine_dependencies(
|
|
270
|
+
engine: str,
|
|
271
|
+
package_manager: str,
|
|
272
|
+
directory: Path,
|
|
273
|
+
):
|
|
274
|
+
dependencies = ENGINE_DEPENDENCIES[engine]
|
|
275
|
+
if package_manager == "uv":
|
|
276
|
+
_run_uv(["add", *dependencies], directory)
|
|
277
|
+
else:
|
|
278
|
+
_add_pip_dependencies(dependencies, directory)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def create_service(name, dataframe_engine: str | None = None):
|
|
282
|
+
if not re.fullmatch(r"[A-Za-z][A-Za-z0-9_]*", name):
|
|
283
|
+
raise ValueError(
|
|
284
|
+
"service_name must start with a letter and contain only letters, "
|
|
285
|
+
"numbers, and underscores"
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
service_path = Path(name)
|
|
289
|
+
if service_path.exists():
|
|
290
|
+
response = input(
|
|
291
|
+
f"Service {name} already exists. Do you want to overwrite it? (y/N): "
|
|
292
|
+
).strip().lower()
|
|
293
|
+
if response != "y":
|
|
294
|
+
print("Cancelled.")
|
|
295
|
+
return
|
|
296
|
+
|
|
297
|
+
dataframe_engine = dataframe_engine or _select_dataframe_engine()
|
|
298
|
+
if dataframe_engine not in SERVICE_TEMPLATES:
|
|
299
|
+
raise ValueError(f"Unsupported dataframe engine: {dataframe_engine}")
|
|
300
|
+
|
|
301
|
+
project_config = _load_project_config(Path.cwd())
|
|
302
|
+
_add_engine_dependencies(
|
|
303
|
+
dataframe_engine,
|
|
304
|
+
project_config["package_manager"],
|
|
305
|
+
Path.cwd(),
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
if service_path.exists():
|
|
309
|
+
shutil.rmtree(service_path)
|
|
310
|
+
service_path.mkdir()
|
|
311
|
+
(service_path / "__init__.py").touch()
|
|
312
|
+
|
|
313
|
+
class_name = to_pascal_case(name) + "Svc"
|
|
314
|
+
replacements = {
|
|
315
|
+
"#CLASS_NAME#": class_name,
|
|
316
|
+
"#MODULE_NAME#": name,
|
|
317
|
+
}
|
|
318
|
+
for src, destination_template in SERVICE_TEMPLATES[dataframe_engine].items():
|
|
319
|
+
destination_name = destination_template
|
|
320
|
+
for placeholder, value in replacements.items():
|
|
321
|
+
destination_name = destination_name.replace(placeholder, value)
|
|
322
|
+
_write_rendered_template(
|
|
323
|
+
src,
|
|
324
|
+
service_path / destination_name,
|
|
325
|
+
replacements,
|
|
326
|
+
)
|
|
327
|
+
_write_rendered_template(
|
|
328
|
+
"service/common/service.yaml",
|
|
329
|
+
service_path / "service.yaml",
|
|
330
|
+
replacements,
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
print(f"Service {name} created with {dataframe_engine}.")
|
|
334
|
+
if project_config["package_manager"] == "pip":
|
|
335
|
+
print("Install updated dependencies with `python -m pip install -r requirements.txt`.")
|
|
336
|
+
print(f"Run the generated service test for `{name}` before editing the template.")
|
|
337
|
+
|
|
338
|
+
|
|
184
339
|
def _get_installed_pam_version() -> str:
|
|
185
340
|
try:
|
|
186
341
|
from pam import __version__
|
|
@@ -195,32 +350,5 @@ def _get_installed_pam_version() -> str:
|
|
|
195
350
|
return "0.0.0"
|
|
196
351
|
|
|
197
352
|
|
|
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
353
|
if __name__ == "__main__":
|
|
226
354
|
main()
|
pam/models/request_command.py
CHANGED
|
@@ -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
|
pam/request_file_format.py
CHANGED
|
@@ -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
|