pam-python 0.2.9__tar.gz → 0.2.10__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.9/pam_python.egg-info → pam_python-0.2.10}/PKG-INFO +1 -1
- {pam_python-0.2.9 → pam_python-0.2.10}/pam/__init__.py +1 -1
- pam_python-0.2.10/pam/datetime_utils.py +20 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/pam/logger.py +3 -1
- {pam_python-0.2.9 → pam_python-0.2.10}/pam/models/data_input_v2.py +3 -9
- {pam_python-0.2.9 → pam_python-0.2.10}/pam/reports/record.py +5 -3
- {pam_python-0.2.9 → pam_python-0.2.10}/pam/reports/store.py +2 -1
- {pam_python-0.2.9 → pam_python-0.2.10}/pam/templates/init/REPORTS.md +4 -3
- {pam_python-0.2.9 → pam_python-0.2.10/pam_python.egg-info}/PKG-INFO +1 -1
- {pam_python-0.2.9 → pam_python-0.2.10}/pam_python.egg-info/SOURCES.txt +2 -0
- pam_python-0.2.10/tests/test_datetime_utils.py +43 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/tests/test_report_store.py +18 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/tests/test_typed_range_bar_report.py +12 -2
- {pam_python-0.2.9 → pam_python-0.2.10}/LICENSE.txt +0 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/README.md +0 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/pam/api.py +0 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/pam/cli.py +0 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/pam/interface_task_manager.py +0 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/pam/models/__init__.py +0 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/pam/models/request_command.py +0 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/pam/models/sqlite_download.py +0 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/pam/reports/__init__.py +0 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/pam/reports/area.py +0 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/pam/reports/axis.py +0 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/pam/reports/bar.py +0 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/pam/reports/box_plot.py +0 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/pam/reports/bubble.py +0 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/pam/reports/candlestick.py +0 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/pam/reports/collection.py +0 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/pam/reports/definition.py +0 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/pam/reports/donut.py +0 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/pam/reports/errors.py +0 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/pam/reports/fields.py +0 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/pam/reports/heatmap.py +0 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/pam/reports/line.py +0 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/pam/reports/manager.py +0 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/pam/reports/metric.py +0 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/pam/reports/pie.py +0 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/pam/reports/polar_area.py +0 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/pam/reports/radar.py +0 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/pam/reports/radial_bar.py +0 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/pam/reports/range_area.py +0 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/pam/reports/range_bar.py +0 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/pam/reports/range_transform.py +0 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/pam/reports/report.py +0 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/pam/reports/scatter.py +0 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/pam/reports/schema.py +0 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/pam/reports/table.py +0 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/pam/reports/transport.py +0 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/pam/reports/treemap.py +0 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/pam/request_file_format.py +0 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/pam/result_batch_uploader.py +0 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/pam/server.py +0 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/pam/service.py +0 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/pam/sqlite.py +0 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/pam/task_manager.py +0 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/pam/temp_file_utils.py +0 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/pam/templates/buildcmd/pamb +0 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/pam/templates/buildcmd/pamb-base.sh +0 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/pam/templates/init/AGENT.md +0 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/pam/templates/init/dockerignore.tmpl +0 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/pam/templates/init/gitignore.tmpl +0 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/pam/templates/init/main.tmpl +0 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/pam/templates/init/pylintrc.tmpl +0 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/pam/templates/init/run_unit_test.bat +0 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/pam/templates/init/run_unit_test.ps1 +0 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/pam/templates/init/run_unit_test.sh +0 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/pam/templates/project/pip/Dockerfile +0 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/pam/templates/project/pip/requirements.txt +0 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/pam/templates/project/uv/Dockerfile +0 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/pam/templates/project/uv/pyproject.toml +0 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/pam/templates/project/uv/python-version +0 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/pam/templates/service/common/service.yaml +0 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/pam/templates/service/pandas/functions.tmpl +0 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/pam/templates/service/pandas/service.test.tmpl +0 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/pam/templates/service/pandas/service_class.tmpl +0 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/pam/templates/service/polars/functions.tmpl +0 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/pam/templates/service/polars/service.test.tmpl +0 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/pam/templates/service/polars/service_class.tmpl +0 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/pam/tester_task.py +0 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/pam/utils.py +0 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/pam_python.egg-info/dependency_links.txt +0 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/pam_python.egg-info/entry_points.txt +0 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/pam_python.egg-info/requires.txt +0 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/pam_python.egg-info/top_level.txt +0 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/pyproject.toml +0 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/setup.cfg +0 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/tests/test_api.py +0 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/tests/test_cli_templates.py +0 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/tests/test_data_error_v2.py +0 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/tests/test_data_input_v2.py +0 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/tests/test_data_timeout_v2.py +0 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/tests/test_range_transform.py +0 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/tests/test_request_data_v2.py +0 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/tests/test_result_batch_uploader.py +0 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/tests/test_service_dataframe_output.py +0 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/tests/test_sqlite_download_v2.py +0 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/tests/test_temp_file_utils.py +0 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/tests/test_typed_area_report.py +0 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/tests/test_typed_bar_report.py +0 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/tests/test_typed_box_plot_report.py +0 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/tests/test_typed_bubble_report.py +0 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/tests/test_typed_candlestick_report.py +0 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/tests/test_typed_donut_report.py +0 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/tests/test_typed_heatmap_report.py +0 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/tests/test_typed_line_report.py +0 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/tests/test_typed_metric_report.py +0 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/tests/test_typed_pie_report.py +0 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/tests/test_typed_polar_area_report.py +0 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/tests/test_typed_radar_report.py +0 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/tests/test_typed_radial_bar_report.py +0 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/tests/test_typed_range_area_report.py +0 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/tests/test_typed_scatter_report.py +0 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/tests/test_typed_table_report.py +0 -0
- {pam_python-0.2.9 → pam_python-0.2.10}/tests/test_typed_treemap_report.py +0 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""PAM's canonical suffix-free UTC+0 datetime contract."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
PAM_DATETIME_FORMAT = "%Y-%m-%d %H:%M:%S"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def parse_pam_datetime(value: str) -> datetime:
|
|
10
|
+
"""Parse an exact PAM datetime string and attach UTC without shifting it."""
|
|
11
|
+
return datetime.strptime(value, PAM_DATETIME_FORMAT).replace(tzinfo=timezone.utc)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def format_pam_datetime(value: datetime) -> str:
|
|
15
|
+
"""Format a datetime as PAM UTC+0 without a timezone suffix."""
|
|
16
|
+
if value.tzinfo is None:
|
|
17
|
+
value = value.replace(tzinfo=timezone.utc)
|
|
18
|
+
else:
|
|
19
|
+
value = value.astimezone(timezone.utc)
|
|
20
|
+
return value.strftime(PAM_DATETIME_FORMAT)
|
|
@@ -4,9 +4,11 @@ from datetime import datetime, timezone
|
|
|
4
4
|
from threading import Lock
|
|
5
5
|
from typing import Any
|
|
6
6
|
|
|
7
|
+
from pam.datetime_utils import format_pam_datetime
|
|
7
8
|
from pam.utils import log as stdout_log
|
|
8
9
|
from pam.service import Service
|
|
9
10
|
|
|
11
|
+
|
|
10
12
|
class Logger:
|
|
11
13
|
def __init__(self, service: Service, buffer_size: int = 20, source: str = "worker"):
|
|
12
14
|
self.service = service
|
|
@@ -92,4 +94,4 @@ class Logger:
|
|
|
92
94
|
|
|
93
95
|
@staticmethod
|
|
94
96
|
def _utc_now() -> str:
|
|
95
|
-
return datetime.now(timezone.utc)
|
|
97
|
+
return format_pam_datetime(datetime.now(timezone.utc))
|
|
@@ -2,12 +2,12 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import csv
|
|
4
4
|
from dataclasses import dataclass
|
|
5
|
-
from datetime import datetime, timezone
|
|
6
5
|
from hashlib import sha256
|
|
7
6
|
import json
|
|
8
7
|
from pathlib import Path
|
|
9
8
|
from typing import Any, Iterator
|
|
10
9
|
|
|
10
|
+
from pam.datetime_utils import parse_pam_datetime
|
|
11
11
|
from pam.temp_file_utils import TempfileUtils
|
|
12
12
|
|
|
13
13
|
|
|
@@ -15,7 +15,6 @@ V2_PROTOCOL_VERSION = "data-plugin-input/v2"
|
|
|
15
15
|
V2_MANIFEST_FILE_NAME = "manifest.json"
|
|
16
16
|
V2_MAX_DATASETS = 5
|
|
17
17
|
V2_POPULATION_MODES = {"contacts", "event_any", "event_all"}
|
|
18
|
-
PAM_TIMESTAMP_FORMAT = "%Y-%m-%d %H:%M:%S"
|
|
19
18
|
|
|
20
19
|
|
|
21
20
|
@dataclass(frozen=True)
|
|
@@ -365,11 +364,6 @@ def _required_optional_string(source: dict[str, Any], key: str) -> str:
|
|
|
365
364
|
return value
|
|
366
365
|
|
|
367
366
|
|
|
368
|
-
def _parse_pam_timestamp(value: str) -> datetime:
|
|
369
|
-
"""Parse PAM's suffix-free timestamp as UTC+0 without shifting it."""
|
|
370
|
-
return datetime.strptime(value, PAM_TIMESTAMP_FORMAT).replace(tzinfo=timezone.utc)
|
|
371
|
-
|
|
372
|
-
|
|
373
367
|
def _parse_time_window(raw_time_window: Any) -> DataInputV2TimeWindow | None:
|
|
374
368
|
if raw_time_window is None:
|
|
375
369
|
return None
|
|
@@ -380,8 +374,8 @@ def _parse_time_window(raw_time_window: Any) -> DataInputV2TimeWindow | None:
|
|
|
380
374
|
start_inclusive = _required_bool(raw_time_window, "start_inclusive")
|
|
381
375
|
end_inclusive = _required_bool(raw_time_window, "end_inclusive")
|
|
382
376
|
try:
|
|
383
|
-
start_time =
|
|
384
|
-
end_time =
|
|
377
|
+
start_time = parse_pam_datetime(start)
|
|
378
|
+
end_time = parse_pam_datetime(end)
|
|
385
379
|
except ValueError as error:
|
|
386
380
|
raise ValueError("V2 manifest time_window timestamps are invalid") from error
|
|
387
381
|
if start_time >= end_time:
|
|
@@ -6,6 +6,7 @@ from dataclasses import dataclass, field
|
|
|
6
6
|
from datetime import date, datetime, timezone
|
|
7
7
|
from typing import Any, Dict, Optional, Tuple
|
|
8
8
|
|
|
9
|
+
from pam.datetime_utils import parse_pam_datetime
|
|
9
10
|
from pam.reports.errors import ReportStoreValidationError
|
|
10
11
|
|
|
11
12
|
|
|
@@ -75,14 +76,15 @@ def normalize_timestamp(value: Any) -> Optional[int]:
|
|
|
75
76
|
parsed = datetime.combine(value, datetime.min.time(), tzinfo=timezone.utc)
|
|
76
77
|
elif isinstance(value, str) and value:
|
|
77
78
|
try:
|
|
78
|
-
parsed =
|
|
79
|
+
parsed = parse_pam_datetime(value)
|
|
79
80
|
except ValueError as exc:
|
|
80
81
|
raise ReportStoreValidationError(
|
|
81
|
-
f"Timestamp value
|
|
82
|
+
f"Timestamp value must use PAM UTC format YYYY-MM-DD HH:mm:ss: {value}"
|
|
82
83
|
) from exc
|
|
83
84
|
else:
|
|
84
85
|
raise ReportStoreValidationError(
|
|
85
|
-
"Timestamp values must be epoch milliseconds, date, datetime,
|
|
86
|
+
"Timestamp values must be epoch milliseconds, date, datetime, "
|
|
87
|
+
"or a PAM UTC string using YYYY-MM-DD HH:mm:ss"
|
|
86
88
|
)
|
|
87
89
|
if parsed.tzinfo is None:
|
|
88
90
|
parsed = parsed.replace(tzinfo=timezone.utc)
|
|
@@ -8,6 +8,7 @@ from datetime import datetime, timezone
|
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
from typing import Any, Iterable, List, Optional, Set
|
|
10
10
|
|
|
11
|
+
from pam.datetime_utils import format_pam_datetime
|
|
11
12
|
from pam.reports.errors import (
|
|
12
13
|
DuplicateReportRecordError,
|
|
13
14
|
ReportDefinitionConflictError,
|
|
@@ -48,7 +49,7 @@ from pam.reports.treemap import TreemapReport
|
|
|
48
49
|
|
|
49
50
|
|
|
50
51
|
def _now() -> str:
|
|
51
|
-
return datetime.now(timezone.utc)
|
|
52
|
+
return format_pam_datetime(datetime.now(timezone.utc))
|
|
52
53
|
|
|
53
54
|
|
|
54
55
|
class ReportStore:
|
|
@@ -230,7 +230,8 @@ Initial value types:
|
|
|
230
230
|
- `number`
|
|
231
231
|
- `boolean`
|
|
232
232
|
- `date` — calendar date, serialized as `YYYY-MM-DD`
|
|
233
|
-
- `datetime` — timestamp normalized to epoch milliseconds internally
|
|
233
|
+
- `datetime` — timestamp normalized to epoch milliseconds internally; string
|
|
234
|
+
inputs must use PAM UTC format `YYYY-MM-DD HH:mm:ss`
|
|
234
235
|
|
|
235
236
|
Objects and arrays are not initial field types. A report that requires a complex
|
|
236
237
|
cell or point must first introduce an explicit framework and CMS contract.
|
|
@@ -1127,8 +1128,8 @@ ranges.append_ranges([
|
|
|
1127
1128
|
{
|
|
1128
1129
|
"task": "Phase A",
|
|
1129
1130
|
"group": "Project 1",
|
|
1130
|
-
"start": "2026-08-
|
|
1131
|
-
"end": "2026-08-
|
|
1131
|
+
"start": "2026-08-01 09:00:00",
|
|
1132
|
+
"end": "2026-08-05 18:00:00",
|
|
1132
1133
|
},
|
|
1133
1134
|
], partition_key="2026-08")
|
|
1134
1135
|
```
|
|
@@ -4,6 +4,7 @@ pyproject.toml
|
|
|
4
4
|
pam/__init__.py
|
|
5
5
|
pam/api.py
|
|
6
6
|
pam/cli.py
|
|
7
|
+
pam/datetime_utils.py
|
|
7
8
|
pam/interface_task_manager.py
|
|
8
9
|
pam/logger.py
|
|
9
10
|
pam/request_file_format.py
|
|
@@ -84,6 +85,7 @@ tests/test_cli_templates.py
|
|
|
84
85
|
tests/test_data_error_v2.py
|
|
85
86
|
tests/test_data_input_v2.py
|
|
86
87
|
tests/test_data_timeout_v2.py
|
|
88
|
+
tests/test_datetime_utils.py
|
|
87
89
|
tests/test_range_transform.py
|
|
88
90
|
tests/test_report_store.py
|
|
89
91
|
tests/test_request_data_v2.py
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
from datetime import datetime, timedelta, timezone
|
|
3
|
+
|
|
4
|
+
from pam.datetime_utils import format_pam_datetime, parse_pam_datetime
|
|
5
|
+
from pam.logger import Logger
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class PAMDateTimeUtilsTest(unittest.TestCase):
|
|
9
|
+
def test_round_trip_uses_suffix_free_utc_format(self):
|
|
10
|
+
parsed = parse_pam_datetime("2026-03-31 02:37:44")
|
|
11
|
+
|
|
12
|
+
self.assertEqual(timezone.utc, parsed.tzinfo)
|
|
13
|
+
self.assertEqual("2026-03-31 02:37:44", format_pam_datetime(parsed))
|
|
14
|
+
|
|
15
|
+
def test_format_converts_aware_datetime_to_utc(self):
|
|
16
|
+
value = datetime(
|
|
17
|
+
2026,
|
|
18
|
+
3,
|
|
19
|
+
31,
|
|
20
|
+
9,
|
|
21
|
+
37,
|
|
22
|
+
44,
|
|
23
|
+
tzinfo=timezone(timedelta(hours=7)),
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
self.assertEqual("2026-03-31 02:37:44", format_pam_datetime(value))
|
|
27
|
+
|
|
28
|
+
def test_parse_rejects_rfc3339(self):
|
|
29
|
+
with self.assertRaises(ValueError):
|
|
30
|
+
parse_pam_datetime("2026-03-31T02:37:44Z")
|
|
31
|
+
|
|
32
|
+
def test_structured_logger_uses_pam_utc_format(self):
|
|
33
|
+
logged_at = Logger._utc_now()
|
|
34
|
+
|
|
35
|
+
self.assertEqual(19, len(logged_at))
|
|
36
|
+
self.assertEqual(
|
|
37
|
+
logged_at,
|
|
38
|
+
format_pam_datetime(parse_pam_datetime(logged_at)),
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
if __name__ == "__main__":
|
|
43
|
+
unittest.main()
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import csv
|
|
2
2
|
import importlib
|
|
3
3
|
import json
|
|
4
|
+
import re
|
|
4
5
|
import shutil
|
|
5
6
|
import sqlite3
|
|
6
7
|
import tempfile
|
|
@@ -136,6 +137,23 @@ class ReportStoreTest(unittest.TestCase):
|
|
|
136
137
|
definition_json="{}",
|
|
137
138
|
)
|
|
138
139
|
|
|
140
|
+
def test_metadata_timestamps_use_pam_utc_format(self):
|
|
141
|
+
self.report()
|
|
142
|
+
self.store._append_records(
|
|
143
|
+
"test-report-a",
|
|
144
|
+
[ReportRecord(dimensions={"customer": "contact-1"}, measures={})],
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
timestamps = self.store._connection.execute(
|
|
148
|
+
"SELECT created_at, updated_at FROM report_definitions "
|
|
149
|
+
"UNION ALL SELECT created_at, updated_at FROM report_records"
|
|
150
|
+
).fetchall()
|
|
151
|
+
pattern = re.compile(r"^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$")
|
|
152
|
+
self.assertTrue(timestamps)
|
|
153
|
+
for created_at, updated_at in timestamps:
|
|
154
|
+
self.assertRegex(created_at, pattern)
|
|
155
|
+
self.assertRegex(updated_at, pattern)
|
|
156
|
+
|
|
139
157
|
with self.assertRaises(ReportStoreValidationError):
|
|
140
158
|
self.store._declare_report(
|
|
141
159
|
key="invalid-range",
|
|
@@ -50,8 +50,8 @@ class TypedRangeBarReportTest(unittest.TestCase):
|
|
|
50
50
|
[{
|
|
51
51
|
"task": "Phase A",
|
|
52
52
|
"group": "Project 1",
|
|
53
|
-
"start": "2026-08-
|
|
54
|
-
"end": "2026-08-
|
|
53
|
+
"start": "2026-08-01 09:00:00",
|
|
54
|
+
"end": "2026-08-05 18:00:00",
|
|
55
55
|
}],
|
|
56
56
|
"2026-08",
|
|
57
57
|
)
|
|
@@ -61,6 +61,16 @@ class TypedRangeBarReportTest(unittest.TestCase):
|
|
|
61
61
|
measures = json.loads(row[0])
|
|
62
62
|
self.assertIsInstance(measures["start"], int)
|
|
63
63
|
self.assertGreater(measures["end"], measures["start"])
|
|
64
|
+
with self.assertRaises(ReportStoreValidationError):
|
|
65
|
+
report.append_ranges(
|
|
66
|
+
[{
|
|
67
|
+
"task": "Wrong format",
|
|
68
|
+
"group": "Project 1",
|
|
69
|
+
"start": "2026-08-01T09:00:00Z",
|
|
70
|
+
"end": "2026-08-05T18:00:00Z",
|
|
71
|
+
}],
|
|
72
|
+
"2026-08",
|
|
73
|
+
)
|
|
64
74
|
with self.assertRaises(ReportStoreValidationError):
|
|
65
75
|
report.append_ranges(
|
|
66
76
|
[{"task": "Bad", "group": "Project 1", "start": 20, "end": 10}],
|
|
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
|