Changeset 59342ce in flexoentity
- Timestamp:
- 10/19/25 11:42:00 (3 months ago)
- Branches:
- master
- Children:
- 0b4a5e6
- Parents:
- 0036877
- Files:
-
- 3 added
- 3 edited
-
flexoentity/__init__.py (modified) (1 diff)
-
flexoentity/flexo_entity.py (modified) (10 diffs)
-
flexoentity/id_factory.py (modified) (3 diffs)
-
tests/test_id_lifecycle.py (added)
-
tests/test_id_stress.py (added)
-
tests/test_persistance_integrity.py (added)
Legend:
- Unmodified
- Added
- Removed
-
flexoentity/__init__.py
r0036877 r59342ce 6 6 - FlexoEntity: lifecycle-tracked base class for all Flex-O domain objects 7 7 """ 8 from importlib.metadata import version, PackageNotFoundError 9 from .id_factory import FlexOID, canonical_seed 10 from .flexo_entity import FlexoEntity, EntityType, EntityState 8 11 9 from .id_factory import FlexOID, canonical_seed 10 from .flexo_entity import FlexoEntity 12 __all__ = [ 13 "FlexOID", 14 "canonical_seed", 15 "FlexoEntity", 16 "EntityType", 17 "EntityState", 18 ] 11 19 12 __all__ = ["FlexOID", "canonical_seed", "FlexoEntity"] 13 __version__ = "1.0.0" 20 # Optional: keep dynamic version retrieval synced with pyproject.toml 21 try: 22 __version__ = version("flexoentity") 23 except PackageNotFoundError: 24 __version__ = "0.0.0-dev" -
flexoentity/flexo_entity.py
r0036877 r59342ce 9 9 from typing import Optional 10 10 from abc import ABC 11 12 from id_factory import FlexOID 13 11 import hashlib 12 13 14 from flexoentity.id_factory import FlexOID 15 from flexoentity import canonical_seed 16 14 17 15 18 # ────────────────────────────────────────────────────────────────────────────── … … 56 59 EntityState.APPROVED: "A", 57 60 EntityState.APPROVED_AND_SIGNED: "S", 61 EntityState.PUBLISHED: "P", 58 62 EntityState.OBSOLETE: "O", 63 59 64 } 60 65 return mapping[self] … … 69 74 "A": cls.APPROVED, 70 75 "S": cls.APPROVED_AND_SIGNED, 76 "P": cls.PUBLISHED, 71 77 "O": cls.OBSOLETE, 72 78 } … … 77 83 78 84 def __str__(self): 79 return self. value85 return self.name 80 86 81 87 … … 139 145 return cls.from_dict(data) 140 146 147 @staticmethod 148 def should_version(state) -> bool: 149 """ 150 Determine if a given lifecycle state should trigger a version increment. 151 152 Entities typically version when they move into more stable or 153 externally visible states, such as APPROVED, SIGNED, or PUBLISHED. 154 """ 155 156 return state in ( 157 EntityState.APPROVED, 158 EntityState.APPROVED_AND_SIGNED, 159 EntityState.PUBLISHED, 160 ) 161 141 162 # ─────────────────────────────────────────────────────────────── 142 163 def _update_fingerprint(self) -> bool: 143 164 """Recalculate fingerprint and return True if content changed.""" 144 165 # extract version from current flexo_id 145 new_oid = FlexOID.generate(self.domain, self.etype , self.text_seed, self.flexo_id.version)166 new_oid = FlexOID.generate(self.domain, self.etype.short(), self.state.short(), self.text_seed, self.flexo_id.version) 146 167 if new_oid.signature != self.flexo_id.signature: 147 168 self.flexo_id = new_oid … … 157 178 158 179 # Check if version should bump 159 if FlexOID.should_version(self.etype,target_state):160 if self._update_fingerprint():161 self.flexo_id = FlexOID.next_version(self.flexo_id)180 if self.should_version(target_state): 181 self._update_fingerprint() 182 self.flexo_id = FlexOID.next_version(self.flexo_id) 162 183 163 184 self.state = target_state … … 184 205 """ 185 206 Move from DRAFT to APPROVED state. 186 Draft entities receive a new permanent FlexOID .207 Draft entities receive a new permanent FlexOID with incremented version. 187 208 """ 188 209 if self.state == EntityState.DRAFT: 189 # Generate a brand new permanent ID210 new_version = self.flexo_id.version + 1 190 211 new_fid = FlexOID.generate( 191 212 self.domain, 192 self.etype, 213 self.etype.short(), 214 EntityState.APPROVED.short(), 193 215 self.text_seed, 194 draft=False216 version=new_version 195 217 ) 196 218 self.previous_id = self.flexo_id # optional: keep audit trail 197 219 self.flexo_id = new_fid 198 199 220 self.state = EntityState.APPROVED 200 221 self.updated_at = datetime.utcnow() … … 203 224 204 225 def sign(self): 205 # FIXME: We need to define clear conditions, when resigning is neccessary and allowed206 # if self.state == EntityState.APPROVED:207 # self._transition(EntityState.APPROVED_AND_SIGNED)208 # self.bump_version()209 226 self._transition(EntityState.APPROVED_AND_SIGNED) 210 self.bump_version()211 227 212 228 def publish(self): … … 220 236 def clone_new_base(self): 221 237 """Start new lineage when obsolete.""" 222 self.flexo_id = FlexOID.clone_new_base(self.domain, self.etype, self.text_seed) 238 self.flexo_id = FlexOID.clone_new_base( 239 self.domain, 240 self.etype.short(), 241 self.state.short(), 242 self.text_seed, 243 ) 223 244 self.state = EntityState.DRAFT 224 245 self.updated_at = datetime.utcnow() … … 235 256 self.updated_at = datetime.utcnow() 236 257 258 259 # ─────────────────────────────────────────────────────────────── 260 # Integrity verification 261 # ─────────────────────────────────────────────────────────────── 262 237 263 @staticmethod 238 264 def verify_integrity(entity) -> bool: 239 265 """ 240 Verify if the stored fingerprint matches recalculated fingerprint. 241 Returns True if intact, False if tampered or corrupted. 242 """ 243 recalculated = FlexOID.generate(entity.domain, entity.etype, entity.text_seed, entity.version) 244 return recalculated.signature == entity.flexo_id.signature 266 Verify *state-aware* integrity. 267 268 This method validates that the entity's stored digital signature 269 matches a freshly recalculated one, based on the combination of: 270 271 text_seed + current lifecycle state (one-letter code) 272 273 Returns 274 ------- 275 bool 276 True if the entity's *state and content* are unchanged, 277 False if either was altered or corrupted. 278 """ 279 seed = canonical_seed(f"{entity.text_seed}:{entity.state.short()}") 280 recalculated_sig = hashlib.blake2s( 281 seed.encode("utf-8"), digest_size=8 282 ).hexdigest().upper() 283 284 return recalculated_sig == entity.flexo_id.signature 285 286 @staticmethod 287 def verify_content_integrity(entity) -> bool: 288 """ 289 Verify *content-only* integrity (ignores lifecycle state). 290 291 This method checks whether the stored entity's signature matches 292 a fresh hash of its text seed alone. It does not include the 293 lifecycle state in the fingerprint. 294 295 Returns 296 ------- 297 bool 298 True if the text content has not been altered, 299 False if it differs from the original content. 300 """ 301 seed = canonical_seed(entity.text_seed) 302 recalculated_sig = hashlib.blake2s( 303 seed.encode("utf-8"), digest_size=8 304 ).hexdigest().upper() 305 return recalculated_sig == entity.flexo_id.signature 245 306 246 307 def allowed_transitions(self) -> list[str]: -
flexoentity/id_factory.py
r0036877 r59342ce 28 28 """ 29 29 if isinstance(obj, str): 30 text = " ".join(obj. lower().split())30 text = " ".join(obj.split()) 31 31 return text 32 32 if isinstance(obj, dict): … … 91 91 # ────────────────────────────────────────────────────────────────────────── 92 92 @staticmethod 93 def generate(domain: str, etype: str, estate: str, text: str, version: int = 1): 93 def generate(domain: str, etype: str, estate: str, text: str, 94 version: int = 1, enforce_unique = True): 94 95 """ 95 96 Generate a new, versioned, and state-aware Flex-O ID. … … 138 139 139 140 base_hash = FlexOID._blake_hash(seed) 140 unique_hash = FlexOID._ensure_unique(base_hash, version) 141 unique_hash = FlexOID._ensure_unique(base_hash, version)if enforce_unique else base_hash 141 142 142 143 ver_part = f"{version:03d}{estate}"
Note:
See TracChangeset
for help on using the changeset viewer.
