pam-python 0.2.10__tar.gz → 0.2.11__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.2.10/pam_python.egg-info → pam_python-0.2.11}/PKG-INFO +1 -1
- {pam_python-0.2.10 → pam_python-0.2.11}/pam/__init__.py +1 -1
- {pam_python-0.2.10 → pam_python-0.2.11}/pam/interface_task_manager.py +1 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/pam/reports/manager.py +30 -2
- {pam_python-0.2.10 → pam_python-0.2.11}/pam/reports/store.py +20 -2
- {pam_python-0.2.10 → pam_python-0.2.11}/pam/reports/transport.py +1 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/pam/task_manager.py +2 -1
- {pam_python-0.2.10 → pam_python-0.2.11}/pam/templates/init/AGENT.md +107 -9
- {pam_python-0.2.10 → pam_python-0.2.11}/pam/templates/init/REPORTS.md +15 -3
- {pam_python-0.2.10 → pam_python-0.2.11}/pam/tester_task.py +22 -1
- {pam_python-0.2.10 → pam_python-0.2.11/pam_python.egg-info}/PKG-INFO +1 -1
- {pam_python-0.2.10 → pam_python-0.2.11}/tests/test_cli_templates.py +17 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/tests/test_report_store.py +40 -6
- {pam_python-0.2.10 → pam_python-0.2.11}/LICENSE.txt +0 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/README.md +0 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/pam/api.py +0 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/pam/cli.py +0 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/pam/datetime_utils.py +0 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/pam/logger.py +0 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/pam/models/__init__.py +0 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/pam/models/data_input_v2.py +0 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/pam/models/request_command.py +0 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/pam/models/sqlite_download.py +0 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/pam/reports/__init__.py +0 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/pam/reports/area.py +0 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/pam/reports/axis.py +0 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/pam/reports/bar.py +0 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/pam/reports/box_plot.py +0 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/pam/reports/bubble.py +0 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/pam/reports/candlestick.py +0 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/pam/reports/collection.py +0 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/pam/reports/definition.py +0 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/pam/reports/donut.py +0 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/pam/reports/errors.py +0 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/pam/reports/fields.py +0 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/pam/reports/heatmap.py +0 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/pam/reports/line.py +0 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/pam/reports/metric.py +0 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/pam/reports/pie.py +0 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/pam/reports/polar_area.py +0 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/pam/reports/radar.py +0 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/pam/reports/radial_bar.py +0 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/pam/reports/range_area.py +0 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/pam/reports/range_bar.py +0 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/pam/reports/range_transform.py +0 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/pam/reports/record.py +0 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/pam/reports/report.py +0 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/pam/reports/scatter.py +0 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/pam/reports/schema.py +0 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/pam/reports/table.py +0 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/pam/reports/treemap.py +0 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/pam/request_file_format.py +0 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/pam/result_batch_uploader.py +0 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/pam/server.py +0 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/pam/service.py +0 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/pam/sqlite.py +0 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/pam/temp_file_utils.py +0 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/pam/templates/buildcmd/pamb +0 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/pam/templates/buildcmd/pamb-base.sh +0 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/pam/templates/init/dockerignore.tmpl +0 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/pam/templates/init/gitignore.tmpl +0 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/pam/templates/init/main.tmpl +0 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/pam/templates/init/pylintrc.tmpl +0 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/pam/templates/init/run_unit_test.bat +0 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/pam/templates/init/run_unit_test.ps1 +0 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/pam/templates/init/run_unit_test.sh +0 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/pam/templates/project/pip/Dockerfile +0 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/pam/templates/project/pip/requirements.txt +0 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/pam/templates/project/uv/Dockerfile +0 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/pam/templates/project/uv/pyproject.toml +0 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/pam/templates/project/uv/python-version +0 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/pam/templates/service/common/service.yaml +0 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/pam/templates/service/pandas/functions.tmpl +0 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/pam/templates/service/pandas/service.test.tmpl +0 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/pam/templates/service/pandas/service_class.tmpl +0 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/pam/templates/service/polars/functions.tmpl +0 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/pam/templates/service/polars/service.test.tmpl +0 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/pam/templates/service/polars/service_class.tmpl +0 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/pam/utils.py +0 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/pam_python.egg-info/SOURCES.txt +0 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/pam_python.egg-info/dependency_links.txt +0 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/pam_python.egg-info/entry_points.txt +0 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/pam_python.egg-info/requires.txt +0 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/pam_python.egg-info/top_level.txt +0 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/pyproject.toml +0 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/setup.cfg +0 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/tests/test_api.py +0 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/tests/test_data_error_v2.py +0 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/tests/test_data_input_v2.py +0 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/tests/test_data_timeout_v2.py +0 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/tests/test_datetime_utils.py +0 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/tests/test_range_transform.py +0 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/tests/test_request_data_v2.py +0 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/tests/test_result_batch_uploader.py +0 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/tests/test_service_dataframe_output.py +0 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/tests/test_sqlite_download_v2.py +0 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/tests/test_temp_file_utils.py +0 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/tests/test_typed_area_report.py +0 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/tests/test_typed_bar_report.py +0 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/tests/test_typed_box_plot_report.py +0 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/tests/test_typed_bubble_report.py +0 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/tests/test_typed_candlestick_report.py +0 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/tests/test_typed_donut_report.py +0 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/tests/test_typed_heatmap_report.py +0 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/tests/test_typed_line_report.py +0 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/tests/test_typed_metric_report.py +0 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/tests/test_typed_pie_report.py +0 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/tests/test_typed_polar_area_report.py +0 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/tests/test_typed_radar_report.py +0 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/tests/test_typed_radial_bar_report.py +0 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/tests/test_typed_range_area_report.py +0 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/tests/test_typed_range_bar_report.py +0 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/tests/test_typed_scatter_report.py +0 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/tests/test_typed_table_report.py +0 -0
- {pam_python-0.2.10 → pam_python-0.2.11}/tests/test_typed_treemap_report.py +0 -0
|
@@ -31,9 +31,15 @@ class ReportStoreManager:
|
|
|
31
31
|
self._sleep = sleep_fn
|
|
32
32
|
self._stores = {}
|
|
33
33
|
|
|
34
|
-
def create(
|
|
34
|
+
def create(
|
|
35
|
+
self,
|
|
36
|
+
file_name: str,
|
|
37
|
+
shared: bool = False,
|
|
38
|
+
tracker_name: str | None = None,
|
|
39
|
+
) -> ReportStore:
|
|
35
40
|
"""Start a fresh local file without performing a download."""
|
|
36
41
|
self._validate_file_name(file_name)
|
|
42
|
+
tracker_name = self._resolve_tracker_name(tracker_name)
|
|
37
43
|
session_key = (file_name, shared)
|
|
38
44
|
self._assert_not_open(session_key)
|
|
39
45
|
path = self._new_local_path(file_name)
|
|
@@ -41,18 +47,29 @@ class ReportStoreManager:
|
|
|
41
47
|
path,
|
|
42
48
|
file_name,
|
|
43
49
|
shared,
|
|
50
|
+
tracker_name=tracker_name,
|
|
44
51
|
transport=self._transport,
|
|
45
52
|
cleanup_on_close=True,
|
|
46
53
|
)
|
|
47
54
|
self._stores[session_key] = store
|
|
48
55
|
return store
|
|
49
56
|
|
|
50
|
-
def open(
|
|
57
|
+
def open(
|
|
58
|
+
self,
|
|
59
|
+
file_name: str,
|
|
60
|
+
shared: bool = False,
|
|
61
|
+
tracker_name: str | None = None,
|
|
62
|
+
) -> ReportStore:
|
|
51
63
|
"""Resume remote state, or create only after an authoritative 404."""
|
|
52
64
|
self._validate_file_name(file_name)
|
|
65
|
+
tracker_name = self._resolve_tracker_name(tracker_name)
|
|
53
66
|
session_key = (file_name, shared)
|
|
54
67
|
existing = self._stores.get(session_key)
|
|
55
68
|
if existing is not None:
|
|
69
|
+
if existing.tracker_name != tracker_name:
|
|
70
|
+
raise ReportStoreLocalIOError(
|
|
71
|
+
"This report store is already open with a different tracker_name"
|
|
72
|
+
)
|
|
56
73
|
return existing
|
|
57
74
|
|
|
58
75
|
path = self._new_local_path(file_name)
|
|
@@ -64,6 +81,7 @@ class ReportStoreManager:
|
|
|
64
81
|
downloaded_path,
|
|
65
82
|
file_name,
|
|
66
83
|
shared,
|
|
84
|
+
tracker_name=tracker_name,
|
|
67
85
|
transport=self._transport,
|
|
68
86
|
cleanup_on_close=True,
|
|
69
87
|
)
|
|
@@ -75,6 +93,7 @@ class ReportStoreManager:
|
|
|
75
93
|
path,
|
|
76
94
|
file_name,
|
|
77
95
|
shared,
|
|
96
|
+
tracker_name=tracker_name,
|
|
78
97
|
transport=self._transport,
|
|
79
98
|
cleanup_on_close=True,
|
|
80
99
|
)
|
|
@@ -138,6 +157,15 @@ class ReportStoreManager:
|
|
|
138
157
|
"This report store is already open for the current service run"
|
|
139
158
|
)
|
|
140
159
|
|
|
160
|
+
def _resolve_tracker_name(self, tracker_name: str | None) -> str:
|
|
161
|
+
if tracker_name is None:
|
|
162
|
+
tracker_name = self._service.request.service_name
|
|
163
|
+
if not isinstance(tracker_name, str) or not tracker_name.strip():
|
|
164
|
+
raise ReportStoreLocalIOError(
|
|
165
|
+
"tracker_name must be a non-empty string"
|
|
166
|
+
)
|
|
167
|
+
return tracker_name.strip()
|
|
168
|
+
|
|
141
169
|
@staticmethod
|
|
142
170
|
def _validate_file_name(file_name: str) -> None:
|
|
143
171
|
if not isinstance(file_name, str) or not file_name.strip():
|
|
@@ -60,12 +60,14 @@ class ReportStore:
|
|
|
60
60
|
path: Path,
|
|
61
61
|
file_name: str,
|
|
62
62
|
shared: bool,
|
|
63
|
+
tracker_name: str = "",
|
|
63
64
|
transport=None,
|
|
64
65
|
cleanup_on_close: bool = False,
|
|
65
66
|
):
|
|
66
67
|
self.path = Path(path)
|
|
67
68
|
self.file_name = file_name
|
|
68
69
|
self.shared = shared
|
|
70
|
+
self.tracker_name = tracker_name
|
|
69
71
|
self._transport = transport
|
|
70
72
|
self._cleanup_on_close = cleanup_on_close
|
|
71
73
|
self._lock = threading.RLock()
|
|
@@ -87,6 +89,7 @@ class ReportStore:
|
|
|
87
89
|
path: Path,
|
|
88
90
|
file_name: str,
|
|
89
91
|
shared: bool,
|
|
92
|
+
tracker_name: str = "",
|
|
90
93
|
transport=None,
|
|
91
94
|
cleanup_on_close: bool = False,
|
|
92
95
|
):
|
|
@@ -97,7 +100,14 @@ class ReportStore:
|
|
|
97
100
|
except OSError as exc:
|
|
98
101
|
raise ReportStoreLocalIOError(str(exc)) from exc
|
|
99
102
|
|
|
100
|
-
store = cls(
|
|
103
|
+
store = cls(
|
|
104
|
+
path,
|
|
105
|
+
file_name,
|
|
106
|
+
shared,
|
|
107
|
+
tracker_name=tracker_name,
|
|
108
|
+
transport=transport,
|
|
109
|
+
cleanup_on_close=cleanup_on_close,
|
|
110
|
+
)
|
|
101
111
|
try:
|
|
102
112
|
with store._connection:
|
|
103
113
|
store._connection.executescript(CREATE_SCHEMA_SQL)
|
|
@@ -116,10 +126,18 @@ class ReportStore:
|
|
|
116
126
|
path: Path,
|
|
117
127
|
file_name: str,
|
|
118
128
|
shared: bool,
|
|
129
|
+
tracker_name: str = "",
|
|
119
130
|
transport=None,
|
|
120
131
|
cleanup_on_close: bool = False,
|
|
121
132
|
):
|
|
122
|
-
store = cls(
|
|
133
|
+
store = cls(
|
|
134
|
+
path,
|
|
135
|
+
file_name,
|
|
136
|
+
shared,
|
|
137
|
+
tracker_name=tracker_name,
|
|
138
|
+
transport=transport,
|
|
139
|
+
cleanup_on_close=cleanup_on_close,
|
|
140
|
+
)
|
|
123
141
|
try:
|
|
124
142
|
store._migrate_compatible_schema()
|
|
125
143
|
store.validate()
|
|
@@ -476,6 +476,7 @@ class TaskManager(ITaskManager):
|
|
|
476
476
|
service: Service,
|
|
477
477
|
report_key: str,
|
|
478
478
|
pointer: dict,
|
|
479
|
+
tracker_name: str,
|
|
479
480
|
) -> bool:
|
|
480
481
|
"""Synchronously publish one V2 pointer through the existing response API."""
|
|
481
482
|
adapter_id = service.get_adapter_id()
|
|
@@ -496,7 +497,7 @@ class TaskManager(ITaskManager):
|
|
|
496
497
|
writer.writeheader()
|
|
497
498
|
writer.writerow({
|
|
498
499
|
"customer": adapter_id,
|
|
499
|
-
"_tracker_name":
|
|
500
|
+
"_tracker_name": tracker_name,
|
|
500
501
|
column_name: json.dumps(pointer, ensure_ascii=False, sort_keys=True),
|
|
501
502
|
})
|
|
502
503
|
|
|
@@ -112,7 +112,10 @@ Notes
|
|
|
112
112
|
|
|
113
113
|
- All values are strings. Cast with defaults to avoid errors.
|
|
114
114
|
- `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.
|
|
115
|
-
- You do not need to check `dry_run` manually.
|
|
115
|
+
- You do not need to check `dry_run` manually. Submit tabular results through
|
|
116
|
+
`ResultBatchUploader` and managed reports through `ReportStore.publish()`; the
|
|
117
|
+
framework suppresses their remote transport during a dry run. Never call the
|
|
118
|
+
private `_upload_result` path directly from plugin code.
|
|
116
119
|
|
|
117
120
|
**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.
|
|
118
121
|
|
|
@@ -146,8 +149,9 @@ Column meaning
|
|
|
146
149
|
|
|
147
150
|
Date format is always:
|
|
148
151
|
`"2025-09-11 13:07:38"`
|
|
149
|
-
|
|
150
|
-
|
|
152
|
+
The timezone of customer-owned source fields is an external data contract and may
|
|
153
|
+
vary by customer. Confirm it when business logic depends on calendar boundaries,
|
|
154
|
+
then convert explicitly. This does not change PAM's internal UTC+0 contract above.
|
|
151
155
|
|
|
152
156
|
---
|
|
153
157
|
|
|
@@ -238,6 +242,20 @@ the optional half-open `data.time_window` expose the captured run semantics.
|
|
|
238
242
|
The framework rejects callback-token/run-ID mismatches and contradictory event
|
|
239
243
|
population or time-window metadata before plugin code runs.
|
|
240
244
|
|
|
245
|
+
Data Input V2 and Report Store V2 are independent contracts. A service may use
|
|
246
|
+
Data Input V2 without producing managed reports, and report calculations are not
|
|
247
|
+
required to use Data Input V2.
|
|
248
|
+
|
|
249
|
+
For Data Input V2 pagination:
|
|
250
|
+
|
|
251
|
+
- Use `data.run_complete`, not legacy `req.is_end`, as the completion signal.
|
|
252
|
+
- Use only the opaque `data.next` cursor when requesting the next page.
|
|
253
|
+
- Materialize every lazy computation that still reads the page before calling
|
|
254
|
+
`data.cleanup()`.
|
|
255
|
+
- Treat a failed cleanup as an error before requesting the next page.
|
|
256
|
+
- If the service produces managed reports, do not publish a partially calculated
|
|
257
|
+
store on an intermediate page. Publish after all intended pages and writes finish.
|
|
258
|
+
|
|
241
259
|
---
|
|
242
260
|
|
|
243
261
|
## Upload Results to CDP
|
|
@@ -400,7 +418,7 @@ Use it for cleanup.
|
|
|
400
418
|
|
|
401
419
|
---
|
|
402
420
|
|
|
403
|
-
#
|
|
421
|
+
# 5. Persist State Between Runs (sqlite/duckdb)
|
|
404
422
|
|
|
405
423
|
Services can be killed and restarted at any time. If you need state across runs, use sqlite or duckdb via the provided API.
|
|
406
424
|
|
|
@@ -434,17 +452,97 @@ Notes
|
|
|
434
452
|
|
|
435
453
|
Note: `file_name` acts like a server key, while `sqlite_file` is the local file path.
|
|
436
454
|
|
|
437
|
-
|
|
455
|
+
# 6. Managed Report Store V2 Agent Contract
|
|
438
456
|
|
|
439
457
|
The low-level SQLite API above is for custom plugin state. Do not use it to invent
|
|
440
458
|
SQLite tables or JSON payloads for reports rendered by PAM CMS.
|
|
441
459
|
|
|
442
|
-
|
|
443
|
-
Report types outside that closed contract require a
|
|
460
|
+
Before implementing a managed report, read `REPORTS.md` completely and use only its
|
|
461
|
+
public typed report builders. Report types outside that closed contract require a
|
|
462
|
+
newer framework and CMS release. Do not use `_request_sqlite`, `_upload_sqlite`,
|
|
463
|
+
`_upload_report`, private declaration methods, raw SQLite schema changes, handwritten
|
|
464
|
+
renderer/schema identifiers, or handcrafted report-pointer CSV files for Report
|
|
465
|
+
Store V2.
|
|
466
|
+
|
|
467
|
+
## Store, tracker group, and report identity
|
|
468
|
+
|
|
469
|
+
These identifiers have different responsibilities:
|
|
470
|
+
|
|
471
|
+
- `file_name` identifies one managed SQLite store. One store may contain many report
|
|
472
|
+
definitions and their records.
|
|
473
|
+
- `tracker_name` is the stable, user-facing Analytics Tracker group shown in PAM CMS.
|
|
474
|
+
Related reports in the same store should normally use one shared tracker name.
|
|
475
|
+
- `report key` uniquely identifies one report inside the store and must remain stable.
|
|
476
|
+
Every changed report key is published as its own Analytics Tracker attribute, so
|
|
477
|
+
PAM CMS can list every report separately even though they share one tracker group
|
|
478
|
+
and one SQLite file.
|
|
479
|
+
|
|
480
|
+
Always pass a non-empty, customer-facing `tracker_name` to both `self.reports.open`
|
|
481
|
+
and `self.reports.create` when the service owns managed reports:
|
|
482
|
+
|
|
483
|
+
```python
|
|
484
|
+
REPORT_FILE = "reports.sqlite"
|
|
485
|
+
REPORT_TRACKER_NAME = "Customer Overall Reports"
|
|
486
|
+
|
|
487
|
+
fresh = self.request.get_runtime_bool("fresh", default=False)
|
|
488
|
+
if fresh:
|
|
489
|
+
self.report_store = self.reports.create(
|
|
490
|
+
REPORT_FILE,
|
|
491
|
+
tracker_name=REPORT_TRACKER_NAME,
|
|
492
|
+
)
|
|
493
|
+
else:
|
|
494
|
+
self.report_store = self.reports.open(
|
|
495
|
+
REPORT_FILE,
|
|
496
|
+
tracker_name=REPORT_TRACKER_NAME,
|
|
497
|
+
)
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
If `tracker_name` is omitted, the framework falls back to `request.service_name`,
|
|
501
|
+
but an explicit label is preferred for customer projects. Never generate one tracker
|
|
502
|
+
name per report merely to separate reports. Never write `_tracker_name` yourself;
|
|
503
|
+
the framework propagates the store's tracker name to every V2 pointer. Reopening the
|
|
504
|
+
same file in one service run with a different tracker name is an error.
|
|
505
|
+
|
|
506
|
+
The framework also publishes the service adapter ID as the Analytics Tracker owner.
|
|
507
|
+
PAM and PAM CMS use that metadata for Hard Refresh routing. Plugin code must not ask
|
|
508
|
+
a user to select an owner, encode the owner in `tracker_name` or `report key`, or add
|
|
509
|
+
an ad-hoc owner field to the pointer.
|
|
510
|
+
|
|
511
|
+
## Required lifecycle
|
|
512
|
+
|
|
513
|
+
1. Open the store once during service initialization. Use `create` only for an
|
|
514
|
+
explicit human-requested fresh reset. Normal first use calls `open`; the framework
|
|
515
|
+
creates a store only after an authoritative not-found response.
|
|
516
|
+
2. Do not catch authorization, transient download, or unknown remote-state errors and
|
|
517
|
+
replace the store. Let the framework retry or raise the typed error.
|
|
518
|
+
3. Declare every report once after opening the store. Multiple reports should share
|
|
519
|
+
the same `ReportStore` when they belong to the same customer report group.
|
|
520
|
+
4. Bind input datasets by their configured positions and complete all required page
|
|
521
|
+
calculations before publishing. Report calculations may accumulate in memory or
|
|
522
|
+
write incrementally to the local store, depending on the domain requirement.
|
|
523
|
+
5. For refreshed `DAY` or `MONTH` facts, delete the source-derived partition before
|
|
524
|
+
writing its complete replacement. Never derive a business partition from plugin
|
|
525
|
+
run time unless that is explicitly the source contract.
|
|
526
|
+
6. Call `self.report_store.publish()` once after the complete calculation succeeds,
|
|
527
|
+
then call `_exit()`. Do not publish after a calculation, validation, or cleanup
|
|
528
|
+
failure, and normally do not publish once per input page.
|
|
529
|
+
|
|
530
|
+
One successful `publish()` uploads the SQLite file once, then publishes one pointer
|
|
531
|
+
for every report key changed during the run. All of those pointers carry the same
|
|
532
|
+
stable tracker name. The framework handles `dry_run` and local-file cleanup; plugin
|
|
533
|
+
code must not reproduce the transport lifecycle.
|
|
534
|
+
|
|
535
|
+
An existing report key cannot silently change renderer, schema, field declaration,
|
|
536
|
+
granularity, or presentation contract. Use a deliberate new key or an explicitly
|
|
537
|
+
approved fresh reset when the compatibility contract changes.
|
|
538
|
+
|
|
539
|
+
Before using a report API, verify that the project's pinned `pam-python` release
|
|
540
|
+
contains it. Customer plugins consume the framework package from their dependency
|
|
541
|
+
manager; do not copy framework internals into the plugin as a workaround.
|
|
444
542
|
|
|
445
543
|
---
|
|
446
544
|
|
|
447
|
-
#
|
|
545
|
+
# 7. Batch upload policy (DataFrame batching)
|
|
448
546
|
|
|
449
547
|
## Goal
|
|
450
548
|
|
|
@@ -518,7 +616,7 @@ After retries are exhausted, the uploader logs the failed batch, records
|
|
|
518
616
|
|
|
519
617
|
---
|
|
520
618
|
|
|
521
|
-
#
|
|
619
|
+
# 8. Memory / RAM safety policy (Data Plugin)
|
|
522
620
|
|
|
523
621
|
## Goal
|
|
524
622
|
|
|
@@ -127,17 +127,29 @@ store; an authoritative not-found response creates the first store automatically
|
|
|
127
127
|
fresh = self.request.get_runtime_bool("fresh", default=False)
|
|
128
128
|
|
|
129
129
|
if fresh:
|
|
130
|
-
store = self.reports.create(
|
|
130
|
+
store = self.reports.create(
|
|
131
|
+
"reports.sqlite",
|
|
132
|
+
tracker_name="Customer Report Group",
|
|
133
|
+
)
|
|
131
134
|
else:
|
|
132
|
-
store = self.reports.open(
|
|
135
|
+
store = self.reports.open(
|
|
136
|
+
"reports.sqlite",
|
|
137
|
+
tracker_name="Customer Report Group",
|
|
138
|
+
)
|
|
133
139
|
```
|
|
134
140
|
|
|
141
|
+
`tracker_name` is the Analytics Tracker group label shown in PAM CMS. Keep it stable
|
|
142
|
+
for all reports in the store. If omitted, the framework uses the service name.
|
|
143
|
+
|
|
135
144
|
Declare reports once after opening the store. Append data across as many input
|
|
136
145
|
pages as required, then publish once after all intended work is complete.
|
|
137
146
|
|
|
138
147
|
```python
|
|
139
148
|
def on_start(self):
|
|
140
|
-
self.report_store = self.reports.open(
|
|
149
|
+
self.report_store = self.reports.open(
|
|
150
|
+
"reports.sqlite",
|
|
151
|
+
tracker_name="Customer Report Group",
|
|
152
|
+
)
|
|
141
153
|
self._declare_reports()
|
|
142
154
|
self._request_data(file_format=RequestFileFormat.PARQUET)
|
|
143
155
|
|
|
@@ -18,7 +18,7 @@ UploadSqliteCallbackType = Callable[[str, bool, str], Optional[str]]
|
|
|
18
18
|
UploadResultCallbackType = Callable[[str], None]
|
|
19
19
|
UploadReportCallbackType = Callable[[str], None]
|
|
20
20
|
RequestReportStoreCallbackType = Callable[[str, bool, str], SQLiteDownloadResult]
|
|
21
|
-
PublishReportPointerCallbackType = Callable[
|
|
21
|
+
PublishReportPointerCallbackType = Callable[..., bool]
|
|
22
22
|
|
|
23
23
|
class TesterTask(ITaskManager):
|
|
24
24
|
"""
|
|
@@ -289,8 +289,29 @@ class TesterTask(ITaskManager):
|
|
|
289
289
|
service: Service,
|
|
290
290
|
report_key: str,
|
|
291
291
|
pointer: dict,
|
|
292
|
+
tracker_name: str,
|
|
292
293
|
) -> bool:
|
|
293
294
|
if self.publish_report_pointer_callback is not None:
|
|
295
|
+
parameters = list(
|
|
296
|
+
inspect.signature(self.publish_report_pointer_callback).parameters.values()
|
|
297
|
+
)
|
|
298
|
+
accepts_varargs = any(
|
|
299
|
+
parameter.kind is inspect.Parameter.VAR_POSITIONAL
|
|
300
|
+
for parameter in parameters
|
|
301
|
+
)
|
|
302
|
+
positional_count = sum(
|
|
303
|
+
parameter.kind in (
|
|
304
|
+
inspect.Parameter.POSITIONAL_ONLY,
|
|
305
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
306
|
+
)
|
|
307
|
+
for parameter in parameters
|
|
308
|
+
)
|
|
309
|
+
if accepts_varargs or positional_count >= 3:
|
|
310
|
+
return self.publish_report_pointer_callback(
|
|
311
|
+
report_key,
|
|
312
|
+
pointer,
|
|
313
|
+
tracker_name,
|
|
314
|
+
)
|
|
294
315
|
return self.publish_report_pointer_callback(report_key, pointer)
|
|
295
316
|
print("Publish Report Store V2 pointer callback is not set.")
|
|
296
317
|
return False
|
|
@@ -47,6 +47,23 @@ class CLITemplateTest(unittest.TestCase):
|
|
|
47
47
|
self.assertIn("Customer-based Pagination Contract", agent_guide)
|
|
48
48
|
self.assertIn("same customer-ID cohort", agent_guide)
|
|
49
49
|
self.assertIn("customer-cohort cursor", agent_guide)
|
|
50
|
+
normalized_agent_guide = " ".join(agent_guide.split())
|
|
51
|
+
self.assertIn(
|
|
52
|
+
"Data Input V2 and Report Store V2 are independent contracts",
|
|
53
|
+
normalized_agent_guide,
|
|
54
|
+
)
|
|
55
|
+
self.assertIn(
|
|
56
|
+
"One store may contain many report",
|
|
57
|
+
normalized_agent_guide,
|
|
58
|
+
)
|
|
59
|
+
self.assertIn(
|
|
60
|
+
"Never generate one tracker name per report",
|
|
61
|
+
normalized_agent_guide,
|
|
62
|
+
)
|
|
63
|
+
self.assertIn(
|
|
64
|
+
"publishes one pointer for every report key changed",
|
|
65
|
+
normalized_agent_guide,
|
|
66
|
+
)
|
|
50
67
|
self.assertTrue((root / "run_unit_test.sh").is_file())
|
|
51
68
|
self.assertTrue(os.access(root / "run_unit_test.sh", os.X_OK))
|
|
52
69
|
self.assertTrue((root / "run_unit_test.ps1").is_file())
|
|
@@ -16,6 +16,7 @@ from pam.reports import (
|
|
|
16
16
|
ReportDefinitionConflictError,
|
|
17
17
|
ReportStore,
|
|
18
18
|
ReportStoreDownloadError,
|
|
19
|
+
ReportStoreLocalIOError,
|
|
19
20
|
ReportStoreManager,
|
|
20
21
|
ReportStoreValidationError,
|
|
21
22
|
)
|
|
@@ -45,7 +46,9 @@ class FakeTransport:
|
|
|
45
46
|
return result
|
|
46
47
|
|
|
47
48
|
def publish(self, store, changed_report_keys):
|
|
48
|
-
self.publish_calls.append(
|
|
49
|
+
self.publish_calls.append(
|
|
50
|
+
(store.file_name, store.tracker_name, tuple(changed_report_keys))
|
|
51
|
+
)
|
|
49
52
|
|
|
50
53
|
|
|
51
54
|
def make_service():
|
|
@@ -70,8 +73,14 @@ class FakeTaskManager:
|
|
|
70
73
|
self.uploads.append((file_name, shared, sqlite_file))
|
|
71
74
|
return file_name
|
|
72
75
|
|
|
73
|
-
def service_publish_report_pointer_v2(
|
|
74
|
-
self
|
|
76
|
+
def service_publish_report_pointer_v2(
|
|
77
|
+
self,
|
|
78
|
+
service,
|
|
79
|
+
report_key,
|
|
80
|
+
pointer,
|
|
81
|
+
tracker_name,
|
|
82
|
+
):
|
|
83
|
+
self.pointers.append((report_key, pointer, tracker_name))
|
|
75
84
|
return True
|
|
76
85
|
|
|
77
86
|
|
|
@@ -277,7 +286,7 @@ class ReportStoreTest(unittest.TestCase):
|
|
|
277
286
|
self.store.publish()
|
|
278
287
|
|
|
279
288
|
self.assertEqual([
|
|
280
|
-
("reports.sqlite", ("test-report-a", "test-report-b"))
|
|
289
|
+
("reports.sqlite", "", ("test-report-a", "test-report-b"))
|
|
281
290
|
], self.transport.publish_calls)
|
|
282
291
|
|
|
283
292
|
def test_generated_consumer_contract_uses_public_framework_api(self):
|
|
@@ -338,9 +347,28 @@ class ReportStoreManagerTest(unittest.TestCase):
|
|
|
338
347
|
store = manager.create("reports.sqlite")
|
|
339
348
|
|
|
340
349
|
self.assertTrue(store.path.is_file())
|
|
350
|
+
self.assertEqual("test-service", store.tracker_name)
|
|
341
351
|
self.assertEqual([], transport.download_calls)
|
|
342
352
|
manager.close()
|
|
343
353
|
|
|
354
|
+
def test_create_accepts_explicit_tracker_name(self):
|
|
355
|
+
transport = FakeTransport()
|
|
356
|
+
manager = ReportStoreManager(self.service, transport=transport)
|
|
357
|
+
|
|
358
|
+
store = manager.create(
|
|
359
|
+
"reports.sqlite",
|
|
360
|
+
tracker_name="Customer Report Group",
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
self.assertEqual("Customer Report Group", store.tracker_name)
|
|
364
|
+
manager.close()
|
|
365
|
+
|
|
366
|
+
def test_create_rejects_empty_tracker_name(self):
|
|
367
|
+
manager = ReportStoreManager(self.service, transport=FakeTransport())
|
|
368
|
+
|
|
369
|
+
with self.assertRaises(ReportStoreLocalIOError):
|
|
370
|
+
manager.create("reports.sqlite", tracker_name=" ")
|
|
371
|
+
|
|
344
372
|
def test_discard_local_removes_only_open_local_store_and_allows_recreate(self):
|
|
345
373
|
transport = FakeTransport()
|
|
346
374
|
manager = ReportStoreManager(self.service, transport=transport)
|
|
@@ -447,6 +475,7 @@ class ReportStoreTransportTest(unittest.TestCase):
|
|
|
447
475
|
store = SimpleNamespace(
|
|
448
476
|
file_name="reports.sqlite",
|
|
449
477
|
shared=False,
|
|
478
|
+
tracker_name="Customer Report Group",
|
|
450
479
|
path=Path("/tmp/reports.sqlite"),
|
|
451
480
|
)
|
|
452
481
|
|
|
@@ -454,10 +483,11 @@ class ReportStoreTransportTest(unittest.TestCase):
|
|
|
454
483
|
|
|
455
484
|
self.assertEqual(1, len(service.task_manager.uploads))
|
|
456
485
|
self.assertEqual(2, len(service.task_manager.pointers))
|
|
457
|
-
report_key, pointer = service.task_manager.pointers[0]
|
|
486
|
+
report_key, pointer, tracker_name = service.task_manager.pointers[0]
|
|
458
487
|
self.assertEqual("test-report-a", report_key)
|
|
459
488
|
self.assertEqual("REPORT_STORE_V2", pointer["protocol"])
|
|
460
489
|
self.assertEqual("adapter-1", pointer["namespace"])
|
|
490
|
+
self.assertEqual("Customer Report Group", tracker_name)
|
|
461
491
|
|
|
462
492
|
def test_task_manager_wraps_pointer_in_existing_report_csv_format(self):
|
|
463
493
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
@@ -495,7 +525,10 @@ class ReportStoreTransportTest(unittest.TestCase):
|
|
|
495
525
|
|
|
496
526
|
try:
|
|
497
527
|
published = manager.service_publish_report_pointer_v2(
|
|
498
|
-
service,
|
|
528
|
+
service,
|
|
529
|
+
"test-report-a",
|
|
530
|
+
pointer,
|
|
531
|
+
tracker_name="Customer Report Group",
|
|
499
532
|
)
|
|
500
533
|
finally:
|
|
501
534
|
TempfileUtils.temp_datasource_path = previous_temp_path
|
|
@@ -503,6 +536,7 @@ class ReportStoreTransportTest(unittest.TestCase):
|
|
|
503
536
|
self.assertTrue(published)
|
|
504
537
|
self.assertEqual(request.response_api, captured["endpoint"])
|
|
505
538
|
self.assertEqual("adapter-1", captured["row"]["customer"])
|
|
539
|
+
self.assertEqual("Customer Report Group", captured["row"]["_tracker_name"])
|
|
506
540
|
self.assertEqual(
|
|
507
541
|
pointer,
|
|
508
542
|
json.loads(captured["row"]["data_test-report-a_no_index"]),
|
|
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
|
|
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
|
|
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
|