pam-python 0.2.19__tar.gz → 0.2.20__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.19/pam_python.egg-info → pam_python-0.2.20}/PKG-INFO +1 -1
- {pam_python-0.2.19 → pam_python-0.2.20}/pam/__init__.py +1 -1
- {pam_python-0.2.19 → pam_python-0.2.20}/pam/models/data_input_v2.py +11 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/pam/task_manager.py +26 -28
- {pam_python-0.2.19 → pam_python-0.2.20}/pam/templates/init/AGENT.md +10 -0
- {pam_python-0.2.19 → pam_python-0.2.20/pam_python.egg-info}/PKG-INFO +1 -1
- {pam_python-0.2.19 → pam_python-0.2.20}/pam_python.egg-info/SOURCES.txt +1 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/tests/test_data_input_v2.py +67 -0
- pam_python-0.2.20/tests/test_report_delivery.py +34 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/LICENSE.txt +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/README.md +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/pam/api.py +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/pam/cli.py +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/pam/datetime_utils.py +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/pam/interface_task_manager.py +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/pam/logger.py +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/pam/models/__init__.py +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/pam/models/request_command.py +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/pam/models/sqlite_download.py +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/pam/reports/__init__.py +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/pam/reports/area.py +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/pam/reports/axis.py +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/pam/reports/bar.py +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/pam/reports/box_plot.py +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/pam/reports/bubble.py +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/pam/reports/candlestick.py +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/pam/reports/collection.py +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/pam/reports/definition.py +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/pam/reports/donut.py +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/pam/reports/errors.py +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/pam/reports/fields.py +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/pam/reports/heatmap.py +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/pam/reports/line.py +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/pam/reports/manager.py +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/pam/reports/metric.py +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/pam/reports/pie.py +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/pam/reports/polar_area.py +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/pam/reports/radar.py +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/pam/reports/radial_bar.py +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/pam/reports/range_area.py +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/pam/reports/range_bar.py +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/pam/reports/range_transform.py +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/pam/reports/record.py +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/pam/reports/report.py +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/pam/reports/scatter.py +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/pam/reports/schema.py +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/pam/reports/store.py +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/pam/reports/table.py +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/pam/reports/temporal.py +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/pam/reports/transport.py +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/pam/reports/treemap.py +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/pam/request_file_format.py +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/pam/result_batch_uploader.py +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/pam/server.py +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/pam/service.py +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/pam/sqlite.py +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/pam/temp_file_utils.py +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/pam/templates/buildcmd/pamb +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/pam/templates/buildcmd/pamb-base.sh +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/pam/templates/init/REPORTS-th.md +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/pam/templates/init/REPORTS.md +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/pam/templates/init/dockerignore.tmpl +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/pam/templates/init/gitignore.tmpl +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/pam/templates/init/main.tmpl +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/pam/templates/init/pylintrc.tmpl +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/pam/templates/init/run_unit_test.bat +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/pam/templates/init/run_unit_test.ps1 +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/pam/templates/init/run_unit_test.sh +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/pam/templates/project/pip/Dockerfile +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/pam/templates/project/pip/requirements.txt +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/pam/templates/project/uv/Dockerfile +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/pam/templates/project/uv/pyproject.toml +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/pam/templates/project/uv/python-version +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/pam/templates/service/common/service.yaml +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/pam/templates/service/pandas/functions.tmpl +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/pam/templates/service/pandas/service.test.tmpl +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/pam/templates/service/pandas/service_class.tmpl +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/pam/templates/service/polars/functions.tmpl +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/pam/templates/service/polars/service.test.tmpl +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/pam/templates/service/polars/service_class.tmpl +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/pam/tester_task.py +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/pam/utils.py +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/pam_python.egg-info/dependency_links.txt +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/pam_python.egg-info/entry_points.txt +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/pam_python.egg-info/requires.txt +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/pam_python.egg-info/top_level.txt +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/pyproject.toml +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/setup.cfg +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/tests/test_api.py +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/tests/test_cli_templates.py +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/tests/test_data_error_v2.py +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/tests/test_data_timeout_v2.py +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/tests/test_datetime_utils.py +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/tests/test_range_transform.py +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/tests/test_report_store.py +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/tests/test_request_data_v2.py +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/tests/test_result_batch_uploader.py +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/tests/test_service_dataframe_output.py +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/tests/test_sqlite_download_v2.py +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/tests/test_task_manager_upload_token.py +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/tests/test_temp_file_utils.py +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/tests/test_temporal_mode.py +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/tests/test_typed_area_report.py +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/tests/test_typed_bar_report.py +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/tests/test_typed_box_plot_report.py +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/tests/test_typed_bubble_report.py +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/tests/test_typed_candlestick_report.py +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/tests/test_typed_donut_report.py +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/tests/test_typed_heatmap_report.py +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/tests/test_typed_line_report.py +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/tests/test_typed_metric_report.py +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/tests/test_typed_pie_report.py +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/tests/test_typed_polar_area_report.py +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/tests/test_typed_radar_report.py +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/tests/test_typed_radial_bar_report.py +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/tests/test_typed_range_area_report.py +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/tests/test_typed_range_bar_report.py +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/tests/test_typed_scatter_report.py +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/tests/test_typed_table_report.py +0 -0
- {pam_python-0.2.19 → pam_python-0.2.20}/tests/test_typed_treemap_report.py +0 -0
|
@@ -127,6 +127,7 @@ class DataInputV2:
|
|
|
127
127
|
datasets: tuple[DataInputV2Dataset, ...]
|
|
128
128
|
run_complete: bool
|
|
129
129
|
next: str
|
|
130
|
+
missing_contacts: DataInputV2File | None = None
|
|
130
131
|
|
|
131
132
|
def iter_population_rows(self) -> Iterator[dict[str, str]]:
|
|
132
133
|
"""Stream the population file for this customer-aligned logical page."""
|
|
@@ -210,6 +211,15 @@ def parse_data_input_v2(input_files: list[str]) -> DataInputV2:
|
|
|
210
211
|
if contact_count > max_contacts_per_batch:
|
|
211
212
|
raise ValueError("V2 population contact_count exceeds configured batch maximum")
|
|
212
213
|
|
|
214
|
+
missing_contacts = None
|
|
215
|
+
if "missing_contacts" in manifest:
|
|
216
|
+
missing_contacts = _parse_and_verify_file(
|
|
217
|
+
manifest["missing_contacts"],
|
|
218
|
+
files_by_name,
|
|
219
|
+
declared_names,
|
|
220
|
+
population_format,
|
|
221
|
+
)
|
|
222
|
+
|
|
213
223
|
raw_datasets = manifest.get("datasets")
|
|
214
224
|
if not isinstance(raw_datasets, list) or len(raw_datasets) > V2_MAX_DATASETS:
|
|
215
225
|
raise ValueError("V2 manifest datasets must be a list of at most five items")
|
|
@@ -283,6 +293,7 @@ def parse_data_input_v2(input_files: list[str]) -> DataInputV2:
|
|
|
283
293
|
max_contacts_per_batch=max_contacts_per_batch,
|
|
284
294
|
time_window=time_window,
|
|
285
295
|
population=population_file,
|
|
296
|
+
missing_contacts=missing_contacts,
|
|
286
297
|
datasets=tuple(datasets),
|
|
287
298
|
run_complete=_required_bool(manifest, "run_complete"),
|
|
288
299
|
next=_required_optional_string(manifest, "next"),
|
|
@@ -9,6 +9,7 @@ import math
|
|
|
9
9
|
import os
|
|
10
10
|
import time
|
|
11
11
|
import threading
|
|
12
|
+
import uuid
|
|
12
13
|
from typing import Dict, TYPE_CHECKING
|
|
13
14
|
from pam.utils import log
|
|
14
15
|
from pam.api import API
|
|
@@ -453,34 +454,31 @@ class TaskManager(ITaskManager):
|
|
|
453
454
|
return response
|
|
454
455
|
|
|
455
456
|
def service_upload_report(self, service: Service, file_path):
|
|
456
|
-
"""
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
log(f"Uploading Report to: {endpoint}")
|
|
482
|
-
http_thread = threading.Thread(target=upload_wrapper)
|
|
483
|
-
http_thread.start()
|
|
457
|
+
"""Wait for PAM to enqueue a legacy report; propagate delivery failures."""
|
|
458
|
+
trace_id = uuid.uuid4().hex
|
|
459
|
+
started = time.monotonic()
|
|
460
|
+
log(f"report_delivery_started trace_id={trace_id} service={service.request.service_name} "
|
|
461
|
+
f"report_file={Path(file_path).name}")
|
|
462
|
+
response = self.api.http_upload(
|
|
463
|
+
service.request.response_api,
|
|
464
|
+
file_path,
|
|
465
|
+
{"token": service.request.token, "report_delivery_id": trace_id},
|
|
466
|
+
)
|
|
467
|
+
status = response.status_code if response is not None else None
|
|
468
|
+
if response is None or not 200 <= status < 300:
|
|
469
|
+
log(f"report_delivery_failed trace_id={trace_id} http_status={status}")
|
|
470
|
+
raise RuntimeError(f"Report delivery failed trace_id={trace_id} http_status={status}")
|
|
471
|
+
try:
|
|
472
|
+
body = response.json()
|
|
473
|
+
except ValueError:
|
|
474
|
+
body = {}
|
|
475
|
+
stage = body.get("stage", "accepted_unconfirmed") if isinstance(body, dict) else "accepted_unconfirmed"
|
|
476
|
+
log(f"report_delivery_response trace_id={trace_id} http_status={status} "
|
|
477
|
+
f"stage={stage} elapsed_s={time.monotonic() - started:.3f}")
|
|
478
|
+
# Older PAM deployments only acknowledge receipt. Do not report that as
|
|
479
|
+
# successful publication: the new endpoint explicitly confirms enqueue.
|
|
480
|
+
if not isinstance(body, dict) or body.get("report_delivery_id") != trace_id or stage != "tracker_queued":
|
|
481
|
+
raise RuntimeError(f"Report delivery was not confirmed; update PAM first trace_id={trace_id}")
|
|
484
482
|
|
|
485
483
|
def service_publish_report_pointer_v2(
|
|
486
484
|
self,
|
|
@@ -680,3 +680,13 @@ Prefer streaming/chunked/incremental processing whenever possible.
|
|
|
680
680
|
- Treat Polars lazy queries as the default compute path.
|
|
681
681
|
- Use Pandas only when the selected service template or an integration requires it.
|
|
682
682
|
- Prefer streaming or incremental durable steps when a query cannot stay in memory.
|
|
683
|
+
|
|
684
|
+
### Missing contacts in V2 input
|
|
685
|
+
|
|
686
|
+
`data.missing_contacts` is an optional verified `DataInputV2File` with one `id`
|
|
687
|
+
column, using the same CSV or Parquet format as population. Read it with
|
|
688
|
+
`scan_polars()` or `read_pandas()`; `iter_csv_rows()` is CSV-only. Older
|
|
689
|
+
manifests expose `None`, while a present zero-row file is valid. These IDs are
|
|
690
|
+
excluded from population and configured datasets. Their absence does not prove
|
|
691
|
+
deletion; plugin developers decide whether and how to handle them. Read the
|
|
692
|
+
file before page cleanup. Do not treat it as a configured dataset position.
|
|
@@ -47,6 +47,7 @@ class DataInputV2Test(unittest.TestCase):
|
|
|
47
47
|
self.assertEqual("contacts", data_input.population_mode)
|
|
48
48
|
self.assertEqual(5000, data_input.max_contacts_per_batch)
|
|
49
49
|
self.assertIsNone(data_input.time_window)
|
|
50
|
+
self.assertIsNone(data_input.missing_contacts)
|
|
50
51
|
self.assertEqual(str(self.population_path.resolve()), data_input.population.path)
|
|
51
52
|
self.assertEqual(1, data_input.population.row_count)
|
|
52
53
|
self.assertEqual(1, len(data_input.datasets))
|
|
@@ -56,6 +57,72 @@ class DataInputV2Test(unittest.TestCase):
|
|
|
56
57
|
data_input.datasets[0].parts[0].path,
|
|
57
58
|
)
|
|
58
59
|
|
|
60
|
+
def test_missing_contacts_readers_and_zip_contract(self):
|
|
61
|
+
import polars as pl
|
|
62
|
+
|
|
63
|
+
for file_format in ("csv", "parquet"):
|
|
64
|
+
for ids in (["contact-2", "contact-3"], []):
|
|
65
|
+
with self.subTest(format=file_format, ids=ids):
|
|
66
|
+
population = self.temp_dir / f"population.{file_format}"
|
|
67
|
+
missing = self.temp_dir / f"missing_contacts.{file_format}"
|
|
68
|
+
population_frame = pl.DataFrame({"id": ["contact-1"]})
|
|
69
|
+
missing_frame = pl.DataFrame({"id": ids}, schema={"id": pl.String})
|
|
70
|
+
if file_format == "csv":
|
|
71
|
+
population_frame.write_csv(population)
|
|
72
|
+
missing_frame.write_csv(missing)
|
|
73
|
+
else:
|
|
74
|
+
population_frame.write_parquet(population)
|
|
75
|
+
missing_frame.write_parquet(missing)
|
|
76
|
+
manifest = self._valid_manifest()
|
|
77
|
+
manifest["population"]["format"] = file_format
|
|
78
|
+
manifest["population"]["file"] = self._metadata(population, 1)
|
|
79
|
+
manifest["missing_contacts"] = self._metadata(missing, len(ids))
|
|
80
|
+
buffer = BytesIO()
|
|
81
|
+
with zipfile.ZipFile(buffer, "w") as archive:
|
|
82
|
+
archive.writestr("manifest.json", json.dumps(manifest))
|
|
83
|
+
for path in (population, missing, self.dataset_path):
|
|
84
|
+
archive.write(path, path.name)
|
|
85
|
+
buffer.seek(0)
|
|
86
|
+
request, error = self._parse_v2_zip(buffer)
|
|
87
|
+
self.assertEqual("", error)
|
|
88
|
+
data = request.data_input_v2
|
|
89
|
+
self.assertIsNotNone(data)
|
|
90
|
+
self.assertIsNotNone(data.missing_contacts)
|
|
91
|
+
self.assertEqual(file_format, data.missing_contacts.format)
|
|
92
|
+
self.assertEqual(len(ids), data.missing_contacts.row_count)
|
|
93
|
+
self.assertEqual(ids, data.missing_contacts.scan_polars().collect()["id"].to_list())
|
|
94
|
+
self.assertEqual(ids, data.missing_contacts.read_pandas()["id"].tolist())
|
|
95
|
+
if file_format == "csv":
|
|
96
|
+
self.assertEqual(ids, [row["id"] for row in data.missing_contacts.iter_csv_rows()])
|
|
97
|
+
self.assertEqual(1, len(data.datasets))
|
|
98
|
+
missing_path = Path(data.missing_contacts.path)
|
|
99
|
+
self.assertTrue(data.cleanup())
|
|
100
|
+
self.assertFalse(missing_path.exists())
|
|
101
|
+
|
|
102
|
+
def test_missing_contacts_uses_existing_file_validation(self):
|
|
103
|
+
for failure in ("checksum", "size", "format", "missing", "metadata"):
|
|
104
|
+
with self.subTest(failure=failure):
|
|
105
|
+
path = self._write_file("missing_contacts.csv", b"id\ncontact-2\n")
|
|
106
|
+
manifest = self._valid_manifest()
|
|
107
|
+
manifest["missing_contacts"] = self._metadata(path, 1)
|
|
108
|
+
files = [str(self.population_path), str(self.dataset_path), str(path)]
|
|
109
|
+
if failure == "checksum":
|
|
110
|
+
manifest["missing_contacts"]["sha256"] = "0" * 64
|
|
111
|
+
elif failure == "size":
|
|
112
|
+
manifest["missing_contacts"]["size_bytes"] += 1
|
|
113
|
+
elif failure == "format":
|
|
114
|
+
other = path.with_suffix(".parquet")
|
|
115
|
+
other.write_bytes(path.read_bytes())
|
|
116
|
+
manifest["missing_contacts"] = self._metadata(other, 1)
|
|
117
|
+
files[-1] = str(other)
|
|
118
|
+
elif failure == "missing":
|
|
119
|
+
files.pop()
|
|
120
|
+
else:
|
|
121
|
+
manifest["missing_contacts"] = None
|
|
122
|
+
manifest_path = self._write_manifest(manifest)
|
|
123
|
+
with self.assertRaises(ValueError):
|
|
124
|
+
parse_data_input_v2([str(manifest_path), *files])
|
|
125
|
+
|
|
59
126
|
def test_pandas_readers_follow_csv_metadata(self):
|
|
60
127
|
manifest_path = self._write_manifest(self._valid_manifest())
|
|
61
128
|
data_input = parse_data_input_v2(
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
from types import SimpleNamespace
|
|
3
|
+
from unittest.mock import Mock, patch
|
|
4
|
+
|
|
5
|
+
from pam.task_manager import TaskManager
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ReportDeliveryTest(unittest.TestCase):
|
|
9
|
+
def manager_and_service(self):
|
|
10
|
+
manager = TaskManager.__new__(TaskManager)
|
|
11
|
+
manager.api = Mock()
|
|
12
|
+
service = SimpleNamespace(request=SimpleNamespace(
|
|
13
|
+
service_name="rfm", response_api="https://pam/response", token="secret"))
|
|
14
|
+
return manager, service
|
|
15
|
+
|
|
16
|
+
def test_waits_for_confirmed_enqueue(self):
|
|
17
|
+
manager, service = self.manager_and_service()
|
|
18
|
+
def upload(endpoint, path, payload):
|
|
19
|
+
return Mock(status_code=200, json=lambda: {
|
|
20
|
+
"report_delivery_id": payload["report_delivery_id"], "stage": "tracker_queued"})
|
|
21
|
+
manager.api.http_upload.side_effect = upload
|
|
22
|
+
with patch("pam.task_manager.threading.Thread") as thread:
|
|
23
|
+
manager.service_upload_report(service, "/tmp/report.csv")
|
|
24
|
+
thread.assert_not_called()
|
|
25
|
+
self.assertEqual("secret", manager.api.http_upload.call_args.args[2]["token"])
|
|
26
|
+
|
|
27
|
+
def test_failure_and_unconfirmed_ack_raise(self):
|
|
28
|
+
for response in (None, Mock(status_code=500), Mock(status_code=200, json=lambda: {}),
|
|
29
|
+
Mock(status_code=200, json=lambda: {"report_delivery_id": "wrong", "stage": "tracker_queued"})):
|
|
30
|
+
with self.subTest(response=response):
|
|
31
|
+
manager, service = self.manager_and_service()
|
|
32
|
+
manager.api.http_upload.return_value = response
|
|
33
|
+
with self.assertRaises(RuntimeError):
|
|
34
|
+
manager.service_upload_report(service, "/tmp/report.csv")
|
|
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
|
|
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
|