diff --git a/README.md b/README.md index f353f72..8f06543 100644 --- a/README.md +++ b/README.md @@ -6,21 +6,25 @@ Package version -This "htagweb" module is the official way to expose htag's apps on the web. +This module exposes htag's runners for the web. It's the official runners to expose +htag apps on the web, to handle multiple clients/session in the right way. -**ULTRA IMPORTANT** ;-) +There are 3 runners: -**This module will be completly rewritten (for the 3rd time ;-)). And it will work, like in the past, as others classicals runners (tag instances will live, in separates processes). (state and session will remain too). Just need to test the new branch (it will use redys ;-) )** + - **AppServer** : the real one ;-) + - SimpleServer : for tests purposes + - HtagServer : (use SimpleServer) to browse/expose python/htag files in an UI. -**Important note** -On the web, the server can handle many clients : so, it's not possible to handle each tag instances per user. SO there are 1 limitation compared to classical htag runners which comes with htag. - - ~~there can be only one managed instance of a htag class, per user~~ (it's the case in classical runners too) - - and tag instances doesn't live as long as the runner lives (when you hit F5, it will be destroyed/recreated). So, keeping states must be done thru the tag.state / tag.root.state (which is the session of the user). +## AppServer -So developping a htag app which work the same on desktop and on the web, should manage its states in tag.state / tag.root.state ;-) +It's the real runner to expose htag apps in a real production environment, and provide +all features, while maintaining tag instances like classical/desktop htag runners. - ## Features +All htag apps are runned in its own process, and an user can only have an instance of an htag app. (so process are recreated when query params changes) +Process live as long as the server live (TODO: a TIMEOUT will be back soon) + +**Features** * based on [starlette](https://pypi.org/project/starlette/) * multiple ways to handle sessions (file, mem, etc ...) @@ -31,21 +35,67 @@ So developping a htag app which work the same on desktop and on the web, should * real starlette session available (in tag.state, and starlette request.session) * compatible with oauth2 authent ( [authlib](https://pypi.org/project/Authlib/) ) * 'parano mode' (can aes encrypt all communications between client & server ... to avoid mitm'proxies on ws exchanges) + * auto reconnect websocket -## Roadmap / futur -priority : +#### debug (bool) + +When False: (default) no debugging facilities +When True: use starlette debugger. + +#### ssl (bool) + +When False: (default) use "ws://" to connect the websocket +When True: use "wss://" to connect the websocket + +non-sense in http_only mode. + +#### parano (bool) + +When False: (default) exchanges between front/ui and back are in clear text (json), readable by a MITM. +When True: exchanges will be encrypted (less readable by a MITM, TODO: will try to use public/private keys in future) + +#### http_only(bool) + +When False: (default) it will use websocket transport (between front/ui and back), with auto-reconnect feature. +When True: it will use http transport (between front/ui and back). But "tag.update" feature will not be available. + +#### sesprovider (htagweb.sessions) + +You can provide a Session Factory to handle the session in different modes. + +- htagweb.sessions.MemDict (default) : sessions are stored in memory (renewed on reboot) +- htagweb.sessions.FileDict : sessions are stored in filesystem (renewed on reboot) +- htagweb.sessions.FilePersistentDict : make sessions persistent during reboot + +## SimpleServer + +It's a special runner for tests purposes. It doesn't provide all features (parano mode, ssl, session factory...). +Its main goal is to provide a simple runner during dev process, befause when you hit "F5" : +it will destroy/recreate the tag instances. + +SimpleServer uses only websocket transport (tag instances exist only during websocket connexions) + +And it uses `htagweb.sessions.FileDict` as session manager. + +## HtagServer + +It's a special runner, which is mainly used by the `python3 -m htagweb`, to expose +current python/htag files in a browser. Its main goal is to test quickly the files +whose are in your folder, using an UI in your browser. + +It uses the SimpleServer, so it does'nt provide all features (parano mode, ssl, session factory ...) + +------------------------------- + +## Roadmap / futur - - ci/cd test python>3.7 with shared_memory_dict - - unittests on sessions.memory (won't work now) - - better unittests on usot + - better unittests !!!!!!!!!!!!!!!! + - better logging !!!!!!!!!!!!!!!! + - process lives : timeout ! + - parano mode : use public/private keys ? -futur: - - ? replace starlette by fastapi ? - - the double rendering (double init creation) is not ideal. But great for SEO bots. Perhaps I could find a better way (and let only one rendering, but how ?!) ?! - - more unittests !!! - - better logging !!! ## Examples diff --git a/htagweb/appserver.py b/htagweb/appserver.py index 5f3a3e5..c7157c5 100644 --- a/htagweb/appserver.py +++ b/htagweb/appserver.py @@ -34,7 +34,7 @@ from . import crypto import redys.v2 -from htagweb.server import hrserver +from htagweb.server import hrserver, wait_hrserver from htagweb.server.client import HrClient logger = logging.getLogger(__name__) @@ -190,8 +190,12 @@ def processHrServer(): async def lifespan(app): process_hrserver=multiprocessing.Process(target=processHrServer) process_hrserver.start() + + await wait_hrserver() + yield - process_hrserver.terminate() + + process_hrserver.kill() class AppServer(Starlette): diff --git a/htagweb/sessions/file.py b/htagweb/sessions/file.py index 571239d..7d47d34 100644 --- a/htagweb/sessions/file.py +++ b/htagweb/sessions/file.py @@ -9,8 +9,8 @@ import os,pickle,tempfile - -class FileDict: # default +from collections import UserDict +class FileDict(dict): # default """ mimic a dict (with minimal methods), unique source of truth, based on FS""" def __init__(self,uid:str,persistent:bool=False): self._uid=uid @@ -22,44 +22,32 @@ def __init__(self,uid:str,persistent:bool=False): if os.path.isfile(self._file): with open(self._file,"rb+") as fid: - self._d=pickle.load(fid) + d=pickle.load(fid) else: - self._d={} - - def __len__(self): - return len(self._d.keys()) - - def __contains__(self,key): - return key in self._d.keys() + d={} - def items(self): - return self._d.items() - - def get(self,k:str,default=None): - return self._d.get(k,default) - - def __getitem__(self,k:str): - return self._d[k] + super().__init__( d ) def __delitem__(self,k:str): - """ save session """ - del self._d[k] - - with open(self._file,"wb+") as fid: - pickle.dump(self._d,fid, protocol=4) + super().__delitem__(k) + self._save() def __setitem__(self,k:str,v): - """ save session """ - self._d[k]=v - - with open(self._file,"wb+") as fid: - pickle.dump(self._d,fid, protocol=4) + super().__setitem__(k,v) + self._save() def clear(self): - """ save session """ - self._d.clear() - if os.path.isfile(self._file): - os.unlink(self._file) + super().clear() + self._save() + + def _save(self): + if len(self): + with open(self._file,"wb+") as fid: + pickle.dump(dict(self),fid, protocol=4) + else: + if os.path.isfile(self._file): + os.unlink(self._file) + class FilePersistentDict(FileDict): # default def __init__(self,uid): diff --git a/htagweb/sessions/mem.py b/htagweb/sessions/mem.py index 8d9abaf..0f891fa 100644 --- a/htagweb/sessions/mem.py +++ b/htagweb/sessions/mem.py @@ -10,40 +10,29 @@ import os,pickle,tempfile import redys.v2 -class MemDict: # default +from collections import UserDict +class MemDict(dict): # default """ mimic a dict (with minimal methods), unique source of truth, based on redys.v2""" def __init__(self,uid:str): self._uid=uid self._bus=redys.v2.Client() - self._d=self._bus.get(self._uid) or {} - - def __len__(self): - return len(self._d.keys()) - - def __contains__(self,key): - return key in self._d.keys() - - def items(self): - return self._d.items() - - def get(self,k:str,default=None): - return self._d.get(k,default) - - def __getitem__(self,k:str): - return self._d[k] + super().__init__( self._bus.get(self._uid) or {} ) def __delitem__(self,k:str): - """ save session """ - del self._d[k] - self._bus.set(self._uid, self._d) + super().__delitem__(k) + self._save() def __setitem__(self,k:str,v): - """ save session """ - self._d[k]=v - self._bus.set(self._uid, self._d) + super().__setitem__(k,v) + self._save() def clear(self): - """ save session """ - self._d.clear() - self._bus.delete(self._uid) + super().clear() + self._save() + + def _save(self): + if len(self): + self._bus.set(self._uid, dict(self)) + else: + self._bus.delete(self._uid) diff --git a/test_sessions.py b/test_sessions.py index a4f3911..4ee503e 100644 --- a/test_sessions.py +++ b/test_sessions.py @@ -6,10 +6,6 @@ def session_test(factory): session = factory("uid") try: - # bad way to clone - with pytest.raises(Exception): - dict(session) - # good way to clone dict(session.items()) @@ -38,6 +34,10 @@ def session_test(factory): session = factory("uid") assert len(session.items())==0 + session["k"]=42 + assert session.pop("k",12)==42 + assert session.pop("k",12)==12 + finally: session.clear()