pam-python 0.1.32__tar.gz → 0.1.34__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.32 → pam_python-0.1.34}/PKG-INFO +1 -1
- {pam_python-0.1.32 → pam_python-0.1.34}/pam/service.py +11 -0
- pam_python-0.1.34/pam/templates/init/AGENT.md +419 -0
- {pam_python-0.1.32 → pam_python-0.1.34}/pam_python.egg-info/PKG-INFO +1 -1
- {pam_python-0.1.32 → pam_python-0.1.34}/setup.py +1 -1
- pam_python-0.1.32/pam/templates/init/AGENT.md +0 -197
- {pam_python-0.1.32 → pam_python-0.1.34}/LICENSE.txt +0 -0
- {pam_python-0.1.32 → pam_python-0.1.34}/README.md +0 -0
- {pam_python-0.1.32 → pam_python-0.1.34}/pam/__init__.py +0 -0
- {pam_python-0.1.32 → pam_python-0.1.34}/pam/api.py +0 -0
- {pam_python-0.1.32 → pam_python-0.1.34}/pam/cli.py +0 -0
- {pam_python-0.1.32 → pam_python-0.1.34}/pam/interface_task_manager.py +0 -0
- {pam_python-0.1.32 → pam_python-0.1.34}/pam/models/__init__.py +0 -0
- {pam_python-0.1.32 → pam_python-0.1.34}/pam/models/request_command.py +0 -0
- {pam_python-0.1.32 → pam_python-0.1.34}/pam/server.py +0 -0
- {pam_python-0.1.32 → pam_python-0.1.34}/pam/sqlite.py +0 -0
- {pam_python-0.1.32 → pam_python-0.1.34}/pam/task_manager.py +0 -0
- {pam_python-0.1.32 → pam_python-0.1.34}/pam/temp_file_utils.py +0 -0
- {pam_python-0.1.32 → pam_python-0.1.34}/pam/templates/buildcmd/pamb +0 -0
- {pam_python-0.1.32 → pam_python-0.1.34}/pam/templates/buildcmd/pamb-base.sh +0 -0
- {pam_python-0.1.32 → pam_python-0.1.34}/pam/templates/docker/Dockerfile +0 -0
- {pam_python-0.1.32 → pam_python-0.1.34}/pam/templates/init/dockerignore.tmpl +0 -0
- {pam_python-0.1.32 → pam_python-0.1.34}/pam/templates/init/gitignore.tmpl +0 -0
- {pam_python-0.1.32 → pam_python-0.1.34}/pam/templates/init/main.tmpl +0 -0
- {pam_python-0.1.32 → pam_python-0.1.34}/pam/templates/init/pylintrc.tmpl +0 -0
- {pam_python-0.1.32 → pam_python-0.1.34}/pam/templates/init/run_unit_test.bat +0 -0
- {pam_python-0.1.32 → pam_python-0.1.34}/pam/templates/init/run_unit_test.ps1 +0 -0
- {pam_python-0.1.32 → pam_python-0.1.34}/pam/templates/init/run_unit_test.sh +0 -0
- {pam_python-0.1.32 → pam_python-0.1.34}/pam/templates/service/functions.tmpl +0 -0
- {pam_python-0.1.32 → pam_python-0.1.34}/pam/templates/service/service.test.tmpl +0 -0
- {pam_python-0.1.32 → pam_python-0.1.34}/pam/templates/service/service.yaml +0 -0
- {pam_python-0.1.32 → pam_python-0.1.34}/pam/templates/service/service_class.tmpl +0 -0
- {pam_python-0.1.32 → pam_python-0.1.34}/pam/tester_task.py +0 -0
- {pam_python-0.1.32 → pam_python-0.1.34}/pam/utils.py +0 -0
- {pam_python-0.1.32 → pam_python-0.1.34}/pam_python.egg-info/SOURCES.txt +0 -0
- {pam_python-0.1.32 → pam_python-0.1.34}/pam_python.egg-info/dependency_links.txt +0 -0
- {pam_python-0.1.32 → pam_python-0.1.34}/pam_python.egg-info/entry_points.txt +0 -0
- {pam_python-0.1.32 → pam_python-0.1.34}/pam_python.egg-info/requires.txt +0 -0
- {pam_python-0.1.32 → pam_python-0.1.34}/pam_python.egg-info/top_level.txt +0 -0
- {pam_python-0.1.32 → pam_python-0.1.34}/setup.cfg +0 -0
|
@@ -80,6 +80,12 @@ class Service:
|
|
|
80
80
|
if not isinstance(report_json, dict):
|
|
81
81
|
raise ValueError("report_json must be a dictionary")
|
|
82
82
|
|
|
83
|
+
try:
|
|
84
|
+
pretty_report_json = json.dumps(report_json, indent=2, ensure_ascii=False, sort_keys=True)
|
|
85
|
+
except Exception:
|
|
86
|
+
pretty_report_json = str(report_json)
|
|
87
|
+
log(f"{self.request.service_name}: report_json\n{pretty_report_json}")
|
|
88
|
+
|
|
83
89
|
adapter_id = get_adapter_id(self.request.response_api)
|
|
84
90
|
service_name = self.request.service_name
|
|
85
91
|
token = self.request.token
|
|
@@ -129,6 +135,11 @@ class Service:
|
|
|
129
135
|
:param sqlite_file: Path to the SQLite file to upload.
|
|
130
136
|
:return: The uploaded file name if successful, or empty string if failed.
|
|
131
137
|
"""
|
|
138
|
+
dry_run = self.request.runtime_parameters.get("dry_run", "false")
|
|
139
|
+
if dry_run is not None and dry_run.lower() == "true":
|
|
140
|
+
log("Dry run mode will not upload sqlite.")
|
|
141
|
+
return sqlite_file
|
|
142
|
+
|
|
132
143
|
return self.task_manager.service_upload_sqlite(self, file_name, is_shared, sqlite_file)
|
|
133
144
|
|
|
134
145
|
def _exit(self) -> None:
|
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
# AGENT
|
|
2
|
+
|
|
3
|
+
This document defines the **data plugin runtime specification** and execution contract so developers and agents can create services and understand runtime behavior inside the CDP environment.
|
|
4
|
+
|
|
5
|
+
This is not a tutorial.
|
|
6
|
+
This document defines behavioral rules, lifecycle expectations, and safety constraints enforced by the platform.
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# 1. Runtime Overview
|
|
11
|
+
|
|
12
|
+
## Project Overview
|
|
13
|
+
|
|
14
|
+
1. This project is a data plugin that will be connected to the main system in production.
|
|
15
|
+
2. The data plugin processes data from CDP and is created as separate services by domain. For example, if we process RFM, we create a new service dedicated to that domain.
|
|
16
|
+
|
|
17
|
+
CDP spawns a service instance when a job starts and terminates it after the job completes. This design supports parallel execution. For example, two runs will spawn two separate instances. The only risk is shared local files (e.g., sqlite) which can cause conflicts.
|
|
18
|
+
|
|
19
|
+
The lifecycle is wired in `rfm_segment/Rfm_segmentSvc.py` (example service name).
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
# 2. Service Creation Contract
|
|
24
|
+
|
|
25
|
+
## Create a New Service
|
|
26
|
+
|
|
27
|
+
1. Create a service with:
|
|
28
|
+
`pam new service [service name]`
|
|
29
|
+
2. The system creates a new folder named `[service name]` with sample code and basic templates.
|
|
30
|
+
3. Example:
|
|
31
|
+
`pam new service rfm_segment`
|
|
32
|
+
This generates `rfm_segment/Rfm_segmentSvc.py`, which is the starting point of the service.
|
|
33
|
+
4. `rfm_segment/service.yaml` registers the service automatically.
|
|
34
|
+
The value `endpoint: /Rfm_segmentSvc` means CDP will call:
|
|
35
|
+
`http://ip-of-this-k8s-pod/services/Rfm_segmentSvc`
|
|
36
|
+
|
|
37
|
+
**IMPORTANT**
|
|
38
|
+
You MUST activate the virtual environment before running `pam` commands.
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Service scaffolding (MUST use pam generator)
|
|
43
|
+
|
|
44
|
+
### Rules
|
|
45
|
+
|
|
46
|
+
- Do NOT create new files or folders manually for service scaffolding.
|
|
47
|
+
- Always generate the service by running the `pam` generator command via a Python virtualenv that has `pam` installed.
|
|
48
|
+
- Only edit files AFTER the generator has created the full template.
|
|
49
|
+
- If the generator cannot be executed, STOP and report the exact error. Do NOT hand-roll templates.
|
|
50
|
+
|
|
51
|
+
### Why (important)
|
|
52
|
+
|
|
53
|
+
The `pam` CLI is a Python console_script pointing to `pam.cli:main` (source: `pam/cli.py`).
|
|
54
|
+
Do NOT rely on `activate` because the agent may execute commands in separate processes.
|
|
55
|
+
|
|
56
|
+
### Virtualenv discovery
|
|
57
|
+
|
|
58
|
+
Prefer an existing venv in the repo:
|
|
59
|
+
|
|
60
|
+
- `.venv/`
|
|
61
|
+
- `venv/`
|
|
62
|
+
- otherwise: any directory containing `pyvenv.cfg` at its root
|
|
63
|
+
|
|
64
|
+
### Run generator (preferred order)
|
|
65
|
+
|
|
66
|
+
Once a venv is found, run ONE of the following:
|
|
67
|
+
|
|
68
|
+
#### 1. Run through venv python as a module (preferred)
|
|
69
|
+
|
|
70
|
+
- macOS/Linux:
|
|
71
|
+
- `<VENV>/bin/python -m pam.cli new service <service_name>`
|
|
72
|
+
|
|
73
|
+
- Windows:
|
|
74
|
+
- `<VENV>\Scripts\python -m pam.cli new service <service_name>`
|
|
75
|
+
|
|
76
|
+
#### 2. If the above fails, run the console_script shim created by pip (still must be inside the venv)
|
|
77
|
+
|
|
78
|
+
- macOS/Linux:
|
|
79
|
+
- `<VENV>/bin/pam new service <service_name>`
|
|
80
|
+
|
|
81
|
+
- Windows:
|
|
82
|
+
- `<VENV>\Scripts\pam new service <service_name>`
|
|
83
|
+
|
|
84
|
+
### Hard stop condition
|
|
85
|
+
|
|
86
|
+
- If no venv is found OR `pam` cannot be imported/executed from the venv:
|
|
87
|
+
- STOP and report the error.
|
|
88
|
+
- Do NOT create the template manually.
|
|
89
|
+
|
|
90
|
+
### Verification step (required)
|
|
91
|
+
|
|
92
|
+
- After running the generator, list generated files and confirm the expected structure exists before editing anything.
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## Code Organization Guidelines
|
|
97
|
+
|
|
98
|
+
- Separate each lifecycle step into clear, dedicated functions. Add comments at critical points so humans can review and audit later.
|
|
99
|
+
- Avoid nested functions unless absolutely necessary.
|
|
100
|
+
- Put service logic in `[service_name]/functions.py`, then import and use it from the service class to keep the overall code easy to read.
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
# 3. Lifecycle Execution Contract
|
|
105
|
+
|
|
106
|
+
## on_start
|
|
107
|
+
|
|
108
|
+
`on_start` runs at startup. Use it to read runtime parameters from CDP and initialize work.
|
|
109
|
+
|
|
110
|
+
Example:
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
def on_start(self):
|
|
114
|
+
log("on_start")
|
|
115
|
+
|
|
116
|
+
some_param = self.request.runtime_parameters.get("some_param", "")
|
|
117
|
+
self._request_data()
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
`runtime_parameters` are configured by CDP to control service behavior without code changes (e.g., `period_day="7"`).
|
|
121
|
+
|
|
122
|
+
Notes
|
|
123
|
+
|
|
124
|
+
- All values are strings. Cast with defaults to avoid errors.
|
|
125
|
+
- `dry_run` is the only built-in parameter. If missing, it runs for real. If `dry_run="true"`, the service runs fully but does not upload results to CDP.
|
|
126
|
+
- You do not need to check `dry_run` manually. Always call `_upload_result`; the system handles it.
|
|
127
|
+
|
|
128
|
+
**Important:** `on_start` must return quickly because CDP calls via HTTP. If it hangs, the client may time out and the job will be terminated. If work is long-running, move it to a thread and return from `on_start` first. This applies to all lifecycle functions.
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## Request Data from CDP
|
|
133
|
+
|
|
134
|
+
Call `self._request_data()` to ask CDP for input data. CDP returns up to 5 CSV files (each file is a separate event).
|
|
135
|
+
|
|
136
|
+
- File order is guaranteed per agreement, so you can index by position.
|
|
137
|
+
- Column names and record counts are defined by CDP configuration.
|
|
138
|
+
|
|
139
|
+
CSV example:
|
|
140
|
+
|
|
141
|
+
```csv
|
|
142
|
+
_id,id,data1,data2,data3
|
|
143
|
+
xx,yy,zz,nn,mm
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Column meaning
|
|
147
|
+
|
|
148
|
+
- `_id` = event id for dedup/processing checks. Often unused and can be dropped.
|
|
149
|
+
- `id` = customer id used by CDP as a key.
|
|
150
|
+
- Other columns depend on the agreement.
|
|
151
|
+
|
|
152
|
+
Date format is always:
|
|
153
|
+
`"02/08/2026, 18:23:54"`
|
|
154
|
+
Timezone may vary by customer data (usually Bangkok time).
|
|
155
|
+
You may sometimes be asked for the exact time zone if it is logically important for the program.
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## on_data_input
|
|
160
|
+
|
|
161
|
+
When CDP finishes collecting data, it calls `on_data_input`.
|
|
162
|
+
|
|
163
|
+
Example:
|
|
164
|
+
|
|
165
|
+
```python
|
|
166
|
+
def on_data_input(self, req: RequestCommand):
|
|
167
|
+
log(f"on_data_input req.is_end = {req.is_end}")
|
|
168
|
+
|
|
169
|
+
# RUN IN A SEPARATED THREAD TO PREVENT HTTP REQUEST BLOCKING
|
|
170
|
+
thread = threading.Thread(
|
|
171
|
+
target=lambda: self.__run_process_data_in_thread(req),
|
|
172
|
+
daemon=True,
|
|
173
|
+
)
|
|
174
|
+
thread.start()
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def __run_process_data_in_thread(self, req: RequestCommand):
|
|
178
|
+
dataframe = self.__process_data(req.input_files)
|
|
179
|
+
self._upload_result(dataframe)
|
|
180
|
+
|
|
181
|
+
if not req.is_end:
|
|
182
|
+
self._request_data(req.next)
|
|
183
|
+
else:
|
|
184
|
+
self._exit()
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Why a thread: to end the HTTP request quickly and avoid timeouts.
|
|
188
|
+
|
|
189
|
+
`req.input_files` is the ordered list of CSV paths.
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## Upload Results to CDP
|
|
194
|
+
|
|
195
|
+
After processing, call `_upload_result` with a dataframe. Example output:
|
|
196
|
+
|
|
197
|
+
```csv
|
|
198
|
+
id,data_x,rfm
|
|
199
|
+
xxx,20,hero
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
- `id` is required for CDP matching.
|
|
203
|
+
- Other columns are defined by agreement.
|
|
204
|
+
|
|
205
|
+
Sometimes you may include an `event` column to create per-user events:
|
|
206
|
+
|
|
207
|
+
```csv
|
|
208
|
+
id,data_x,event
|
|
209
|
+
xxx,20,purchase
|
|
210
|
+
yyy,50,refund
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
If `event` is missing, CDP uses the default event configured in the system.
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
## Pagination
|
|
218
|
+
|
|
219
|
+
```python
|
|
220
|
+
if not req.is_end:
|
|
221
|
+
self._request_data(req.next)
|
|
222
|
+
else:
|
|
223
|
+
self._exit()
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
- `req.is_end` indicates whether this is the last page.
|
|
227
|
+
- If not end, call `self._request_data(req.next)` for the next page.
|
|
228
|
+
- If end, call `self._exit()` to terminate the instance.
|
|
229
|
+
|
|
230
|
+
CDP will not send the next page until you call `_request_data`, so the flow is serialized and no two threads will handle the same data unless you explicitly design it that way.
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
## Paging Model (Customer-based Pagination Contract)
|
|
235
|
+
|
|
236
|
+
CDP paging is **customer-based**, not row-based.
|
|
237
|
+
|
|
238
|
+
When requesting data page by page:
|
|
239
|
+
|
|
240
|
+
- CDP collects customers in batches of approximately **10,000 customers per page**.
|
|
241
|
+
- All events belonging to those customers are merged into **a single CSV file**.
|
|
242
|
+
- The number of rows is **unknown** and depends on how many events each customer has.
|
|
243
|
+
|
|
244
|
+
This means:
|
|
245
|
+
|
|
246
|
+
- Each page represents a logical customer group, not a fixed number of records.
|
|
247
|
+
- A page may contain a small or very large number of rows depending on behavior density.
|
|
248
|
+
|
|
249
|
+
### Execution Implications
|
|
250
|
+
|
|
251
|
+
Because paging is based on customers:
|
|
252
|
+
|
|
253
|
+
- In many cases, a service can process **one page (≈10,000 customers)** independently.
|
|
254
|
+
- If the logic allows, you may process and upload results immediately because the dataset is self-contained.
|
|
255
|
+
|
|
256
|
+
However, upload behavior must balance two constraints:
|
|
257
|
+
|
|
258
|
+
- Avoid uploading too frequently (too many small uploads).
|
|
259
|
+
- Avoid generating very large files that exceed safe batch size.
|
|
260
|
+
|
|
261
|
+
### Recommended Strategy
|
|
262
|
+
|
|
263
|
+
Example approach:
|
|
264
|
+
|
|
265
|
+
- If each page represents ~10,000 customers:
|
|
266
|
+
- You may accumulate multiple pages locally until reaching the configured `batch_size`.
|
|
267
|
+
- Then upload results in a single batch to reduce upload overhead.
|
|
268
|
+
|
|
269
|
+
Conversely:
|
|
270
|
+
|
|
271
|
+
- If processing local files, sqlite, or duckdb produces a very large result in one pass:
|
|
272
|
+
- You MUST split output according to `batch_size`.
|
|
273
|
+
- Do NOT upload extremely large files in one call.
|
|
274
|
+
|
|
275
|
+
### Key Principle
|
|
276
|
+
|
|
277
|
+
Paging size (≈10,000 customers) and upload batch size (`batch_size`) are **independent controls**:
|
|
278
|
+
|
|
279
|
+
- Paging controls **input grouping**.
|
|
280
|
+
- Batch size controls **output safety**.
|
|
281
|
+
|
|
282
|
+
Services must design processing flow with both limits in mind.
|
|
283
|
+
|
|
284
|
+
---
|
|
285
|
+
|
|
286
|
+
## on_terminate
|
|
287
|
+
|
|
288
|
+
Before termination, the system calls:
|
|
289
|
+
|
|
290
|
+
```python
|
|
291
|
+
def on_terminate(self):
|
|
292
|
+
log("on_terminate")
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
Use it for cleanup.
|
|
296
|
+
|
|
297
|
+
---
|
|
298
|
+
|
|
299
|
+
# 4. Persist State Between Runs (sqlite/duckdb)
|
|
300
|
+
|
|
301
|
+
Services can be killed and restarted at any time. If you need state across runs, use sqlite or duckdb via the provided API.
|
|
302
|
+
|
|
303
|
+
Example (sqlite):
|
|
304
|
+
|
|
305
|
+
```python
|
|
306
|
+
SQLITE_FILE_NAME = "mydata.sqlite"
|
|
307
|
+
|
|
308
|
+
sqlite_file = self._request_sqlite(file_name=SQLITE_FILE_NAME, is_shared=False)
|
|
309
|
+
if sqlite_file is not None:
|
|
310
|
+
pass
|
|
311
|
+
|
|
312
|
+
self._upload_sqlite(
|
|
313
|
+
file_name=SQLITE_FILE_NAME,
|
|
314
|
+
is_shared=False,
|
|
315
|
+
sqlite_file="/local/path/to/upload/sqlite.sqlite",
|
|
316
|
+
)
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
Notes
|
|
320
|
+
|
|
321
|
+
- `_request_sqlite` downloads from the server and is automatically scoped to this plugin. It will not mix with other plugins even if the name matches.
|
|
322
|
+
- The call is synchronous and returns the local path.
|
|
323
|
+
- If the return value is `None`, no file exists on the server; you can create a new one.
|
|
324
|
+
- `_upload_sqlite` always overwrites the previous file.
|
|
325
|
+
|
|
326
|
+
`is_shared` behavior
|
|
327
|
+
|
|
328
|
+
- `False` (recommended): isolate per service. Multiple instances get their own copies.
|
|
329
|
+
- `True`: shared storage. If another plugin updates the file, this plugin can download the latest version.
|
|
330
|
+
|
|
331
|
+
Note: `file_name` acts like a server key, while `sqlite_file` is the local file path.
|
|
332
|
+
|
|
333
|
+
---
|
|
334
|
+
|
|
335
|
+
# 5. Batch upload policy (DataFrame batching)
|
|
336
|
+
|
|
337
|
+
## Goal
|
|
338
|
+
|
|
339
|
+
When uploading large DataFrames, always upload in batches to avoid memory/network spikes.
|
|
340
|
+
Batch size MUST be configurable via `request.runtime_parameters["batch_size"]`.
|
|
341
|
+
|
|
342
|
+
## Required behavior
|
|
343
|
+
|
|
344
|
+
- Read batch size from `self.request.runtime_parameters.get("batch_size", "50000")`
|
|
345
|
+
- Convert to int safely:
|
|
346
|
+
- If parsing fails (TypeError/ValueError), fallback to 50000
|
|
347
|
+
|
|
348
|
+
- Validate:
|
|
349
|
+
- If batch_size <= 0, fallback to 50000
|
|
350
|
+
|
|
351
|
+
- Upload logic:
|
|
352
|
+
- If total_rows == 0: upload once (empty DF) and return
|
|
353
|
+
- Otherwise, iterate `start in range(0, total_rows, batch_size)` and upload slices:
|
|
354
|
+
- `batch_df = result_df.iloc[start : start + batch_size]`
|
|
355
|
+
- `_upload_result(batch_df)`
|
|
356
|
+
|
|
357
|
+
## Reference implementation pattern
|
|
358
|
+
|
|
359
|
+
Use the same pattern as `Rfm_segmentSvc.__get_batch_size()` and `Rfm_segmentSvc.__upload_in_batches()`.
|
|
360
|
+
|
|
361
|
+
## Notes
|
|
362
|
+
|
|
363
|
+
- Do NOT hardcode a fixed batch size inside upload loops.
|
|
364
|
+
- Do NOT read environment variables for batch sizing unless explicitly required; use `runtime_parameters` only.
|
|
365
|
+
|
|
366
|
+
---
|
|
367
|
+
|
|
368
|
+
# 6. Memory / RAM safety policy (Data Plugin)
|
|
369
|
+
|
|
370
|
+
## Goal
|
|
371
|
+
|
|
372
|
+
Avoid high RAM usage and OOM by preventing creation of very large DataFrames.
|
|
373
|
+
Prefer streaming/chunked/incremental processing whenever possible.
|
|
374
|
+
|
|
375
|
+
## Hard rules
|
|
376
|
+
|
|
377
|
+
- Do NOT load entire large CSVs into a single DataFrame if it can be avoided.
|
|
378
|
+
- Do NOT build large intermediate DataFrames by wide merges/joins/concat unless necessary.
|
|
379
|
+
- Do NOT create duplicate full-size DataFrames (e.g., repeated `.copy()`, chained transforms that materialize multiple full frames).
|
|
380
|
+
|
|
381
|
+
## Preferred approaches (in order)
|
|
382
|
+
|
|
383
|
+
### 1. Chunked ingestion
|
|
384
|
+
|
|
385
|
+
- Use `pandas.read_csv(..., chunksize=N)` and process each chunk incrementally.
|
|
386
|
+
- Write/insert each chunk into the DB (DuckDB/SQLite) immediately.
|
|
387
|
+
- Aggregate results at the DB layer, not in pandas, when feasible.
|
|
388
|
+
|
|
389
|
+
### 2. DB-first computation
|
|
390
|
+
|
|
391
|
+
- Persist raw rows into DuckDB early.
|
|
392
|
+
- Perform joins, filters, grouping, segmentation rules in SQL inside DuckDB.
|
|
393
|
+
- Only pull final result set into pandas at the end (and still batch-upload it).
|
|
394
|
+
|
|
395
|
+
### 3. Reduce payload early
|
|
396
|
+
|
|
397
|
+
- Select only needed columns at read time (`usecols=[...]`).
|
|
398
|
+
- Use explicit dtypes to reduce memory (`string`, `Int32`, `Float32`, categories where appropriate).
|
|
399
|
+
- Filter rows as early as possible (refunds, invalid rows, null timestamps).
|
|
400
|
+
|
|
401
|
+
### 4. Avoid wide merges in pandas
|
|
402
|
+
|
|
403
|
+
- If a merge can be done in SQL, do it in DuckDB instead of pandas.
|
|
404
|
+
- If merging in pandas is unavoidable, keep only necessary columns from both sides before merge.
|
|
405
|
+
|
|
406
|
+
## Output upload policy
|
|
407
|
+
|
|
408
|
+
- Upload results in batches using `runtime_parameters["batch_size"]` (see Batch upload policy).
|
|
409
|
+
- If the final result can be generated in streaming form, avoid materializing a full `result_df`.
|
|
410
|
+
|
|
411
|
+
## When a full DataFrame is allowed
|
|
412
|
+
|
|
413
|
+
- Only when the expected row count is small enough, or when chunking/DB-first is impractical.
|
|
414
|
+
- If unsure, default to chunking + DB-first.
|
|
415
|
+
|
|
416
|
+
## Required implementation mindset
|
|
417
|
+
|
|
418
|
+
- Treat pandas as a convenience layer, not the primary compute engine for large datasets.
|
|
419
|
+
- Prefer "incremental + durable" steps (chunk -> DB insert -> SQL compute -> batched upload).
|
|
@@ -1,197 +0,0 @@
|
|
|
1
|
-
# AGENT
|
|
2
|
-
|
|
3
|
-
This document explains the data plugin service lifecycle so developers can create new services and understand runtime behavior inside the CDP environment.
|
|
4
|
-
|
|
5
|
-
## Project Overview
|
|
6
|
-
|
|
7
|
-
1. This project is a data plugin that will be connected to the main system in production.
|
|
8
|
-
2. The data plugin processes data from CDP and is created as separate services by domain. For example, if we process RFM, we create a new service dedicated to that domain.
|
|
9
|
-
|
|
10
|
-
## Create a New Service
|
|
11
|
-
|
|
12
|
-
1. Create a service with:
|
|
13
|
-
`pam new service [service name]`
|
|
14
|
-
2. The system creates a new folder named `[service name]` with sample code and basic templates.
|
|
15
|
-
3. Example:
|
|
16
|
-
`pam new service rfm_segment`
|
|
17
|
-
This generates `rfm_segment/Rfm_segmentSvc.py`, which is the starting point of the service.
|
|
18
|
-
4. `rfm_segment/service.yaml` registers the service automatically.
|
|
19
|
-
The value `endpoint: /Rfm_segmentSvc` means CDP will call:
|
|
20
|
-
`http://ip-of-this-k8s-pod/services/Rfm_segmentSvc`
|
|
21
|
-
|
|
22
|
-
### Code Organization Guidelines
|
|
23
|
-
|
|
24
|
-
- Separate each lifecycle step into clear, dedicated functions. Add comments at critical points so humans can review and audit later.
|
|
25
|
-
- Avoid nested functions unless absolutely necessary.
|
|
26
|
-
- Put service logic in `[service_name]/functions.py`, then import and use it from the service class to keep the overall code easy to read.
|
|
27
|
-
|
|
28
|
-
## Service Lifecycle
|
|
29
|
-
|
|
30
|
-
CDP spawns a service instance when a job starts and terminates it after the job completes. This design supports parallel execution. For example, two runs will spawn two separate instances. The only risk is shared local files (e.g., sqlite) which can cause conflicts.
|
|
31
|
-
|
|
32
|
-
The lifecycle is wired in `rfm_segment/Rfm_segmentSvc.py` (example service name).
|
|
33
|
-
|
|
34
|
-
### on_start
|
|
35
|
-
|
|
36
|
-
`on_start` runs at startup. Use it to read runtime parameters from CDP and initialize work.
|
|
37
|
-
|
|
38
|
-
Example:
|
|
39
|
-
|
|
40
|
-
```python
|
|
41
|
-
def on_start(self):
|
|
42
|
-
log("on_start")
|
|
43
|
-
|
|
44
|
-
some_param = self.request.runtime_parameters.get("some_param", "")
|
|
45
|
-
self._request_data()
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
`runtime_parameters` are configured by CDP to control service behavior without code changes (e.g., `period_day="7"`).
|
|
49
|
-
|
|
50
|
-
Notes
|
|
51
|
-
|
|
52
|
-
- All values are strings. Cast with defaults to avoid errors.
|
|
53
|
-
- `dry_run` is the only built-in parameter. If missing, it runs for real. If `dry_run="true"`, the service runs fully but does not upload results to CDP.
|
|
54
|
-
- You do not need to check `dry_run` manually. Always call `_upload_result`; the system handles it.
|
|
55
|
-
|
|
56
|
-
**Important:** `on_start` must return quickly because CDP calls via HTTP. If it hangs, the client may time out and the job will be terminated. If work is long-running, move it to a thread and return from `on_start` first. This applies to all lifecycle functions.
|
|
57
|
-
|
|
58
|
-
### Request Data from CDP
|
|
59
|
-
|
|
60
|
-
Call `self._request_data()` to ask CDP for input data. CDP returns up to 5 CSV files (each file is a separate event).
|
|
61
|
-
|
|
62
|
-
- File order is guaranteed per agreement, so you can index by position.
|
|
63
|
-
- Column names and record counts are defined by CDP configuration.
|
|
64
|
-
|
|
65
|
-
CSV example:
|
|
66
|
-
|
|
67
|
-
```csv
|
|
68
|
-
_id,id,data1,data2,data3
|
|
69
|
-
xx,yy,zz,nn,mm
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
Column meaning
|
|
73
|
-
|
|
74
|
-
- `_id` = event id for dedup/processing checks. Often unused and can be dropped.
|
|
75
|
-
- `id` = customer id used by CDP as a key.
|
|
76
|
-
- Other columns depend on the agreement.
|
|
77
|
-
|
|
78
|
-
Date format is always:
|
|
79
|
-
`"02/08/2026, 18:23:54"`
|
|
80
|
-
Timezone may vary by customer data (usually Bangkok time).
|
|
81
|
-
You may sometimes be asked for the exact time zone if it is logically important for the program.
|
|
82
|
-
|
|
83
|
-
### on_data_input
|
|
84
|
-
|
|
85
|
-
When CDP finishes collecting data, it calls `on_data_input`.
|
|
86
|
-
|
|
87
|
-
Example:
|
|
88
|
-
|
|
89
|
-
```python
|
|
90
|
-
def on_data_input(self, req: RequestCommand):
|
|
91
|
-
log(f"on_data_input req.is_end = {req.is_end}")
|
|
92
|
-
|
|
93
|
-
# RUN IN A SEPARATED THREAD TO PREVENT HTTP REQUEST BLOCKING
|
|
94
|
-
thread = threading.Thread(
|
|
95
|
-
target=lambda: self.__run_process_data_in_thread(req),
|
|
96
|
-
daemon=True,
|
|
97
|
-
)
|
|
98
|
-
thread.start()
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
def __run_process_data_in_thread(self, req: RequestCommand):
|
|
102
|
-
dataframe = self.__process_data(req.input_files)
|
|
103
|
-
self._upload_result(dataframe)
|
|
104
|
-
|
|
105
|
-
if not req.is_end:
|
|
106
|
-
self._request_data(req.next)
|
|
107
|
-
else:
|
|
108
|
-
self._exit()
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
Why a thread: to end the HTTP request quickly and avoid timeouts.
|
|
112
|
-
|
|
113
|
-
`req.input_files` is the ordered list of CSV paths.
|
|
114
|
-
|
|
115
|
-
### Upload Results to CDP
|
|
116
|
-
|
|
117
|
-
After processing, call `_upload_result` with a dataframe. Example output:
|
|
118
|
-
|
|
119
|
-
```csv
|
|
120
|
-
id,data_x,rfm
|
|
121
|
-
xxx,20,hero
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
- `id` is required for CDP matching.
|
|
125
|
-
- Other columns are defined by agreement.
|
|
126
|
-
|
|
127
|
-
Sometimes you may include an `event` column to create per-user events:
|
|
128
|
-
|
|
129
|
-
```csv
|
|
130
|
-
id,data_x,event
|
|
131
|
-
xxx,20,purchase
|
|
132
|
-
yyy,50,refund
|
|
133
|
-
```
|
|
134
|
-
|
|
135
|
-
If `event` is missing, CDP uses the default event configured in the system.
|
|
136
|
-
|
|
137
|
-
### Pagination
|
|
138
|
-
|
|
139
|
-
In the example:
|
|
140
|
-
|
|
141
|
-
```python
|
|
142
|
-
if not req.is_end:
|
|
143
|
-
self._request_data(req.next)
|
|
144
|
-
else:
|
|
145
|
-
self._exit()
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
- `req.is_end` indicates whether this is the last page.
|
|
149
|
-
- If not end, call `self._request_data(req.next)` for the next page.
|
|
150
|
-
- If end, call `self._exit()` to terminate the instance.
|
|
151
|
-
|
|
152
|
-
CDP will not send the next page until you call `_request_data`, so the flow is serialized and no two threads will handle the same data unless you explicitly design it that way.
|
|
153
|
-
|
|
154
|
-
### on_terminate
|
|
155
|
-
|
|
156
|
-
Before termination, the system calls:
|
|
157
|
-
|
|
158
|
-
```python
|
|
159
|
-
def on_terminate(self):
|
|
160
|
-
log("on_terminate")
|
|
161
|
-
```
|
|
162
|
-
|
|
163
|
-
Use it for cleanup.
|
|
164
|
-
|
|
165
|
-
## Persist State Between Runs (sqlite/duckdb)
|
|
166
|
-
|
|
167
|
-
Services can be killed and restarted at any time. If you need state across runs, use sqlite or duckdb via the provided API.
|
|
168
|
-
|
|
169
|
-
Example (sqlite):
|
|
170
|
-
|
|
171
|
-
```python
|
|
172
|
-
SQLITE_FILE_NAME = "mydata.sqlite"
|
|
173
|
-
|
|
174
|
-
sqlite_file = self._request_sqlite(file_name=SQLITE_FILE_NAME, is_shared=False)
|
|
175
|
-
if sqlite_file is not None:
|
|
176
|
-
pass
|
|
177
|
-
|
|
178
|
-
self._upload_sqlite(
|
|
179
|
-
file_name=SQLITE_FILE_NAME,
|
|
180
|
-
is_shared=False,
|
|
181
|
-
sqlite_file="/local/path/to/upload/sqlite.sqlite",
|
|
182
|
-
)
|
|
183
|
-
```
|
|
184
|
-
|
|
185
|
-
Notes
|
|
186
|
-
|
|
187
|
-
- `_request_sqlite` downloads from the server and is automatically scoped to this plugin. It will not mix with other plugins even if the name matches.
|
|
188
|
-
- The call is synchronous and returns the local path.
|
|
189
|
-
- If the return value is `None`, no file exists on the server; you can create a new one.
|
|
190
|
-
- `_upload_sqlite` always overwrites the previous file.
|
|
191
|
-
|
|
192
|
-
`is_shared` behavior
|
|
193
|
-
|
|
194
|
-
- `False` (recommended): isolate per service. Multiple instances get their own copies.
|
|
195
|
-
- `True`: shared storage. If another plugin updates the file, this plugin can download the latest version.
|
|
196
|
-
|
|
197
|
-
Note: `file_name` acts like a server key, while `sqlite_file` is the local file path.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|