Skip to content

Commit

Permalink
session as real dict, and better wait before start
Browse files Browse the repository at this point in the history
  • Loading branch information
manatlan committed Oct 3, 2023
1 parent 6837b17 commit bb3f1c5
Show file tree
Hide file tree
Showing 5 changed files with 114 additions and 83 deletions.
88 changes: 69 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,25 @@
<img src="https://badge.fury.io/py/htagweb.svg?x" alt="Package version">
</a>

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 ...)
Expand All @@ -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
Expand Down
8 changes: 6 additions & 2 deletions htagweb/appserver.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)
Expand Down Expand Up @@ -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):
Expand Down
52 changes: 20 additions & 32 deletions htagweb/sessions/file.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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):
Expand Down
41 changes: 15 additions & 26 deletions htagweb/sessions/mem.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

8 changes: 4 additions & 4 deletions test_sessions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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())

Expand Down Expand Up @@ -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()

Expand Down

0 comments on commit bb3f1c5

Please sign in to comment.