pam-python 0.2.2__tar.gz → 0.2.3__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 (106) hide show
  1. {pam_python-0.2.2/pam_python.egg-info → pam_python-0.2.3}/PKG-INFO +2 -2
  2. {pam_python-0.2.2 → pam_python-0.2.3}/README.md +1 -1
  3. {pam_python-0.2.2 → pam_python-0.2.3}/pam/__init__.py +1 -1
  4. {pam_python-0.2.2 → pam_python-0.2.3}/pam/templates/init/AGENT.md +37 -14
  5. {pam_python-0.2.2 → pam_python-0.2.3}/pam/templates/service/pandas/service_class.tmpl +17 -5
  6. {pam_python-0.2.2 → pam_python-0.2.3}/pam/templates/service/polars/service_class.tmpl +17 -5
  7. {pam_python-0.2.2 → pam_python-0.2.3/pam_python.egg-info}/PKG-INFO +2 -2
  8. {pam_python-0.2.2 → pam_python-0.2.3}/tests/test_cli_templates.py +7 -0
  9. {pam_python-0.2.2 → pam_python-0.2.3}/LICENSE.txt +0 -0
  10. {pam_python-0.2.2 → pam_python-0.2.3}/pam/api.py +0 -0
  11. {pam_python-0.2.2 → pam_python-0.2.3}/pam/cli.py +0 -0
  12. {pam_python-0.2.2 → pam_python-0.2.3}/pam/interface_task_manager.py +0 -0
  13. {pam_python-0.2.2 → pam_python-0.2.3}/pam/logger.py +0 -0
  14. {pam_python-0.2.2 → pam_python-0.2.3}/pam/models/__init__.py +0 -0
  15. {pam_python-0.2.2 → pam_python-0.2.3}/pam/models/request_command.py +0 -0
  16. {pam_python-0.2.2 → pam_python-0.2.3}/pam/models/sqlite_download.py +0 -0
  17. {pam_python-0.2.2 → pam_python-0.2.3}/pam/reports/__init__.py +0 -0
  18. {pam_python-0.2.2 → pam_python-0.2.3}/pam/reports/area.py +0 -0
  19. {pam_python-0.2.2 → pam_python-0.2.3}/pam/reports/axis.py +0 -0
  20. {pam_python-0.2.2 → pam_python-0.2.3}/pam/reports/bar.py +0 -0
  21. {pam_python-0.2.2 → pam_python-0.2.3}/pam/reports/box_plot.py +0 -0
  22. {pam_python-0.2.2 → pam_python-0.2.3}/pam/reports/bubble.py +0 -0
  23. {pam_python-0.2.2 → pam_python-0.2.3}/pam/reports/candlestick.py +0 -0
  24. {pam_python-0.2.2 → pam_python-0.2.3}/pam/reports/collection.py +0 -0
  25. {pam_python-0.2.2 → pam_python-0.2.3}/pam/reports/definition.py +0 -0
  26. {pam_python-0.2.2 → pam_python-0.2.3}/pam/reports/donut.py +0 -0
  27. {pam_python-0.2.2 → pam_python-0.2.3}/pam/reports/errors.py +0 -0
  28. {pam_python-0.2.2 → pam_python-0.2.3}/pam/reports/fields.py +0 -0
  29. {pam_python-0.2.2 → pam_python-0.2.3}/pam/reports/heatmap.py +0 -0
  30. {pam_python-0.2.2 → pam_python-0.2.3}/pam/reports/line.py +0 -0
  31. {pam_python-0.2.2 → pam_python-0.2.3}/pam/reports/manager.py +0 -0
  32. {pam_python-0.2.2 → pam_python-0.2.3}/pam/reports/metric.py +0 -0
  33. {pam_python-0.2.2 → pam_python-0.2.3}/pam/reports/pie.py +0 -0
  34. {pam_python-0.2.2 → pam_python-0.2.3}/pam/reports/polar_area.py +0 -0
  35. {pam_python-0.2.2 → pam_python-0.2.3}/pam/reports/radar.py +0 -0
  36. {pam_python-0.2.2 → pam_python-0.2.3}/pam/reports/radial_bar.py +0 -0
  37. {pam_python-0.2.2 → pam_python-0.2.3}/pam/reports/range_area.py +0 -0
  38. {pam_python-0.2.2 → pam_python-0.2.3}/pam/reports/range_bar.py +0 -0
  39. {pam_python-0.2.2 → pam_python-0.2.3}/pam/reports/range_transform.py +0 -0
  40. {pam_python-0.2.2 → pam_python-0.2.3}/pam/reports/record.py +0 -0
  41. {pam_python-0.2.2 → pam_python-0.2.3}/pam/reports/report.py +0 -0
  42. {pam_python-0.2.2 → pam_python-0.2.3}/pam/reports/scatter.py +0 -0
  43. {pam_python-0.2.2 → pam_python-0.2.3}/pam/reports/schema.py +0 -0
  44. {pam_python-0.2.2 → pam_python-0.2.3}/pam/reports/store.py +0 -0
  45. {pam_python-0.2.2 → pam_python-0.2.3}/pam/reports/table.py +0 -0
  46. {pam_python-0.2.2 → pam_python-0.2.3}/pam/reports/transport.py +0 -0
  47. {pam_python-0.2.2 → pam_python-0.2.3}/pam/reports/treemap.py +0 -0
  48. {pam_python-0.2.2 → pam_python-0.2.3}/pam/request_file_format.py +0 -0
  49. {pam_python-0.2.2 → pam_python-0.2.3}/pam/result_batch_uploader.py +0 -0
  50. {pam_python-0.2.2 → pam_python-0.2.3}/pam/server.py +0 -0
  51. {pam_python-0.2.2 → pam_python-0.2.3}/pam/service.py +0 -0
  52. {pam_python-0.2.2 → pam_python-0.2.3}/pam/sqlite.py +0 -0
  53. {pam_python-0.2.2 → pam_python-0.2.3}/pam/task_manager.py +0 -0
  54. {pam_python-0.2.2 → pam_python-0.2.3}/pam/temp_file_utils.py +0 -0
  55. {pam_python-0.2.2 → pam_python-0.2.3}/pam/templates/buildcmd/pamb +0 -0
  56. {pam_python-0.2.2 → pam_python-0.2.3}/pam/templates/buildcmd/pamb-base.sh +0 -0
  57. {pam_python-0.2.2 → pam_python-0.2.3}/pam/templates/init/REPORTS.md +0 -0
  58. {pam_python-0.2.2 → pam_python-0.2.3}/pam/templates/init/dockerignore.tmpl +0 -0
  59. {pam_python-0.2.2 → pam_python-0.2.3}/pam/templates/init/gitignore.tmpl +0 -0
  60. {pam_python-0.2.2 → pam_python-0.2.3}/pam/templates/init/main.tmpl +0 -0
  61. {pam_python-0.2.2 → pam_python-0.2.3}/pam/templates/init/pylintrc.tmpl +0 -0
  62. {pam_python-0.2.2 → pam_python-0.2.3}/pam/templates/init/run_unit_test.bat +0 -0
  63. {pam_python-0.2.2 → pam_python-0.2.3}/pam/templates/init/run_unit_test.ps1 +0 -0
  64. {pam_python-0.2.2 → pam_python-0.2.3}/pam/templates/init/run_unit_test.sh +0 -0
  65. {pam_python-0.2.2 → pam_python-0.2.3}/pam/templates/project/pip/Dockerfile +0 -0
  66. {pam_python-0.2.2 → pam_python-0.2.3}/pam/templates/project/pip/requirements.txt +0 -0
  67. {pam_python-0.2.2 → pam_python-0.2.3}/pam/templates/project/uv/Dockerfile +0 -0
  68. {pam_python-0.2.2 → pam_python-0.2.3}/pam/templates/project/uv/pyproject.toml +0 -0
  69. {pam_python-0.2.2 → pam_python-0.2.3}/pam/templates/project/uv/python-version +0 -0
  70. {pam_python-0.2.2 → pam_python-0.2.3}/pam/templates/service/common/service.yaml +0 -0
  71. {pam_python-0.2.2 → pam_python-0.2.3}/pam/templates/service/pandas/functions.tmpl +0 -0
  72. {pam_python-0.2.2 → pam_python-0.2.3}/pam/templates/service/pandas/service.test.tmpl +0 -0
  73. {pam_python-0.2.2 → pam_python-0.2.3}/pam/templates/service/polars/functions.tmpl +0 -0
  74. {pam_python-0.2.2 → pam_python-0.2.3}/pam/templates/service/polars/service.test.tmpl +0 -0
  75. {pam_python-0.2.2 → pam_python-0.2.3}/pam/tester_task.py +0 -0
  76. {pam_python-0.2.2 → pam_python-0.2.3}/pam/utils.py +0 -0
  77. {pam_python-0.2.2 → pam_python-0.2.3}/pam_python.egg-info/SOURCES.txt +0 -0
  78. {pam_python-0.2.2 → pam_python-0.2.3}/pam_python.egg-info/dependency_links.txt +0 -0
  79. {pam_python-0.2.2 → pam_python-0.2.3}/pam_python.egg-info/entry_points.txt +0 -0
  80. {pam_python-0.2.2 → pam_python-0.2.3}/pam_python.egg-info/requires.txt +0 -0
  81. {pam_python-0.2.2 → pam_python-0.2.3}/pam_python.egg-info/top_level.txt +0 -0
  82. {pam_python-0.2.2 → pam_python-0.2.3}/pyproject.toml +0 -0
  83. {pam_python-0.2.2 → pam_python-0.2.3}/setup.cfg +0 -0
  84. {pam_python-0.2.2 → pam_python-0.2.3}/tests/test_range_transform.py +0 -0
  85. {pam_python-0.2.2 → pam_python-0.2.3}/tests/test_report_store.py +0 -0
  86. {pam_python-0.2.2 → pam_python-0.2.3}/tests/test_result_batch_uploader.py +0 -0
  87. {pam_python-0.2.2 → pam_python-0.2.3}/tests/test_service_dataframe_output.py +0 -0
  88. {pam_python-0.2.2 → pam_python-0.2.3}/tests/test_sqlite_download_v2.py +0 -0
  89. {pam_python-0.2.2 → pam_python-0.2.3}/tests/test_typed_area_report.py +0 -0
  90. {pam_python-0.2.2 → pam_python-0.2.3}/tests/test_typed_bar_report.py +0 -0
  91. {pam_python-0.2.2 → pam_python-0.2.3}/tests/test_typed_box_plot_report.py +0 -0
  92. {pam_python-0.2.2 → pam_python-0.2.3}/tests/test_typed_bubble_report.py +0 -0
  93. {pam_python-0.2.2 → pam_python-0.2.3}/tests/test_typed_candlestick_report.py +0 -0
  94. {pam_python-0.2.2 → pam_python-0.2.3}/tests/test_typed_donut_report.py +0 -0
  95. {pam_python-0.2.2 → pam_python-0.2.3}/tests/test_typed_heatmap_report.py +0 -0
  96. {pam_python-0.2.2 → pam_python-0.2.3}/tests/test_typed_line_report.py +0 -0
  97. {pam_python-0.2.2 → pam_python-0.2.3}/tests/test_typed_metric_report.py +0 -0
  98. {pam_python-0.2.2 → pam_python-0.2.3}/tests/test_typed_pie_report.py +0 -0
  99. {pam_python-0.2.2 → pam_python-0.2.3}/tests/test_typed_polar_area_report.py +0 -0
  100. {pam_python-0.2.2 → pam_python-0.2.3}/tests/test_typed_radar_report.py +0 -0
  101. {pam_python-0.2.2 → pam_python-0.2.3}/tests/test_typed_radial_bar_report.py +0 -0
  102. {pam_python-0.2.2 → pam_python-0.2.3}/tests/test_typed_range_area_report.py +0 -0
  103. {pam_python-0.2.2 → pam_python-0.2.3}/tests/test_typed_range_bar_report.py +0 -0
  104. {pam_python-0.2.2 → pam_python-0.2.3}/tests/test_typed_scatter_report.py +0 -0
  105. {pam_python-0.2.2 → pam_python-0.2.3}/tests/test_typed_table_report.py +0 -0
  106. {pam_python-0.2.2 → pam_python-0.2.3}/tests/test_typed_treemap_report.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pam-python
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Summary: Pam Python Library
5
5
  Author-email: Narongrit Kanhanoi <narongrit@pams.ai>
6
6
  Project-URL: Homepage, https://github.com/heart/pam-python
@@ -122,7 +122,7 @@ The runtime calls your service in two main phases.
122
122
  2. `on_data_input`
123
123
 
124
124
  - Called when CDP sends input files
125
- - `req.input_files` contains ordered Parquet files
125
+ - `req.input_files` is the ordered positional data-source contract configured in PAM
126
126
  - `req.file_format` is the typed format sent by PAM
127
127
  - Should also return quickly (use a thread if needed)
128
128
 
@@ -102,7 +102,7 @@ The runtime calls your service in two main phases.
102
102
  2. `on_data_input`
103
103
 
104
104
  - Called when CDP sends input files
105
- - `req.input_files` contains ordered Parquet files
105
+ - `req.input_files` is the ordered positional data-source contract configured in PAM
106
106
  - `req.file_format` is the typed format sent by PAM
107
107
  - Should also return quickly (use a thread if needed)
108
108
 
@@ -1,3 +1,3 @@
1
1
  """PAM Data Plugin framework."""
2
2
 
3
- __version__ = "0.2.2"
3
+ __version__ = "0.2.3"
@@ -117,15 +117,17 @@ input data. CDP returns up to 5 Parquet files (each file is a separate event).
117
117
 
118
118
  - File order is guaranteed per agreement, so you can index by position.
119
119
  - Column names and record counts are defined by CDP configuration.
120
+ - `input_files` is not a list of interchangeable files. Each position represents
121
+ a different configured data source and must be bound to a meaningful domain name.
122
+ - Never process `input_files` with `for file_path in req.input_files`.
120
123
 
121
124
  Example file order usage:
122
125
 
123
126
  ```python
124
- input_files = req.input_files
125
- # Index 0: purchase events
126
- # Index 1: use points events
127
- purchase_path = input_files[0]
128
- use_points_path = input_files[1]
127
+ if len(req.input_files) != 2:
128
+ raise ValueError("Expected purchase_event and point_received_event inputs")
129
+
130
+ purchase_event_file, point_received_event_file = req.input_files
129
131
  ```
130
132
 
131
133
  Column meaning
@@ -163,20 +165,30 @@ def __run_process_data_in_thread(self, req: RequestCommand):
163
165
  if req.file_format is not RequestFileFormat.PARQUET:
164
166
  raise ValueError(f"Expected parquet, received {req.file_format.value}")
165
167
 
166
- result = transform(pl.scan_parquet(req.input_files[0]))
168
+ if len(req.input_files) != 2:
169
+ raise ValueError("Expected purchase_event and point_received_event inputs")
170
+
171
+ purchase_event_file, point_received_event_file = req.input_files
172
+ result = transform(
173
+ pl.scan_parquet(purchase_event_file),
174
+ pl.scan_parquet(point_received_event_file),
175
+ )
167
176
  self._upload_result(result)
168
177
 
169
178
  if not req.is_end:
170
- self._request_data(req.next, file_format=RequestFileFormat.PARQUET)
179
+ self._request_data(
180
+ page=req.next,
181
+ file_format=RequestFileFormat.PARQUET,
182
+ )
171
183
  else:
172
184
  self._exit()
173
185
  ```
174
186
 
175
187
  Why a thread: to end the HTTP request quickly and avoid timeouts.
176
188
 
177
- `req.input_files` is the ordered list of Parquet paths. `req.file_format` is the
178
- typed metadata sent by PAM and is the source of truth; validate file extensions as
179
- an additional consistency check.
189
+ `req.input_files` is the ordered positional contract of Parquet paths.
190
+ `req.file_format` is the typed metadata sent by PAM and is the source of truth;
191
+ validate file extensions as an additional consistency check.
180
192
 
181
193
  ---
182
194
 
@@ -258,21 +270,32 @@ CDP paging is **customer-based**, not row-based.
258
270
 
259
271
  When requesting data page by page:
260
272
 
261
- - CDP collects customers in batches of approximately **10,000 customers per page**.
262
- - All events belonging to those customers are merged into **a single Parquet file**.
263
- - The number of rows is **unknown** and depends on how many events each customer has.
273
+ - PAM first selects a cohort of approximately **10,000 unique customers per page**.
274
+ - PAM then queries **every configured dataset** with that same customer-ID cohort.
275
+ - Dataset 1 through dataset N in one callback therefore belong to the same group
276
+ of customers, while retaining their configured positional meanings.
277
+ - Each dataset has its own Parquet file and may have a different row count because
278
+ event density differs and some customers may have no rows in a dataset.
279
+ - A customer's relevant data is kept within one logical page instead of being
280
+ split across customer pages.
281
+ - `req.next` advances the customer-cohort cursor, not an event-row offset.
264
282
 
265
283
  This means:
266
284
 
267
285
  - Each page represents a logical customer group, not a fixed number of records.
268
286
  - A page may contain a small or very large number of rows depending on behavior density.
287
+ - Files at different input positions must be joined or compared by the agreed
288
+ customer key; row positions across files do not correspond.
269
289
 
270
290
  ### Execution Implications
271
291
 
272
292
  Because paging is based on customers:
273
293
 
274
294
  - In many cases, a service can process **one page (≈10,000 customers)** independently.
275
- - If the logic allows, you may process and upload results immediately because the dataset is self-contained.
295
+ - If the logic allows, you may process and upload results immediately because all
296
+ configured datasets for that customer cohort arrive together.
297
+ - Do not create row-offset pagination or carry partial state because one dataset
298
+ happens to have more event rows than another.
276
299
 
277
300
  However, upload behavior must balance two constraints:
278
301
 
@@ -13,6 +13,7 @@ from #MODULE_NAME#.functions import load_input_file, transform
13
13
 
14
14
  class #CLASS_NAME#(Service):
15
15
  INPUT_FILE_FORMAT = RequestFileFormat.PARQUET
16
+ EXPECTED_DATASET_COUNT = 1
16
17
 
17
18
  def __init__(self, task_manager, req):
18
19
  super().__init__(task_manager, req)
@@ -40,11 +41,22 @@ class #CLASS_NAME#(Service):
40
41
  f"received {req.file_format.value}"
41
42
  )
42
43
 
43
- for file_path in req.input_files:
44
- if not file_path.lower().endswith(".parquet"):
45
- raise ValueError(f"Expected a .parquet input file: {file_path}")
46
- result = transform(load_input_file(file_path))
47
- self.batch_uploader.upload(result, name="main")
44
+ if len(req.input_files) != self.EXPECTED_DATASET_COUNT:
45
+ raise ValueError(
46
+ f"Expected {self.EXPECTED_DATASET_COUNT} configured dataset, "
47
+ f"received {len(req.input_files)}"
48
+ )
49
+
50
+ # Bind every position to the data source name configured in PAM.
51
+ # Add more named positions here when the service contract has more datasets.
52
+ (input_dataset_file,) = req.input_files
53
+ if not input_dataset_file.lower().endswith(".parquet"):
54
+ raise ValueError(
55
+ f"Expected a .parquet input file: {input_dataset_file}"
56
+ )
57
+
58
+ result = transform(load_input_file(input_dataset_file))
59
+ self.batch_uploader.upload(result, name="main")
48
60
 
49
61
  if not req.is_end:
50
62
  self._request_data(
@@ -12,6 +12,7 @@ from #MODULE_NAME#.functions import load_input_file, transform
12
12
 
13
13
  class #CLASS_NAME#(Service):
14
14
  INPUT_FILE_FORMAT = RequestFileFormat.PARQUET
15
+ EXPECTED_DATASET_COUNT = 1
15
16
 
16
17
  def on_start(self):
17
18
  log("on_start")
@@ -35,11 +36,22 @@ class #CLASS_NAME#(Service):
35
36
  f"received {req.file_format.value}"
36
37
  )
37
38
 
38
- for file_path in req.input_files:
39
- if not file_path.lower().endswith(".parquet"):
40
- raise ValueError(f"Expected a .parquet input file: {file_path}")
41
- result = transform(load_input_file(file_path))
42
- self._upload_result(result)
39
+ if len(req.input_files) != self.EXPECTED_DATASET_COUNT:
40
+ raise ValueError(
41
+ f"Expected {self.EXPECTED_DATASET_COUNT} configured dataset, "
42
+ f"received {len(req.input_files)}"
43
+ )
44
+
45
+ # Bind every position to the data source name configured in PAM.
46
+ # Add more named positions here when the service contract has more datasets.
47
+ (input_dataset_file,) = req.input_files
48
+ if not input_dataset_file.lower().endswith(".parquet"):
49
+ raise ValueError(
50
+ f"Expected a .parquet input file: {input_dataset_file}"
51
+ )
52
+
53
+ result = transform(load_input_file(input_dataset_file))
54
+ self._upload_result(result)
43
55
 
44
56
  if not req.is_end:
45
57
  self._request_data(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pam-python
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Summary: Pam Python Library
5
5
  Author-email: Narongrit Kanhanoi <narongrit@pams.ai>
6
6
  Project-URL: Homepage, https://github.com/heart/pam-python
@@ -122,7 +122,7 @@ The runtime calls your service in two main phases.
122
122
  2. `on_data_input`
123
123
 
124
124
  - Called when CDP sends input files
125
- - `req.input_files` contains ordered Parquet files
125
+ - `req.input_files` is the ordered positional data-source contract configured in PAM
126
126
  - `req.file_format` is the typed format sent by PAM
127
127
  - Should also return quickly (use a thread if needed)
128
128
 
@@ -43,6 +43,10 @@ class CLITemplateTest(unittest.TestCase):
43
43
  self.assertIn("pyarrow>=19.0.1", (root / "requirements.txt").read_text())
44
44
  self.assertFalse((root / "pyproject.toml").exists())
45
45
  self.assertTrue((root / "REPORTS.md").is_file())
46
+ agent_guide = (root / "AGENT.md").read_text()
47
+ self.assertIn("Customer-based Pagination Contract", agent_guide)
48
+ self.assertIn("same customer-ID cohort", agent_guide)
49
+ self.assertIn("customer-cohort cursor", agent_guide)
46
50
  self.assertTrue((root / "run_unit_test.sh").is_file())
47
51
  self.assertTrue(os.access(root / "run_unit_test.sh", os.X_OK))
48
52
  self.assertTrue((root / "run_unit_test.ps1").is_file())
@@ -62,6 +66,9 @@ class CLITemplateTest(unittest.TestCase):
62
66
  ):
63
67
  service_source = service_path.read_text()
64
68
  self.assertIn("page=req.next", service_source)
69
+ self.assertIn("EXPECTED_DATASET_COUNT = 1", service_source)
70
+ self.assertIn("(input_dataset_file,) = req.input_files", service_source)
71
+ self.assertNotIn("for file_path in req.input_files", service_source)
65
72
  self.assertEqual(
66
73
  2,
67
74
  service_source.count("file_format=self.INPUT_FILE_FORMAT"),
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