from __future__ import annotations from abc import abstractmethod from typing import List, Dict, Optional, TypeVar, Generic from geniusweb.actions.Action import Action from geniusweb.actions.PartyId import PartyId from geniusweb.progress.Progress import Progress from geniusweb.protocol.ProtocolException import ProtocolException from geniusweb.protocol.session.SessionSettings import SessionSettings from geniusweb.protocol.session.SessionState import SessionState from geniusweb.references.PartyWithProfile import PartyWithProfile from geniusweb.utils import toTuple, val S = TypeVar('S',bound=SessionSettings) P = TypeVar('P',bound="DefaultSessionState") class DefaultSessionState (SessionState, Generic[P,S]): ''' The default current state of the session. immutable. @param

the actual SessionState object @param the actual SessionSettings object ''' # error: Optional[ProtocolException] # progress: Progress # partyprofiles: Optional[Dict[PartyId, PartyWithProfile]] # settings: S def __init__(self, actions:List[Action], connections:List[PartyId], progress:Optional[Progress], settings:S, partyprofiles:Optional[Dict[PartyId, PartyWithProfile]]=None, error:Optional[ProtocolException]=None): ''' @param actions value for actions done so far. None equals to empty list @param conns the currently existing connections. Can be empty. If None it is assumed to be empty. Each connection represents another party. Normally there is exactly 1 connection for every party. The protocol should check this. @param progr the {@link Progress} that governs this session. Can be null if session did not yet start. @param settings the settings used for the session @param partyprofiles map with the {@link PartyWithProfile} for connected parties. None is equivalent to an empty map. @param e the exception that occured, usually None. All errors occuring due to faulty {@link Action}s translate to {@link ProtocolException}s. All errors in our own code are bugs (not ProtocolExceptions) and should result in a throw and terminate the session. ''' self._partyprofiles = dict(partyprofiles) if partyprofiles else {} self._connections = list(connections) if connections else [] self._actions = list(actions) if actions else [] if len(self._connections) != len(set(self._connections)): raise ValueError("There can not be multiple connections for a party:" + str(self._connections)) if not settings: raise ValueError("Settings must be not null"); self._progress = progress; self._settings = settings; self._error = error; @abstractmethod def With(self,actions1:List[Action] , conns:List[PartyId], progress1:Optional[Progress] , settings1:S, partyprofiles1:Dict[PartyId, PartyWithProfile] , e: Optional[ProtocolException]) -> P : ''' Construct a new session state, where the DefaultSessionState changes while the rest of the state remains unchanged. Notice the return type P. @param actions1 the new {@link Action}s @param conns the new connected {@link PartyId}s @param progress1 the new {@link Progress}, can be None still @param settings1 the new {@link SessionSettings}. Normally this is constant during a session. @param partyprofiles1 the new {@link PartyWithProfile}s for all parties. Normally this remains constant during a session. @param e an error that occured that caused the session to reach its terminated/final state. If an error does not cause termination, only a warning should be logged. @return the new state of the derived sessionstate. ''' def getConnections(self)->List[PartyId] : ''' @return existing connections. ''' return list(self._connections) def getPartyProfiles(self)->Dict[PartyId, PartyWithProfile] : ''' @return map with {@link PartyWithProfile} for the parties. May be an incomplete map, as more parties with their profiles may be set only later. ''' return dict(self._partyprofiles) def getActions(self)->List[Action] : ''' @return unmodifyable list of actions done so far. ''' return list(self._actions) def getProgress(self)->Optional[Progress] : return self._progress def getSettings(self)->S : return self._settings def isFinal(self,currentTimeMs:int) ->bool : return self._error != None or \ (self._progress!=None and self._progress.isPastDeadline(currentTimeMs)) #type:ignore def getError(self)-> Optional[ProtocolException]: ''' @return an error that occured that caused the session to reach its terminated/final state. If an error does not cause termination, only a warning should be logged. None if no error occured. ''' return self._error def WithDeadlineReached(self)->P : return self.With(self._actions, self._connections, val(self._progress), self._settings, self._partyprofiles, self._error) def WithoutParty(self, party:PartyId )->P : assert isinstance(party, PartyId) newconn = list(self._connections) newconn.remove(party); return self.With(self._actions, newconn, val(self._progress), \ self._settings, self._partyprofiles, self._error) def WithException(self, e:ProtocolException)-> P: ''' @param e the error that occured @return a new state with the error set/updated. ''' assert isinstance(e, ProtocolException) return self.With(self._actions, self._connections, val(self._progress), self._settings, self._partyprofiles, e) def WithParty(self, connection:PartyId, partyprofile:PartyWithProfile) -> P : assert isinstance(connection, PartyId) assert isinstance(partyprofile, PartyWithProfile) newconns = list(self.getConnections()) newconns.append(connection) newprofiles = dict(self.getPartyProfiles()) newprofiles[connection]= partyprofile return self.With(self.getActions(), newconns, self._progress, self.getSettings(), newprofiles, None) def WithProgress(self,newprogress:Progress )->P : ''' Sets the new progress for this session. @param newprogress the new progress @return new SAOPState with the progress set to new value ''' assert isinstance(newprogress, Progress) if not newprogress: raise ValueError("newprogress must be not null"); return self.With(self._actions, self._connections, newprogress, self._settings, self._partyprofiles,self._error) def WithAction(self,actor:PartyId , action:Action )->P : ''' @param actor the actor that did this action. Can be used to check if action is valid. NOTICE caller has to make sure the current state is not final. @param action the action that was proposed by actor. @return new SAOPState with the action added as last action. ''' msg = self.checkAction(actor, action) if msg: raise ValueError(msg) newactions = list(self.getActions()) newactions.append(action) return self.With(newactions, self._connections, val(self._progress), self._settings, self._partyprofiles,self._error) def checkAction(self, actor:PartyId , action:Action) -> Optional[str]: ''' @param actor the known real actor that did this action @param action an {@link Action} @return null if action seems ok, or message explaining why not. ''' if not actor : return "actor must not be null" if not action: return "action is null" if actor != action.getActor(): return "act contains wrong credentials: " + str(action) return None def __repr__(self)->str: return type(self).__name__ + "[" + str(self._actions) + ","\ + str(self._connections) + "," + str(self._progress) + "," + \ str(self._settings) + "," + str(self._error); def __hash__(self): return hash((tuple(self._actions), tuple(self._connections), toTuple(self._partyprofiles), self._progress, self._settings, self._error)) def __eq__(self, other) -> bool: return isinstance(other, self.__class__) and \ self._partyprofiles == other._partyprofiles and \ self._actions==other._actions and \ self._connections == other._connections and \ self._progress == other._progress and \ self._settings == other._settings and \ self._error == other._error