"""Pure decoder and safe tree editor for 1C scheduled-job schedules.""" from __future__ import annotations import copy import re from datetime import datetime from typing import Any WRITABLE_SCALAR_FIELDS = { "begin_date", "end_date", "begin_time", "end_time", "completion_time", "completion_interval", "repeat_period_in_day", "repeat_pause", "week_day_in_month", "day_in_month", "weeks_period", "days_repeat_period", } WRITABLE_LIST_FIELDS = {"week_days", "months"} INTEGER_RANGES = { "completion_interval": (0, 2_147_483_647), "repeat_period_in_day": (0, 2_147_483_647), "repeat_pause": (0, 2_147_483_647), "week_day_in_month": (0, 5), "day_in_month": (0, 31), "weeks_period": (0, 2_147_483_647), "days_repeat_period": (0, 2_147_483_647), } def _scalar(node: Any) -> str: if not isinstance(node, dict): return str(node or "") if node.get("type") in {"atom", "string"}: return str(node.get("value") or "") return "" def schedule_datetime(raw: str) -> tuple[str | None, str | None]: if not re.fullmatch(r"\d{14}", raw): return None, None return ( f"{raw[0:4]}-{raw[4:6]}-{raw[6:8]}", f"{raw[8:10]}:{raw[10:12]}:{raw[12:14]}", ) def schedule_layout(tree: Any) -> dict[str, Any]: items = tree.get("items") if isinstance(tree, dict) and isinstance(tree.get("items"), list) else [] raw = [_scalar(item) for item in items] if len(raw) < 13: return { "status": "invalid_schedule_payload", "diagnostics": {"message": "The SQL schedule payload is shorter than the supported format."}, } week_day_count = int(raw[8]) if re.fullmatch(r"-?\d+", raw[8]) else None cursor = 9 if week_day_count is None or week_day_count < 0 or cursor + week_day_count > len(raw): return { "status": "invalid_schedule_payload", "diagnostics": {"message": "Invalid weekday collection in the SQL schedule payload."}, } week_days_start = cursor cursor += week_day_count if cursor + 3 > len(raw): return { "status": "invalid_schedule_payload", "diagnostics": {"message": "The SQL schedule payload has no month collection header."}, } week_day_in_month_index = cursor day_in_month_index = cursor + 1 month_count_index = cursor + 2 month_count = int(raw[month_count_index]) if re.fullmatch(r"-?\d+", raw[month_count_index]) else None cursor += 3 if month_count is None or month_count < 0 or cursor + month_count > len(raw): return { "status": "invalid_schedule_payload", "diagnostics": {"message": "Invalid month collection in the SQL schedule payload."}, } months_start = cursor cursor += month_count if cursor + 2 > len(raw): return { "status": "invalid_schedule_payload", "diagnostics": {"message": "The SQL schedule payload has no repeat-period tail."}, } return { "status": "ok", "raw": raw, "indexes": { "begin_date": 0, "end_date": 1, "begin_time": 2, "end_time": 3, "completion_time": 4, "completion_interval": 5, "repeat_period_in_day": 6, "repeat_pause": 7, "week_day_count": 8, "week_days": list(range(week_days_start, week_days_start + week_day_count)), "week_day_in_month": week_day_in_month_index, "day_in_month": day_in_month_index, "month_count": month_count_index, "months": list(range(months_start, months_start + month_count)), "weeks_period": cursor, "days_repeat_period": cursor + 1, }, "cursor": cursor + 2, } def decode_schedule(tree: Any, *, include_storage: bool = False) -> dict[str, Any]: layout = schedule_layout(tree) if layout.get("status") != "ok": return layout raw = layout["raw"] indexes = layout["indexes"] def integer(field: str) -> int | None: value = raw[indexes[field]] return int(value) if re.fullmatch(r"-?\d+", value) else None begin_date, _ = schedule_datetime(raw[indexes["begin_date"]]) end_date, _ = schedule_datetime(raw[indexes["end_date"]]) _, begin_time = schedule_datetime(raw[indexes["begin_time"]]) _, end_time = schedule_datetime(raw[indexes["end_time"]]) _, completion_time = schedule_datetime(raw[indexes["completion_time"]]) result = { "status": "ok", "begin_date": begin_date, "end_date": end_date, "begin_time": begin_time, "end_time": end_time, "completion_time": completion_time, "completion_interval": integer("completion_interval"), "repeat_period_in_day": integer("repeat_period_in_day"), "repeat_pause": integer("repeat_pause"), "week_days": [int(raw[index]) for index in indexes["week_days"] if raw[index].isdigit()], "week_day_in_month": integer("week_day_in_month"), "day_in_month": integer("day_in_month"), "months": [int(raw[index]) for index in indexes["months"] if raw[index].isdigit()], "weeks_period": integer("weeks_period"), "days_repeat_period": integer("days_repeat_period"), "evidence": "live_sql_config_schedule_decoder", } if include_storage: result["storage"] = { "format": "scheduled_job_config_suffix_0", "raw_values": raw, "trailing_values": raw[int(layout["cursor"]) :], } return result def rebuild_schedule_tree(tree: Any, requested: dict[str, Any]) -> dict[str, Any]: layout = schedule_layout(tree) if layout.get("status") != "ok": return layout raw = list(layout["raw"]) indexes = layout["indexes"] current = decode_schedule(tree) for field in WRITABLE_SCALAR_FIELDS: if field not in requested: continue value = requested[field] index = int(indexes[field]) if field in {"begin_date", "end_date"}: raw[index] = str(value).replace("-", "") + ( raw[index][8:] if re.fullmatch(r"\d{14}", raw[index]) else "000000" ) elif field in {"begin_time", "end_time", "completion_time"}: raw[index] = ( raw[index][:8] if re.fullmatch(r"\d{14}", raw[index]) else "00010101" ) + str(value).replace(":", "") else: raw[index] = str(value) week_days = list(requested.get("week_days", current.get("week_days") or [])) months = list(requested.get("months", current.get("months") or [])) rebuilt_raw = [ *raw[:8], str(len(week_days)), *(str(value) for value in week_days), raw[int(indexes["week_day_in_month"])], raw[int(indexes["day_in_month"])], str(len(months)), *(str(value) for value in months), raw[int(indexes["weeks_period"])], raw[int(indexes["days_repeat_period"])], *raw[int(layout["cursor"]) :], ] rebuilt_tree = copy.deepcopy(tree) rebuilt_tree["items"] = [{"type": "atom", "value": value} for value in rebuilt_raw] verification = decode_schedule(rebuilt_tree) if verification.get("status") != "ok": return { "status": "error", "error": "schedule_rebuild_verification_failed", "diagnostics": {"message": "The rebuilt scheduled-job tree did not pass the schedule decoder."}, } for field, expected in requested.items(): if verification.get(field) != expected: return { "status": "error", "error": "schedule_rebuild_verification_failed", "field": field, "diagnostics": { "message": "A named schedule field changed during tree rebuild verification.", "expected": expected, "actual": verification.get(field), }, } return { "status": "ok", "tree": rebuilt_tree, "schedule": verification, "old_counts": { "week_days": len(indexes["week_days"]), "months": len(indexes["months"]), }, "new_counts": { "week_days": len(week_days), "months": len(months), }, } def schedule_write_edits(tree: Any, requested: Any) -> dict[str, Any]: if not isinstance(requested, dict) or not requested: return { "status": "invalid_argument", "error": "schedule_required", "diagnostics": {"message": "schedule must be a non-empty JSON object."}, } unknown = sorted(set(requested) - WRITABLE_SCALAR_FIELDS - WRITABLE_LIST_FIELDS) if unknown: return { "status": "invalid_argument", "error": "unsupported_schedule_fields", "diagnostics": { "message": "The request contains unsupported scheduled-job fields.", "fields": unknown, "allowed_fields": sorted(WRITABLE_SCALAR_FIELDS | WRITABLE_LIST_FIELDS), }, } layout = schedule_layout(tree) if layout.get("status") != "ok": return layout current = decode_schedule(tree) raw = layout["raw"] indexes = layout["indexes"] normalized: dict[str, Any] = {} edits: list[dict[str, Any]] = [] resized_collections: list[str] = [] for field, value in requested.items(): if field in {"begin_date", "end_date"}: if value is None: compact = "00010101" elif isinstance(value, str) and re.fullmatch(r"\d{4}-\d{2}-\d{2}", value): try: datetime.fromisoformat(value) except ValueError: return { "status": "invalid_argument", "error": "invalid_schedule_value", "field": field, "diagnostics": {"message": f"{field} must be a real ISO date YYYY-MM-DD."}, } compact = value.replace("-", "") else: return { "status": "invalid_argument", "error": "invalid_schedule_value", "field": field, "diagnostics": {"message": f"{field} must be an ISO date YYYY-MM-DD or null."}, } index = int(indexes[field]) encoded = compact + (raw[index][8:] if re.fullmatch(r"\d{14}", raw[index]) else "000000") normalized[field] = "0001-01-01" if value is None else value if encoded != raw[index]: edits.append( { "path": str(index), "value": encoded, "node_type": "atom", "expected_old": raw[index], "field": field, } ) continue if field in {"begin_time", "end_time", "completion_time"}: if value is None: compact = "000000" elif isinstance(value, str) and re.fullmatch(r"\d{2}:\d{2}:\d{2}", value): try: datetime.strptime(value, "%H:%M:%S") except ValueError: return { "status": "invalid_argument", "error": "invalid_schedule_value", "field": field, "diagnostics": {"message": f"{field} must be a real time HH:MM:SS."}, } compact = value.replace(":", "") else: return { "status": "invalid_argument", "error": "invalid_schedule_value", "field": field, "diagnostics": {"message": f"{field} must be a time HH:MM:SS or null."}, } index = int(indexes[field]) encoded = ( raw[index][:8] if re.fullmatch(r"\d{14}", raw[index]) else "00010101" ) + compact normalized[field] = "00:00:00" if value is None else value if encoded != raw[index]: edits.append( { "path": str(index), "value": encoded, "node_type": "atom", "expected_old": raw[index], "field": field, } ) continue if field in WRITABLE_LIST_FIELDS: maximum = 7 if field == "week_days" else 12 if ( not isinstance(value, list) or any( isinstance(item, bool) or not isinstance(item, int) or item < 1 or item > maximum for item in value ) or len(set(value)) != len(value) ): return { "status": "invalid_argument", "error": "invalid_schedule_value", "field": field, "diagnostics": { "message": f"{field} must be a JSON array of unique integers from 1 to {maximum}." }, } field_indexes = list(indexes[field]) if len(value) != len(field_indexes): resized_collections.append(field) normalized[field] = list(value) continue normalized[field] = list(value) for index, item in zip(field_indexes, value): encoded = str(item) if encoded != raw[index]: edits.append( { "path": str(index), "value": encoded, "node_type": "atom", "expected_old": raw[index], "field": field, } ) continue minimum, maximum = INTEGER_RANGES[field] if isinstance(value, bool) or not isinstance(value, int) or value < minimum or value > maximum: return { "status": "invalid_argument", "error": "invalid_schedule_value", "field": field, "diagnostics": { "message": f"{field} must be a JSON integer from {minimum} to {maximum}." }, } index = int(indexes[field]) encoded = str(value) normalized[field] = value if encoded != raw[index]: edits.append( { "path": str(index), "value": encoded, "node_type": "atom", "expected_old": raw[index], "field": field, } ) if resized_collections: rebuilt = rebuild_schedule_tree(tree, normalized) if rebuilt.get("status") != "ok": return rebuilt edits = [ { "replace_root": rebuilt["tree"], "fields": sorted(normalized), "resized_collections": sorted(resized_collections), "old_counts": rebuilt["old_counts"], "new_counts": rebuilt["new_counts"], } ] return { "status": "ok", "current": current, "requested": normalized, "edits": edits, "counts": { "requested_fields": len(normalized), "edits": len(edits), "resized_collections": len(resized_collections), }, } # Compatibility aliases retained by adapter_1c_server and existing clients. config_schedule_datetime = schedule_datetime scheduled_job_schedule_layout = schedule_layout scheduled_job_sql_schedule = decode_schedule scheduled_job_schedule_rebuild_tree = rebuild_schedule_tree scheduled_job_schedule_write_edits = schedule_write_edits SCHEDULED_JOB_WRITABLE_SCALAR_FIELDS = WRITABLE_SCALAR_FIELDS SCHEDULED_JOB_WRITABLE_LIST_FIELDS = WRITABLE_LIST_FIELDS SCHEDULED_JOB_INTEGER_RANGES = INTEGER_RANGES __all__ = [ "INTEGER_RANGES", "WRITABLE_LIST_FIELDS", "WRITABLE_SCALAR_FIELDS", "config_schedule_datetime", "decode_schedule", "rebuild_schedule_tree", "schedule_datetime", "schedule_layout", "schedule_write_edits", "scheduled_job_schedule_layout", "scheduled_job_schedule_rebuild_tree", "scheduled_job_schedule_write_edits", "scheduled_job_sql_schedule", ]