From 13f46d75f1f8d0ffd195153336c239efe946be02 Mon Sep 17 00:00:00 2001 From: "cap@CASA" Date: Wed, 31 Dec 2025 21:39:24 +0100 Subject: [PATCH 01/33] Migrated to datetime instead of struct_time --- src/CAPcore/DictLoggedDict.py | 74 +++++++-------- src/CAPcore/LoggedDict.py | 23 ++--- src/CAPcore/LoggedValue.py | 32 ++++--- tests/CAPcore/test_DictData.py | 19 ++-- tests/CAPcore/test_DictLoggedDict.py | 130 ++++++++++++++------------- tests/CAPcore/test_loggedDict.py | 37 ++++---- tests/CAPcore/test_loggedValue.py | 22 ++--- 7 files changed, 179 insertions(+), 158 deletions(-) diff --git a/src/CAPcore/DictLoggedDict.py b/src/CAPcore/DictLoggedDict.py index b7ef4ca..56c3cbe 100644 --- a/src/CAPcore/DictLoggedDict.py +++ b/src/CAPcore/DictLoggedDict.py @@ -1,10 +1,10 @@ +from datetime import datetime from functools import wraps -from time import gmtime, struct_time, strftime from typing import Optional, Set, List, Dict, Tuple from .LoggedDict import LoggedDict from .LoggedValue import DATEFORMAT -from .Misc import compareSets, SetDiff, chainKargs +from .Misc import compareSets, SetDiff, chainKargs, getUTC def _checkDeletedUpdate(func, canDiff=False): @@ -23,7 +23,7 @@ def wrapper(self, *kargs, **kwargs): result = func(self, *kargs, **kwargs) if result: - dateField = kwargs.get('timestamp', gmtime()) + dateField = kwargs.get('timestamp', getUTC()) self.addHistory(dateField, f"Updated data {changes}") return result @@ -42,9 +42,9 @@ def wrapper(self, *kargs, **kwargs): class DictData(LoggedDict): - def __init__(self, timestamp: Optional[struct_time] = None, exclusions: Optional[Set] = None): + def __init__(self, timestamp: Optional[datetime] = None, exclusions: Optional[Set] = None): super().__init__(exclusions=exclusions) - self.last_updated = timestamp or gmtime() + self.last_updated = timestamp or getUTC() self.deleted = False self.history: List = [] @@ -53,25 +53,25 @@ def __init__(self, timestamp: Optional[struct_time] = None, exclusions: Optional def isDeleted(self): return self.deleted - def addHistory(self, data, timestamp: Optional[struct_time] = None): - dateField = timestamp or gmtime() + def addHistory(self, data, timestamp: Optional[datetime] = None): + dateField = timestamp or getUTC() self.history.append((dateField, data)) - def delete(self, timestamp: Optional[struct_time] = None) -> bool: + def delete(self, timestamp: Optional[datetime] = None) -> bool: if self.isDeleted(): return False - dateField = timestamp or gmtime() + dateField = timestamp or getUTC() self.last_updated = dateField self.deleted = True self.addHistory(data="Deleted", timestamp=dateField) return True - def restore(self, timestamp: Optional[struct_time] = None) -> bool: + def restore(self, timestamp: Optional[datetime] = None) -> bool: if not self.isDeleted(): return False - dateField = timestamp or gmtime() + dateField = timestamp or getUTC() self.last_updated = dateField self.deleted = False self.addHistory(data="Restored", timestamp=dateField) @@ -80,7 +80,7 @@ def restore(self, timestamp: Optional[struct_time] = None) -> bool: def showV(self, compact=True, indent: int = 0, firstIndent: Optional[int] = None): delTxt = " D" if self.deleted else "" - dateTxt = strftime(DATEFORMAT, self.last_updated) + dateTxt = self.last_updated.strftime(DATEFORMAT) lenTxt = f"l"":"f"{len(self.history)}" result = (f"{super().show(compact=compact, indent=indent, firstIndent=firstIndent)}" @@ -139,18 +139,18 @@ def __repr__(self): class DictOfLoggedDict: - def __init__(self, exclusions: Optional[Set[str]] = None, timestamp: Optional[struct_time] = None): - changeTime = timestamp or gmtime() + def __init__(self, exclusions: Optional[Set[str]] = None, timestamp: Optional[datetime] = None): + changeTime = timestamp or getUTC() if exclusions is not None and not isinstance(exclusions, (set, list, tuple)): raise TypeError( f"DictOfLoggedDict: expected set/list/tuple for exclusions: '{exclusions}' ({type(exclusions)}") - self.current: Dict[str, DictData] = dict() + self.current: Dict[str, DictData] = {} self.exclusions: Set[str] = set(exclusions) if exclusions else set() - self.timestamp: struct_time = changeTime + self.timestamp: datetime = changeTime self.numChanges: int = 0 - self.history: List[Tuple[struct_time, str]] = [] + self.history: List[Tuple[datetime, str]] = [] self.addHistory("Created", changeTime) @@ -162,8 +162,8 @@ def __getitem__(self, k): raise KeyError(f"Attempting to get a deleted item '{k}'.You must undelete first") return auxResult._asdict() - def __setitem__(self, k, v, timestamp: Optional[struct_time] = None): - changeTime = timestamp or gmtime() + def __setitem__(self, k, v, timestamp: Optional[datetime] = None): + changeTime = timestamp or getUTC() currVal = self.current.get(k, DictData(exclusions=self.exclusions, timestamp=changeTime)) changes = currVal.replace(v, timestamp=changeTime) @@ -174,8 +174,8 @@ def __setitem__(self, k, v, timestamp: Optional[struct_time] = None): self.addHistory(f"Set '{k}':{currVal}") return changes - def addHistory(self, data: str, timestamp: Optional[struct_time] = None): - dateField = timestamp or gmtime() + def addHistory(self, data: str, timestamp: Optional[datetime] = None): + dateField = timestamp or getUTC() self.history.append((dateField, data)) def get(self, key): @@ -190,8 +190,8 @@ def getV(self, key): raise KeyError(f"Unknown key '{key}'") return self.current.get(key) - def pop(self, key, *kargs, timestamp: Optional[struct_time] = None): - changeTime = timestamp or gmtime() + def pop(self, key, *kargs, timestamp: Optional[datetime] = None): + changeTime = timestamp or getUTC() if (key not in self.current) or (self.current[key].isDeleted()): if kargs: return kargs[0] # default @@ -206,8 +206,8 @@ def pop(self, key, *kargs, timestamp: Optional[struct_time] = None): return result - def update(self, newValues, timestamp: Optional[struct_time] = None, replaceInner: bool = False): - changeTime = timestamp or gmtime() + def update(self, newValues, timestamp: Optional[datetime] = None, replaceInner: bool = False): + changeTime = timestamp or getUTC() result = False if not isinstance(newValues, (dict, DictOfLoggedDict)): @@ -231,8 +231,8 @@ def update(self, newValues, timestamp: Optional[struct_time] = None, replaceInne self.addHistory(f"Update {newValues}", timestamp=timestamp) return result - def purge(self, *kargs, timestamp: Optional[struct_time] = None): - changeTime = timestamp or gmtime() + def purge(self, *kargs, timestamp: Optional[datetime] = None): + changeTime = timestamp or getUTC() result = False keys2delete = set(chainKargs(*kargs)) @@ -241,15 +241,15 @@ def purge(self, *kargs, timestamp: Optional[struct_time] = None): result |= self.current[k].delete(timestamp=changeTime) if result: - keysStr = ",".join(map(lambda x: f"'{x}'", keys2delete)) + keysStr = ",".join(f"'{x}'" for x in keys2delete) self.timestamp = changeTime self.numChanges += 1 self.addHistory(f"Purged {keysStr}", timestamp=timestamp) return result - def replace(self, newValues, timestamp=None) -> bool: - changeTime = timestamp or gmtime() + def replace(self, newValues, timestamp: Optional[datetime] = None) -> bool: + changeTime = timestamp or getUTC() result = False if not isinstance(newValues, (dict, DictOfLoggedDict)): @@ -273,7 +273,7 @@ def replace(self, newValues, timestamp=None) -> bool: return result - def addExclusion(self, *kargs, timestamp: Optional[struct_time] = None) -> bool: + def addExclusion(self, *kargs, timestamp: Optional[datetime] = None) -> bool: keys2add = set(chainKargs(*kargs)) changed = False self.exclusions.update(keys2add) @@ -319,9 +319,9 @@ def itemsV(self): def valuesV(self): return self.current.values() - def renameKeys(self, keyMapping: Dict[str, str], timestamp: Optional[struct_time] = None, includeDeleted=False + def renameKeys(self, keyMapping: Dict[str, str], timestamp: Optional[datetime] = None, includeDeleted=False ) -> bool: - changeTime = timestamp or gmtime() + changeTime = timestamp or getUTC() result = False @@ -400,8 +400,8 @@ def show(self, compact: bool = False, indent: int = 0, firstIndent: Optional[int if self.lenV() == 0: return f"{self.current} {metadataStr}" if compact: - result = "{" + ", ".join(map(lambda k: f"'{k}':{self.current[k].showV(compact, indent, firstIndent)}", - claves)) + "}" + f" {metadataStr}" + result = "{" + ", ".join(f"'{k}':{self.current[k].showV(compact, indent, firstIndent)}" for k in + claves) + "}" + f" {metadataStr}" else: longestK = 0 if compact else max(len(k) for k in claves) linesList = [] @@ -418,7 +418,7 @@ def show(self, compact: bool = False, indent: int = 0, firstIndent: Optional[int return result def buildMetadataStr(self): - dateTxt = strftime(DATEFORMAT, self.timestamp) + dateTxt = self.timestamp.strftime(DATEFORMAT) lenTxt = f"l"":"f"{self.numChanges}" metadataStr = f"[t:{dateTxt} {lenTxt}]" return metadataStr @@ -427,7 +427,7 @@ def compareWithOtherKeys(self, newValues) -> SetDiff: if not isinstance(newValues, (dict, DictOfLoggedDict)): raise TypeError(f"Parameter expected to be a dict or DictOfLoggedDict. Provided {type(newValues)}") - otherKeys = set(newValues.keys()) if isinstance(newValues, DictOfLoggedDict) else set(newValues.keys()) + otherKeys = set(newValues.keys()) currentKeys = set(self.keys()) return compareSets(currentKeys, otherKeys) diff --git a/src/CAPcore/LoggedDict.py b/src/CAPcore/LoggedDict.py index 88bbc89..c8cdfbb 100644 --- a/src/CAPcore/LoggedDict.py +++ b/src/CAPcore/LoggedDict.py @@ -1,8 +1,9 @@ -from time import gmtime, struct_time +from datetime import datetime +from time import struct_time from typing import Set, Optional, Dict from .LoggedValue import LoggedValue -from .Misc import compareSets, SetDiff, chainKargs +from .Misc import compareSets, SetDiff, chainKargs, getUTC class LoggedDictDiff: @@ -62,19 +63,19 @@ def __repr__(self): class LoggedDict: - def __init__(self, exclusions: Optional[Set] = None, timestamp=None): + def __init__(self, exclusions: Optional[Set] = None, timestamp: Optional[datetime] = None): if exclusions is not None and not isinstance(exclusions, (set, list, tuple)): raise TypeError(f"LoggedDict: expected set/list/tuple for exclusions: {exclusions}") self.current: Dict[LoggedValue] = {} self.exclusions: Set[str] = set(exclusions) if exclusions else set() - self.timestamp = timestamp or gmtime() + self.timestamp = timestamp or getUTC() def __getitem__(self, item): return self.current.__getitem__(item).get() - def __setitem__(self, k, v, timestamp=None): + def __setitem__(self, k, v, timestamp: Optional[datetime] = None): if k in self.exclusions: raise KeyError(f"Key '{k}' in exclusions: {sorted(self.exclusions)}") currVal = self.current.get(k, LoggedValue()) # default= @@ -95,8 +96,8 @@ def get(self, key, default=None): def getV(self, key, default=None): return self.current.get(key, default) - def update(self, newValues, timestamp=None): - changeTime = timestamp or gmtime() + def update(self, newValues, timestamp: Optional[datetime] = None): + changeTime = timestamp or getUTC() result = False newValIter = newValues if isinstance(newValues, dict): @@ -114,8 +115,8 @@ def update(self, newValues, timestamp=None): return result - def purge(self, *kargs, timestamp=None) -> bool: - changeTime = timestamp or gmtime() + def purge(self, *kargs, timestamp: Optional[datetime] = None) -> bool: + changeTime = timestamp or getUTC() result = False keys2delete = set(chainKargs(*kargs)) for k in keys2delete: @@ -164,14 +165,14 @@ def _asdict(self): result = dict(self.items()) return result - def replace(self, other, timestamp=None) -> bool: + def replace(self, other, timestamp: Optional[datetime] = None) -> bool: result = False if not isinstance(other, (dict, LoggedDict)): raise TypeError(f"Parameter expected to be a dict or LoggedDict. Provided {type(other)}") compKeys = self.compareWithOtherKeys(other) result |= self.purge(compKeys.missing, timestamp=timestamp) - for k in sorted((compKeys.new).union(compKeys.shared)): + for k in sorted(compKeys.new.union(compKeys.shared)): if k in self.exclusions: continue result |= self.__setitem__(k, other.get(k), timestamp=timestamp) diff --git a/src/CAPcore/LoggedValue.py b/src/CAPcore/LoggedValue.py index f419554..0ae5f7f 100644 --- a/src/CAPcore/LoggedValue.py +++ b/src/CAPcore/LoggedValue.py @@ -1,21 +1,26 @@ -from time import gmtime, strftime +from datetime import datetime +from pprint import pp +from typing import Any, Optional -DATEFORMAT = "%Y-%m-%d %H:%M:%S%z" +from src.CAPcore.Misc import getUTC + +DATEFORMAT = "%Y-%m-%d %H:%M:%S.%f%z" class LoggedValue: - def __init__(self, v=None, timestamp=None): - self.last_updated = timestamp or gmtime() + def __init__(self, v=None, timestamp: Optional[datetime] = None): + self.last_updated = timestamp or getUTC() self.deleted = False self.value = None self.history = [] self.set(v, timestamp, change=True) - def set(self, v, timestamp=None, change=False): + def set(self, v: Any, timestamp: Optional[datetime] = None, change: bool = False): result = change if self.deleted or (v != self.value): - changeTime = timestamp or gmtime() + pp(self.last_updated) + changeTime = timestamp or getUTC() action = 'U' if self.deleted: action = 'C' @@ -24,19 +29,20 @@ def set(self, v, timestamp=None, change=False): self.deleted = False return result - def _set(self, v, action, changeTime): - if changeTime < self.last_updated: - raise ValueError((f"changeTime value '{strftime(DATEFORMAT, changeTime)}' is before the last" - f" recorded change '{strftime(DATEFORMAT, self.last_updated)}'")) + def _set(self, v: Any, action: str, changeTime: datetime): + if changeTime.replace(microsecond=0) < self.last_updated.replace(microsecond=0): + raise ValueError(( + f"changeTime value '{changeTime.strftime(DATEFORMAT)}' is before the last" + f" recorded change '{self.last_updated.strftime(DATEFORMAT)}'")) newLog = (action, changeTime, v) self.last_updated = changeTime self.value = v self.history.append(newLog) - def clear(self, timestamp=None): + def clear(self, timestamp: Optional[datetime] = None): if self.deleted: return False - changeTime = timestamp or gmtime() + changeTime = timestamp or getUTC() self._set(None, 'D', changeTime) self.deleted = True @@ -52,7 +58,7 @@ def isDeleted(self): def __repr__(self): delTxt = " D" if self.deleted else "" - dateTxt = strftime(DATEFORMAT, self.last_updated) + dateTxt = self.last_updated.strftime(DATEFORMAT) lenTxt = f"l"":"f"{len(self.history)}" return f"{self.value.__repr__()} [t:{dateTxt}{delTxt} {lenTxt}]" diff --git a/tests/CAPcore/test_DictData.py b/tests/CAPcore/test_DictData.py index f09e198..6a55e51 100644 --- a/tests/CAPcore/test_DictData.py +++ b/tests/CAPcore/test_DictData.py @@ -1,5 +1,5 @@ import unittest -from time import struct_time +from datetime import datetime, timezone from src.CAPcore.DictLoggedDict import DictOfLoggedDict, LoggedDict, DictData @@ -33,14 +33,15 @@ def test_DDrestore2(self): self.assertFalse(r2) def test_DDshowV1(self): - time0 = struct_time((2024, 12, 13, 23, 4, 24, 4, 348, 0)) - time1 = struct_time((2024, 12, 13, 23, 4, 34, 4, 348, 0)) - time2 = struct_time((2024, 12, 13, 23, 4, 44, 4, 348, 0)) - - res1 = {'a': "{'a1': 1 [t:2024-12-13 23:04:34+0000 l:1], 'a2': 'ce' [t:2024-12-13 23:04:34+0000 l:1]} (" - "t:2024-12-13 23:04:44+0000 D l:3)", - 'b': "{'a1': 1 [t:2024-12-13 23:04:34+0000 l:1], 'a2': 'ce' [t:2024-12-13 23:04:34+0000 l:1]} (" - "t:2024-12-13 23:04:34+0000 l:2)"} + time0 = datetime(2024, 12, 13, 23, 4, 24, 4, tzinfo=timezone.utc) + time1 = datetime(2024, 12, 13, 23, 4, 34, 4, tzinfo=timezone.utc) + time2 = datetime(2024, 12, 13, 23, 4, 44, 4, tzinfo=timezone.utc) + + res1 = { + 'a': "{'a1': 1 [t:2024-12-13 23:04:34.000004+0000 l:1], 'a2': 'ce' [t:2024-12-13 23:04:34.000004+0000 l:1]}" + " (t:2024-12-13 23:04:44.000004+0000 D l:3)", + 'b': "{'a1': 1 [t:2024-12-13 23:04:34.000004+0000 l:1], 'a2': 'ce' [t:2024-12-13 23:04:34.000004+0000 l:1]}" + " (t:2024-12-13 23:04:34.000004+0000 l:2)"} d1 = DictOfLoggedDict(timestamp=time0) dAux1 = {'a1': 1, 'a2': 'ce'} diff --git a/tests/CAPcore/test_DictLoggedDict.py b/tests/CAPcore/test_DictLoggedDict.py index 4c1e9b4..4ef91ea 100644 --- a/tests/CAPcore/test_DictLoggedDict.py +++ b/tests/CAPcore/test_DictLoggedDict.py @@ -1,5 +1,5 @@ import unittest -from time import struct_time +from datetime import datetime, timezone from src.CAPcore.DictLoggedDict import DictOfLoggedDict, DictData from src.CAPcore.LoggedDict import LoggedDict @@ -367,61 +367,67 @@ def test_lenX(self): self.assertEqual(rV2, 2) def test_show(self): - time0 = struct_time((2024, 12, 13, 23, 4, 24, 4, 348, 0)) - time1 = struct_time((2024, 12, 13, 23, 4, 34, 4, 348, 0)) - time2 = struct_time((2024, 12, 13, 23, 4, 44, 4, 348, 0)) + time0 = datetime(2024, 12, 13, 23, 4, 24, 4, tzinfo=timezone.utc) + time1 = datetime(2024, 12, 13, 23, 4, 34, 4, tzinfo=timezone.utc) + time2 = datetime(2024, 12, 13, 23, 4, 44, 4, tzinfo=timezone.utc) dAux1 = {'a1': 1, 'a2': 'ce'} - expStr0C = "{} [t:2024-12-13 23:04:24+0000 l:0]" - expStr1C = ("{'a':{'a1': 1 [t:2024-12-13 23:04:34+0000 l:1], 'a2': 'ce' [t:2024-12-13 23:04:34+0000 l:1]} (" - "t:2024-12-13 23:04:34+0000 l:2)} [t:2024-12-13 23:04:34+0000 l:1]") - expStr2C = ("{'a':{'a1': 1 [t:2024-12-13 23:04:34+0000 l:1], 'a2': 'ce' [t:2024-12-13 23:04:34+0000 l:1]} (" - "t:2024-12-13 23:04:34+0000 l:2), 'b1':{'a1': 1 [t:2024-12-13 23:04:34+0000 l:1], 'a2': 'ce' [" - "t:2024-12-13 23:04:34+0000 l:1]} (t:2024-12-13 23:04:34+0000 l:2)} [t:2024-12-13 23:04:34+0000 " - "l:1]") - expStr3C = ("{'a':{'a1': 1 [t:2024-12-13 23:04:34+0000 l:1], 'a2': 'ce' [t:2024-12-13 23:04:34+0000 l:1]} (" - "t:2024-12-13 23:04:34+0000 l:2), 'b1':{'a1': 1 [t:2024-12-13 23:04:34+0000 l:1], 'a2': 'ce' [" - "t:2024-12-13 23:04:34+0000 l:1]} (t:2024-12-13 23:04:34+0000 l:2), 'c12':{'a1': 1 [t:2024-12-13 " - "23:04:34+0000 l:1], 'a2': 'ce' [t:2024-12-13 23:04:34+0000 l:1]} (t:2024-12-13 23:04:34+0000 " - "l:2)} [t:2024-12-13 23:04:34+0000 l:1]") - expStr4C = ("{'a':{'a1': 1 [t:2024-12-13 23:04:34+0000 l:1], 'a2': 'ce' [t:2024-12-13 23:04:34+0000 l:1]} (" - "t:2024-12-13 23:04:34+0000 l:2), 'b1':{'a1': 1 [t:2024-12-13 23:04:34+0000 l:1], 'a2': 'ce' [" - "t:2024-12-13 23:04:34+0000 l:1]} (t:2024-12-13 23:04:34+0000 l:2), 'c12':{'a1': 1 [t:2024-12-13 " - "23:04:34+0000 l:1], 'a2': 'ce' [t:2024-12-13 23:04:34+0000 l:1]} (t:2024-12-13 23:04:44+0000 D " - "l:3)} [t:2024-12-13 23:04:44+0000 l:2]") - - expStr1 = """{ 'a': { 'a1': 1 [t:2024-12-13 23:04:34+0000 l:1], - 'a2': 'ce' [t:2024-12-13 23:04:34+0000 l:1] - } (t:2024-12-13 23:04:34+0000 l:2) -} [t:2024-12-13 23:04:34+0000 l:1]""" - expStr2 = """{ 'a': { 'a1': 1 [t:2024-12-13 23:04:34+0000 l:1], - 'a2': 'ce' [t:2024-12-13 23:04:34+0000 l:1] - } (t:2024-12-13 23:04:34+0000 l:2), - 'b1': { 'a1': 1 [t:2024-12-13 23:04:34+0000 l:1], - 'a2': 'ce' [t:2024-12-13 23:04:34+0000 l:1] - } (t:2024-12-13 23:04:34+0000 l:2) -} [t:2024-12-13 23:04:34+0000 l:1]""" - expStr4 = """{ 'a': { 'a1': 1 [t:2024-12-13 23:04:34+0000 l:1], - 'a2': 'ce' [t:2024-12-13 23:04:34+0000 l:1] - } (t:2024-12-13 23:04:34+0000 l:2), - 'b1': { 'a1': 1 [t:2024-12-13 23:04:34+0000 l:1], - 'a2': 'ce' [t:2024-12-13 23:04:34+0000 l:1] - } (t:2024-12-13 23:04:34+0000 l:2), - 'c12': { 'a1': 1 [t:2024-12-13 23:04:34+0000 l:1], - 'a2': 'ce' [t:2024-12-13 23:04:34+0000 l:1] - } (t:2024-12-13 23:04:44+0000 D l:3) -} [t:2024-12-13 23:04:44+0000 l:2]""" - expStr4F = """{ 'a': { 'a1': 1 [t:2024-12-13 23:04:34+0000 l:1], - 'a2': 'ce' [t:2024-12-13 23:04:34+0000 l:1] - } (t:2024-12-13 23:04:34+0000 l:2), - 'b1': { 'a1': 1 [t:2024-12-13 23:04:34+0000 l:1], - 'a2': 'ce' [t:2024-12-13 23:04:34+0000 l:1] - } (t:2024-12-13 23:04:34+0000 l:2), - 'c12': { 'a1': 1 [t:2024-12-13 23:04:34+0000 l:1], - 'a2': 'ce' [t:2024-12-13 23:04:34+0000 l:1] - } (t:2024-12-13 23:04:44+0000 D l:3) -} [t:2024-12-13 23:04:44+0000 l:2]""" + expStr0C = "{} [t:2024-12-13 23:04:24.000004+0000 l:0]" + expStr1C = ( + "{'a':{'a1': 1 [t:2024-12-13 23:04:34.000004+0000 l:1], 'a2': 'ce' [t:2024-12-13 23:04:34.000004+0000 l:1]}" + " (t:2024-12-13 23:04:34.000004+0000 l:2)} [t:2024-12-13 23:04:34.000004+0000 l:1]") + expStr2C = ( + "{'a':{'a1': 1 [t:2024-12-13 23:04:34.000004+0000 l:1], 'a2': 'ce' [t:2024-12-13 23:04:34.000004+0000 l:1]}" + " (t:2024-12-13 23:04:34.000004+0000 l:2), 'b1':{'a1': 1 [t:2024-12-13 23:04:34.000004+0000 l:1], " + "'a2': 'ce' [t:2024-12-13 23:04:34.000004+0000 l:1]} (t:2024-12-13 23:04:34.000004+0000 l:2)} " + "[t:2024-12-13 23:04:34.000004+0000 l:1]") + expStr3C = ( + "{'a':{'a1': 1 [t:2024-12-13 23:04:34.000004+0000 l:1], 'a2': 'ce' [t:2024-12-13 23:04:34.000004+0000 l:1]}" + " (t:2024-12-13 23:04:34.000004+0000 l:2), 'b1':{'a1': 1 [t:2024-12-13 23:04:34.000004+0000 l:1]," + " 'a2': 'ce' [t:2024-12-13 23:04:34.000004+0000 l:1]} (t:2024-12-13 23:04:34.000004+0000 l:2)," + " 'c12':{'a1': 1 [t:2024-12-13 23:04:34.000004+0000 l:1]," + " 'a2': 'ce' [t:2024-12-13 23:04:34.000004+0000 l:1]} (t:2024-12-13 23:04:34.000004+0000 l:2)}" + " [t:2024-12-13 23:04:34.000004+0000 l:1]") + expStr4C = ( + "{'a':{'a1': 1 [t:2024-12-13 23:04:34.000004+0000 l:1], 'a2': 'ce' [t:2024-12-13 23:04:34.000004+0000 l:1]}" + " (t:2024-12-13 23:04:34.000004+0000 l:2), 'b1':{'a1': 1 [t:2024-12-13 23:04:34.000004+0000 l:1]," + " 'a2': 'ce' [t:2024-12-13 23:04:34.000004+0000 l:1]} (t:2024-12-13 23:04:34.000004+0000 l:2)," + " 'c12':{'a1': 1 [t:2024-12-13 23:04:34.000004+0000 l:1]," + " 'a2': 'ce' [t:2024-12-13 23:04:34.000004+0000 l:1]} (t:2024-12-13 23:04:44.000004+0000 D l:3)}" + " [t:2024-12-13 23:04:44.000004+0000 l:2]") + + expStr1 = """{ 'a': { 'a1': 1 [t:2024-12-13 23:04:34.000004+0000 l:1], + 'a2': 'ce' [t:2024-12-13 23:04:34.000004+0000 l:1] + } (t:2024-12-13 23:04:34.000004+0000 l:2) +} [t:2024-12-13 23:04:34.000004+0000 l:1]""" + expStr2 = """{ 'a': { 'a1': 1 [t:2024-12-13 23:04:34.000004+0000 l:1], + 'a2': 'ce' [t:2024-12-13 23:04:34.000004+0000 l:1] + } (t:2024-12-13 23:04:34.000004+0000 l:2), + 'b1': { 'a1': 1 [t:2024-12-13 23:04:34.000004+0000 l:1], + 'a2': 'ce' [t:2024-12-13 23:04:34.000004+0000 l:1] + } (t:2024-12-13 23:04:34.000004+0000 l:2) +} [t:2024-12-13 23:04:34.000004+0000 l:1]""" + expStr4 = """{ 'a': { 'a1': 1 [t:2024-12-13 23:04:34.000004+0000 l:1], + 'a2': 'ce' [t:2024-12-13 23:04:34.000004+0000 l:1] + } (t:2024-12-13 23:04:34.000004+0000 l:2), + 'b1': { 'a1': 1 [t:2024-12-13 23:04:34.000004+0000 l:1], + 'a2': 'ce' [t:2024-12-13 23:04:34.000004+0000 l:1] + } (t:2024-12-13 23:04:34.000004+0000 l:2), + 'c12': { 'a1': 1 [t:2024-12-13 23:04:34.000004+0000 l:1], + 'a2': 'ce' [t:2024-12-13 23:04:34.000004+0000 l:1] + } (t:2024-12-13 23:04:44.000004+0000 D l:3) +} [t:2024-12-13 23:04:44.000004+0000 l:2]""" + expStr4F = """{ 'a': { 'a1': 1 [t:2024-12-13 23:04:34.000004+0000 l:1], + 'a2': 'ce' [t:2024-12-13 23:04:34.000004+0000 l:1] + } (t:2024-12-13 23:04:34.000004+0000 l:2), + 'b1': { 'a1': 1 [t:2024-12-13 23:04:34.000004+0000 l:1], + 'a2': 'ce' [t:2024-12-13 23:04:34.000004+0000 l:1] + } (t:2024-12-13 23:04:34.000004+0000 l:2), + 'c12': { 'a1': 1 [t:2024-12-13 23:04:34.000004+0000 l:1], + 'a2': 'ce' [t:2024-12-13 23:04:34.000004+0000 l:1] + } (t:2024-12-13 23:04:44.000004+0000 D l:3) +} [t:2024-12-13 23:04:44.000004+0000 l:2]""" d0 = DictOfLoggedDict(timestamp=time0) d1 = DictOfLoggedDict(timestamp=time0) @@ -468,17 +474,19 @@ def test_show(self): self.assertEqual(expStr4F, expStr4) def test_diffShow(self): - time0 = struct_time((2024, 12, 13, 23, 4, 24, 4, 348, 0)) - time1 = struct_time((2024, 12, 13, 23, 4, 34, 4, 348, 0)) + time0 = datetime(2024, 12, 13, 23, 4, 24, 4, tzinfo=timezone.utc) + time1 = datetime(2024, 12, 13, 23, 4, 34, 4, tzinfo=timezone.utc) expRes0C = "" - expRes1C = (" 'a': D {'a1': 1 [t:2024-12-13 23:04:34+0000 l:1], 'a2': 'ce' [t:2024-12-13 23:04:34+0000 l:1]} (" - "t:2024-12-13 23:04:34+0000 l:2), 'c': C 'a1': C '1' -> '2', 'a2': D 'ce', 'd': A {'a1': 1, " - "'a2': 'ce'}") + expRes1C = ( + " 'a': D {'a1': 1 [t:2024-12-13 23:04:34.000004+0000 l:1], 'a2': 'ce'" + " [t:2024-12-13 23:04:34.000004+0000 l:1]} (t:2024-12-13 23:04:34.000004+0000 l:2)," + " 'c': C 'a1': C '1' -> '2', 'a2': D 'ce', 'd': A {'a1': 1, 'a2': 'ce'}") expRes0 = "" - expRes1 = (" 'a': D {'a1': 1 [t:2024-12-13 23:04:34+0000 l:1], 'a2': 'ce' [t:2024-12-13 23:04:34+0000 l:1]} (" - "t:2024-12-13 23:04:34+0000 l:2)\n 'c': C 'a1': C '1' -> '2', 'a2': D 'ce'\n 'd': A {'a1': 1, " - "'a2': 'ce'}") + expRes1 = ( + " 'a': D {'a1': 1 [t:2024-12-13 23:04:34.000004+0000 l:1], 'a2': 'ce'" + " [t:2024-12-13 23:04:34.000004+0000 l:1]} (t:2024-12-13 23:04:34.000004+0000 l:2)\n 'c': C 'a1': C '1'" + " -> '2', 'a2': D 'ce'\n 'd': A {'a1': 1, 'a2': 'ce'}") d1 = DictOfLoggedDict(timestamp=time0) diff --git a/tests/CAPcore/test_loggedDict.py b/tests/CAPcore/test_loggedDict.py index 540d233..43e6758 100644 --- a/tests/CAPcore/test_loggedDict.py +++ b/tests/CAPcore/test_loggedDict.py @@ -1,5 +1,5 @@ import unittest -from time import struct_time +from datetime import datetime, timezone from src.CAPcore.LoggedDict import LoggedDict from src.CAPcore.Misc import SetDiff @@ -438,13 +438,15 @@ def test_compare10(self): self.assertEqual(len(dif1), 2) def test_show1(self): - time1 = struct_time((2024, 12, 13, 23, 4, 34, 4, 348, 0)) - time2 = struct_time((2024, 12, 13, 23, 4, 44, 4, 348, 0)) + time1 = datetime(2024, 12, 13, 23, 4, 34, 4, tzinfo=timezone.utc) + time2 = datetime(2024, 12, 13, 23, 4, 44, 4, tzinfo=timezone.utc) di1 = {'a': 1, 'b': 2} - expStr1 = "{'a': None [t:2024-12-13 23:04:44+0000 D l:2], 'b': 2 [t:2024-12-13 23:04:34+0000 l:1]}" - expStr2 = "{ 'a': None [t:2024-12-13 23:04:44+0000 D l:2],\n 'b': 2 [t:2024-12-13 23:04:34+0000 l:1]\n}" + expStr1 = ("{'a': None [t:2024-12-13 23:04:44.000004+0000 D l:2], " + "'b': 2 [t:2024-12-13 23:04:34.000004+0000 l:1]}") + expStr2 = ("{ 'a': None [t:2024-12-13 23:04:44.000004+0000 D l:2],\n " + "'b': 2 [t:2024-12-13 23:04:34.000004+0000 l:1]\n}") d1 = LoggedDict() d1.update(di1, timestamp=time1) @@ -461,17 +463,20 @@ def test_show2(self): self.assertEqual(repr(d1), expStr1) def test_show3(self): - time1 = struct_time((2024, 12, 13, 23, 4, 34, 4, 348, 0)) - time2 = struct_time((2024, 12, 13, 23, 4, 44, 4, 348, 0)) - time3 = struct_time((2024, 12, 13, 23, 4, 54, 4, 348, 0)) - time4 = struct_time((2024, 12, 13, 23, 4, 58, 4, 348, 0)) - - expStr1 = "{'b': 2 [t:2024-12-13 23:04:44+0000 l:1]}" - expStr2 = "{'b': 2 [t:2024-12-13 23:04:44+0000 l:1]}" - expStr3 = "{'b': 2 [t:2024-12-13 23:04:44+0000 l:1], 'c': 3 [t:2024-12-13 23:04:54+0000 l:1]}" - expStr4 = "{ 'b': 2 [t:2024-12-13 23:04:44+0000 l:1],\n 'c': 3 [t:2024-12-13 23:04:54+0000 l:1]\n}" - expStr5 = "{'b': None [t:2024-12-13 23:04:58+0000 D l:2], 'c': 3 [t:2024-12-13 23:04:54+0000 l:1]}" - expStr6 = "{ 'b': None [t:2024-12-13 23:04:58+0000 D l:2],\n 'c': 3 [t:2024-12-13 23:04:54+0000 l:1]\n}" + time1 = datetime(2024, 12, 13, 23, 4, 34, 4, tzinfo=timezone.utc) + time2 = datetime(2024, 12, 13, 23, 4, 44, 4, tzinfo=timezone.utc) + time3 = datetime(2024, 12, 13, 23, 4, 54, 4, tzinfo=timezone.utc) + time4 = datetime(2024, 12, 13, 23, 4, 58, 4, tzinfo=timezone.utc) + + expStr1 = "{'b': 2 [t:2024-12-13 23:04:44.000004+0000 l:1]}" + expStr2 = "{'b': 2 [t:2024-12-13 23:04:44.000004+0000 l:1]}" + expStr3 = "{'b': 2 [t:2024-12-13 23:04:44.000004+0000 l:1], 'c': 3 [t:2024-12-13 23:04:54.000004+0000 l:1]}" + expStr4 = ("{ 'b': 2 [t:2024-12-13 23:04:44.000004+0000 l:1],\n " + "'c': 3 [t:2024-12-13 23:04:54.000004+0000 l:1]\n}") + expStr5 = ("{'b': None [t:2024-12-13 23:04:58.000004+0000 D l:2], " + "'c': 3 [t:2024-12-13 23:04:54.000004+0000 l:1]}") + expStr6 = ("{ 'b': None [t:2024-12-13 23:04:58.000004+0000 D l:2],\n " + "'c': 3 [t:2024-12-13 23:04:54.000004+0000 l:1]\n}") di1 = {'b': 2} di2 = {'c': 3} diff --git a/tests/CAPcore/test_loggedValue.py b/tests/CAPcore/test_loggedValue.py index 139b886..2ace023 100644 --- a/tests/CAPcore/test_loggedValue.py +++ b/tests/CAPcore/test_loggedValue.py @@ -1,5 +1,5 @@ import unittest -from time import struct_time +from datetime import datetime, timezone from src.CAPcore.LoggedValue import LoggedValue @@ -95,8 +95,8 @@ def test_set5(self): self.assertFalse(r1) def test_set6(self): - time1 = struct_time((2024, 12, 13, 23, 4, 44, 4, 348, 0)) - time2 = struct_time((2024, 12, 13, 23, 4, 34, 4, 348, 0)) + time1 = datetime(2024, 12, 13, 23, 4, 44, 4, tzinfo=timezone.utc) + time2 = datetime(2024, 12, 13, 23, 4, 34, 4, tzinfo=timezone.utc) v1 = LoggedValue(v=5, timestamp=time1) with self.assertRaises(ValueError): @@ -146,19 +146,19 @@ def test_eq8(self): self.assertNotEqual(v1, v2) def test_repr1(self): - time1 = struct_time((2024, 12, 13, 23, 4, 34, 4, 348, 0)) - time2 = struct_time((2024, 12, 13, 23, 4, 44, 4, 348, 0)) - time3 = struct_time((2024, 12, 13, 23, 4, 54, 4, 348, 0)) + time1 = datetime(2024, 12, 13, 23, 4, 34, 4, tzinfo=timezone.utc) + time2 = datetime(2024, 12, 13, 23, 4, 44, 4, tzinfo=timezone.utc) + time3 = datetime(2024, 12, 13, 23, 4, 54, 4, tzinfo=timezone.utc) v1 = LoggedValue(v=5, timestamp=time1) - self.assertEqual(repr(v1), '5 [t:2024-12-13 23:04:34+0000 l:1]') + self.assertEqual(repr(v1), '5 [t:2024-12-13 23:04:34.000004+0000 l:1]') v1.set(6, timestamp=time2) - self.assertEqual(repr(v1), '6 [t:2024-12-13 23:04:44+0000 l:2]') + self.assertEqual(repr(v1), '6 [t:2024-12-13 23:04:44.000004+0000 l:2]') v1.clear(timestamp=time3) - self.assertEqual(repr(v1), 'None [t:2024-12-13 23:04:54+0000 D l:3]') + self.assertEqual(repr(v1), 'None [t:2024-12-13 23:04:54.000004+0000 D l:3]') def test_repr2(self): - time1 = struct_time((2024, 12, 13, 23, 4, 34, 4, 348, 0)) + time1 = datetime(2024, 12, 13, 23, 4, 34, 4, tzinfo=timezone.utc) v1 = LoggedValue(timestamp=time1) - self.assertEqual(repr(v1), 'None [t:2024-12-13 23:04:34+0000 l:0]') + self.assertEqual(repr(v1), 'None [t:2024-12-13 23:04:34.000004+0000 l:0]') From 71253538bb70ce688204dc9a76f37525e1851997 Mon Sep 17 00:00:00 2001 From: "cap@CASA" Date: Wed, 31 Dec 2025 21:56:30 +0100 Subject: [PATCH 02/33] Brought new functions from ACB project --- src/CAPcore/Misc.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/CAPcore/Misc.py b/src/CAPcore/Misc.py index ef8a6cf..0e71e32 100644 --- a/src/CAPcore/Misc.py +++ b/src/CAPcore/Misc.py @@ -1,10 +1,11 @@ import re from collections import defaultdict from collections import namedtuple +from collections.abc import Hashable from datetime import datetime, timezone from pathlib import Path from types import NoneType -from typing import Callable, Dict, Iterable, Optional, Tuple, Set, Any, List +from typing import Callable, Dict, Iterable, Optional, Tuple, Set, Any, List, Sequence, Union from dateutil import tz @@ -314,3 +315,25 @@ def copyDictWithTranslation(source: Dict, translation: Optional[Dict] = None, ex result = {translation.get(k, k): v for k, v in source.items() if k not in excludes} return result + +def sortedByStringLength(data: Iterable[str], reverse: bool = False) -> Iterable[str]: + return sorted(data, key=lambda x: (len(x), x), reverse=reverse) + + +def createDictFromGenerator(keys: Sequence[Hashable], genFunc: Union[Any, Callable]) -> Dict: + """ + Creates a Dict with the result of a generator. + :param keys: + :param genFunc: + :return: + """ + + result = {k: genFunc() for k in keys} + + return result + + +def iterable2quotedString(data: Iterable[str], charQuote: str = "'", mergedStr: str = ", ") -> str: + result = mergedStr.join(f"{charQuote}{s}{charQuote}" for s in sorted(data)) + return result + From 0274179b705c7976c64a1af80fe097c744579059 Mon Sep 17 00:00:00 2001 From: "cap@CASA" Date: Thu, 1 Jan 2026 00:53:53 +0100 Subject: [PATCH 03/33] Version bump --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index fc1b61a..687fe05 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,13 +1,13 @@ #https://packaging.python.org/en/latest/tutorials/packaging-projects/ [project] name = "CAPcore-python" -version = "0.2.2" +version = "0.2.3" authors = [ { name="Example Author", email="author@example.com" }, ] description = "Support functions for CAP projects" readme = "README.md" -requires-python = ">=3.12" +requires-python = ">=3.14" dependencies = [ "beautifulsoup4>=4.13.4", "lxml>=6.0.2", From 229aa625d2505104e71536bc37f2f343fdf6c422 Mon Sep 17 00:00:00 2001 From: "cap@CASA" Date: Thu, 1 Jan 2026 00:59:31 +0100 Subject: [PATCH 04/33] src/CAPcore/LoggedValue.py: fixed location of Misc --- src/CAPcore/LoggedValue.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CAPcore/LoggedValue.py b/src/CAPcore/LoggedValue.py index 0ae5f7f..e0fc017 100644 --- a/src/CAPcore/LoggedValue.py +++ b/src/CAPcore/LoggedValue.py @@ -2,7 +2,7 @@ from pprint import pp from typing import Any, Optional -from src.CAPcore.Misc import getUTC +from .Misc import getUTC DATEFORMAT = "%Y-%m-%d %H:%M:%S.%f%z" From a3f264baf88e084ca463168833b0a22d04d69951 Mon Sep 17 00:00:00 2001 From: "cap@CASA" Date: Fri, 2 Jan 2026 09:35:51 +0100 Subject: [PATCH 05/33] Changed required python for CodeQL & 2 new functions --- pyproject.toml | 2 +- src/CAPcore/LoggedValue.py | 12 ++++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 687fe05..13e5125 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ authors = [ ] description = "Support functions for CAP projects" readme = "README.md" -requires-python = ">=3.14" +requires-python = ">=3.13" dependencies = [ "beautifulsoup4>=4.13.4", "lxml>=6.0.2", diff --git a/src/CAPcore/LoggedValue.py b/src/CAPcore/LoggedValue.py index e0fc017..12dfefc 100644 --- a/src/CAPcore/LoggedValue.py +++ b/src/CAPcore/LoggedValue.py @@ -1,5 +1,4 @@ from datetime import datetime -from pprint import pp from typing import Any, Optional from .Misc import getUTC @@ -19,7 +18,6 @@ def __init__(self, v=None, timestamp: Optional[datetime] = None): def set(self, v: Any, timestamp: Optional[datetime] = None, change: bool = False): result = change if self.deleted or (v != self.value): - pp(self.last_updated) changeTime = timestamp or getUTC() action = 'U' if self.deleted: @@ -70,3 +68,13 @@ def __eq__(self, other): if isinstance(other, self.__class__): return self.value == other.get() return self.value == other + + +def setNewValue(oldV: LoggedValue | Any, newVal: Any, timestamp: Optional[datetime] = None) -> Any: + newVal = oldV.set(newVal, timestamp=timestamp) if isinstance(oldV, LoggedValue) else newVal + return newVal + + +def extractValue(oldV) -> Any: + v = oldV.get() if isinstance(oldV, LoggedValue) else oldV + return v From 755092c2796f3f89a97cc7c8cd5c066db8de7ba5 Mon Sep 17 00:00:00 2001 From: "cap@CASA" Date: Sat, 3 Jan 2026 12:36:01 +0100 Subject: [PATCH 06/33] src/CAPcore/LoggedValue.py: fixed setNewValue --- src/CAPcore/LoggedValue.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/CAPcore/LoggedValue.py b/src/CAPcore/LoggedValue.py index 12dfefc..f860293 100644 --- a/src/CAPcore/LoggedValue.py +++ b/src/CAPcore/LoggedValue.py @@ -70,10 +70,12 @@ def __eq__(self, other): return self.value == other -def setNewValue(oldV: LoggedValue | Any, newVal: Any, timestamp: Optional[datetime] = None) -> Any: - newVal = oldV.set(newVal, timestamp=timestamp) if isinstance(oldV, LoggedValue) else newVal - return newVal - +def setNewValue(oldV: LoggedValue | Any, newVal:Any,timestamp:Optional[datetime]=None) -> Any: + result = newVal + if isinstance(oldV, LoggedValue): + oldV.set(newVal, timestamp=timestamp) + result=oldV + return result def extractValue(oldV) -> Any: v = oldV.get() if isinstance(oldV, LoggedValue) else oldV From c87b1c4ee4caf9c8d73de1837b4bdabe96a19e0e Mon Sep 17 00:00:00 2001 From: "cap@CASA" Date: Sat, 3 Jan 2026 13:34:27 +0100 Subject: [PATCH 07/33] Added LoggedClass --- src/CAPcore/LoggedClass.py | 58 ++++++++++++++++++++++++++++++++++++++ src/CAPcore/Web.py | 3 ++ 2 files changed, 61 insertions(+) create mode 100644 src/CAPcore/LoggedClass.py diff --git a/src/CAPcore/LoggedClass.py b/src/CAPcore/LoggedClass.py new file mode 100644 index 0000000..dbd2855 --- /dev/null +++ b/src/CAPcore/LoggedClass.py @@ -0,0 +1,58 @@ +from datetime import datetime +from typing import Optional, Dict, Tuple, Any + +from .Misc import getUTC +from .Web import sentinel + + +class LoggedClass: + def __init__(self, **kwargs): + timestamp = kwargs.get('timestamp', getUTC()) + + self.timestamp: Optional[datetime] = timestamp + self.changeLog: Dict[datetime, DataChanges] = {} + + def updateDataLog(self, changeInfo, timestamp=sentinel): + if timestamp is sentinel: + timestamp = getUTC() + + if self.timestamp > timestamp: + raise ValueError( + f"Trying top update in the past. Current: {self.timestamp.strftime()}. Parameter: {timestamp.strftime()}") + if changeInfo: + if timestamp not in self.changeLog: + self.changeLog[timestamp] = DataChanges() + self.changeLog[timestamp].update(timestamp=timestamp, changeInfo=changeInfo) + self.timestamp = timestamp + + +class DataChanges: + def __init__(self): + self.timestamp: Optional[datetime] = None + self.changeSet: Dict[str, Tuple[Any, Any]] = {} + + def update(self, timestamp: datetime, changeInfo: Dict[str, Tuple[Any, Any]]) -> bool: + changes: bool = False + + if self.timestamp and (self.timestamp != timestamp): + raise ValueError( + f"Updating a datachange set with a different timestamp. Current: {self.timestamp.strftime()}. New: {timestamp.strftime()}") + + for k, (vOld, vNew) in changeInfo.items(): + if k not in self.changeSet: + self.changeSet[k] = (vOld, vNew) + changes |= True + else: + (currOld, currNew) = self.changeSet[k] + if (currOld, currNew) == (vOld, vNew): + continue + if currNew != vOld: + raise ValueError( + f"Updating a datachange set. Breaking a transition. Key: '{k}'. Old: '{currNew}'. New: '{vOld}'") + self.changeSet[k] = (currOld, vNew) + changes |= True + + if changes: + self.timestamp = getUTC() if timestamp is None else timestamp + + return changes diff --git a/src/CAPcore/Web.py b/src/CAPcore/Web.py index 4f46772..74d4900 100644 --- a/src/CAPcore/Web.py +++ b/src/CAPcore/Web.py @@ -11,6 +11,9 @@ from .Misc import getUTC +# https://effbot.org/zone/default-values.htm#what-to-do-instead +sentinel = object() + # (connect timeout, read timeout) From https://requests.readthedocs.io/en/latest/api/#requests.request TIMEOUT = (180, 300) From c316ed7c5533303334ef613281717aa607fb0937 Mon Sep 17 00:00:00 2001 From: "cap@CASA" Date: Sat, 3 Jan 2026 13:38:56 +0100 Subject: [PATCH 08/33] Push version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 13e5125..b9968ff 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ #https://packaging.python.org/en/latest/tutorials/packaging-projects/ [project] name = "CAPcore-python" -version = "0.2.3" +version = "0.2.3.0" authors = [ { name="Example Author", email="author@example.com" }, ] From 79e7db2ba08e8bf8f9921f962bb13426f4e124e8 Mon Sep 17 00:00:00 2001 From: "cap@CASA" Date: Sun, 4 Jan 2026 10:03:49 +0100 Subject: [PATCH 09/33] Converted changelog to list instead of tuples --- src/CAPcore/LoggedClass.py | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/src/CAPcore/LoggedClass.py b/src/CAPcore/LoggedClass.py index dbd2855..40bc238 100644 --- a/src/CAPcore/LoggedClass.py +++ b/src/CAPcore/LoggedClass.py @@ -1,5 +1,5 @@ from datetime import datetime -from typing import Optional, Dict, Tuple, Any +from typing import Optional, Dict, Tuple, Any, List from .Misc import getUTC from .Web import sentinel @@ -18,7 +18,8 @@ def updateDataLog(self, changeInfo, timestamp=sentinel): if self.timestamp > timestamp: raise ValueError( - f"Trying top update in the past. Current: {self.timestamp.strftime()}. Parameter: {timestamp.strftime()}") + f"Trying top update in the past. Current: {self.timestamp.strftime()}. " + f"Parameter: {timestamp.strftime()}") if changeInfo: if timestamp not in self.changeLog: self.changeLog[timestamp] = DataChanges() @@ -29,27 +30,29 @@ def updateDataLog(self, changeInfo, timestamp=sentinel): class DataChanges: def __init__(self): self.timestamp: Optional[datetime] = None - self.changeSet: Dict[str, Tuple[Any, Any]] = {} + self.changeSet: Dict[str, List[Any]] = {} def update(self, timestamp: datetime, changeInfo: Dict[str, Tuple[Any, Any]]) -> bool: changes: bool = False if self.timestamp and (self.timestamp != timestamp): raise ValueError( - f"Updating a datachange set with a different timestamp. Current: {self.timestamp.strftime()}. New: {timestamp.strftime()}") + f"Updating a datachange set with a different timestamp. Current: {self.timestamp.strftime()}. " + f"New: {timestamp.strftime()}") for k, (vOld, vNew) in changeInfo.items(): if k not in self.changeSet: - self.changeSet[k] = (vOld, vNew) + self.changeSet[k] = [vOld, vNew] changes |= True else: - (currOld, currNew) = self.changeSet[k] - if (currOld, currNew) == (vOld, vNew): - continue - if currNew != vOld: + currLast = self.changeSet[k][-1] + if currLast != vOld: raise ValueError( - f"Updating a datachange set. Breaking a transition. Key: '{k}'. Old: '{currNew}'. New: '{vOld}'") - self.changeSet[k] = (currOld, vNew) + f"Updating a datachange set. Breaking a transition. Key: '{k}'. Old: '{currLast}'. " + f"New: '{vOld}'") + if currLast == vNew: + continue + self.changeSet[k].append(vNew) changes |= True if changes: From 9f378ab39fdab38e97d33e7080c019b7fa557c57 Mon Sep 17 00:00:00 2001 From: "cap@CASA" Date: Sun, 4 Jan 2026 10:19:14 +0100 Subject: [PATCH 10/33] src/CAPcore/LoggedClass.py: added class2dict --- src/CAPcore/LoggedClass.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/CAPcore/LoggedClass.py b/src/CAPcore/LoggedClass.py index 40bc238..ea53e58 100644 --- a/src/CAPcore/LoggedClass.py +++ b/src/CAPcore/LoggedClass.py @@ -1,5 +1,5 @@ from datetime import datetime -from typing import Optional, Dict, Tuple, Any, List +from typing import Optional, Dict, Tuple, Any, List, Callable from .Misc import getUTC from .Web import sentinel @@ -26,6 +26,16 @@ def updateDataLog(self, changeInfo, timestamp=sentinel): self.changeLog[timestamp].update(timestamp=timestamp, changeInfo=changeInfo) self.timestamp = timestamp + def class2dict(self, keyList: List[str], mapFunc: Optional[Callable] = None) -> Dict[str, Any]: + result: Dict = {} + for k in keyList: + if not hasattr(self, k): + continue + val = getattr(self, k) + result[k] = val if mapFunc is None else mapFunc(val) + + return result + class DataChanges: def __init__(self): From 0301a8568020a05fbe33de3f176844caf70f01b2 Mon Sep 17 00:00:00 2001 From: "cap@CASA" Date: Sun, 4 Jan 2026 20:28:28 +0100 Subject: [PATCH 11/33] LoggedClass.updateDataFields & diffDicts & param renaming --- pyproject.toml | 2 +- src/CAPcore/LoggedClass.py | 35 +++++++++++++++++++++++++++++++++++ src/CAPcore/LoggedValue.py | 13 +++++++------ 3 files changed, 43 insertions(+), 7 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index b9968ff..783454d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ #https://packaging.python.org/en/latest/tutorials/packaging-projects/ [project] name = "CAPcore-python" -version = "0.2.3.0" +version = "0.2.3.1" authors = [ { name="Example Author", email="author@example.com" }, ] diff --git a/src/CAPcore/LoggedClass.py b/src/CAPcore/LoggedClass.py index ea53e58..9bdd96b 100644 --- a/src/CAPcore/LoggedClass.py +++ b/src/CAPcore/LoggedClass.py @@ -1,6 +1,7 @@ from datetime import datetime from typing import Optional, Dict, Tuple, Any, List, Callable +from .LoggedValue import extractValue, setNewValue from .Misc import getUTC from .Web import sentinel @@ -36,6 +37,23 @@ def class2dict(self, keyList: List[str], mapFunc: Optional[Callable] = None) -> return result + def updateDataFields(self, excludes: Optional[List[str]] = sentinel, **kwargs) -> bool: + timestamp = kwargs['timestamp'] = kwargs.get('timestamp', getUTC()) + if excludes is sentinel: + excludes = set() + changes = False + + for k, newVal in kwargs.items(): + if k in excludes: + continue + if hasattr(self, k): + currVal = extractValue(getattr(self, k)) + if currVal != newVal: + setattr(self, k, setNewValue(currVal, newVal=newVal, timestamp=timestamp)) + changes |= True + + return changes + class DataChanges: def __init__(self): @@ -69,3 +87,20 @@ def update(self, timestamp: datetime, changeInfo: Dict[str, Tuple[Any, Any]]) -> self.timestamp = getUTC() if timestamp is None else timestamp return changes + + +def diffDicts(oldDict: Dict[str, Any], newDict: Dict[str, Any]) -> Dict[str, Tuple[Any, Any]]: + result = {} + + for k, oldV in oldDict.items(): + newV = newDict.get(k, None) + if newV == oldV: + continue + result[k] = (oldV, newV) + + for k, newV in newDict.items(): + if k in oldDict: + continue + result[k] = (None, newV) + + return result diff --git a/src/CAPcore/LoggedValue.py b/src/CAPcore/LoggedValue.py index f860293..9d76e8b 100644 --- a/src/CAPcore/LoggedValue.py +++ b/src/CAPcore/LoggedValue.py @@ -70,13 +70,14 @@ def __eq__(self, other): return self.value == other -def setNewValue(oldV: LoggedValue | Any, newVal:Any,timestamp:Optional[datetime]=None) -> Any: +def setNewValue(val: LoggedValue | Any, newVal: Any, timestamp: Optional[datetime] = None) -> Any: result = newVal - if isinstance(oldV, LoggedValue): - oldV.set(newVal, timestamp=timestamp) - result=oldV + if isinstance(val, LoggedValue): + val.set(newVal, timestamp=timestamp) + result = val return result -def extractValue(oldV) -> Any: - v = oldV.get() if isinstance(oldV, LoggedValue) else oldV + +def extractValue(val) -> Any: + v = val.get() if isinstance(val, LoggedValue) else val return v From 407890eba4acbf01772f3e6e9ffd320ba5d66c8a Mon Sep 17 00:00:00 2001 From: "cap@CASA" Date: Sun, 4 Jan 2026 21:15:21 +0100 Subject: [PATCH 12/33] LoggedClass.updateDataFields: ignore timestamp in kwargs --- pyproject.toml | 2 +- src/CAPcore/LoggedClass.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 783454d..d0c6874 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ #https://packaging.python.org/en/latest/tutorials/packaging-projects/ [project] name = "CAPcore-python" -version = "0.2.3.1" +version = "0.2.3.2" authors = [ { name="Example Author", email="author@example.com" }, ] diff --git a/src/CAPcore/LoggedClass.py b/src/CAPcore/LoggedClass.py index 9bdd96b..d965c7c 100644 --- a/src/CAPcore/LoggedClass.py +++ b/src/CAPcore/LoggedClass.py @@ -44,7 +44,7 @@ def updateDataFields(self, excludes: Optional[List[str]] = sentinel, **kwargs) - changes = False for k, newVal in kwargs.items(): - if k in excludes: + if k in excludes or k == 'timestamp': continue if hasattr(self, k): currVal = extractValue(getattr(self, k)) @@ -52,6 +52,9 @@ def updateDataFields(self, excludes: Optional[List[str]] = sentinel, **kwargs) - setattr(self, k, setNewValue(currVal, newVal=newVal, timestamp=timestamp)) changes |= True + if changes: + self.timestamp=timestamp + return changes From 9c72b4ed0e72f4ac74bafc2e629b1e6b4370e00e Mon Sep 17 00:00:00 2001 From: "cap@CASA" Date: Sun, 4 Jan 2026 21:15:51 +0100 Subject: [PATCH 13/33] LoggedClass.updateDataFields: ignore timestamp in kwargs 2 --- src/CAPcore/LoggedClass.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CAPcore/LoggedClass.py b/src/CAPcore/LoggedClass.py index d965c7c..0ff3f5d 100644 --- a/src/CAPcore/LoggedClass.py +++ b/src/CAPcore/LoggedClass.py @@ -53,7 +53,7 @@ def updateDataFields(self, excludes: Optional[List[str]] = sentinel, **kwargs) - changes |= True if changes: - self.timestamp=timestamp + self.timestamp = timestamp return changes From e6c6847c14abcd94d2e15dd3ff01a91c9b04ab54 Mon Sep 17 00:00:00 2001 From: "cap@CASA" Date: Tue, 6 Jan 2026 09:00:41 +0100 Subject: [PATCH 14/33] Created a LoggedClass with different storage [WIP] --- src/CAPcore/LoggedClass.py | 116 +++++++++++++++++++++++++------------ src/CAPcore/Misc.py | 10 ++-- 2 files changed, 83 insertions(+), 43 deletions(-) diff --git a/src/CAPcore/LoggedClass.py b/src/CAPcore/LoggedClass.py index 0ff3f5d..748d8ac 100644 --- a/src/CAPcore/LoggedClass.py +++ b/src/CAPcore/LoggedClass.py @@ -5,13 +5,80 @@ from .Misc import getUTC from .Web import sentinel +DATEFORMAT = "%Y-%m-%d %H:%M:%S.%f%z" + + +class DataChanges: + def __init__(self): + self.timestamp: Optional[datetime] = None + self.changeSet: Dict[str, List[Any]] = {} + + def update(self, timestamp: datetime, changeInfo: Dict[str, Any]) -> bool: + changes: bool = False + + if self.timestamp and (self.timestamp != timestamp): + raise ValueError( + f"Updating a datachange set with a different timestamp. Current: {self.timestamp.strftime(DATEFORMAT)}. " + f"New: {timestamp.strftime(DATEFORMAT)}") + + for k, vNew in changeInfo.items(): + if k not in self.changeSet: + self.changeSet[k] = vNew + changes |= True + else: + currLast = self.changeSet[k][-1] + if currLast == vNew: + continue + self.changeSet[k].append(vNew) + changes |= True + + if changes: + self.timestamp = getUTC() if timestamp is None else timestamp + + return changes + + +class DataChangesTuples(DataChanges): + def __init__(self): + super().__init__() + + def update(self, timestamp: datetime, changeInfo: Dict[str, Tuple[Any, Any]]) -> bool: + changes: bool = False + + if self.timestamp and (self.timestamp != timestamp): + raise ValueError( + f"Updating a datachange set with a different timestamp. Current: {self.timestamp.strftime(DATEFORMAT)}. " + f"New: {timestamp.strftime(DATEFORMAT)}") + + for k, (vOld, vNew) in changeInfo.items(): + if k not in self.changeSet: + self.changeSet[k] = [vOld, vNew] + changes |= True + else: + currLast = self.changeSet[k][-1] + if currLast != vOld: + raise ValueError( + f"Updating a datachange set. Breaking a transition. Key: '{k}'. Old: '{currLast}'. " + f"New: '{vOld}'") + if currLast == vNew: + continue + self.changeSet[k].append(vNew) + changes |= True + + if changes: + self.timestamp = getUTC() if timestamp is None else timestamp + + return changes + class LoggedClass: + changesClass = DataChangesTuples + def __init__(self, **kwargs): timestamp = kwargs.get('timestamp', getUTC()) self.timestamp: Optional[datetime] = timestamp - self.changeLog: Dict[datetime, DataChanges] = {} + self.changeLog: Dict[datetime, Any] = {} def updateDataLog(self, changeInfo, timestamp=sentinel): if timestamp is sentinel: @@ -19,11 +86,11 @@ def updateDataLog(self, changeInfo, timestamp=sentinel): if self.timestamp > timestamp: raise ValueError( - f"Trying top update in the past. Current: {self.timestamp.strftime()}. " - f"Parameter: {timestamp.strftime()}") + f"Trying top update in the past. Current: {self.timestamp.strftime(format=DATEFORMAT)}. " + f"Parameter: {timestamp.strftime(format=DATEFORMAT)}") if changeInfo: if timestamp not in self.changeLog: - self.changeLog[timestamp] = DataChanges() + self.changeLog[timestamp] = self.changesClass() self.changeLog[timestamp].update(timestamp=timestamp, changeInfo=changeInfo) self.timestamp = timestamp @@ -58,40 +125,6 @@ def updateDataFields(self, excludes: Optional[List[str]] = sentinel, **kwargs) - return changes -class DataChanges: - def __init__(self): - self.timestamp: Optional[datetime] = None - self.changeSet: Dict[str, List[Any]] = {} - - def update(self, timestamp: datetime, changeInfo: Dict[str, Tuple[Any, Any]]) -> bool: - changes: bool = False - - if self.timestamp and (self.timestamp != timestamp): - raise ValueError( - f"Updating a datachange set with a different timestamp. Current: {self.timestamp.strftime()}. " - f"New: {timestamp.strftime()}") - - for k, (vOld, vNew) in changeInfo.items(): - if k not in self.changeSet: - self.changeSet[k] = [vOld, vNew] - changes |= True - else: - currLast = self.changeSet[k][-1] - if currLast != vOld: - raise ValueError( - f"Updating a datachange set. Breaking a transition. Key: '{k}'. Old: '{currLast}'. " - f"New: '{vOld}'") - if currLast == vNew: - continue - self.changeSet[k].append(vNew) - changes |= True - - if changes: - self.timestamp = getUTC() if timestamp is None else timestamp - - return changes - - def diffDicts(oldDict: Dict[str, Any], newDict: Dict[str, Any]) -> Dict[str, Tuple[Any, Any]]: result = {} @@ -107,3 +140,10 @@ def diffDicts(oldDict: Dict[str, Any], newDict: Dict[str, Any]) -> Dict[str, Tup result[k] = (None, newV) return result + + +class LoggedClassRaw(LoggedClass): + changesClass = DataChanges + + def __init__(self): + super().__init__() diff --git a/src/CAPcore/Misc.py b/src/CAPcore/Misc.py index 0e71e32..f9ed670 100644 --- a/src/CAPcore/Misc.py +++ b/src/CAPcore/Misc.py @@ -117,7 +117,7 @@ def deepDict(dic, keys, tipoFinal): if len(keys) == 0: return dic if keys[0] not in dic and len(keys) == 1: - dic[keys[0]] = (tipoFinal)() + dic[keys[0]] = tipoFinal() return deepDict(dic.setdefault(keys[0], {}), keys[1:], tipoFinal) @@ -132,7 +132,7 @@ def generaDefaultDict(listaClaves, tipoFinal): def actGenera(objLen, tipo): if objLen == 1: - return defaultdict((tipo)) + return defaultdict(tipo) return defaultdict(lambda: actGenera(objLen - 1, tipo)) @@ -183,7 +183,7 @@ def onlySetElement(myset): :param myset: a set :return: """ - return (list(myset.copy())[0] if isinstance(myset, (set, list)) and len(myset) == 1 else myset) + return list(myset.copy())[0] if isinstance(myset, (set, list)) and len(myset) == 1 else myset def cosaCorta(c1, c2): @@ -292,7 +292,7 @@ def cmp(a, b): Compares two values that can be compared (< and > must work) :param a: :param b: - :return: 1 if a is bigger thanb, 0 if they are equal, -1 if b is bigger than a + :return: 1 if 'a' is bigger thanb, 0 if they are equal, -1 if 'b' is bigger than 'a' From https://docs.python.org/3.0/whatsnew/3.0.html#ordering-comparisons """ @@ -316,6 +316,7 @@ def copyDictWithTranslation(source: Dict, translation: Optional[Dict] = None, ex result = {translation.get(k, k): v for k, v in source.items() if k not in excludes} return result + def sortedByStringLength(data: Iterable[str], reverse: bool = False) -> Iterable[str]: return sorted(data, key=lambda x: (len(x), x), reverse=reverse) @@ -336,4 +337,3 @@ def createDictFromGenerator(keys: Sequence[Hashable], genFunc: Union[Any, Callab def iterable2quotedString(data: Iterable[str], charQuote: str = "'", mergedStr: str = ", ") -> str: result = mergedStr.join(f"{charQuote}{s}{charQuote}" for s in sorted(data)) return result - From a8619359c6238184fce76914e6468c8d9b40a700 Mon Sep 17 00:00:00 2001 From: "cap@CASA" Date: Tue, 6 Jan 2026 09:02:41 +0100 Subject: [PATCH 15/33] Created a LoggedClass with different storage [WIP] 2 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index d0c6874..48c8cc2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ #https://packaging.python.org/en/latest/tutorials/packaging-projects/ [project] name = "CAPcore-python" -version = "0.2.3.2" +version = "0.2.3.3" authors = [ { name="Example Author", email="author@example.com" }, ] From 5bfe3efdc1e084c07c151a9811af2ef7d7d2e332 Mon Sep 17 00:00:00 2001 From: "cap@CASA" Date: Tue, 6 Jan 2026 09:27:33 +0100 Subject: [PATCH 16/33] Created a LoggedClass with different storage [WIP] 3 --- pyproject.toml | 2 +- src/CAPcore/DataChangeLogger.py | 69 ++++++++++++++++++++++++++++++ src/CAPcore/LoggedClass.py | 74 +++------------------------------ 3 files changed, 75 insertions(+), 70 deletions(-) create mode 100644 src/CAPcore/DataChangeLogger.py diff --git a/pyproject.toml b/pyproject.toml index 48c8cc2..48d407f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ #https://packaging.python.org/en/latest/tutorials/packaging-projects/ [project] name = "CAPcore-python" -version = "0.2.3.3" +version = "0.2.3.4" authors = [ { name="Example Author", email="author@example.com" }, ] diff --git a/src/CAPcore/DataChangeLogger.py b/src/CAPcore/DataChangeLogger.py new file mode 100644 index 0000000..fd1ae13 --- /dev/null +++ b/src/CAPcore/DataChangeLogger.py @@ -0,0 +1,69 @@ +from datetime import datetime +from typing import Optional, Dict, List, Any, Tuple + +from src.CAPcore.Misc import getUTC + +DATEFORMAT = "%Y-%m-%d %H:%M:%S.%f%z" + + +class DataChangesRaw: + def __init__(self): + self.timestamp: Optional[datetime] = None + self.changeSet: Dict[str, List[Any]] = {} + + def update(self, timestamp: datetime, changeInfo: Dict[str, Any]) -> bool: + changes: bool = False + + if self.timestamp and (self.timestamp != timestamp): + raise ValueError( + f"Updating a datachange set with a different timestamp. Current: {self.timestamp.strftime(DATEFORMAT)}. " + f"New: {timestamp.strftime(DATEFORMAT)}") + + for k, vNew in changeInfo.items(): + if k not in self.changeSet: + self.changeSet[k] = vNew + changes |= True + else: + currLast = self.changeSet[k][-1] + if currLast == vNew: + continue + self.changeSet[k].append(vNew) + changes |= True + + if changes: + self.timestamp = getUTC() if timestamp is None else timestamp + + return changes + + +class DataChangesTuples(DataChangesRaw): + def __init__(self): + super().__init__() + + def update(self, timestamp: datetime, changeInfo: Dict[str, Tuple[Any, Any]]) -> bool: + changes: bool = False + + if self.timestamp and (self.timestamp != timestamp): + raise ValueError( + f"Updating a datachange set with a different timestamp. Current: {self.timestamp.strftime(DATEFORMAT)}. " + f"New: {timestamp.strftime(DATEFORMAT)}") + + for k, (vOld, vNew) in changeInfo.items(): + if k not in self.changeSet: + self.changeSet[k] = [vOld, vNew] + changes |= True + else: + currLast = self.changeSet[k][-1] + if currLast != vOld: + raise ValueError( + f"Updating a datachange set. Breaking a transition. Key: '{k}'. Old: '{currLast}'. " + f"New: '{vOld}'") + if currLast == vNew: + continue + self.changeSet[k].append(vNew) + changes |= True + + if changes: + self.timestamp = getUTC() if timestamp is None else timestamp + + return changes diff --git a/src/CAPcore/LoggedClass.py b/src/CAPcore/LoggedClass.py index 748d8ac..3c8e435 100644 --- a/src/CAPcore/LoggedClass.py +++ b/src/CAPcore/LoggedClass.py @@ -1,75 +1,11 @@ from datetime import datetime from typing import Optional, Dict, Tuple, Any, List, Callable +from .DataChangeLogger import DATEFORMAT, DataChangesRaw, DataChangesTuples from .LoggedValue import extractValue, setNewValue from .Misc import getUTC from .Web import sentinel -DATEFORMAT = "%Y-%m-%d %H:%M:%S.%f%z" - - -class DataChanges: - def __init__(self): - self.timestamp: Optional[datetime] = None - self.changeSet: Dict[str, List[Any]] = {} - - def update(self, timestamp: datetime, changeInfo: Dict[str, Any]) -> bool: - changes: bool = False - - if self.timestamp and (self.timestamp != timestamp): - raise ValueError( - f"Updating a datachange set with a different timestamp. Current: {self.timestamp.strftime(DATEFORMAT)}. " - f"New: {timestamp.strftime(DATEFORMAT)}") - - for k, vNew in changeInfo.items(): - if k not in self.changeSet: - self.changeSet[k] = vNew - changes |= True - else: - currLast = self.changeSet[k][-1] - if currLast == vNew: - continue - self.changeSet[k].append(vNew) - changes |= True - - if changes: - self.timestamp = getUTC() if timestamp is None else timestamp - - return changes - - -class DataChangesTuples(DataChanges): - def __init__(self): - super().__init__() - - def update(self, timestamp: datetime, changeInfo: Dict[str, Tuple[Any, Any]]) -> bool: - changes: bool = False - - if self.timestamp and (self.timestamp != timestamp): - raise ValueError( - f"Updating a datachange set with a different timestamp. Current: {self.timestamp.strftime(DATEFORMAT)}. " - f"New: {timestamp.strftime(DATEFORMAT)}") - - for k, (vOld, vNew) in changeInfo.items(): - if k not in self.changeSet: - self.changeSet[k] = [vOld, vNew] - changes |= True - else: - currLast = self.changeSet[k][-1] - if currLast != vOld: - raise ValueError( - f"Updating a datachange set. Breaking a transition. Key: '{k}'. Old: '{currLast}'. " - f"New: '{vOld}'") - if currLast == vNew: - continue - self.changeSet[k].append(vNew) - changes |= True - - if changes: - self.timestamp = getUTC() if timestamp is None else timestamp - - return changes - class LoggedClass: changesClass = DataChangesTuples @@ -142,8 +78,8 @@ def diffDicts(oldDict: Dict[str, Any], newDict: Dict[str, Any]) -> Dict[str, Tup return result -class LoggedClassRaw(LoggedClass): - changesClass = DataChanges +def LoggedClassGenerator(dataChangeLogger=DataChangesTuples): - def __init__(self): - super().__init__() + result=LoggedClass + result.changesClass=dataChangeLogger + return result From 1879d16bafdf76035fa5141601f93cf8d4ee7489 Mon Sep 17 00:00:00 2001 From: "cap@CASA" Date: Tue, 6 Jan 2026 10:35:40 +0100 Subject: [PATCH 17/33] Created a LoggedClass with different storage [WIP] 4 --- pyproject.toml | 2 +- src/CAPcore/DataChangeLogger.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 48d407f..9af63ac 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ #https://packaging.python.org/en/latest/tutorials/packaging-projects/ [project] name = "CAPcore-python" -version = "0.2.3.4" +version = "0.2.3.5" authors = [ { name="Example Author", email="author@example.com" }, ] diff --git a/src/CAPcore/DataChangeLogger.py b/src/CAPcore/DataChangeLogger.py index fd1ae13..58f9a11 100644 --- a/src/CAPcore/DataChangeLogger.py +++ b/src/CAPcore/DataChangeLogger.py @@ -1,7 +1,7 @@ from datetime import datetime from typing import Optional, Dict, List, Any, Tuple -from src.CAPcore.Misc import getUTC +from .Misc import getUTC DATEFORMAT = "%Y-%m-%d %H:%M:%S.%f%z" From 29cbf4f4541f4f2734012fd62a7fb159cac823a8 Mon Sep 17 00:00:00 2001 From: "cap@CASA" Date: Wed, 7 Jan 2026 06:55:51 +0100 Subject: [PATCH 18/33] Extend LoggedClass to accept LoggedDict* --- pyproject.toml | 2 +- src/CAPcore/LoggedClass.py | 13 ++++++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 9af63ac..716cd26 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ #https://packaging.python.org/en/latest/tutorials/packaging-projects/ [project] name = "CAPcore-python" -version = "0.2.3.5" +version = "0.2.3.6" authors = [ { name="Example Author", email="author@example.com" }, ] diff --git a/src/CAPcore/LoggedClass.py b/src/CAPcore/LoggedClass.py index 3c8e435..7ba29b5 100644 --- a/src/CAPcore/LoggedClass.py +++ b/src/CAPcore/LoggedClass.py @@ -1,7 +1,9 @@ from datetime import datetime from typing import Optional, Dict, Tuple, Any, List, Callable -from .DataChangeLogger import DATEFORMAT, DataChangesRaw, DataChangesTuples +from .DataChangeLogger import DATEFORMAT, DataChangesTuples +from .DictLoggedDict import DictOfLoggedDict +from .LoggedDict import LoggedDict from .LoggedValue import extractValue, setNewValue from .Misc import getUTC from .Web import sentinel @@ -51,7 +53,9 @@ def updateDataFields(self, excludes: Optional[List[str]] = sentinel, **kwargs) - continue if hasattr(self, k): currVal = extractValue(getattr(self, k)) - if currVal != newVal: + if isinstance(currVal, (DictOfLoggedDict, LoggedDict)): + changes |= currVal.update(newVal, timestamp=timestamp) + elif currVal != newVal: setattr(self, k, setNewValue(currVal, newVal=newVal, timestamp=timestamp)) changes |= True @@ -79,7 +83,6 @@ def diffDicts(oldDict: Dict[str, Any], newDict: Dict[str, Any]) -> Dict[str, Tup def LoggedClassGenerator(dataChangeLogger=DataChangesTuples): - - result=LoggedClass - result.changesClass=dataChangeLogger + result = LoggedClass + result.changesClass = dataChangeLogger return result From 545b4fe64256951894972b2e9650a8e875b36830 Mon Sep 17 00:00:00 2001 From: "cap@CASA" Date: Wed, 7 Jan 2026 07:37:55 +0100 Subject: [PATCH 19/33] src/CAPcore/LoggedClass.py: remove default from LoggedClassGenerator --- pyproject.toml | 2 +- src/CAPcore/LoggedClass.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 716cd26..33e8c88 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ #https://packaging.python.org/en/latest/tutorials/packaging-projects/ [project] name = "CAPcore-python" -version = "0.2.3.6" +version = "0.2.3.7" authors = [ { name="Example Author", email="author@example.com" }, ] diff --git a/src/CAPcore/LoggedClass.py b/src/CAPcore/LoggedClass.py index 7ba29b5..3713751 100644 --- a/src/CAPcore/LoggedClass.py +++ b/src/CAPcore/LoggedClass.py @@ -82,7 +82,7 @@ def diffDicts(oldDict: Dict[str, Any], newDict: Dict[str, Any]) -> Dict[str, Tup return result -def LoggedClassGenerator(dataChangeLogger=DataChangesTuples): +def LoggedClassGenerator(dataChangeLogger): result = LoggedClass result.changesClass = dataChangeLogger return result From 908cb193200685f5eb903741c4ceeb9f27245404 Mon Sep 17 00:00:00 2001 From: "cap@CASA" Date: Wed, 7 Jan 2026 07:40:44 +0100 Subject: [PATCH 20/33] src/CAPcore/LoggedClass.py: remove default from LoggedClassGenerator 2 --- pyproject.toml | 2 +- src/CAPcore/LoggedClass.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 33e8c88..2176317 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ #https://packaging.python.org/en/latest/tutorials/packaging-projects/ [project] name = "CAPcore-python" -version = "0.2.3.7" +version = "0.2.3.8" authors = [ { name="Example Author", email="author@example.com" }, ] diff --git a/src/CAPcore/LoggedClass.py b/src/CAPcore/LoggedClass.py index 3713751..6c2647c 100644 --- a/src/CAPcore/LoggedClass.py +++ b/src/CAPcore/LoggedClass.py @@ -10,7 +10,6 @@ class LoggedClass: - changesClass = DataChangesTuples def __init__(self, **kwargs): timestamp = kwargs.get('timestamp', getUTC()) From 2c6e98b73aa119e72641b416cd0bbe24e67c975f Mon Sep 17 00:00:00 2001 From: "cap@CASA" Date: Wed, 7 Jan 2026 07:44:45 +0100 Subject: [PATCH 21/33] src/CAPcore/LoggedClass.py: remove default from LoggedClassGenerator 3 --- pyproject.toml | 2 +- src/CAPcore/DataChangeLogger.py | 11 +++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 2176317..9af88f6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ #https://packaging.python.org/en/latest/tutorials/packaging-projects/ [project] name = "CAPcore-python" -version = "0.2.3.8" +version = "0.2.3.9" authors = [ { name="Example Author", email="author@example.com" }, ] diff --git a/src/CAPcore/DataChangeLogger.py b/src/CAPcore/DataChangeLogger.py index 58f9a11..1587fa4 100644 --- a/src/CAPcore/DataChangeLogger.py +++ b/src/CAPcore/DataChangeLogger.py @@ -5,8 +5,15 @@ DATEFORMAT = "%Y-%m-%d %H:%M:%S.%f%z" +class DataChanges: + def __init__(self): + self.timestamp: Optional[datetime] = None + self.changeSet: Dict[str, List[Any]] = {} + + def update(self, timestamp: datetime, changeInfo: Dict[str, Any]): + return NotImplementedError("You must use a derived class") -class DataChangesRaw: +class DataChangesRaw(DataChanges): def __init__(self): self.timestamp: Optional[datetime] = None self.changeSet: Dict[str, List[Any]] = {} @@ -36,7 +43,7 @@ def update(self, timestamp: datetime, changeInfo: Dict[str, Any]) -> bool: return changes -class DataChangesTuples(DataChangesRaw): +class DataChangesTuples(DataChanges): def __init__(self): super().__init__() From 612d257de1cca4e57db69f4b40e8311d930362be Mon Sep 17 00:00:00 2001 From: "cap@CASA" Date: Wed, 7 Jan 2026 07:58:30 +0100 Subject: [PATCH 22/33] src/CAPcore/LoggedClass.py: remove default from LoggedClassGenerator 4 --- pyproject.toml | 2 +- src/CAPcore/LoggedClass.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 9af88f6..5aff5a5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ #https://packaging.python.org/en/latest/tutorials/packaging-projects/ [project] name = "CAPcore-python" -version = "0.2.3.9" +version = "0.2.3.10" authors = [ { name="Example Author", email="author@example.com" }, ] diff --git a/src/CAPcore/LoggedClass.py b/src/CAPcore/LoggedClass.py index 6c2647c..8b0d3a0 100644 --- a/src/CAPcore/LoggedClass.py +++ b/src/CAPcore/LoggedClass.py @@ -82,6 +82,5 @@ def diffDicts(oldDict: Dict[str, Any], newDict: Dict[str, Any]) -> Dict[str, Tup def LoggedClassGenerator(dataChangeLogger): - result = LoggedClass - result.changesClass = dataChangeLogger + result = type(f"LoggedClass{dataChangeLogger.__name__}",(LoggedClass,), {'changesClass':dataChangeLogger}) return result From 4693f513de23bbb61931c5d2c81fd6a89a029f7f Mon Sep 17 00:00:00 2001 From: "cap@CASA" Date: Wed, 7 Jan 2026 08:31:36 +0100 Subject: [PATCH 23/33] src/CAPcore/DataChangeLogger.py: Fix DataChangesRaw.update --- pyproject.toml | 2 +- src/CAPcore/DataChangeLogger.py | 7 ++++--- src/CAPcore/LoggedClass.py | 5 +++-- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 5aff5a5..a755e8e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ #https://packaging.python.org/en/latest/tutorials/packaging-projects/ [project] name = "CAPcore-python" -version = "0.2.3.10" +version = "0.2.3.11" authors = [ { name="Example Author", email="author@example.com" }, ] diff --git a/src/CAPcore/DataChangeLogger.py b/src/CAPcore/DataChangeLogger.py index 1587fa4..12ebd90 100644 --- a/src/CAPcore/DataChangeLogger.py +++ b/src/CAPcore/DataChangeLogger.py @@ -5,6 +5,7 @@ DATEFORMAT = "%Y-%m-%d %H:%M:%S.%f%z" + class DataChanges: def __init__(self): self.timestamp: Optional[datetime] = None @@ -13,10 +14,10 @@ def __init__(self): def update(self, timestamp: datetime, changeInfo: Dict[str, Any]): return NotImplementedError("You must use a derived class") + class DataChangesRaw(DataChanges): def __init__(self): - self.timestamp: Optional[datetime] = None - self.changeSet: Dict[str, List[Any]] = {} + super().__init__() def update(self, timestamp: datetime, changeInfo: Dict[str, Any]) -> bool: changes: bool = False @@ -28,7 +29,7 @@ def update(self, timestamp: datetime, changeInfo: Dict[str, Any]) -> bool: for k, vNew in changeInfo.items(): if k not in self.changeSet: - self.changeSet[k] = vNew + self.changeSet[k] = [vNew] changes |= True else: currLast = self.changeSet[k][-1] diff --git a/src/CAPcore/LoggedClass.py b/src/CAPcore/LoggedClass.py index 8b0d3a0..e650ea6 100644 --- a/src/CAPcore/LoggedClass.py +++ b/src/CAPcore/LoggedClass.py @@ -1,7 +1,7 @@ from datetime import datetime from typing import Optional, Dict, Tuple, Any, List, Callable -from .DataChangeLogger import DATEFORMAT, DataChangesTuples +from .DataChangeLogger import DATEFORMAT from .DictLoggedDict import DictOfLoggedDict from .LoggedDict import LoggedDict from .LoggedValue import extractValue, setNewValue @@ -10,6 +10,7 @@ class LoggedClass: + changesClass = None def __init__(self, **kwargs): timestamp = kwargs.get('timestamp', getUTC()) @@ -82,5 +83,5 @@ def diffDicts(oldDict: Dict[str, Any], newDict: Dict[str, Any]) -> Dict[str, Tup def LoggedClassGenerator(dataChangeLogger): - result = type(f"LoggedClass{dataChangeLogger.__name__}",(LoggedClass,), {'changesClass':dataChangeLogger}) + result = type(f"LoggedClass{dataChangeLogger.__name__}", (LoggedClass,), {'changesClass': dataChangeLogger}) return result From 992ffe3f4cb5c182decbac4da688f1f3a2d781e5 Mon Sep 17 00:00:00 2001 From: "cap@CASA" Date: Tue, 13 Jan 2026 23:23:23 +0100 Subject: [PATCH 24/33] CAPcore/LoggedClass.py: added class2dictStr --- pyproject.toml | 2 +- src/CAPcore/LoggedClass.py | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index a755e8e..7bdab26 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ #https://packaging.python.org/en/latest/tutorials/packaging-projects/ [project] name = "CAPcore-python" -version = "0.2.3.11" +version = "0.2.3.12" authors = [ { name="Example Author", email="author@example.com" }, ] diff --git a/src/CAPcore/LoggedClass.py b/src/CAPcore/LoggedClass.py index e650ea6..1705abf 100644 --- a/src/CAPcore/LoggedClass.py +++ b/src/CAPcore/LoggedClass.py @@ -64,6 +64,24 @@ def updateDataFields(self, excludes: Optional[List[str]] = sentinel, **kwargs) - return changes + def class2dictStr(self, keyList: Optional[str] = None, + formatters: Optional[Dict[str, Callable[[Any], str]]] = None) -> Dict: + auxFormatters = formatters or {} + auxFormatters.update(self.funcsValClass2Str if hasattr(self, 'funcsValClass2Str') else {}) + auxFormatters.update(self.funcsValSubClass2Str if hasattr(self, 'funcsValSubClass2Str') else {}) + + keyList = keyList or [] + + aux: Dict[str, Any] = self.class2dict(keyList=keyList, mapFunc=extractValue) + + result = {k: {'value': v} for k, v in aux.items()} + for k, v in aux.items(): + result[k] = {'value': v} + reprFunc = auxFormatters.get(k, lambda v: f"'{v}'") + result[k]['repr'] = reprFunc(v) + + return result + def diffDicts(oldDict: Dict[str, Any], newDict: Dict[str, Any]) -> Dict[str, Tuple[Any, Any]]: result = {} From 70cbfd1c9ec8fd586ddacb82049ba00cbd745593 Mon Sep 17 00:00:00 2001 From: "cap@CASA" Date: Wed, 14 Jan 2026 20:16:55 +0100 Subject: [PATCH 25/33] Updated reqs --- pyproject.toml | 4 ++-- requirements.txt | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 7bdab26..cbf0323 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ #https://packaging.python.org/en/latest/tutorials/packaging-projects/ [project] name = "CAPcore-python" -version = "0.2.3.12" +version = "0.2.3.13" authors = [ { name="Example Author", email="author@example.com" }, ] @@ -15,7 +15,7 @@ dependencies = [ "MechanicalSoup>=1.4.0", "python-dateutil>=2.9.0.post0", "PyYAML>=6.0.2", - "urllib3>=2.6.2" + "urllib3>=2.6.3" ] classifiers = [ "License :: CAP license", diff --git a/requirements.txt b/requirements.txt index 7f2247d..1bbba91 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,11 +4,12 @@ lxml>=6.0.2 requests>=2.32.5 MechanicalSoup>=1.4.0 python-dateutil>=2.9.0.post0 -PyYAML>=6.0.2 +PyYAML>=6.0.3 setuptools>=80.9.0 build==1.4.0 ### -certifi>=2025.6.15 -charset-normalizer>=3.4.2 +certifi>=2026.1.4 +charset-normalizer>=3.4.4 urllib3>=2.6.2 +soupsieve>=2.8.1 From 02664bd90e1d058499365b42e055e38981fba38c Mon Sep 17 00:00:00 2001 From: "cap@CASA" Date: Sun, 18 Jan 2026 13:13:59 +0100 Subject: [PATCH 26/33] Support function splitCl2Str for .class2dictStr --- pyproject.toml | 2 +- src/CAPcore/LoggedClass.py | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index cbf0323..c8d3dd8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ #https://packaging.python.org/en/latest/tutorials/packaging-projects/ [project] name = "CAPcore-python" -version = "0.2.3.13" +version = "0.2.3.14" authors = [ { name="Example Author", email="author@example.com" }, ] diff --git a/src/CAPcore/LoggedClass.py b/src/CAPcore/LoggedClass.py index 1705abf..d3dd041 100644 --- a/src/CAPcore/LoggedClass.py +++ b/src/CAPcore/LoggedClass.py @@ -103,3 +103,21 @@ def diffDicts(oldDict: Dict[str, Any], newDict: Dict[str, Any]) -> Dict[str, Tup def LoggedClassGenerator(dataChangeLogger): result = type(f"LoggedClass{dataChangeLogger.__name__}", (LoggedClass,), {'changesClass': dataChangeLogger}) return result + + +def splitCl2Str(data: Dict) -> Tuple[Dict[str, Any], Dict[str, str]]: + """ + Splits result from .class2dictStr into one with values and one with reprs + :param data: result from .class2dictStr + :return: tuple with values and reprs + """ + values = {} + reprs = {} + + for k, v in data.items(): + val = v.get('value', None) + rep = v.get('repr', f"'{val}'") + values[k] = val + reprs[k] = rep + + return values, reprs From 08ee5d6f459f725d404abc6975380d284b3f90ee Mon Sep 17 00:00:00 2001 From: "cap@CASA" Date: Sat, 31 Jan 2026 11:43:35 +0100 Subject: [PATCH 27/33] Added merge to DataChangeLogger --- pyproject.toml | 2 +- src/CAPcore/DataChangeLogger.py | 139 ++++++++++++++++++++++++++++++-- 2 files changed, 132 insertions(+), 9 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index c8d3dd8..7ff6df6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ #https://packaging.python.org/en/latest/tutorials/packaging-projects/ [project] name = "CAPcore-python" -version = "0.2.3.14" +version = "0.2.3.15" authors = [ { name="Example Author", email="author@example.com" }, ] diff --git a/src/CAPcore/DataChangeLogger.py b/src/CAPcore/DataChangeLogger.py index 12ebd90..f42f360 100644 --- a/src/CAPcore/DataChangeLogger.py +++ b/src/CAPcore/DataChangeLogger.py @@ -1,6 +1,9 @@ from datetime import datetime +from pprint import pformat from typing import Optional, Dict, List, Any, Tuple +from .DictLoggedDict import DictOfLoggedDictDiff +from .LoggedDict import LoggedDictDiff from .Misc import getUTC DATEFORMAT = "%Y-%m-%d %H:%M:%S.%f%z" @@ -12,19 +15,24 @@ def __init__(self): self.changeSet: Dict[str, List[Any]] = {} def update(self, timestamp: datetime, changeInfo: Dict[str, Any]): - return NotImplementedError("You must use a derived class") + raise NotImplementedError("update: You must use a derived class") + def __lt__(self, other): + return self.timestamp < other.timestamp -class DataChangesRaw(DataChanges): - def __init__(self): - super().__init__() + @classmethod + def merge(cls, *kargs): + raise NotImplementedError("merge: You must use derived classes") + +class DataChangesRaw(DataChanges): def update(self, timestamp: datetime, changeInfo: Dict[str, Any]) -> bool: changes: bool = False if self.timestamp and (self.timestamp != timestamp): raise ValueError( - f"Updating a datachange set with a different timestamp. Current: {self.timestamp.strftime(DATEFORMAT)}. " + f"Updating a datachange set with a different timestamp. " + f"Current: {self.timestamp.strftime(DATEFORMAT)}. " f"New: {timestamp.strftime(DATEFORMAT)}") for k, vNew in changeInfo.items(): @@ -43,17 +51,48 @@ def update(self, timestamp: datetime, changeInfo: Dict[str, Any]) -> bool: return changes + @classmethod + def merge(cls, *kargs): + if not all(isinstance(k, DataChangesRaw) for k in kargs): + paramList = [type(v).__name__ for v in kargs] + message = (f"DataChanges.merge requires all parameters to be class {DataChangesRaw.__name__}. " + f"Provided: [{','.join(paramList)} ") + raise ValueError(message) + + result = genStoreDict() + + c: DataChangesRaw + for c in sorted(kargs): + if 'dicts' not in c.changeSet: + continue + result['timestamps'].append(c.timestamp) + for chgItem in c.changeSet.get('dicts', []): + key: str + val: Tuple[Any, DictOfLoggedDictDiff | LoggedDictDiff] + for key, val in chgItem.items(): + if isinstance(val, LoggedDictDiff): + result['values'][key] = MergeLoggedDictDiff(result['values'].get(key, genStoreDict()), val, + c.timestamp) + elif isinstance(val, DictOfLoggedDictDiff): + result['values'][key] = MergeDictLoggedDictDiff(result['values'].get(key, genStoreDict()), val, + c.timestamp) + else: + raise TypeError( + f"Don't know hot to handle {type(val).__name__}. " + f"Accepted types:DictOfLoggedDictDiff,LoggedDictDiff ") + + return result + class DataChangesTuples(DataChanges): - def __init__(self): - super().__init__() def update(self, timestamp: datetime, changeInfo: Dict[str, Tuple[Any, Any]]) -> bool: changes: bool = False if self.timestamp and (self.timestamp != timestamp): raise ValueError( - f"Updating a datachange set with a different timestamp. Current: {self.timestamp.strftime(DATEFORMAT)}. " + f"Updating a datachange set with a different timestamp. " + f"Current: {self.timestamp.strftime(DATEFORMAT)}. " f"New: {timestamp.strftime(DATEFORMAT)}") for k, (vOld, vNew) in changeInfo.items(): @@ -75,3 +114,87 @@ def update(self, timestamp: datetime, changeInfo: Dict[str, Tuple[Any, Any]]) -> self.timestamp = getUTC() if timestamp is None else timestamp return changes + + @classmethod + def merge(cls, *kargs): + if not all(isinstance(k, DataChangesTuples) for k in kargs): + paramList = [type(v).__name__ for v in kargs] + message = (f"DataChanges.merge requires all parameters to be class {DataChangesTuples.__name__}. " + f"Provided: [{','.join(paramList)} ") + raise ValueError(message) + + result = genStoreDict() + + c: DataChangesTuples + for c in sorted(kargs): + result['timestamps'].append(c.timestamp) + + k: str + v: Tuple[Any, Any] + for k, v in c.changeSet.items(): + result['values'][k] = updateDataSeq(currData=result['values'].get(k, genStoreValue()), newValues=v, + timestamp=c.timestamp) + + return result + + +def genStoreValue(): + return {'changeCounter': 0, 'values': [], 'timestamps': []} + + +def genStoreDict(): + return {'timestamps': [], 'values': {}} + + +def MergeLoggedDictDiff(mergedData: Dict, change2add: LoggedDictDiff, timestamp: datetime): + for k, v in change2add.added.items(): + mergedData[k] = updateDataSeq(currData=mergedData.get(k, genStoreValue()), newValues=(None, v), + timestamp=timestamp) + mergedData[k]['addedKey'] = True + + for k, vals in change2add.changed.items(): + mergedData[k] = updateDataSeq(currData=mergedData.get(k, genStoreValue()), newValues=vals, timestamp=timestamp) + + for k, v in change2add.removed.items(): + mergedData[k]['removedKey'] = True + print(f"TODO MergeLoggedDictDiff removed: {k} -> {pformat(v)}") + + return mergedData + + +def MergeDictLoggedDictDiff(mergedData: Dict, change2add: DictOfLoggedDictDiff, timestamp: datetime): + mergedData['timestamps'].append(timestamp) + + for k, v in change2add.added.items(): + mergedData['values'][k] = genStoreDict() + mergedData['values'][k]['timestamps'].append(timestamp) + for subk, subv in v.items(): + mergedData['values'][k]['values'][subk] = updateDataSeq( + currData=mergedData['values'][k]['values'].get(subk, genStoreValue()), newValues=(None, subv), + timestamp=timestamp) + mergedData['values'][k]['addedValue'] = True + for k, vals in change2add.changed.items(): + mergedData['values'][k]['timestamps'].append(timestamp) + mergedData['values'][k]['values'] = MergeLoggedDictDiff(mergedData=mergedData['values'][k]['values'], + change2add=vals, + timestamp=timestamp) + + for k, v in change2add.removed.items(): + print(f"TODO MergeDictLoggedDictDiff removed: {k} -> {pformat(v)}") + mergedData['values'][k]['removedValue'] = True + + return mergedData + + +def updateDataSeq(currData: Dict, newValues: Tuple[Any, Any], timestamp: datetime): + if currData['changeCounter'] == 0: + currData['values'].extend(list(newValues)) + else: + if currData['values'][-1] != newValues[0]: + raise ValueError( + f"DataChanges.merge: broken sequence for key: registering {newValues}. Sequence {currData['values']}") + currData['values'].append(newValues[1]) + currData['timestamps'].append(timestamp) + currData['changeCounter'] += 1 + + return currData From 6b328db3db24a94eb4c067ea06053b087ade7585 Mon Sep 17 00:00:00 2001 From: "cap@CASA" Date: Mon, 2 Feb 2026 22:48:52 +0100 Subject: [PATCH 28/33] Fixed MergeDictLoggedDictDiff --- pyproject.toml | 2 +- src/CAPcore/DataChangeLogger.py | 36 ++++++++++++++++++--------------- 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 7ff6df6..8162658 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ #https://packaging.python.org/en/latest/tutorials/packaging-projects/ [project] name = "CAPcore-python" -version = "0.2.3.15" +version = "0.2.3.16" authors = [ { name="Example Author", email="author@example.com" }, ] diff --git a/src/CAPcore/DataChangeLogger.py b/src/CAPcore/DataChangeLogger.py index f42f360..cf3ab81 100644 --- a/src/CAPcore/DataChangeLogger.py +++ b/src/CAPcore/DataChangeLogger.py @@ -1,5 +1,5 @@ from datetime import datetime -from pprint import pformat +from pprint import pp, pformat from typing import Optional, Dict, List, Any, Tuple from .DictLoggedDict import DictOfLoggedDictDiff @@ -53,10 +53,11 @@ def update(self, timestamp: datetime, changeInfo: Dict[str, Any]) -> bool: @classmethod def merge(cls, *kargs): - if not all(isinstance(k, DataChangesRaw) for k in kargs): + if not all(isinstance(k, cls) for k in kargs): paramList = [type(v).__name__ for v in kargs] - message = (f"DataChanges.merge requires all parameters to be class {DataChangesRaw.__name__}. " - f"Provided: [{','.join(paramList)} ") + statusList = [isinstance(v, cls) for v in kargs] + message = (f"{cls.__name__}.merge requires all parameters to be class {cls.__name__}. " + f"Provided: [{','.join(str(p) for p in zip(paramList, statusList))}]") raise ValueError(message) result = genStoreDict() @@ -74,6 +75,7 @@ def merge(cls, *kargs): result['values'][key] = MergeLoggedDictDiff(result['values'].get(key, genStoreDict()), val, c.timestamp) elif isinstance(val, DictOfLoggedDictDiff): + pp(result) result['values'][key] = MergeDictLoggedDictDiff(result['values'].get(key, genStoreDict()), val, c.timestamp) else: @@ -117,10 +119,11 @@ def update(self, timestamp: datetime, changeInfo: Dict[str, Tuple[Any, Any]]) -> @classmethod def merge(cls, *kargs): - if not all(isinstance(k, DataChangesTuples) for k in kargs): + if not all(isinstance(k, cls) for k in kargs): paramList = [type(v).__name__ for v in kargs] - message = (f"DataChanges.merge requires all parameters to be class {DataChangesTuples.__name__}. " - f"Provided: [{','.join(paramList)} ") + statusList = [isinstance(v, cls) for v in kargs] + message = (f"{cls.__name__}.merge requires all parameters to be class {cls.__name__}. " + f"Provided: [{','.join(str(p) for p in zip(paramList, statusList))}]") raise ValueError(message) result = genStoreDict() @@ -128,7 +131,6 @@ def merge(cls, *kargs): c: DataChangesTuples for c in sorted(kargs): result['timestamps'].append(c.timestamp) - k: str v: Tuple[Any, Any] for k, v in c.changeSet.items(): @@ -148,15 +150,17 @@ def genStoreDict(): def MergeLoggedDictDiff(mergedData: Dict, change2add: LoggedDictDiff, timestamp: datetime): for k, v in change2add.added.items(): - mergedData[k] = updateDataSeq(currData=mergedData.get(k, genStoreValue()), newValues=(None, v), - timestamp=timestamp) - mergedData[k]['addedKey'] = True + mergedData['values'][k] = updateDataSeq(currData=mergedData['values'].get(k, genStoreValue()), + newValues=(None, v), + timestamp=timestamp) + mergedData['values'][k]['addedKey'] = True for k, vals in change2add.changed.items(): - mergedData[k] = updateDataSeq(currData=mergedData.get(k, genStoreValue()), newValues=vals, timestamp=timestamp) + mergedData['values'][k] = updateDataSeq(currData=mergedData['values'].get(k, genStoreValue()), newValues=vals, + timestamp=timestamp) for k, v in change2add.removed.items(): - mergedData[k]['removedKey'] = True + mergedData['values'][k]['removedKey'] = True print(f"TODO MergeLoggedDictDiff removed: {k} -> {pformat(v)}") return mergedData @@ -175,9 +179,9 @@ def MergeDictLoggedDictDiff(mergedData: Dict, change2add: DictOfLoggedDictDiff, mergedData['values'][k]['addedValue'] = True for k, vals in change2add.changed.items(): mergedData['values'][k]['timestamps'].append(timestamp) - mergedData['values'][k]['values'] = MergeLoggedDictDiff(mergedData=mergedData['values'][k]['values'], - change2add=vals, - timestamp=timestamp) + mergedData['values'][k] = MergeLoggedDictDiff(mergedData=mergedData['values'][k], + change2add=vals, + timestamp=timestamp) for k, v in change2add.removed.items(): print(f"TODO MergeDictLoggedDictDiff removed: {k} -> {pformat(v)}") From ee21369ea5d18f8709f53f79a611df85e52da527 Mon Sep 17 00:00:00 2001 From: "cap@CASA" Date: Mon, 2 Feb 2026 23:01:23 +0100 Subject: [PATCH 29/33] Fixed MergeDictLoggedDictDiff 2 & updates --- pyproject.toml | 4 ++-- requirements.txt | 6 +++--- src/CAPcore/DataChangeLogger.py | 1 - 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 8162658..40ed644 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ #https://packaging.python.org/en/latest/tutorials/packaging-projects/ [project] name = "CAPcore-python" -version = "0.2.3.16" +version = "0.2.3.17" authors = [ { name="Example Author", email="author@example.com" }, ] @@ -9,7 +9,7 @@ description = "Support functions for CAP projects" readme = "README.md" requires-python = ">=3.13" dependencies = [ - "beautifulsoup4>=4.13.4", + "beautifulsoup4>=4.13.3", "lxml>=6.0.2", "requests>=2.32.5", "MechanicalSoup>=1.4.0", diff --git a/requirements.txt b/requirements.txt index 1bbba91..b4743db 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,11 +5,11 @@ requests>=2.32.5 MechanicalSoup>=1.4.0 python-dateutil>=2.9.0.post0 PyYAML>=6.0.3 -setuptools>=80.9.0 +setuptools>=80.10.2 build==1.4.0 ### certifi>=2026.1.4 charset-normalizer>=3.4.4 -urllib3>=2.6.2 -soupsieve>=2.8.1 +urllib3>=2.6.3 +soupsieve>=2.8.3 diff --git a/src/CAPcore/DataChangeLogger.py b/src/CAPcore/DataChangeLogger.py index cf3ab81..c7a88d5 100644 --- a/src/CAPcore/DataChangeLogger.py +++ b/src/CAPcore/DataChangeLogger.py @@ -75,7 +75,6 @@ def merge(cls, *kargs): result['values'][key] = MergeLoggedDictDiff(result['values'].get(key, genStoreDict()), val, c.timestamp) elif isinstance(val, DictOfLoggedDictDiff): - pp(result) result['values'][key] = MergeDictLoggedDictDiff(result['values'].get(key, genStoreDict()), val, c.timestamp) else: From 2d4cb2d12c3e5aa3b2f11c8864b36d50c34311bb Mon Sep 17 00:00:00 2001 From: "cap@CASA" Date: Mon, 2 Feb 2026 23:02:01 +0100 Subject: [PATCH 30/33] Fixed MergeDictLoggedDictDiff 3 --- src/CAPcore/DataChangeLogger.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CAPcore/DataChangeLogger.py b/src/CAPcore/DataChangeLogger.py index c7a88d5..83af4a0 100644 --- a/src/CAPcore/DataChangeLogger.py +++ b/src/CAPcore/DataChangeLogger.py @@ -1,5 +1,5 @@ from datetime import datetime -from pprint import pp, pformat +from pprint import pformat from typing import Optional, Dict, List, Any, Tuple from .DictLoggedDict import DictOfLoggedDictDiff From 092c2eb651efef30b8cd0c63d5caff908ebb9f95 Mon Sep 17 00:00:00 2001 From: "cap@CASA" Date: Tue, 3 Feb 2026 08:03:36 +0100 Subject: [PATCH 31/33] Fixed MergeDictLoggedDictDiff 4 --- pyproject.toml | 2 +- src/CAPcore/DataChangeLogger.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 40ed644..0ec48c6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ #https://packaging.python.org/en/latest/tutorials/packaging-projects/ [project] name = "CAPcore-python" -version = "0.2.3.17" +version = "0.2.3.18" authors = [ { name="Example Author", email="author@example.com" }, ] diff --git a/src/CAPcore/DataChangeLogger.py b/src/CAPcore/DataChangeLogger.py index 83af4a0..c5e2ca1 100644 --- a/src/CAPcore/DataChangeLogger.py +++ b/src/CAPcore/DataChangeLogger.py @@ -148,6 +148,7 @@ def genStoreDict(): def MergeLoggedDictDiff(mergedData: Dict, change2add: LoggedDictDiff, timestamp: datetime): + mergedData['timestamps'].append(timestamp) for k, v in change2add.added.items(): mergedData['values'][k] = updateDataSeq(currData=mergedData['values'].get(k, genStoreValue()), newValues=(None, v), @@ -177,7 +178,6 @@ def MergeDictLoggedDictDiff(mergedData: Dict, change2add: DictOfLoggedDictDiff, timestamp=timestamp) mergedData['values'][k]['addedValue'] = True for k, vals in change2add.changed.items(): - mergedData['values'][k]['timestamps'].append(timestamp) mergedData['values'][k] = MergeLoggedDictDiff(mergedData=mergedData['values'][k], change2add=vals, timestamp=timestamp) From aafceb22174917e70c1618d99cb4287756259f79 Mon Sep 17 00:00:00 2001 From: "cap@CASA" Date: Sun, 15 Feb 2026 10:37:42 +0100 Subject: [PATCH 32/33] Added presentation functions to LoggedClass --- pyproject.toml | 2 +- src/CAPcore/LoggedClass.py | 21 ++++++++++++++++++++- src/CAPcore/Misc.py | 8 ++++++-- 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 0ec48c6..3e96015 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ #https://packaging.python.org/en/latest/tutorials/packaging-projects/ [project] name = "CAPcore-python" -version = "0.2.3.18" +version = "0.2.3.19" authors = [ { name="Example Author", email="author@example.com" }, ] diff --git a/src/CAPcore/LoggedClass.py b/src/CAPcore/LoggedClass.py index d3dd041..c4cb0f5 100644 --- a/src/CAPcore/LoggedClass.py +++ b/src/CAPcore/LoggedClass.py @@ -1,11 +1,12 @@ from datetime import datetime from typing import Optional, Dict, Tuple, Any, List, Callable +from _collections import defaultdict from .DataChangeLogger import DATEFORMAT from .DictLoggedDict import DictOfLoggedDict from .LoggedDict import LoggedDict from .LoggedValue import extractValue, setNewValue -from .Misc import getUTC +from .Misc import getUTC, transDict from .Web import sentinel @@ -82,6 +83,24 @@ def class2dictStr(self, keyList: Optional[str] = None, return result + def getAttrFormatters(self, formatters: Optional[Dict[str, Callable[[Any], str]]] = None) -> Dict[str, + Callable[[Any], + str]]: + result = defaultdict(lambda: (lambda s: f"'{str(s)}'")) + result.update(self.funcsValClass2Str if hasattr(self, 'funcsValClass2Str') else {}) + result.update(self.funcsValSubClass2Str if hasattr(self, 'funcsValSubClass2Str') else {}) + result.update(formatters or {}) + + return result + + def getAttrNameTranslator(self, translations: Optional[Dict[str, str]] = None) -> Dict[str, str]: + result = transDict() + result.update(self.transValClass if hasattr(self, 'transValClass') else {}) + result.update(self.transValSubClass if hasattr(self, 'transValSubClass') else {}) + result.update(translations or {}) + + return result + def diffDicts(oldDict: Dict[str, Any], newDict: Dict[str, Any]) -> Dict[str, Tuple[Any, Any]]: result = {} diff --git a/src/CAPcore/Misc.py b/src/CAPcore/Misc.py index f9ed670..cc53a7f 100644 --- a/src/CAPcore/Misc.py +++ b/src/CAPcore/Misc.py @@ -1,6 +1,5 @@ import re -from collections import defaultdict -from collections import namedtuple +from collections import defaultdict, namedtuple from collections.abc import Hashable from datetime import datetime, timezone from pathlib import Path @@ -337,3 +336,8 @@ def createDictFromGenerator(keys: Sequence[Hashable], genFunc: Union[Any, Callab def iterable2quotedString(data: Iterable[str], charQuote: str = "'", mergedStr: str = ", ") -> str: result = mergedStr.join(f"{charQuote}{s}{charQuote}" for s in sorted(data)) return result + + +class transDict(dict): + def __getitem__(self, item): + return self.get(item, item) From ee7c20e424ee6da2d7c66ecf85958232c047c8e6 Mon Sep 17 00:00:00 2001 From: "cap@CASA" Date: Thu, 2 Apr 2026 11:07:08 +0200 Subject: [PATCH 33/33] Dev release --- pyproject.toml | 2 +- src/CAPcore/LoggedClass.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 3e96015..e079601 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ #https://packaging.python.org/en/latest/tutorials/packaging-projects/ [project] name = "CAPcore-python" -version = "0.2.3.19" +version = "0.2.3.20" authors = [ { name="Example Author", email="author@example.com" }, ] diff --git a/src/CAPcore/LoggedClass.py b/src/CAPcore/LoggedClass.py index c4cb0f5..4a080db 100644 --- a/src/CAPcore/LoggedClass.py +++ b/src/CAPcore/LoggedClass.py @@ -25,7 +25,7 @@ def updateDataLog(self, changeInfo, timestamp=sentinel): if self.timestamp > timestamp: raise ValueError( - f"Trying top update in the past. Current: {self.timestamp.strftime(format=DATEFORMAT)}. " + f"Trying to update in the past. Current: {self.timestamp.strftime(format=DATEFORMAT)}. " f"Parameter: {timestamp.strftime(format=DATEFORMAT)}") if changeInfo: if timestamp not in self.changeLog: