pam-python 0.1.34__tar.gz → 0.1.36__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. pam_python-0.1.36/PKG-INFO +257 -0
  2. pam_python-0.1.36/README.md +218 -0
  3. {pam_python-0.1.34 → pam_python-0.1.36}/pam/api.py +6 -3
  4. {pam_python-0.1.34 → pam_python-0.1.36}/pam/cli.py +75 -6
  5. {pam_python-0.1.34 → pam_python-0.1.36}/pam/interface_task_manager.py +2 -2
  6. {pam_python-0.1.34 → pam_python-0.1.36}/pam/models/request_command.py +5 -3
  7. {pam_python-0.1.34 → pam_python-0.1.36}/pam/server.py +12 -3
  8. {pam_python-0.1.34 → pam_python-0.1.36}/pam/task_manager.py +53 -6
  9. {pam_python-0.1.34 → pam_python-0.1.36}/pam/temp_file_utils.py +49 -0
  10. {pam_python-0.1.34 → pam_python-0.1.36}/pam/templates/init/AGENT.md +38 -0
  11. {pam_python-0.1.34 → pam_python-0.1.36}/pam/templates/init/main.tmpl +2 -1
  12. {pam_python-0.1.34 → pam_python-0.1.36}/pam/templates/service/functions.tmpl +17 -1
  13. {pam_python-0.1.34 → pam_python-0.1.36}/pam/templates/service/service.test.tmpl +19 -19
  14. {pam_python-0.1.34 → pam_python-0.1.36}/pam/templates/service/service_class.tmpl +25 -7
  15. {pam_python-0.1.34 → pam_python-0.1.36}/pam/tester_task.py +8 -5
  16. pam_python-0.1.36/pam_python.egg-info/PKG-INFO +257 -0
  17. {pam_python-0.1.34 → pam_python-0.1.36}/setup.py +1 -1
  18. pam_python-0.1.34/PKG-INFO +0 -138
  19. pam_python-0.1.34/README.md +0 -99
  20. pam_python-0.1.34/pam_python.egg-info/PKG-INFO +0 -138
  21. {pam_python-0.1.34 → pam_python-0.1.36}/LICENSE.txt +0 -0
  22. {pam_python-0.1.34 → pam_python-0.1.36}/pam/__init__.py +0 -0
  23. {pam_python-0.1.34 → pam_python-0.1.36}/pam/models/__init__.py +0 -0
  24. {pam_python-0.1.34 → pam_python-0.1.36}/pam/service.py +0 -0
  25. {pam_python-0.1.34 → pam_python-0.1.36}/pam/sqlite.py +0 -0
  26. {pam_python-0.1.34 → pam_python-0.1.36}/pam/templates/buildcmd/pamb +0 -0
  27. {pam_python-0.1.34 → pam_python-0.1.36}/pam/templates/buildcmd/pamb-base.sh +0 -0
  28. {pam_python-0.1.34 → pam_python-0.1.36}/pam/templates/docker/Dockerfile +0 -0
  29. {pam_python-0.1.34 → pam_python-0.1.36}/pam/templates/init/dockerignore.tmpl +0 -0
  30. {pam_python-0.1.34 → pam_python-0.1.36}/pam/templates/init/gitignore.tmpl +0 -0
  31. {pam_python-0.1.34 → pam_python-0.1.36}/pam/templates/init/pylintrc.tmpl +0 -0
  32. {pam_python-0.1.34 → pam_python-0.1.36}/pam/templates/init/run_unit_test.bat +0 -0
  33. {pam_python-0.1.34 → pam_python-0.1.36}/pam/templates/init/run_unit_test.ps1 +0 -0
  34. {pam_python-0.1.34 → pam_python-0.1.36}/pam/templates/init/run_unit_test.sh +0 -0
  35. {pam_python-0.1.34 → pam_python-0.1.36}/pam/templates/service/service.yaml +0 -0
  36. {pam_python-0.1.34 → pam_python-0.1.36}/pam/utils.py +0 -0
  37. {pam_python-0.1.34 → pam_python-0.1.36}/pam_python.egg-info/SOURCES.txt +0 -0
  38. {pam_python-0.1.34 → pam_python-0.1.36}/pam_python.egg-info/dependency_links.txt +0 -0
  39. {pam_python-0.1.34 → pam_python-0.1.36}/pam_python.egg-info/entry_points.txt +0 -0
  40. {pam_python-0.1.34 → pam_python-0.1.36}/pam_python.egg-info/requires.txt +0 -0
  41. {pam_python-0.1.34 → pam_python-0.1.36}/pam_python.egg-info/top_level.txt +0 -0
  42. {pam_python-0.1.34 → pam_python-0.1.36}/setup.cfg +0 -0
@@ -0,0 +1,257 @@
1
+ Metadata-Version: 2.4
2
+ Name: pam-python
3
+ Version: 0.1.36
4
+ Summary: Pam Python Library
5
+ Home-page: https://github.com/heart/pam-python
6
+ Author: Narongrit Kanhanoi
7
+ Author-email: narongrit@pams.ai
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.8
10
+ Classifier: Programming Language :: Python :: 3.9
11
+ Classifier: License :: Other/Proprietary License
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Intended Audience :: Customer Service
15
+ Classifier: Topic :: Software Development :: Libraries
16
+ Requires-Python: >=3.8
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE.txt
19
+ Requires-Dist: setuptools>=70.0.0
20
+ Requires-Dist: Flask>=3.0.2
21
+ Requires-Dist: aiohttp>=3.11.11
22
+ Requires-Dist: pandas>=2.2.3
23
+ Requires-Dist: Faker>=33.1.0
24
+ Requires-Dist: dask>=2024.12.1
25
+ Requires-Dist: dask-expr>=1.1.21
26
+ Requires-Dist: requests>=2.32.3
27
+ Requires-Dist: gunicorn>=23.0.0
28
+ Requires-Dist: pyarrow>=19.0.1
29
+ Dynamic: author
30
+ Dynamic: author-email
31
+ Dynamic: classifier
32
+ Dynamic: description
33
+ Dynamic: description-content-type
34
+ Dynamic: home-page
35
+ Dynamic: license-file
36
+ Dynamic: requires-dist
37
+ Dynamic: requires-python
38
+ Dynamic: summary
39
+
40
+ # pam-python-data-plugin-framework
41
+
42
+ This repository provides the `pam` CLI and runtime framework to build Data Plugin services for PAM Real CDP. It generates a ready-to-run project, standardizes service lifecycle, and handles common tasks like input handling, temp storage, uploads, and service monitoring.
43
+
44
+ This README is a practical, step-by-step guide you can follow to create and run a real service.
45
+
46
+ **What you get**
47
+
48
+ - CLI to initialize a project and scaffold services
49
+ - Service lifecycle contract (start, data input, upload, exit)
50
+ - Temp file and SQLite helpers
51
+ - A monitoring loop for service timeouts and periodic cleanup
52
+
53
+ ---
54
+
55
+ **Table of Contents**
56
+
57
+ 1. Prerequisites
58
+ 2. Install
59
+ 3. Initialize a Project
60
+ 4. Create a Service
61
+ 5. Understand the Lifecycle
62
+ 6. Using Temp Files Correctly
63
+ 7. Running the Server
64
+ 8. Testing a Service
65
+ 9. Configuration
66
+ 10. Project Structure
67
+ 11. Troubleshooting
68
+
69
+ ---
70
+
71
+ **Prerequisites**
72
+
73
+ - Python 3.8+ recommended
74
+ - `pip` and a working virtual environment
75
+
76
+ ---
77
+
78
+ **Install**
79
+ Create a project folder and a virtual environment.
80
+
81
+ ```bash
82
+ mkdir my_data_plugin
83
+ cd my_data_plugin
84
+ python3 -m venv venv
85
+ source venv/bin/activate
86
+ ```
87
+
88
+ Install the framework:
89
+
90
+ ```bash
91
+ pip install pam-python
92
+ ```
93
+
94
+ ---
95
+
96
+ **Initialize a Project**
97
+ This creates a runnable project with templates (including `AGENT.md`).
98
+
99
+ ```bash
100
+ pam init
101
+ ```
102
+
103
+ When `requirements.txt` already exists, you will be prompted to choose how to proceed:
104
+
105
+ - overwrite
106
+ - keep
107
+ - merge
108
+
109
+ ---
110
+
111
+ **Create a Service**
112
+ Generate a service scaffold. Do not hand-create service templates.
113
+
114
+ ```bash
115
+ pam new service rfm_segment
116
+ ```
117
+
118
+ This creates a new folder (e.g. `rfm_segment/`) with:
119
+
120
+ - a service class (`RfmSegmentSvc.py`)
121
+ - `functions.py` for your logic
122
+ - `service.yaml` for registration
123
+ - a test file
124
+
125
+ ---
126
+
127
+ **Understand the Lifecycle**
128
+ The runtime calls your service in two main phases.
129
+
130
+ 1. `on_start`
131
+
132
+ - Called once at the beginning
133
+ - Read parameters from `self.request.runtime_parameters`
134
+ - Should return quickly (start a thread for long work)
135
+
136
+ 2. `on_data_input`
137
+
138
+ - Called when CDP sends input files
139
+ - `req.input_files` contains ordered CSV files
140
+ - Should also return quickly (use a thread if needed)
141
+
142
+ When your service is done:
143
+
144
+ - Call `self._upload_result(...)` or `self._upload_report(...)`
145
+ - Call `self._exit()` to signal completion
146
+
147
+ ---
148
+
149
+ **Using Temp Files Correctly**
150
+ Temp storage is managed by the framework. Do not delete temp files manually.
151
+
152
+ Standard helpers:
153
+
154
+ - `TempfileUtils.get_temp_path_for_service(self, self.service_name)`
155
+ - `TempfileUtils.get_temp_file_name_for_service(self, self.service_name, prefix, extension)`
156
+
157
+ Notes:
158
+
159
+ - `get_temp_path_for_service(...)` returns a directory path without a trailing slash.
160
+ - The temp path includes date/service/token in this structure:
161
+ `TEMP_DATASOURCE_PATH/YYYY_MM_DD/<service>/<token>`
162
+
163
+ ---
164
+
165
+ **Uploading Results in Batches**
166
+ If your service produces too many rows (or too few per event), use the batch uploader to handle chunking and flushing automatically.
167
+
168
+ Recommended usage:
169
+
170
+ ```python
171
+ from pam.result_batch_uploader import ResultBatchUploader
172
+
173
+ batch_uploader = ResultBatchUploader(self, batch_size=50000)
174
+ batch_uploader.upload(df, "data-name")
175
+ batch_uploader.flush()
176
+ status = batch_uploader.get_status()
177
+ ```
178
+
179
+ Notes:
180
+
181
+ - `name` separates different result streams (A/B) to avoid schema conflicts.
182
+ - `flush()` uploads any remaining rows that are below the batch size.
183
+
184
+ ---
185
+
186
+ **Running the Server**
187
+ The generated `main.py` runs the Flask server.
188
+
189
+ ```bash
190
+ python main.py
191
+ ```
192
+
193
+ By default it binds to `0.0.0.0:8000`. You can override with:
194
+
195
+ ```bash
196
+ export SERVER_HOST=0.0.0.0
197
+ export SERVER_PORT=8000
198
+ ```
199
+
200
+ ---
201
+
202
+ **Testing a Service**
203
+ Run unit tests for a service:
204
+
205
+ ```bash
206
+ pam test rfm_segment
207
+ ```
208
+
209
+ If you write custom tests, place them in the service folder and name them `test_<service>.py`.
210
+
211
+ ---
212
+
213
+ **Configuration**
214
+ Environment variables you can set:
215
+
216
+ - `SERVER_HOST`
217
+ - `SERVER_PORT`
218
+ - `TEMP_BASE_PATH` (default `/app/data`)
219
+ - `TEMP_DATASOURCE_PATH` (default `/app/data/data_sources`)
220
+ - `TEMP_CLEAN_DAYS` (default `10`)
221
+ - `TEMP_CLEAN_INTERVAL_HOURS` (default `6`, set empty to disable periodic cleanup)
222
+
223
+ ---
224
+
225
+ **Project Structure**
226
+ After `pam init` and one service:
227
+
228
+ ```
229
+ .
230
+ ├── main.py
231
+ ├── AGENT.md
232
+ ├── Dockerfile
233
+ ├── rfm_segment/
234
+ │ ├── RfmSegmentSvc.py
235
+ │ ├── functions.py
236
+ │ ├── service.yaml
237
+ │ └── test_rfm_segment.py
238
+ ├── requirements.txt
239
+ └── run_unit_test.sh
240
+ ```
241
+
242
+ ---
243
+
244
+ **Troubleshooting**
245
+
246
+ - If `pam` command is missing, ensure your virtualenv is activated.
247
+ - If `pam new service` fails, confirm the service name is provided.
248
+ - If temp cleanup is too frequent or too slow, adjust `TEMP_CLEAN_INTERVAL_HOURS` and `TEMP_CLEAN_DAYS`.
249
+
250
+ ---
251
+
252
+ **Next Steps**
253
+
254
+ - Implement your logic in `functions.py`.
255
+ - Wire it into `on_start` and `on_data_input` in your service class.
256
+ - Use the temp utilities to write intermediate files.
257
+ - Use `_upload_result` to return output to CDP.
@@ -0,0 +1,218 @@
1
+ # pam-python-data-plugin-framework
2
+
3
+ This repository provides the `pam` CLI and runtime framework to build Data Plugin services for PAM Real CDP. It generates a ready-to-run project, standardizes service lifecycle, and handles common tasks like input handling, temp storage, uploads, and service monitoring.
4
+
5
+ This README is a practical, step-by-step guide you can follow to create and run a real service.
6
+
7
+ **What you get**
8
+
9
+ - CLI to initialize a project and scaffold services
10
+ - Service lifecycle contract (start, data input, upload, exit)
11
+ - Temp file and SQLite helpers
12
+ - A monitoring loop for service timeouts and periodic cleanup
13
+
14
+ ---
15
+
16
+ **Table of Contents**
17
+
18
+ 1. Prerequisites
19
+ 2. Install
20
+ 3. Initialize a Project
21
+ 4. Create a Service
22
+ 5. Understand the Lifecycle
23
+ 6. Using Temp Files Correctly
24
+ 7. Running the Server
25
+ 8. Testing a Service
26
+ 9. Configuration
27
+ 10. Project Structure
28
+ 11. Troubleshooting
29
+
30
+ ---
31
+
32
+ **Prerequisites**
33
+
34
+ - Python 3.8+ recommended
35
+ - `pip` and a working virtual environment
36
+
37
+ ---
38
+
39
+ **Install**
40
+ Create a project folder and a virtual environment.
41
+
42
+ ```bash
43
+ mkdir my_data_plugin
44
+ cd my_data_plugin
45
+ python3 -m venv venv
46
+ source venv/bin/activate
47
+ ```
48
+
49
+ Install the framework:
50
+
51
+ ```bash
52
+ pip install pam-python
53
+ ```
54
+
55
+ ---
56
+
57
+ **Initialize a Project**
58
+ This creates a runnable project with templates (including `AGENT.md`).
59
+
60
+ ```bash
61
+ pam init
62
+ ```
63
+
64
+ When `requirements.txt` already exists, you will be prompted to choose how to proceed:
65
+
66
+ - overwrite
67
+ - keep
68
+ - merge
69
+
70
+ ---
71
+
72
+ **Create a Service**
73
+ Generate a service scaffold. Do not hand-create service templates.
74
+
75
+ ```bash
76
+ pam new service rfm_segment
77
+ ```
78
+
79
+ This creates a new folder (e.g. `rfm_segment/`) with:
80
+
81
+ - a service class (`RfmSegmentSvc.py`)
82
+ - `functions.py` for your logic
83
+ - `service.yaml` for registration
84
+ - a test file
85
+
86
+ ---
87
+
88
+ **Understand the Lifecycle**
89
+ The runtime calls your service in two main phases.
90
+
91
+ 1. `on_start`
92
+
93
+ - Called once at the beginning
94
+ - Read parameters from `self.request.runtime_parameters`
95
+ - Should return quickly (start a thread for long work)
96
+
97
+ 2. `on_data_input`
98
+
99
+ - Called when CDP sends input files
100
+ - `req.input_files` contains ordered CSV files
101
+ - Should also return quickly (use a thread if needed)
102
+
103
+ When your service is done:
104
+
105
+ - Call `self._upload_result(...)` or `self._upload_report(...)`
106
+ - Call `self._exit()` to signal completion
107
+
108
+ ---
109
+
110
+ **Using Temp Files Correctly**
111
+ Temp storage is managed by the framework. Do not delete temp files manually.
112
+
113
+ Standard helpers:
114
+
115
+ - `TempfileUtils.get_temp_path_for_service(self, self.service_name)`
116
+ - `TempfileUtils.get_temp_file_name_for_service(self, self.service_name, prefix, extension)`
117
+
118
+ Notes:
119
+
120
+ - `get_temp_path_for_service(...)` returns a directory path without a trailing slash.
121
+ - The temp path includes date/service/token in this structure:
122
+ `TEMP_DATASOURCE_PATH/YYYY_MM_DD/<service>/<token>`
123
+
124
+ ---
125
+
126
+ **Uploading Results in Batches**
127
+ If your service produces too many rows (or too few per event), use the batch uploader to handle chunking and flushing automatically.
128
+
129
+ Recommended usage:
130
+
131
+ ```python
132
+ from pam.result_batch_uploader import ResultBatchUploader
133
+
134
+ batch_uploader = ResultBatchUploader(self, batch_size=50000)
135
+ batch_uploader.upload(df, "data-name")
136
+ batch_uploader.flush()
137
+ status = batch_uploader.get_status()
138
+ ```
139
+
140
+ Notes:
141
+
142
+ - `name` separates different result streams (A/B) to avoid schema conflicts.
143
+ - `flush()` uploads any remaining rows that are below the batch size.
144
+
145
+ ---
146
+
147
+ **Running the Server**
148
+ The generated `main.py` runs the Flask server.
149
+
150
+ ```bash
151
+ python main.py
152
+ ```
153
+
154
+ By default it binds to `0.0.0.0:8000`. You can override with:
155
+
156
+ ```bash
157
+ export SERVER_HOST=0.0.0.0
158
+ export SERVER_PORT=8000
159
+ ```
160
+
161
+ ---
162
+
163
+ **Testing a Service**
164
+ Run unit tests for a service:
165
+
166
+ ```bash
167
+ pam test rfm_segment
168
+ ```
169
+
170
+ If you write custom tests, place them in the service folder and name them `test_<service>.py`.
171
+
172
+ ---
173
+
174
+ **Configuration**
175
+ Environment variables you can set:
176
+
177
+ - `SERVER_HOST`
178
+ - `SERVER_PORT`
179
+ - `TEMP_BASE_PATH` (default `/app/data`)
180
+ - `TEMP_DATASOURCE_PATH` (default `/app/data/data_sources`)
181
+ - `TEMP_CLEAN_DAYS` (default `10`)
182
+ - `TEMP_CLEAN_INTERVAL_HOURS` (default `6`, set empty to disable periodic cleanup)
183
+
184
+ ---
185
+
186
+ **Project Structure**
187
+ After `pam init` and one service:
188
+
189
+ ```
190
+ .
191
+ ├── main.py
192
+ ├── AGENT.md
193
+ ├── Dockerfile
194
+ ├── rfm_segment/
195
+ │ ├── RfmSegmentSvc.py
196
+ │ ├── functions.py
197
+ │ ├── service.yaml
198
+ │ └── test_rfm_segment.py
199
+ ├── requirements.txt
200
+ └── run_unit_test.sh
201
+ ```
202
+
203
+ ---
204
+
205
+ **Troubleshooting**
206
+
207
+ - If `pam` command is missing, ensure your virtualenv is activated.
208
+ - If `pam new service` fails, confirm the service name is provided.
209
+ - If temp cleanup is too frequent or too slow, adjust `TEMP_CLEAN_INTERVAL_HOURS` and `TEMP_CLEAN_DAYS`.
210
+
211
+ ---
212
+
213
+ **Next Steps**
214
+
215
+ - Implement your logic in `functions.py`.
216
+ - Wire it into `on_start` and `on_data_input` in your service class.
217
+ - Use the temp utilities to write intermediate files.
218
+ - Use `_upload_result` to return output to CDP.
@@ -66,12 +66,15 @@ class API:
66
66
  """
67
67
  headers = {'Content-Type': 'application/json'}
68
68
  try:
69
- response = self.session.post(url, json=data, timeout=60, headers=headers)
69
+ response = self.session.post(
70
+ url, json=data, timeout=60, headers=headers, stream=True
71
+ )
70
72
  response.raise_for_status()
71
73
 
72
- # Save response content to file
73
74
  with open(output_path, "wb") as f:
74
- f.write(response.content)
75
+ for chunk in response.iter_content(chunk_size=1024 * 1024):
76
+ if chunk:
77
+ f.write(chunk)
75
78
 
76
79
  log(f"SQLite file saved to {output_path}")
77
80
  return True
@@ -16,12 +16,20 @@ def main():
16
16
  if cmd == "init":
17
17
  init_project()
18
18
  elif cmd == "new":
19
+ if len(args) < 2:
20
+ print("Usage: pam new service <service_name>")
21
+ return
19
22
  create_type = args[1]
20
23
  if create_type == "service":
24
+ if len(args) < 3:
25
+ print("Usage: pam new service <service_name>")
26
+ return
21
27
  name = args[2]
22
28
  create_service(name)
29
+ else:
30
+ print(f"Unknown new target: {create_type}")
23
31
  else:
24
- print(f"Unknown command: {args.command}")
32
+ print(f"Unknown command: {cmd}")
25
33
 
26
34
 
27
35
  def to_pascal_case(input_string: str) -> str:
@@ -99,14 +107,75 @@ def init_project():
99
107
  cpy("init/run_unit_test.ps1", "run_unit_test.ps1")
100
108
  cpy("init/AGENT.md", "AGENT.md")
101
109
 
102
- if not os.path.exists("requirements.txt"):
103
- open("requirements.txt", 'a', encoding='utf-8').close()
104
-
105
- with open("requirements.txt", "w", encoding='utf-8') as f:
106
- subprocess.run(["pip", "freeze"], stdout=f, check=True)
110
+ requirements_path = "requirements.txt"
111
+ has_requirements = os.path.exists(requirements_path)
112
+
113
+ mode = "overwrite"
114
+ if has_requirements:
115
+ print("requirements.txt already exists. Choose how to proceed:")
116
+ print("1) overwrite")
117
+ print("2) keep")
118
+ print("3) merge")
119
+ selection = input("Select [1-3] (default: 1): ").strip()
120
+ if selection == "2":
121
+ mode = "keep"
122
+ elif selection == "3":
123
+ mode = "merge"
124
+
125
+ if mode == "keep":
126
+ print("Keeping existing requirements.txt")
127
+ else:
128
+ freeze = subprocess.run(
129
+ ["pip", "freeze"],
130
+ check=True,
131
+ text=True,
132
+ capture_output=True
133
+ ).stdout.splitlines()
134
+
135
+ if mode == "overwrite" or not has_requirements:
136
+ with open(requirements_path, "w", encoding="utf-8") as f:
137
+ f.write("\n".join(freeze) + ("\n" if freeze else ""))
138
+ elif mode == "merge":
139
+ with open(requirements_path, "r", encoding="utf-8") as f:
140
+ existing_lines = f.read().splitlines()
141
+ merged_lines = _merge_requirements(existing_lines, freeze)
142
+ with open(requirements_path, "w", encoding="utf-8") as f:
143
+ f.write("\n".join(merged_lines) + ("\n" if merged_lines else ""))
107
144
 
108
145
  if not os.path.exists("__init__.py"):
109
146
  open("__init__.py", 'a', encoding='utf-8').close()
110
147
 
111
148
  print("--- Welcome to PAM ---\n")
112
149
  print("To create a new servive run\n`pam new service <service_name>`\n\n")
150
+
151
+
152
+ def _parse_requirement_name(line: str) -> str:
153
+ line = line.strip()
154
+ if not line or line.startswith("#"):
155
+ return ""
156
+ if line.startswith("-"):
157
+ return line
158
+ # Normalize common requirement formats like pkg==1.2, pkg>=1.2, pkg[extra]==1.2
159
+ splitters = ["==", ">=", "<=", "~=", ">", "<"]
160
+ for splitter in splitters:
161
+ if splitter in line:
162
+ return line.split(splitter, 1)[0].strip()
163
+ return line
164
+
165
+
166
+ def _merge_requirements(existing_lines, frozen_lines):
167
+ """
168
+ Merge frozen requirements into existing lines while keeping existing entries.
169
+ """
170
+ existing_keys = {_parse_requirement_name(line) for line in existing_lines}
171
+ existing_keys.discard("")
172
+
173
+ merged = list(existing_lines)
174
+ for line in frozen_lines:
175
+ key = _parse_requirement_name(line)
176
+ if not key:
177
+ continue
178
+ if key not in existing_keys:
179
+ merged.append(line)
180
+ existing_keys.add(key)
181
+ return merged
@@ -22,11 +22,11 @@ class ITaskManager(ABC):
22
22
  pass
23
23
 
24
24
  @abstractmethod
25
- def service_request_sqlite(self, service: Service, file_name: str = "", is_shared: bool = False) -> str:
25
+ def service_request_sqlite(self, service: Service, file_name: str = "", is_shared: bool = False):
26
26
  pass
27
27
 
28
28
  @abstractmethod
29
- def service_upload_sqlite(self, service: Service, file_name: str = "", is_shared: bool = False, sqlite_file: str = "") -> str:
29
+ def service_upload_sqlite(self, service: Service, file_name: str = "", is_shared: bool = False, sqlite_file: str = ""):
30
30
  pass
31
31
 
32
32
  @abstractmethod
@@ -19,6 +19,7 @@ Methods:
19
19
 
20
20
  """
21
21
  import os
22
+ import shutil
22
23
  from pathlib import Path
23
24
  from dataclasses import dataclass
24
25
  import zipfile
@@ -69,6 +70,8 @@ class RequestCommand:
69
70
  """
70
71
  Extract data from a Flask request object based on its content type.
71
72
  """
73
+ if request.content_type is None:
74
+ return {}
72
75
  if request.content_type == 'application/json':
73
76
  return request.get_json() or {}
74
77
  elif request.content_type.startswith('multipart/form-data'):
@@ -93,12 +96,11 @@ class RequestCommand:
93
96
  log("Invalid file type uploaded.")
94
97
  return [], "Invalid file type"
95
98
 
96
- temp_dir = TempfileUtils.get_temp_file_name(service_name, token, "dataset_")
99
+ temp_dir = TempfileUtils.get_temp_subdir(service_name, token, "dataset_")
97
100
  zip_file_path = f"{temp_dir}.zip"
98
101
  zip_file.save(zip_file_path)
99
102
 
100
103
  extract_dir = Path(temp_dir)
101
- extract_dir.mkdir(parents=True, exist_ok=True)
102
104
 
103
105
  try:
104
106
  with zipfile.ZipFile(zip_file_path, 'r') as zip_ref:
@@ -109,7 +111,7 @@ class RequestCommand:
109
111
  unwanted_path = extract_dir / unwanted
110
112
  if unwanted_path.exists():
111
113
  if unwanted_path.is_dir():
112
- os.rmdir(unwanted_path)
114
+ shutil.rmtree(unwanted_path, ignore_errors=True)
113
115
  else:
114
116
  unwanted_path.unlink()
115
117
 
@@ -12,7 +12,6 @@ from pam.models.request_command import RequestCommand
12
12
  from pam.task_manager import TaskManager
13
13
  from pam.temp_file_utils import TempfileUtils
14
14
  from threading import Lock
15
- from pam.utils import log
16
15
 
17
16
  class Server:
18
17
  """
@@ -23,9 +22,10 @@ class Server:
23
22
  servicePool = {}
24
23
  _lock = Lock()
25
24
 
26
- def __init__(self, app: Flask):
25
+ def __init__(self, app: Flask, temp_clean_days: int = None):
27
26
  self.task_manager = TaskManager(self)
28
27
  self.app = app
28
+ self.temp_clean_days = self._resolve_temp_clean_days(temp_clean_days)
29
29
  self.register_service()
30
30
  self.task_manager.start_service_monitoring_schedul()
31
31
 
@@ -36,7 +36,7 @@ class Server:
36
36
  @app.route('/service/<service_name>', methods=['POST'])
37
37
  def run(service_name):
38
38
  log(f"[run] service = {service_name}")
39
- TempfileUtils.clean_temp()
39
+ TempfileUtils.clean_temp(self.temp_clean_days)
40
40
  log(f"[run] service = {service_name} after clean_temp")
41
41
  req, error_request = RequestCommand.parse(request, service_name)
42
42
  if error_request:
@@ -119,6 +119,15 @@ class Server:
119
119
  data = jsonify(json)
120
120
  return make_response(data, code)
121
121
 
122
+ @staticmethod
123
+ def _resolve_temp_clean_days(temp_clean_days: int):
124
+ if temp_clean_days is not None:
125
+ return temp_clean_days
126
+ try:
127
+ return int(os.getenv("TEMP_CLEAN_DAYS", "10"))
128
+ except ValueError:
129
+ return 10
130
+
122
131
  def response_ok(self, json: dict, code: int = 200):
123
132
  """
124
133
  Create a response object with 200 OK.