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.
- pam_python-0.1.36/PKG-INFO +257 -0
- pam_python-0.1.36/README.md +218 -0
- {pam_python-0.1.34 → pam_python-0.1.36}/pam/api.py +6 -3
- {pam_python-0.1.34 → pam_python-0.1.36}/pam/cli.py +75 -6
- {pam_python-0.1.34 → pam_python-0.1.36}/pam/interface_task_manager.py +2 -2
- {pam_python-0.1.34 → pam_python-0.1.36}/pam/models/request_command.py +5 -3
- {pam_python-0.1.34 → pam_python-0.1.36}/pam/server.py +12 -3
- {pam_python-0.1.34 → pam_python-0.1.36}/pam/task_manager.py +53 -6
- {pam_python-0.1.34 → pam_python-0.1.36}/pam/temp_file_utils.py +49 -0
- {pam_python-0.1.34 → pam_python-0.1.36}/pam/templates/init/AGENT.md +38 -0
- {pam_python-0.1.34 → pam_python-0.1.36}/pam/templates/init/main.tmpl +2 -1
- {pam_python-0.1.34 → pam_python-0.1.36}/pam/templates/service/functions.tmpl +17 -1
- {pam_python-0.1.34 → pam_python-0.1.36}/pam/templates/service/service.test.tmpl +19 -19
- {pam_python-0.1.34 → pam_python-0.1.36}/pam/templates/service/service_class.tmpl +25 -7
- {pam_python-0.1.34 → pam_python-0.1.36}/pam/tester_task.py +8 -5
- pam_python-0.1.36/pam_python.egg-info/PKG-INFO +257 -0
- {pam_python-0.1.34 → pam_python-0.1.36}/setup.py +1 -1
- pam_python-0.1.34/PKG-INFO +0 -138
- pam_python-0.1.34/README.md +0 -99
- pam_python-0.1.34/pam_python.egg-info/PKG-INFO +0 -138
- {pam_python-0.1.34 → pam_python-0.1.36}/LICENSE.txt +0 -0
- {pam_python-0.1.34 → pam_python-0.1.36}/pam/__init__.py +0 -0
- {pam_python-0.1.34 → pam_python-0.1.36}/pam/models/__init__.py +0 -0
- {pam_python-0.1.34 → pam_python-0.1.36}/pam/service.py +0 -0
- {pam_python-0.1.34 → pam_python-0.1.36}/pam/sqlite.py +0 -0
- {pam_python-0.1.34 → pam_python-0.1.36}/pam/templates/buildcmd/pamb +0 -0
- {pam_python-0.1.34 → pam_python-0.1.36}/pam/templates/buildcmd/pamb-base.sh +0 -0
- {pam_python-0.1.34 → pam_python-0.1.36}/pam/templates/docker/Dockerfile +0 -0
- {pam_python-0.1.34 → pam_python-0.1.36}/pam/templates/init/dockerignore.tmpl +0 -0
- {pam_python-0.1.34 → pam_python-0.1.36}/pam/templates/init/gitignore.tmpl +0 -0
- {pam_python-0.1.34 → pam_python-0.1.36}/pam/templates/init/pylintrc.tmpl +0 -0
- {pam_python-0.1.34 → pam_python-0.1.36}/pam/templates/init/run_unit_test.bat +0 -0
- {pam_python-0.1.34 → pam_python-0.1.36}/pam/templates/init/run_unit_test.ps1 +0 -0
- {pam_python-0.1.34 → pam_python-0.1.36}/pam/templates/init/run_unit_test.sh +0 -0
- {pam_python-0.1.34 → pam_python-0.1.36}/pam/templates/service/service.yaml +0 -0
- {pam_python-0.1.34 → pam_python-0.1.36}/pam/utils.py +0 -0
- {pam_python-0.1.34 → pam_python-0.1.36}/pam_python.egg-info/SOURCES.txt +0 -0
- {pam_python-0.1.34 → pam_python-0.1.36}/pam_python.egg-info/dependency_links.txt +0 -0
- {pam_python-0.1.34 → pam_python-0.1.36}/pam_python.egg-info/entry_points.txt +0 -0
- {pam_python-0.1.34 → pam_python-0.1.36}/pam_python.egg-info/requires.txt +0 -0
- {pam_python-0.1.34 → pam_python-0.1.36}/pam_python.egg-info/top_level.txt +0 -0
- {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(
|
|
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
|
-
|
|
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: {
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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)
|
|
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 = "")
|
|
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.
|
|
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
|
-
|
|
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.
|