pam-python 0.2.11__tar.gz → 0.2.12__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 (117) hide show
  1. {pam_python-0.2.11/pam_python.egg-info → pam_python-0.2.12}/PKG-INFO +1 -1
  2. {pam_python-0.2.11 → pam_python-0.2.12}/pam/__init__.py +1 -1
  3. {pam_python-0.2.11 → pam_python-0.2.12}/pam/reports/__init__.py +2 -0
  4. {pam_python-0.2.11 → pam_python-0.2.12}/pam/reports/area.py +10 -0
  5. {pam_python-0.2.11 → pam_python-0.2.12}/pam/reports/bar.py +10 -0
  6. {pam_python-0.2.11 → pam_python-0.2.12}/pam/reports/definition.py +16 -0
  7. {pam_python-0.2.11 → pam_python-0.2.12}/pam/reports/donut.py +6 -0
  8. {pam_python-0.2.11 → pam_python-0.2.12}/pam/reports/line.py +10 -0
  9. {pam_python-0.2.11 → pam_python-0.2.12}/pam/reports/metric.py +10 -0
  10. {pam_python-0.2.11 → pam_python-0.2.12}/pam/reports/pie.py +6 -0
  11. {pam_python-0.2.11 → pam_python-0.2.12}/pam/reports/polar_area.py +6 -0
  12. {pam_python-0.2.11 → pam_python-0.2.12}/pam/reports/radial_bar.py +10 -0
  13. {pam_python-0.2.11 → pam_python-0.2.12}/pam/reports/store.py +33 -8
  14. {pam_python-0.2.11 → pam_python-0.2.12}/pam/reports/table.py +10 -0
  15. pam_python-0.2.12/pam/reports/temporal.py +55 -0
  16. {pam_python-0.2.11 → pam_python-0.2.12}/pam/templates/init/AGENT.md +9 -4
  17. {pam_python-0.2.11 → pam_python-0.2.12}/pam/templates/init/REPORTS.md +137 -14
  18. {pam_python-0.2.11 → pam_python-0.2.12/pam_python.egg-info}/PKG-INFO +1 -1
  19. {pam_python-0.2.11 → pam_python-0.2.12}/pam_python.egg-info/SOURCES.txt +2 -0
  20. {pam_python-0.2.11 → pam_python-0.2.12}/tests/test_cli_templates.py +23 -0
  21. pam_python-0.2.12/tests/test_temporal_mode.py +116 -0
  22. {pam_python-0.2.11 → pam_python-0.2.12}/LICENSE.txt +0 -0
  23. {pam_python-0.2.11 → pam_python-0.2.12}/README.md +0 -0
  24. {pam_python-0.2.11 → pam_python-0.2.12}/pam/api.py +0 -0
  25. {pam_python-0.2.11 → pam_python-0.2.12}/pam/cli.py +0 -0
  26. {pam_python-0.2.11 → pam_python-0.2.12}/pam/datetime_utils.py +0 -0
  27. {pam_python-0.2.11 → pam_python-0.2.12}/pam/interface_task_manager.py +0 -0
  28. {pam_python-0.2.11 → pam_python-0.2.12}/pam/logger.py +0 -0
  29. {pam_python-0.2.11 → pam_python-0.2.12}/pam/models/__init__.py +0 -0
  30. {pam_python-0.2.11 → pam_python-0.2.12}/pam/models/data_input_v2.py +0 -0
  31. {pam_python-0.2.11 → pam_python-0.2.12}/pam/models/request_command.py +0 -0
  32. {pam_python-0.2.11 → pam_python-0.2.12}/pam/models/sqlite_download.py +0 -0
  33. {pam_python-0.2.11 → pam_python-0.2.12}/pam/reports/axis.py +0 -0
  34. {pam_python-0.2.11 → pam_python-0.2.12}/pam/reports/box_plot.py +0 -0
  35. {pam_python-0.2.11 → pam_python-0.2.12}/pam/reports/bubble.py +0 -0
  36. {pam_python-0.2.11 → pam_python-0.2.12}/pam/reports/candlestick.py +0 -0
  37. {pam_python-0.2.11 → pam_python-0.2.12}/pam/reports/collection.py +0 -0
  38. {pam_python-0.2.11 → pam_python-0.2.12}/pam/reports/errors.py +0 -0
  39. {pam_python-0.2.11 → pam_python-0.2.12}/pam/reports/fields.py +0 -0
  40. {pam_python-0.2.11 → pam_python-0.2.12}/pam/reports/heatmap.py +0 -0
  41. {pam_python-0.2.11 → pam_python-0.2.12}/pam/reports/manager.py +0 -0
  42. {pam_python-0.2.11 → pam_python-0.2.12}/pam/reports/radar.py +0 -0
  43. {pam_python-0.2.11 → pam_python-0.2.12}/pam/reports/range_area.py +0 -0
  44. {pam_python-0.2.11 → pam_python-0.2.12}/pam/reports/range_bar.py +0 -0
  45. {pam_python-0.2.11 → pam_python-0.2.12}/pam/reports/range_transform.py +0 -0
  46. {pam_python-0.2.11 → pam_python-0.2.12}/pam/reports/record.py +0 -0
  47. {pam_python-0.2.11 → pam_python-0.2.12}/pam/reports/report.py +0 -0
  48. {pam_python-0.2.11 → pam_python-0.2.12}/pam/reports/scatter.py +0 -0
  49. {pam_python-0.2.11 → pam_python-0.2.12}/pam/reports/schema.py +0 -0
  50. {pam_python-0.2.11 → pam_python-0.2.12}/pam/reports/transport.py +0 -0
  51. {pam_python-0.2.11 → pam_python-0.2.12}/pam/reports/treemap.py +0 -0
  52. {pam_python-0.2.11 → pam_python-0.2.12}/pam/request_file_format.py +0 -0
  53. {pam_python-0.2.11 → pam_python-0.2.12}/pam/result_batch_uploader.py +0 -0
  54. {pam_python-0.2.11 → pam_python-0.2.12}/pam/server.py +0 -0
  55. {pam_python-0.2.11 → pam_python-0.2.12}/pam/service.py +0 -0
  56. {pam_python-0.2.11 → pam_python-0.2.12}/pam/sqlite.py +0 -0
  57. {pam_python-0.2.11 → pam_python-0.2.12}/pam/task_manager.py +0 -0
  58. {pam_python-0.2.11 → pam_python-0.2.12}/pam/temp_file_utils.py +0 -0
  59. {pam_python-0.2.11 → pam_python-0.2.12}/pam/templates/buildcmd/pamb +0 -0
  60. {pam_python-0.2.11 → pam_python-0.2.12}/pam/templates/buildcmd/pamb-base.sh +0 -0
  61. {pam_python-0.2.11 → pam_python-0.2.12}/pam/templates/init/dockerignore.tmpl +0 -0
  62. {pam_python-0.2.11 → pam_python-0.2.12}/pam/templates/init/gitignore.tmpl +0 -0
  63. {pam_python-0.2.11 → pam_python-0.2.12}/pam/templates/init/main.tmpl +0 -0
  64. {pam_python-0.2.11 → pam_python-0.2.12}/pam/templates/init/pylintrc.tmpl +0 -0
  65. {pam_python-0.2.11 → pam_python-0.2.12}/pam/templates/init/run_unit_test.bat +0 -0
  66. {pam_python-0.2.11 → pam_python-0.2.12}/pam/templates/init/run_unit_test.ps1 +0 -0
  67. {pam_python-0.2.11 → pam_python-0.2.12}/pam/templates/init/run_unit_test.sh +0 -0
  68. {pam_python-0.2.11 → pam_python-0.2.12}/pam/templates/project/pip/Dockerfile +0 -0
  69. {pam_python-0.2.11 → pam_python-0.2.12}/pam/templates/project/pip/requirements.txt +0 -0
  70. {pam_python-0.2.11 → pam_python-0.2.12}/pam/templates/project/uv/Dockerfile +0 -0
  71. {pam_python-0.2.11 → pam_python-0.2.12}/pam/templates/project/uv/pyproject.toml +0 -0
  72. {pam_python-0.2.11 → pam_python-0.2.12}/pam/templates/project/uv/python-version +0 -0
  73. {pam_python-0.2.11 → pam_python-0.2.12}/pam/templates/service/common/service.yaml +0 -0
  74. {pam_python-0.2.11 → pam_python-0.2.12}/pam/templates/service/pandas/functions.tmpl +0 -0
  75. {pam_python-0.2.11 → pam_python-0.2.12}/pam/templates/service/pandas/service.test.tmpl +0 -0
  76. {pam_python-0.2.11 → pam_python-0.2.12}/pam/templates/service/pandas/service_class.tmpl +0 -0
  77. {pam_python-0.2.11 → pam_python-0.2.12}/pam/templates/service/polars/functions.tmpl +0 -0
  78. {pam_python-0.2.11 → pam_python-0.2.12}/pam/templates/service/polars/service.test.tmpl +0 -0
  79. {pam_python-0.2.11 → pam_python-0.2.12}/pam/templates/service/polars/service_class.tmpl +0 -0
  80. {pam_python-0.2.11 → pam_python-0.2.12}/pam/tester_task.py +0 -0
  81. {pam_python-0.2.11 → pam_python-0.2.12}/pam/utils.py +0 -0
  82. {pam_python-0.2.11 → pam_python-0.2.12}/pam_python.egg-info/dependency_links.txt +0 -0
  83. {pam_python-0.2.11 → pam_python-0.2.12}/pam_python.egg-info/entry_points.txt +0 -0
  84. {pam_python-0.2.11 → pam_python-0.2.12}/pam_python.egg-info/requires.txt +0 -0
  85. {pam_python-0.2.11 → pam_python-0.2.12}/pam_python.egg-info/top_level.txt +0 -0
  86. {pam_python-0.2.11 → pam_python-0.2.12}/pyproject.toml +0 -0
  87. {pam_python-0.2.11 → pam_python-0.2.12}/setup.cfg +0 -0
  88. {pam_python-0.2.11 → pam_python-0.2.12}/tests/test_api.py +0 -0
  89. {pam_python-0.2.11 → pam_python-0.2.12}/tests/test_data_error_v2.py +0 -0
  90. {pam_python-0.2.11 → pam_python-0.2.12}/tests/test_data_input_v2.py +0 -0
  91. {pam_python-0.2.11 → pam_python-0.2.12}/tests/test_data_timeout_v2.py +0 -0
  92. {pam_python-0.2.11 → pam_python-0.2.12}/tests/test_datetime_utils.py +0 -0
  93. {pam_python-0.2.11 → pam_python-0.2.12}/tests/test_range_transform.py +0 -0
  94. {pam_python-0.2.11 → pam_python-0.2.12}/tests/test_report_store.py +0 -0
  95. {pam_python-0.2.11 → pam_python-0.2.12}/tests/test_request_data_v2.py +0 -0
  96. {pam_python-0.2.11 → pam_python-0.2.12}/tests/test_result_batch_uploader.py +0 -0
  97. {pam_python-0.2.11 → pam_python-0.2.12}/tests/test_service_dataframe_output.py +0 -0
  98. {pam_python-0.2.11 → pam_python-0.2.12}/tests/test_sqlite_download_v2.py +0 -0
  99. {pam_python-0.2.11 → pam_python-0.2.12}/tests/test_temp_file_utils.py +0 -0
  100. {pam_python-0.2.11 → pam_python-0.2.12}/tests/test_typed_area_report.py +0 -0
  101. {pam_python-0.2.11 → pam_python-0.2.12}/tests/test_typed_bar_report.py +0 -0
  102. {pam_python-0.2.11 → pam_python-0.2.12}/tests/test_typed_box_plot_report.py +0 -0
  103. {pam_python-0.2.11 → pam_python-0.2.12}/tests/test_typed_bubble_report.py +0 -0
  104. {pam_python-0.2.11 → pam_python-0.2.12}/tests/test_typed_candlestick_report.py +0 -0
  105. {pam_python-0.2.11 → pam_python-0.2.12}/tests/test_typed_donut_report.py +0 -0
  106. {pam_python-0.2.11 → pam_python-0.2.12}/tests/test_typed_heatmap_report.py +0 -0
  107. {pam_python-0.2.11 → pam_python-0.2.12}/tests/test_typed_line_report.py +0 -0
  108. {pam_python-0.2.11 → pam_python-0.2.12}/tests/test_typed_metric_report.py +0 -0
  109. {pam_python-0.2.11 → pam_python-0.2.12}/tests/test_typed_pie_report.py +0 -0
  110. {pam_python-0.2.11 → pam_python-0.2.12}/tests/test_typed_polar_area_report.py +0 -0
  111. {pam_python-0.2.11 → pam_python-0.2.12}/tests/test_typed_radar_report.py +0 -0
  112. {pam_python-0.2.11 → pam_python-0.2.12}/tests/test_typed_radial_bar_report.py +0 -0
  113. {pam_python-0.2.11 → pam_python-0.2.12}/tests/test_typed_range_area_report.py +0 -0
  114. {pam_python-0.2.11 → pam_python-0.2.12}/tests/test_typed_range_bar_report.py +0 -0
  115. {pam_python-0.2.11 → pam_python-0.2.12}/tests/test_typed_scatter_report.py +0 -0
  116. {pam_python-0.2.11 → pam_python-0.2.12}/tests/test_typed_table_report.py +0 -0
  117. {pam_python-0.2.11 → pam_python-0.2.12}/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.11
3
+ Version: 0.2.12
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
@@ -1,3 +1,3 @@
1
1
  """PAM Data Plugin framework."""
2
2
 
3
- __version__ = "0.2.11"
3
+ __version__ = "0.2.12"
@@ -31,6 +31,7 @@ from pam.reports.range_bar import RangeBarChartReport
31
31
  from pam.reports.scatter import ScatterChartReport
32
32
  from pam.reports.store import ReportStore
33
33
  from pam.reports.table import TableReport
34
+ from pam.reports.temporal import TemporalMode
34
35
  from pam.reports.treemap import TreemapReport
35
36
 
36
37
  __all__ = [
@@ -68,6 +69,7 @@ __all__ = [
68
69
  "ScatterChartReport",
69
70
  "Sum",
70
71
  "TableReport",
72
+ "TemporalMode",
71
73
  "TimeRangeTransform",
72
74
  "TreemapReport",
73
75
  ]
@@ -8,6 +8,7 @@ from pam.reports.definition import ReportDefinition
8
8
  from pam.reports.errors import ReportStoreValidationError
9
9
  from pam.reports.fields import Dimension, Measure
10
10
  from pam.reports.range_transform import TimeRangeTransform, normalize_range_transform
11
+ from pam.reports.temporal import TemporalMode, normalize_temporal_mode
11
12
 
12
13
 
13
14
  AREA_RENDERER = "area"
@@ -50,6 +51,7 @@ class AreaChartReport(TypedCollectionReport):
50
51
  series: Optional[Dimension] = None,
51
52
  stacked: bool = False,
52
53
  range_transform: Optional[TimeRangeTransform] = None,
54
+ temporal_mode: Optional[TemporalMode] = None,
53
55
  ):
54
56
  if not isinstance(stacked, bool):
55
57
  raise ReportStoreValidationError("Area stacked must be boolean")
@@ -61,6 +63,13 @@ class AreaChartReport(TypedCollectionReport):
61
63
  normalized_range_transform = normalize_range_transform(
62
64
  range_transform, granularity, normalized_values, "Area Chart"
63
65
  )
66
+ normalized_temporal_mode = normalize_temporal_mode(
67
+ temporal_mode,
68
+ granularity,
69
+ normalized_values,
70
+ "Area Chart",
71
+ range_transform,
72
+ )
64
73
  definition = ReportDefinition.create(
65
74
  report_type="area",
66
75
  contract_version=AREA_CONTRACT_VERSION,
@@ -72,6 +81,7 @@ class AreaChartReport(TypedCollectionReport):
72
81
  },
73
82
  options={"stacked": stacked},
74
83
  range_transform=normalized_range_transform,
84
+ temporal_mode=normalized_temporal_mode,
75
85
  )
76
86
  report = store._declare_typed_report(
77
87
  key=key,
@@ -8,6 +8,7 @@ from pam.reports.definition import ReportDefinition
8
8
  from pam.reports.errors import ReportStoreValidationError
9
9
  from pam.reports.fields import Dimension, Measure
10
10
  from pam.reports.range_transform import TimeRangeTransform, normalize_range_transform
11
+ from pam.reports.temporal import TemporalMode, normalize_temporal_mode
11
12
 
12
13
 
13
14
  BAR_RENDERER = "bar"
@@ -56,6 +57,7 @@ class BarChartReport(TypedCollectionReport):
56
57
  orientation: str = "vertical",
57
58
  stacked: bool = False,
58
59
  range_transform: Optional[TimeRangeTransform] = None,
60
+ temporal_mode: Optional[TemporalMode] = None,
59
61
  ):
60
62
  if not isinstance(category, Dimension):
61
63
  raise ReportStoreValidationError(
@@ -110,6 +112,13 @@ class BarChartReport(TypedCollectionReport):
110
112
  normalized_range_transform = normalize_range_transform(
111
113
  range_transform, granularity, normalized_values, "Bar Chart"
112
114
  )
115
+ normalized_temporal_mode = normalize_temporal_mode(
116
+ temporal_mode,
117
+ granularity,
118
+ normalized_values,
119
+ "Bar Chart",
120
+ range_transform,
121
+ )
113
122
  definition = ReportDefinition.create(
114
123
  report_type="bar",
115
124
  contract_version=BAR_CONTRACT_VERSION,
@@ -121,6 +130,7 @@ class BarChartReport(TypedCollectionReport):
121
130
  },
122
131
  options={"orientation": orientation, "stacked": stacked},
123
132
  range_transform=normalized_range_transform,
133
+ temporal_mode=normalized_temporal_mode,
124
134
  )
125
135
  report = store._declare_typed_report(
126
136
  key=key,
@@ -18,6 +18,7 @@ class ReportDefinition:
18
18
  binding: Dict[str, Any]
19
19
  options: Dict[str, Any] = field(default_factory=dict)
20
20
  range_transform: Optional[Dict[str, Any]] = None
21
+ temporal_mode: Optional[str] = None
21
22
 
22
23
  def __post_init__(self) -> None:
23
24
  if not isinstance(self.report_type, str) or not self.report_type:
@@ -41,6 +42,17 @@ class ReportDefinition:
41
42
  raise ReportStoreValidationError(
42
43
  "range_transform must be a dictionary or None"
43
44
  )
45
+ if self.temporal_mode is not None and self.temporal_mode not in {
46
+ "AGGREGATE",
47
+ "SNAPSHOT",
48
+ }:
49
+ raise ReportStoreValidationError(
50
+ "temporal_mode must be AGGREGATE, SNAPSHOT, or None"
51
+ )
52
+ if self.temporal_mode is not None and self.range_transform is not None:
53
+ raise ReportStoreValidationError(
54
+ "temporal_mode and range_transform cannot be declared together"
55
+ )
44
56
 
45
57
  # Validate serializability while the declaration is still close to its caller.
46
58
  canonical_json(self.to_dict())
@@ -54,6 +66,7 @@ class ReportDefinition:
54
66
  binding: Dict[str, Any],
55
67
  options: Dict[str, Any] = None,
56
68
  range_transform: Dict[str, Any] = None,
69
+ temporal_mode: str = None,
57
70
  ):
58
71
  return cls(
59
72
  report_type=report_type,
@@ -64,6 +77,7 @@ class ReportDefinition:
64
77
  range_transform=(
65
78
  dict(range_transform) if range_transform is not None else None
66
79
  ),
80
+ temporal_mode=temporal_mode,
67
81
  )
68
82
 
69
83
  def to_dict(self) -> Dict[str, Any]:
@@ -76,6 +90,8 @@ class ReportDefinition:
76
90
  }
77
91
  if self.range_transform is not None:
78
92
  definition["range_transform"] = self.range_transform
93
+ if self.temporal_mode is not None:
94
+ definition["temporal_mode"] = self.temporal_mode
79
95
  return definition
80
96
 
81
97
  def to_json(self) -> str:
@@ -8,6 +8,7 @@ from pam.reports.definition import ReportDefinition
8
8
  from pam.reports.errors import ReportStoreValidationError
9
9
  from pam.reports.fields import Dimension, Measure
10
10
  from pam.reports.range_transform import TimeRangeTransform, normalize_range_transform
11
+ from pam.reports.temporal import TemporalMode, normalize_temporal_mode
11
12
 
12
13
 
13
14
  DONUT_RENDERER = "donut"
@@ -51,6 +52,7 @@ class DonutChartReport(TypedCollectionReport):
51
52
  category: Dimension,
52
53
  value: Measure,
53
54
  range_transform: Optional[TimeRangeTransform] = None,
55
+ temporal_mode: Optional[TemporalMode] = None,
54
56
  ):
55
57
  if not isinstance(category, Dimension):
56
58
  raise ReportStoreValidationError(
@@ -80,12 +82,16 @@ class DonutChartReport(TypedCollectionReport):
80
82
  normalized_range_transform = normalize_range_transform(
81
83
  range_transform, granularity, (value,), "Donut"
82
84
  )
85
+ normalized_temporal_mode = normalize_temporal_mode(
86
+ temporal_mode, granularity, (value,), "Donut", range_transform
87
+ )
83
88
  definition = ReportDefinition.create(
84
89
  report_type="donut",
85
90
  contract_version=DONUT_CONTRACT_VERSION,
86
91
  fields=(category, value),
87
92
  binding={"category": category.name, "value": value.name},
88
93
  range_transform=normalized_range_transform,
94
+ temporal_mode=normalized_temporal_mode,
89
95
  )
90
96
  report = store._declare_typed_report(
91
97
  key=key,
@@ -8,6 +8,7 @@ from pam.reports.definition import ReportDefinition
8
8
  from pam.reports.errors import ReportStoreValidationError
9
9
  from pam.reports.fields import Dimension, Measure
10
10
  from pam.reports.range_transform import TimeRangeTransform, normalize_range_transform
11
+ from pam.reports.temporal import TemporalMode, normalize_temporal_mode
11
12
 
12
13
 
13
14
  LINE_RENDERER = "line"
@@ -46,6 +47,7 @@ class LineChartReport(TypedCollectionReport):
46
47
  values: Iterable[Measure],
47
48
  series: Optional[Dimension] = None,
48
49
  range_transform: Optional[TimeRangeTransform] = None,
50
+ temporal_mode: Optional[TemporalMode] = None,
49
51
  ):
50
52
  if granularity == "NONE":
51
53
  raise ReportStoreValidationError("Line granularity must be DAY or MONTH")
@@ -55,6 +57,13 @@ class LineChartReport(TypedCollectionReport):
55
57
  normalized_range_transform = normalize_range_transform(
56
58
  range_transform, granularity, normalized_values, "Line"
57
59
  )
60
+ normalized_temporal_mode = normalize_temporal_mode(
61
+ temporal_mode,
62
+ granularity,
63
+ normalized_values,
64
+ "Line",
65
+ range_transform,
66
+ )
58
67
  definition = ReportDefinition.create(
59
68
  report_type="line",
60
69
  contract_version=LINE_CONTRACT_VERSION,
@@ -65,6 +74,7 @@ class LineChartReport(TypedCollectionReport):
65
74
  "values": [value.name for value in normalized_values],
66
75
  },
67
76
  range_transform=normalized_range_transform,
77
+ temporal_mode=normalized_temporal_mode,
68
78
  )
69
79
  report = store._declare_typed_report(
70
80
  key=key,
@@ -7,6 +7,7 @@ from pam.reports.errors import ReportStoreValidationError
7
7
  from pam.reports.fields import Measure
8
8
  from pam.reports.record import ReportRecord, build_record_key, validate_partition_key
9
9
  from pam.reports.range_transform import TimeRangeTransform, normalize_range_transform
10
+ from pam.reports.temporal import TemporalMode, normalize_temporal_mode
10
11
 
11
12
 
12
13
  METRIC_RENDERER = "metric"
@@ -31,6 +32,7 @@ class MetricReport:
31
32
  granularity: str,
32
33
  metrics: Iterable[Measure],
33
34
  range_transform: Optional[TimeRangeTransform] = None,
35
+ temporal_mode: Optional[TemporalMode] = None,
34
36
  ):
35
37
  if isinstance(metrics, (str, bytes)):
36
38
  raise ReportStoreValidationError("metrics must contain Measure declarations")
@@ -50,6 +52,13 @@ class MetricReport:
50
52
  normalized_range_transform = normalize_range_transform(
51
53
  range_transform, granularity, normalized_metrics, "Metric"
52
54
  )
55
+ normalized_temporal_mode = normalize_temporal_mode(
56
+ temporal_mode,
57
+ granularity,
58
+ normalized_metrics,
59
+ "Metric",
60
+ range_transform,
61
+ )
53
62
 
54
63
  definition = ReportDefinition.create(
55
64
  report_type="metric",
@@ -57,6 +66,7 @@ class MetricReport:
57
66
  fields=normalized_metrics,
58
67
  binding={"metrics": [metric.name for metric in normalized_metrics]},
59
68
  range_transform=normalized_range_transform,
69
+ temporal_mode=normalized_temporal_mode,
60
70
  )
61
71
  report = store._declare_typed_report(
62
72
  key=key,
@@ -8,6 +8,7 @@ from pam.reports.definition import ReportDefinition
8
8
  from pam.reports.errors import ReportStoreValidationError
9
9
  from pam.reports.fields import Dimension, Measure
10
10
  from pam.reports.range_transform import TimeRangeTransform, normalize_range_transform
11
+ from pam.reports.temporal import TemporalMode, normalize_temporal_mode
11
12
 
12
13
 
13
14
  PIE_RENDERER = "pie"
@@ -51,6 +52,7 @@ class PieChartReport(TypedCollectionReport):
51
52
  category: Dimension,
52
53
  value: Measure,
53
54
  range_transform: Optional[TimeRangeTransform] = None,
55
+ temporal_mode: Optional[TemporalMode] = None,
54
56
  ):
55
57
  if not isinstance(category, Dimension):
56
58
  raise ReportStoreValidationError(
@@ -80,12 +82,16 @@ class PieChartReport(TypedCollectionReport):
80
82
  normalized_range_transform = normalize_range_transform(
81
83
  range_transform, granularity, (value,), "Pie"
82
84
  )
85
+ normalized_temporal_mode = normalize_temporal_mode(
86
+ temporal_mode, granularity, (value,), "Pie", range_transform
87
+ )
83
88
  definition = ReportDefinition.create(
84
89
  report_type="pie",
85
90
  contract_version=PIE_CONTRACT_VERSION,
86
91
  fields=(category, value),
87
92
  binding={"category": category.name, "value": value.name},
88
93
  range_transform=normalized_range_transform,
94
+ temporal_mode=normalized_temporal_mode,
89
95
  )
90
96
  report = store._declare_typed_report(
91
97
  key=key,
@@ -8,6 +8,7 @@ from pam.reports.definition import ReportDefinition
8
8
  from pam.reports.errors import ReportStoreValidationError
9
9
  from pam.reports.fields import Dimension, Measure
10
10
  from pam.reports.range_transform import TimeRangeTransform, normalize_range_transform
11
+ from pam.reports.temporal import TemporalMode, normalize_temporal_mode
11
12
 
12
13
 
13
14
  POLAR_AREA_RENDERER = "polar_area"
@@ -52,6 +53,7 @@ class PolarAreaChartReport(TypedCollectionReport):
52
53
  category: Dimension,
53
54
  value: Measure,
54
55
  range_transform: Optional[TimeRangeTransform] = None,
56
+ temporal_mode: Optional[TemporalMode] = None,
55
57
  ):
56
58
  if not isinstance(category, Dimension):
57
59
  raise ReportStoreValidationError(
@@ -81,12 +83,16 @@ class PolarAreaChartReport(TypedCollectionReport):
81
83
  normalized_range_transform = normalize_range_transform(
82
84
  range_transform, granularity, (value,), "Polar Area"
83
85
  )
86
+ normalized_temporal_mode = normalize_temporal_mode(
87
+ temporal_mode, granularity, (value,), "Polar Area", range_transform
88
+ )
84
89
  definition = ReportDefinition.create(
85
90
  report_type="polar_area",
86
91
  contract_version=POLAR_AREA_CONTRACT_VERSION,
87
92
  fields=(category, value),
88
93
  binding={"category": category.name, "value": value.name},
89
94
  range_transform=normalized_range_transform,
95
+ temporal_mode=normalized_temporal_mode,
90
96
  )
91
97
  report = store._declare_typed_report(
92
98
  key=key,
@@ -9,6 +9,7 @@ from pam.reports.errors import ReportStoreValidationError
9
9
  from pam.reports.fields import Measure
10
10
  from pam.reports.record import ReportRecord, build_record_key, validate_partition_key
11
11
  from pam.reports.range_transform import TimeRangeTransform, normalize_range_transform
12
+ from pam.reports.temporal import TemporalMode, normalize_temporal_mode
12
13
 
13
14
 
14
15
  RADIAL_BAR_RENDERER = "radial_bar"
@@ -32,6 +33,7 @@ class RadialBarChartReport:
32
33
  cls, store, key: str, granularity: str, metrics: Iterable[Measure],
33
34
  min_value: float, max_value: float,
34
35
  range_transform: Optional[TimeRangeTransform] = None,
36
+ temporal_mode: Optional[TemporalMode] = None,
35
37
  ):
36
38
  try:
37
39
  normalized_metrics = tuple(metrics)
@@ -66,6 +68,13 @@ class RadialBarChartReport:
66
68
  normalized_transform = normalize_range_transform(
67
69
  range_transform, granularity, normalized_metrics, "Radial Bar"
68
70
  )
71
+ normalized_temporal_mode = normalize_temporal_mode(
72
+ temporal_mode,
73
+ granularity,
74
+ normalized_metrics,
75
+ "Radial Bar",
76
+ range_transform,
77
+ )
69
78
  definition = ReportDefinition.create(
70
79
  report_type="radial_bar",
71
80
  contract_version=RADIAL_BAR_CONTRACT_VERSION,
@@ -73,6 +82,7 @@ class RadialBarChartReport:
73
82
  binding={"metrics": [metric.name for metric in normalized_metrics]},
74
83
  options={"min_value": min_value, "max_value": max_value},
75
84
  range_transform=normalized_transform,
85
+ temporal_mode=normalized_temporal_mode,
76
86
  )
77
87
  report = store._declare_typed_report(
78
88
  key, RADIAL_BAR_RENDERER, RADIAL_BAR_SCHEMA, granularity,
@@ -45,6 +45,7 @@ from pam.reports.schema import (
45
45
  STORE_VERSION,
46
46
  )
47
47
  from pam.reports.table import TableReport
48
+ from pam.reports.temporal import TemporalMode
48
49
  from pam.reports.treemap import TreemapReport
49
50
 
50
51
 
@@ -155,9 +156,10 @@ class ReportStore:
155
156
  key: str,
156
157
  granularity: str,
157
158
  columns: Iterable[Field],
159
+ temporal_mode: Optional[TemporalMode] = None,
158
160
  ) -> TableReport:
159
161
  """Declare a typed Table report."""
160
- return TableReport.declare(self, key, granularity, columns)
162
+ return TableReport.declare(self, key, granularity, columns, temporal_mode)
161
163
 
162
164
  def metric(
163
165
  self,
@@ -165,10 +167,11 @@ class ReportStore:
165
167
  granularity: str,
166
168
  metrics: Iterable[Measure],
167
169
  range_transform: Optional[TimeRangeTransform] = None,
170
+ temporal_mode: Optional[TemporalMode] = None,
168
171
  ) -> MetricReport:
169
172
  """Declare a typed Metric report."""
170
173
  return MetricReport.declare(
171
- self, key, granularity, metrics, range_transform
174
+ self, key, granularity, metrics, range_transform, temporal_mode
172
175
  )
173
176
 
174
177
  def line_chart(
@@ -178,10 +181,11 @@ class ReportStore:
178
181
  values: Iterable[Measure],
179
182
  series: Optional[Dimension] = None,
180
183
  range_transform: Optional[TimeRangeTransform] = None,
184
+ temporal_mode: Optional[TemporalMode] = None,
181
185
  ) -> LineChartReport:
182
186
  """Declare a typed Line Chart report."""
183
187
  return LineChartReport.declare(
184
- self, key, granularity, values, series, range_transform
188
+ self, key, granularity, values, series, range_transform, temporal_mode
185
189
  )
186
190
 
187
191
  def area_chart(
@@ -192,10 +196,18 @@ class ReportStore:
192
196
  series: Optional[Dimension] = None,
193
197
  stacked: bool = False,
194
198
  range_transform: Optional[TimeRangeTransform] = None,
199
+ temporal_mode: Optional[TemporalMode] = None,
195
200
  ) -> AreaChartReport:
196
201
  """Declare a typed Area Chart report."""
197
202
  return AreaChartReport.declare(
198
- self, key, granularity, values, series, stacked, range_transform
203
+ self,
204
+ key,
205
+ granularity,
206
+ values,
207
+ series,
208
+ stacked,
209
+ range_transform,
210
+ temporal_mode,
199
211
  )
200
212
 
201
213
  def bar_chart(
@@ -208,6 +220,7 @@ class ReportStore:
208
220
  orientation: str = "vertical",
209
221
  stacked: bool = False,
210
222
  range_transform: Optional[TimeRangeTransform] = None,
223
+ temporal_mode: Optional[TemporalMode] = None,
211
224
  ) -> BarChartReport:
212
225
  """Declare a typed Bar/Column Chart report."""
213
226
  return BarChartReport.declare(
@@ -220,6 +233,7 @@ class ReportStore:
220
233
  orientation,
221
234
  stacked,
222
235
  range_transform,
236
+ temporal_mode,
223
237
  )
224
238
 
225
239
  def pie_chart(
@@ -229,10 +243,11 @@ class ReportStore:
229
243
  category: Dimension,
230
244
  value: Measure,
231
245
  range_transform: Optional[TimeRangeTransform] = None,
246
+ temporal_mode: Optional[TemporalMode] = None,
232
247
  ) -> PieChartReport:
233
248
  """Declare a typed Pie Chart report."""
234
249
  return PieChartReport.declare(
235
- self, key, granularity, category, value, range_transform
250
+ self, key, granularity, category, value, range_transform, temporal_mode
236
251
  )
237
252
 
238
253
  def donut_chart(
@@ -242,10 +257,11 @@ class ReportStore:
242
257
  category: Dimension,
243
258
  value: Measure,
244
259
  range_transform: Optional[TimeRangeTransform] = None,
260
+ temporal_mode: Optional[TemporalMode] = None,
245
261
  ) -> DonutChartReport:
246
262
  """Declare a typed Donut Chart report."""
247
263
  return DonutChartReport.declare(
248
- self, key, granularity, category, value, range_transform
264
+ self, key, granularity, category, value, range_transform, temporal_mode
249
265
  )
250
266
 
251
267
  def radial_bar_chart(
@@ -256,10 +272,18 @@ class ReportStore:
256
272
  min_value: float,
257
273
  max_value: float,
258
274
  range_transform: Optional[TimeRangeTransform] = None,
275
+ temporal_mode: Optional[TemporalMode] = None,
259
276
  ) -> RadialBarChartReport:
260
277
  """Declare a typed Radial Bar Chart report."""
261
278
  return RadialBarChartReport.declare(
262
- self, key, granularity, metrics, min_value, max_value, range_transform
279
+ self,
280
+ key,
281
+ granularity,
282
+ metrics,
283
+ min_value,
284
+ max_value,
285
+ range_transform,
286
+ temporal_mode,
263
287
  )
264
288
 
265
289
  def polar_area_chart(
@@ -269,10 +293,11 @@ class ReportStore:
269
293
  category: Dimension,
270
294
  value: Measure,
271
295
  range_transform: Optional[TimeRangeTransform] = None,
296
+ temporal_mode: Optional[TemporalMode] = None,
272
297
  ) -> PolarAreaChartReport:
273
298
  """Declare a typed Polar Area Chart report."""
274
299
  return PolarAreaChartReport.declare(
275
- self, key, granularity, category, value, range_transform
300
+ self, key, granularity, category, value, range_transform, temporal_mode
276
301
  )
277
302
 
278
303
  def scatter_chart(
@@ -6,6 +6,7 @@ from pam.reports.definition import ReportDefinition
6
6
  from pam.reports.errors import ReportStoreValidationError
7
7
  from pam.reports.fields import Dimension, Field, Measure
8
8
  from pam.reports.record import ReportRecord, build_record_key, validate_partition_key
9
+ from pam.reports.temporal import TemporalMode, normalize_temporal_mode
9
10
 
10
11
 
11
12
  TABLE_RENDERER = "table"
@@ -40,6 +41,7 @@ class TableReport:
40
41
  key: str,
41
42
  granularity: str,
42
43
  columns: Iterable[Field],
44
+ temporal_mode: Optional[TemporalMode] = None,
43
45
  ):
44
46
  if isinstance(columns, (str, bytes)):
45
47
  raise ReportStoreValidationError("columns must contain field declarations")
@@ -63,11 +65,19 @@ class TableReport:
63
65
  "A Table report requires at least one Dimension column"
64
66
  )
65
67
 
68
+ measures = tuple(
69
+ column for column in normalized_columns if isinstance(column, Measure)
70
+ )
71
+ normalized_temporal_mode = normalize_temporal_mode(
72
+ temporal_mode, granularity, measures, "Table"
73
+ )
74
+
66
75
  definition = ReportDefinition.create(
67
76
  report_type="table",
68
77
  contract_version=TABLE_CONTRACT_VERSION,
69
78
  fields=normalized_columns,
70
79
  binding={"columns": [column.name for column in normalized_columns]},
80
+ temporal_mode=normalized_temporal_mode,
71
81
  )
72
82
  report = store._declare_typed_report(
73
83
  key=key,
@@ -0,0 +1,55 @@
1
+ """Business meaning for records stored across report partitions."""
2
+
3
+ from enum import Enum
4
+ from typing import Optional, Sequence
5
+
6
+ from pam.reports.axis import NUMERIC_TYPES
7
+ from pam.reports.errors import ReportStoreValidationError
8
+ from pam.reports.fields import Measure
9
+
10
+
11
+ class TemporalMode(str, Enum):
12
+ """How CMS must interpret a report's values across partitions."""
13
+
14
+ AGGREGATE = "AGGREGATE"
15
+ SNAPSHOT = "SNAPSHOT"
16
+
17
+
18
+ def normalize_temporal_mode(
19
+ temporal_mode,
20
+ granularity: str,
21
+ measures: Sequence[Measure],
22
+ report_name: str,
23
+ range_transform=None,
24
+ ) -> Optional[str]:
25
+ """Validate one optional temporal declaration and serialize its value."""
26
+ if temporal_mode is None:
27
+ return None
28
+ if not isinstance(temporal_mode, TemporalMode):
29
+ raise ReportStoreValidationError(
30
+ f"{report_name} temporal_mode must be TemporalMode.AGGREGATE or "
31
+ "TemporalMode.SNAPSHOT"
32
+ )
33
+ if range_transform is not None:
34
+ raise ReportStoreValidationError(
35
+ f"{report_name} cannot declare both temporal_mode and range_transform"
36
+ )
37
+ if granularity not in {"DAY", "MONTH"}:
38
+ raise ReportStoreValidationError(
39
+ f"{report_name} temporal_mode requires DAY or MONTH granularity"
40
+ )
41
+ if not measures:
42
+ raise ReportStoreValidationError(
43
+ f"{report_name} temporal_mode requires at least one numeric measure"
44
+ )
45
+ non_numeric = sorted(
46
+ measure.name
47
+ for measure in measures
48
+ if measure.value_type not in NUMERIC_TYPES
49
+ )
50
+ if non_numeric:
51
+ raise ReportStoreValidationError(
52
+ f"{report_name} temporal_mode requires numeric measures; "
53
+ f"non-numeric measures: {non_numeric}"
54
+ )
55
+ return temporal_mode.value
@@ -503,10 +503,15 @@ name per report merely to separate reports. Never write `_tracker_name` yourself
503
503
  the framework propagates the store's tracker name to every V2 pointer. Reopening the
504
504
  same file in one service run with a different tracker name is an error.
505
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.
506
+ The Dynamic Report is a reader of the latest successfully published store. Opening
507
+ or reloading a report must never force-start its Data Plugin: plugin execution is an
508
+ expensive dataflow operation owned by its scheduler or an explicit Data Adapter
509
+ operation. A V2 Dynamic Report may keep `adapter_id` as Data Plugin ownership and
510
+ tracker lookup metadata, together with the selected `adapter_data_attr`, matching
511
+ the original Data Plugin report flow. It must never use `adapter_id` for
512
+ report-triggered execution. Do not require a V2 pointer to move to the
513
+ `analytics-reports` database or to be selected through an Analytic Report Group.
514
+ A report reload only reads the latest published SQLite.
510
515
 
511
516
  ## Required lifecycle
512
517