Changeset c296f76 in flexoentity


Ignore:
Timestamp:
12/03/25 16:24:38 (6 weeks ago)
Author:
Enrico Schwass <ennoausberlin@…>
Branches:
master
Children:
4459fa4
Parents:
4f91fed
Message:

add support for persistance backends

Files:
7 added
5 edited

Legend:

Unmodified
Added
Removed
  • flexoentity/__init__.py

    r4f91fed rc296f76  
    1414from .flexo_entity import FlexoEntity, EntityType, EntityState
    1515from .flexo_collection import FlexoCollection
     16from .typed_collection import TypedCollection
     17from .in_memory_backend import InMemoryBackend
     18from .composite_backend import CompositeBackend
     19from .entity_manager import EntityManager
     20from .sqlite_entity_backend import SQLiteEntityBackend
    1621from .domain import Domain
    1722from .domain_manager import DomainManager, DuplicateDomainError
     
    2631    "EntityType",
    2732    "Domain",
     33    "EntityManager",
     34    "SQLiteEntityBackend",
     35    "InMemoryBackend",
     36    "CompositeBackend",
    2837    "DomainManager",
    2938    "DuplicateDomainError",
    3039    "EntityState",
    3140    "FlexoCollection",
     41    "TypedCollection",
    3242    "FlexoSignature",
    3343    "EntityRegistry",
  • flexoentity/domain_manager.py

    r4f91fed rc296f76  
    1 # flexoentity/domain_manager.py
    2 
    31from __future__ import annotations
     2import json
    43
    54from typing import Iterable
     
    87from .domain import Domain
    98from .flexo_entity import EntityState
     9from .entity_manager import EntityManager
    1010
    1111
     
    2222
    2323
    24 class DomainManager:
     24class DomainManager(EntityManager):
    2525    """
    26     Manager for Domain entities, implemented using composition.
     26    Manager for Domain entities using the new backend architecture.
    2727
    28     - Uses FlexoCollection internally.
    29     - Domains are keyed by domain.domain_id (explicit override).
    30     - No inheritance from FlexoCollection.
    31     - Responsible ONLY for domain-specific rules.
     28    - Primary storage: backend.load_all() (InMemory, SQLite, JSON, ...)
     29    - Secondary index: domain_id → Domain
     30    - Domain-specific business rules included.
    3231    """
    3332
    34     def __init__(self, registry):
    35         # store is a general-purpose collection
    36         self._store = FlexoCollection()
     33    ENTITY_CLASS = Domain
     34
     35    def __init__(self, backend, registry=None):
     36        super().__init__(backend)
    3737        self._registry = registry
     38        self._index_by_domain_id = {}
    3839
    39     # ---------------------------------------------------------------
    40     # Core API
    41     # ---------------------------------------------------------------
     40        self._rebuild_index()
    4241
    43     def add(self, domain: Domain) -> None:
    44         """
    45         Add a domain using domain.domain_id as key.
    46         """
    47         if not isinstance(domain, Domain):
    48             raise TypeError(
    49                 f"DomainManager accepts only Domain instances, got {type(domain)}"
    50             )
    51         if domain.domain_id in self._store:
    52             raise DuplicateDomainError(f"Domain '{domain.domain_id} already exists.")
    53         self._store.add(domain, key=domain.domain_id)
     42    # ------------------------------------------------------------
     43    # Index rebuild
     44    # ------------------------------------------------------------
    5445
    55     def get(self, domain_id: str) -> Domain:
    56         """Retrieve a domain or raise DomainNotFoundError."""
    57         if isinstance(domain_id, Domain):
    58             raise TypeError("DomainManager.get() expects a domain_id string, not a Domain object.")
    59         result = self._store.get(domain_id)
    60         if result is None:
    61             raise DomainNotFoundError(f"Domain '{domain_id}' not found.")
    62         return result
     46    def _rebuild_index(self):
     47        """Rebuild domain_id lookup table from backend contents."""
     48        self._index_by_domain_id.clear()
     49        for dom in self.backend.load_all():
     50            self._index_by_domain_id[dom.domain_id] = dom
    6351
    64     def can_delete(self, domain_id):
     52    # ------------------------------------------------------------
     53    # Overrides for CRUD
     54    # ------------------------------------------------------------
     55
     56    def add(self, domain: Domain):
     57        # Domain-specific uniqueness rule
     58        if domain.domain_id in self._index_by_domain_id:
     59            raise DuplicateDomainError(f"Domain '{domain.domain_id}' already exists.")
     60
     61        super().add(domain)
     62        self._index_by_domain_id[domain.domain_id] = domain
     63
     64    def update(self, domain: Domain):
     65        super().update(domain)
     66        self._index_by_domain_id[domain.domain_id] = domain
     67
     68    def delete(self, flexo_id: str):
     69        dom = self.backend.load(flexo_id)
     70        if dom is None:
     71            return
     72
     73        domain_id = dom.domain_id
     74
     75        # Check domain deletion rules
     76        if not self.can_delete(domain_id):
     77            raise ValueError(f"Domain '{domain_id}' is still referenced.")
     78
     79        super().delete(flexo_id)
     80
     81        # Remove from index
     82        self._index_by_domain_id.pop(domain_id, None)
     83
     84    # ------------------------------------------------------------
     85    # Domain-specific lookup
     86    # ------------------------------------------------------------
     87
     88    def get_by_domain_id(self, domain_id: str) -> Domain:
     89        dom = self._index_by_domain_id.get(domain_id)
     90        if not dom:
     91            raise KeyError(f"Domain '{domain_id}' not found.")
     92        return dom
     93
     94    def find_by_domain_id(self, domain_id: str):
     95        return self._index_by_domain_id.get(domain_id)
     96
     97    def all_domain_ids(self):
     98        return list(self._index_by_domain_id.keys())
     99
     100    def all(self):
     101        return list(self._index_by_domain_id.values())
     102    # ------------------------------------------------------------
     103    # Domain deletion rules
     104    # ------------------------------------------------------------
     105
     106    def can_delete(self, domain_id: str) -> bool:
     107        if not self._registry:
     108            return True
    65109        return not any(self._registry.entities_by_domain(domain_id))
    66110
    67     def delete(self, domain_id):
    68         if not self.can_delete(domain_id):
    69             raise ValueError(f"Domain {domain_id} is still referenced.")
    70         self.remove(domain_id)
    71 
    72     def remove(self, domain_id: str) -> None:
    73         """Remove a domain (rarely used, mostly for tests)."""
    74         if domain_id not in self._store:
    75             raise DomainNotFoundError(domain_id)
    76         self._store.remove(domain_id)
    77 
    78     def all(self) -> list[Domain]:
    79         """List of all domains."""
    80         return self._store.entities()
    81 
    82     def all_domain_ids(self):
    83         return self._store.ids()
    84 
    85     def clear(self):
    86         self._store.clear()
    87 
    88     # ---------------------------------------------------------------
    89     # Lifecycle / Approval Helpers
    90     # ---------------------------------------------------------------
     111    # ------------------------------------------------------------
     112    # Domain lifecycle helpers
     113    # ------------------------------------------------------------
    91114
    92115    def ensure_approved(self, domain_id: str) -> None:
    93         """
    94         Used during entity.transition_to().
    95         Raises if the domain does not exist or is not APPROVED.
    96         """
    97         dom = self.get(domain_id)
     116        dom = self.get_by_domain_id(domain_id)
    98117
    99118        if dom.state != EntityState.APPROVED:
    100             raise InvalidDomainError(
     119            raise ValueError(
    101120                f"Domain '{domain_id}' must be APPROVED before "
    102                 f"entities in this domain may be promoted. "
     121                f"entities in this domain can be promoted. "
    103122                f"(Current state: {dom.state})"
    104123            )
    105124
    106     # ---------------------------------------------------------------
    107     # Representations
    108     # ---------------------------------------------------------------
     125    def clear(self):
     126        self.backend.clear()
    109127
    110     def __len__(self) -> int:
    111         return len(self._store)
     128    def __repr__(self):
     129        return f"<DomainManager domains={self.all_domain_ids()}>"
    112130
    113     def __contains__(self, domain_id: str) -> bool:
    114         return domain_id in self._store
    115 
    116     def __repr__(self) -> str:
    117         return f"<DomainManager domains={self._store.keys()}>"
  • flexoentity/flexo_collection.py

    r4f91fed rc296f76  
    1111- Backwards compatible API
    1212"""
    13 
     13from abc import abstractmethod
    1414from .flexo_entity import FlexoEntity
    1515from .id_factory import FlexOID
     
    115115    # ------------------------------------------------------------------
    116116
    117     def to_dict_list(self):
     117    def serialize(self):
    118118        """Serialize all entities to list of dicts."""
    119         return [e.to_dict() for e in self._items.values()]
     119        return [e.to_dict() for e in self.entities()]
    120120
    121121    @classmethod
    122     def from_dict_list(cls, data):
     122    @abstractmethod
     123    def deserialize(cls, data):
    123124        """
    124125        Deserialize a list of dicts into a FlexoCollection.
    125126
    126         Uses FlexoEntity.from_dict() to dispatch to the correct subclass.
     127        Uses from_dict() to dispatch to the correct subclass.
    127128        """
    128         c = cls()
    129         for d in data:
    130             entity = FlexoEntity.from_dict(d)
    131             c.add(entity)
    132         return c
     129        pass
    133130
    134131    # ------------------------------------------------------------------
  • tests/conftest.py

    r4f91fed rc296f76  
    33from pathlib import Path
    44from datetime import datetime
    5 from flexoentity import Domain, FlexoSignature, DomainManager, EntityRegistry
     5from flexoentity import Domain, FlexoSignature, DomainManager, EntityRegistry, CompositeBackend
    66from flexoentity import get_signing_backend, CertificateReference
    77
     
    3030@pytest.fixture
    3131def sample_domain_manager():
    32     return DomainManager(EntityRegistry())
     32    return DomainManager(CompositeBackend(Domain), EntityRegistry())
    3333
    3434# ─────────────────────────────────────────────────────────────
  • tests/test_domain.py

    r4f91fed rc296f76  
    2020    sample_domain_manager.add(sample_domain)
    2121    # Manager must know it now
    22     assert sample_domain_manager.get("PY_ARITHM") is sample_domain
     22    assert sample_domain_manager.get_by_domain_id("PY_ARITHM") is sample_domain
    2323
    2424
     
    3838def test_lookup_by_oid(sample_domain, sample_domain_manager):
    3939    sample_domain_manager.add(sample_domain)
    40     found = sample_domain_manager.get(sample_domain.domain_id)
     40    found = sample_domain_manager.get_by_domain_id(sample_domain.domain_id)
    4141    assert found is sample_domain
    42 
    4342
    4443# ---------------------------------------------------------------
Note: See TracChangeset for help on using the changeset viewer.